feat(ui): adopt persona visibility directives on mounted shells [SPRINT-016]
Apply stellaAuditorOnly and stellaOperatorOnly structural directives on evidence-audit, promotions, and evidence-export surfaces with ViewModeToggle surfaced for persona switching. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# Persona Visibility Directive Adoption
|
||||
|
||||
Sprint: SPRINT_20260308_016_FE_orphan_persona_visibility_directives
|
||||
Tasks: FE-OPV-001 through FE-OPV-004
|
||||
|
||||
## Summary
|
||||
|
||||
Adopted the dormant `*stellaAuditorOnly` and `*stellaOperatorOnly` structural directives
|
||||
on six mounted evidence, release, and promotion shells. The `ViewModeToggleComponent`
|
||||
is surfaced on each adopted shell so operators and auditors can switch persona mode.
|
||||
|
||||
## Adopted Consumers
|
||||
|
||||
| Consumer | Persona Section | Directive |
|
||||
|---|---|---|
|
||||
| `evidence-audit-overview.component.ts` | Audit Events Today stat | `*stellaAuditorOnly` |
|
||||
| `evidence-audit-overview.component.ts` | Proof Chains stat | `*stellaAuditorOnly` |
|
||||
| `evidence-audit-overview.component.ts` | Pending Exports stat | `*stellaOperatorOnly` |
|
||||
| `release-detail.component.ts` | Decisioning/Promote/Deploy buttons | `*stellaOperatorOnly` |
|
||||
| `release-detail.component.ts` | Proof Chain and Replay section | `*stellaAuditorOnly` |
|
||||
| `promotion-detail.component.ts` | Decision box (approve/reject) | `*stellaOperatorOnly` |
|
||||
| `promotion-detail.component.ts` | Evidence tab content | `*stellaAuditorOnly` |
|
||||
| `promotion-detail.component.ts` | Replay tab content | `*stellaAuditorOnly` |
|
||||
| `provenance-visualization.component.ts` | Node detail rows | `*stellaAuditorOnly` |
|
||||
| `provenance-visualization.component.ts` | Raw Data button | `*stellaAuditorOnly` |
|
||||
| `evidence-bundles.component.ts` | Checksum (SHA-256) detail | `*stellaAuditorOnly` |
|
||||
| `export-center.component.ts` | Profile action buttons (Run/Edit/Delete) | `*stellaOperatorOnly` |
|
||||
|
||||
## ViewModeToggle Placement
|
||||
|
||||
| Consumer | Placement |
|
||||
|---|---|
|
||||
| Evidence Audit Overview | Header row, right of title |
|
||||
| Release Detail | Actions bar, inline with buttons |
|
||||
| Promotion Detail | Header row, right of status badge |
|
||||
| Provenance Visualization | Header row, right of title |
|
||||
| Evidence Bundles | Header row, right of title |
|
||||
| Export Center | Header row, right of title and quick actions |
|
||||
|
||||
## Tests
|
||||
|
||||
- `evidence-audit-overview.component.spec.ts`: 5 focused tests covering mode switching,
|
||||
conditional stat visibility, and deterministic toggle behavior.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Findings and policy consumers are excluded (reserved for other sprints).
|
||||
- All consumers are mounted shells (no dead route trees).
|
||||
- Persona distinction is operationally meaningful in every case.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Sprint 20260308-016 - FE Orphan Persona Visibility Directives
|
||||
|
||||
## Topic & Scope
|
||||
- Revive `stellaAuditorOnly` and `stellaOperatorOnly` by adopting them in mounted shells that already present persona-specific decisions or detail density.
|
||||
- Keep the sprint focused on conditional visibility and view-mode behavior, not on introducing separate persona route trees.
|
||||
- Limit the first rollout to active release, evidence, and promotion workflows so this sprint stays independent from findings and policy component adoption.
|
||||
- Working directory: `src/Web/StellaOps.Web`.
|
||||
- Allowed coordination edits: `docs/modules/ui/orphan-revival-batch/README.md`, `docs/modules/ui/TASKS.md`, `docs/modules/ui/implementation_plan.md`, `docs/features/checked/web/`, `src/Web/StellaOps.Web/src/app/shared/directives/`, `src/Web/StellaOps.Web/src/app/shared/components/view-mode-toggle/`, `src/Web/StellaOps.Web/src/app/core/services/view-mode.service.ts`, `src/Web/StellaOps.Web/src/app/features/evidence-audit/`, `src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/release-detail/`, `src/Web/StellaOps.Web/src/app/features/promotions/`, and `src/Web/StellaOps.Web/src/app/features/evidence-export/`.
|
||||
- Expected evidence: focused Angular tests, one checked-feature note, and sprint execution-log updates.
|
||||
|
||||
## Dependencies & Concurrency
|
||||
- Hard dependency inside the orphan revival batch: none.
|
||||
- External prerequisite already satisfied: the host shells are already mounted and the existing `ViewModeService` contract exists.
|
||||
- Safe parallelism:
|
||||
- Can run in parallel with all route reconnection sprints.
|
||||
- Can run in parallel with sprints `017`, `018`, `019`, and `020` because this sprint excludes their primary consumer files.
|
||||
|
||||
## Documentation Prerequisites
|
||||
- `docs/modules/ui/orphan-revival-batch/README.md`
|
||||
- `src/Web/StellaOps.Web/src/app/shared/directives/auditor-only.directive.ts`
|
||||
- `src/Web/StellaOps.Web/src/app/shared/directives/operator-only.directive.ts`
|
||||
- `src/Web/StellaOps.Web/src/app/core/services/view-mode.service.ts`
|
||||
|
||||
## Delivery Tracker
|
||||
|
||||
### FE-OPV-001 - Freeze mounted persona-sensitive consumer list
|
||||
Status: DONE
|
||||
Dependency: none
|
||||
Owners: Developer (FE), Product Manager
|
||||
Task description:
|
||||
- Freeze the mounted consumer list where persona-specific visibility is already implied by the product, such as evidence detail, release actions, or promotion review.
|
||||
- Keep the first adoption set small and high-signal.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Consumer list is recorded in the execution log.
|
||||
- [x] Every consumer belongs to a mounted shell.
|
||||
- [x] Consumers are selected because the persona distinction is operationally meaningful, not cosmetic.
|
||||
|
||||
### FE-OPV-002 - Adopt persona visibility directives
|
||||
Status: DONE
|
||||
Dependency: FE-OPV-001
|
||||
Owners: Developer (FE)
|
||||
Task description:
|
||||
- Replace imperative persona toggling or always-on dense detail with `stellaAuditorOnly` and `stellaOperatorOnly` in the frozen consumer list.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Adopted consumers render persona-specific sections via the shared directives.
|
||||
- [x] View-mode changes update the UI deterministically.
|
||||
- [x] No adopted screen loses required operator actions.
|
||||
|
||||
### FE-OPV-003 - Surface the existing view-mode toggle where needed
|
||||
Status: DONE
|
||||
Dependency: FE-OPV-002
|
||||
Owners: Developer (FE), UX
|
||||
Task description:
|
||||
- Expose `ViewModeToggleComponent` on the selected mounted shells if the mode switch is not already reachable from the page context.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Every adopted consumer has a clear way to switch persona mode.
|
||||
- [x] Toggle placement matches current shell or header patterns.
|
||||
- [x] Mode state persists according to the existing `ViewModeService` contract.
|
||||
|
||||
### FE-OPV-004 - Verify and document persona revival
|
||||
Status: DONE
|
||||
Dependency: FE-OPV-002
|
||||
Owners: Test Automation, Documentation author
|
||||
Task description:
|
||||
- Add focused Angular coverage for directive-driven visibility and document the shipped persona slice.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Angular tests cover mode switching and conditional rendering on adopted consumers.
|
||||
- [x] Checked-feature note exists under `docs/features/checked/web/`.
|
||||
- [x] UI plan/task docs reflect the shipped persona adoption.
|
||||
|
||||
## Execution Log
|
||||
| Date (UTC) | Update | Owner |
|
||||
| --- | --- | --- |
|
||||
| 2026-03-08 | Sprint created from the orphan-revival batch to adopt dormant persona-visibility directives on mounted evidence, release, and promotion shells. | Project Manager |
|
||||
| 2026-03-08 | FE-OPV-001: Frozen consumer list -- 6 mounted shells: evidence-audit-overview, release-detail, promotion-detail, provenance-visualization, evidence-bundles, export-center. 12 directive placements total (7 auditor-only, 5 operator-only). All consumers are operationally meaningful: auditor sections show proof chains/checksums/replay, operator sections show decision actions/promote/deploy. | Developer (FE) |
|
||||
| 2026-03-08 | FE-OPV-002: Applied `*stellaAuditorOnly` and `*stellaOperatorOnly` structural directives on all frozen consumers. Imported directives as standalone. View-mode changes update visibility deterministically via Angular signal-driven effects. | Developer (FE) |
|
||||
| 2026-03-08 | FE-OPV-003: Surfaced `ViewModeToggleComponent` on all 6 adopted shells in header/action-bar positions consistent with existing layout patterns. Mode state persists via localStorage through `ViewModeService`. | Developer (FE) |
|
||||
| 2026-03-08 | FE-OPV-004: Created `evidence-audit-overview.component.spec.ts` with 5 focused tests (toggle rendering, operator/auditor stat visibility, deterministic toggle cycle). Created checked-feature note at `docs/features/checked/web/persona-visibility-directive-adoption.md`. | Test Automation |
|
||||
|
||||
## Decisions & Risks
|
||||
- Decision: this sprint revives persona visibility, not persona-specific route trees.
|
||||
- Risk: teams may overuse the directives and hide content that should remain common to both personas.
|
||||
- Mitigation: freeze the first adoption set and require operationally meaningful persona value in the execution log.
|
||||
|
||||
## Next Checkpoints
|
||||
- 2026-03-09: consumer set frozen. (DONE)
|
||||
- 2026-03-10: directive adoption criteria agreed. (DONE)
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { EvidenceAuditOverviewComponent } from './evidence-audit-overview.component';
|
||||
import { ViewModeService } from '../../core/services/view-mode.service';
|
||||
|
||||
describe('EvidenceAuditOverviewComponent (persona visibility)', () => {
|
||||
let fixture: ComponentFixture<EvidenceAuditOverviewComponent>;
|
||||
let service: ViewModeService;
|
||||
|
||||
beforeEach(async () => {
|
||||
localStorage.clear();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [EvidenceAuditOverviewComponent, RouterTestingModule],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(EvidenceAuditOverviewComponent);
|
||||
service = TestBed.inject(ViewModeService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('should render view-mode toggle', () => {
|
||||
service.setMode('operator');
|
||||
fixture.detectChanges();
|
||||
|
||||
const toggle = fixture.nativeElement.querySelector('stella-view-mode-toggle');
|
||||
expect(toggle).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should hide audit events and proof chains in operator mode', () => {
|
||||
service.setMode('operator');
|
||||
fixture.detectChanges();
|
||||
|
||||
const labels: string[] = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('.stat-label')
|
||||
).map((el: any) => el.textContent.trim());
|
||||
|
||||
expect(labels).toContain('Evidence Packs');
|
||||
expect(labels).toContain('Pending Exports');
|
||||
expect(labels).not.toContain('Audit Events Today');
|
||||
expect(labels).not.toContain('Proof Chains');
|
||||
});
|
||||
|
||||
it('should show audit events and proof chains in auditor mode', () => {
|
||||
service.setMode('auditor');
|
||||
fixture.detectChanges();
|
||||
|
||||
const labels: string[] = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('.stat-label')
|
||||
).map((el: any) => el.textContent.trim());
|
||||
|
||||
expect(labels).toContain('Evidence Packs');
|
||||
expect(labels).toContain('Audit Events Today');
|
||||
expect(labels).toContain('Proof Chains');
|
||||
});
|
||||
|
||||
it('should hide pending exports in auditor mode', () => {
|
||||
service.setMode('auditor');
|
||||
fixture.detectChanges();
|
||||
|
||||
const labels: string[] = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('.stat-label')
|
||||
).map((el: any) => el.textContent.trim());
|
||||
|
||||
expect(labels).not.toContain('Pending Exports');
|
||||
});
|
||||
|
||||
it('should react deterministically to mode toggle', () => {
|
||||
service.setMode('operator');
|
||||
fixture.detectChanges();
|
||||
|
||||
let labels: string[] = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('.stat-label')
|
||||
).map((el: any) => el.textContent.trim());
|
||||
expect(labels).not.toContain('Proof Chains');
|
||||
|
||||
service.setMode('auditor');
|
||||
fixture.detectChanges();
|
||||
|
||||
labels = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('.stat-label')
|
||||
).map((el: any) => el.textContent.trim());
|
||||
expect(labels).toContain('Proof Chains');
|
||||
|
||||
service.setMode('operator');
|
||||
fixture.detectChanges();
|
||||
|
||||
labels = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('.stat-label')
|
||||
).map((el: any) => el.textContent.trim());
|
||||
expect(labels).not.toContain('Proof Chains');
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,9 @@ import {
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { AuditorOnlyDirective } from '../../shared/directives/auditor-only.directive';
|
||||
import { OperatorOnlyDirective } from '../../shared/directives/operator-only.directive';
|
||||
import { ViewModeToggleComponent } from '../../shared/components/view-mode-toggle/view-mode-toggle.component';
|
||||
|
||||
interface EvidenceQuickViewTile {
|
||||
id: string;
|
||||
@@ -29,7 +32,7 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
|
||||
@Component({
|
||||
selector: 'app-evidence-audit-overview',
|
||||
standalone: true,
|
||||
imports: [RouterLink],
|
||||
imports: [RouterLink, AuditorOnlyDirective, OperatorOnlyDirective, ViewModeToggleComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="evidence-audit-overview">
|
||||
@@ -40,6 +43,7 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
|
||||
Retrieve, verify, export, and audit evidence for every release, bundle, environment, and approval decision.
|
||||
</p>
|
||||
</div>
|
||||
<stella-view-mode-toggle />
|
||||
</header>
|
||||
|
||||
<section class="mode-toggle" aria-label="Evidence home state mode">
|
||||
@@ -125,22 +129,22 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<!-- Quick Stats (auditor detail: audit events, proof chains) -->
|
||||
<section class="stats-section" aria-label="Evidence statistics">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ stats().totalPacks.toLocaleString() }}</span>
|
||||
<span class="stat-label">Evidence Packs</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-item" *stellaAuditorOnly>
|
||||
<span class="stat-value">{{ stats().auditEventsToday.toLocaleString() }}</span>
|
||||
<span class="stat-label">Audit Events Today</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-item" *stellaAuditorOnly>
|
||||
<span class="stat-value">{{ stats().proofChains.toLocaleString() }}</span>
|
||||
<span class="stat-label">Proof Chains</span>
|
||||
</div>
|
||||
<div class="stat-item" [class.warning]="stats().pendingExports > 0">
|
||||
<div class="stat-item" *stellaOperatorOnly [class.warning]="stats().pendingExports > 0">
|
||||
<span class="stat-value">{{ stats().pendingExports }}</span>
|
||||
<span class="stat-label">Pending Exports</span>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
StellaBundleExportResult,
|
||||
} from './evidence-export.models';
|
||||
import { StellaBundleExportButtonComponent } from './stella-bundle-export-button/stella-bundle-export-button.component';
|
||||
import { OperatorOnlyDirective } from '../../shared/directives/operator-only.directive';
|
||||
import { ViewModeToggleComponent } from '../../shared/components/view-mode-toggle/view-mode-toggle.component';
|
||||
|
||||
/**
|
||||
* Export Center Component (Sprint: SPRINT_20251229_016)
|
||||
@@ -25,7 +27,7 @@ import { StellaBundleExportButtonComponent } from './stella-bundle-export-button
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-export-center',
|
||||
imports: [FormsModule, StellaBundleExportButtonComponent],
|
||||
imports: [FormsModule, StellaBundleExportButtonComponent, OperatorOnlyDirective, ViewModeToggleComponent],
|
||||
template: `
|
||||
<div class="export-center">
|
||||
<header class="page-header">
|
||||
@@ -34,6 +36,7 @@ import { StellaBundleExportButtonComponent } from './stella-bundle-export-button
|
||||
<h1>Export Center</h1>
|
||||
<p>Configure export profiles and monitor export runs.</p>
|
||||
</div>
|
||||
<stella-view-mode-toggle />
|
||||
<!-- Quick Actions (SB-003) -->
|
||||
<div class="quick-actions">
|
||||
<app-stella-bundle-export-button
|
||||
@@ -150,7 +153,7 @@ import { StellaBundleExportButtonComponent } from './stella-bundle-export-button
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="profile-actions">
|
||||
<div class="profile-actions" *stellaOperatorOnly>
|
||||
<button class="btn btn-sm btn-primary" (click)="runProfile(profile)">
|
||||
Run Now
|
||||
</button>
|
||||
|
||||
@@ -13,6 +13,9 @@ import { catchError, of } from 'rxjs';
|
||||
|
||||
import { APPROVAL_API } from '../../core/api/approval.client';
|
||||
import type { ApprovalDetail, GateStatus } from '../../core/api/approval.models';
|
||||
import { AuditorOnlyDirective } from '../../shared/directives/auditor-only.directive';
|
||||
import { OperatorOnlyDirective } from '../../shared/directives/operator-only.directive';
|
||||
import { ViewModeToggleComponent } from '../../shared/components/view-mode-toggle/view-mode-toggle.component';
|
||||
|
||||
type DetailTab =
|
||||
| 'overview'
|
||||
@@ -27,7 +30,7 @@ type DetailTab =
|
||||
@Component({
|
||||
selector: 'app-promotion-detail',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, RouterLink],
|
||||
imports: [CommonModule, FormsModule, RouterLink, AuditorOnlyDirective, OperatorOnlyDirective, ViewModeToggleComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="promotion-detail">
|
||||
@@ -54,9 +57,12 @@ type DetailTab =
|
||||
| requested by {{ promotion()!.requestedBy }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="status-badge status-badge--{{ promotion()!.status }}">
|
||||
{{ promotion()!.status }}
|
||||
</span>
|
||||
<div class="promotion-detail__header-actions">
|
||||
<stella-view-mode-toggle />
|
||||
<span class="status-badge status-badge--{{ promotion()!.status }}">
|
||||
{{ promotion()!.status }}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="promotion-detail__identity" aria-label="Bundle version identity">
|
||||
@@ -104,7 +110,7 @@ type DetailTab =
|
||||
</div>
|
||||
|
||||
@if (promotion()!.status === 'pending') {
|
||||
<div class="decision-box">
|
||||
<div class="decision-box" *stellaOperatorOnly>
|
||||
<label for="decisionComment">Decision comment</label>
|
||||
<textarea
|
||||
id="decisionComment"
|
||||
@@ -197,7 +203,7 @@ type DetailTab =
|
||||
</section>
|
||||
}
|
||||
@case ('evidence') {
|
||||
<section class="panel" aria-label="Evidence snapshot">
|
||||
<section class="panel" aria-label="Evidence snapshot" *stellaAuditorOnly>
|
||||
<h2>Evidence Used for Decision</h2>
|
||||
<p>
|
||||
Evidence packet identifiers are not provided in this contract; use canonical Evidence and Audit surfaces for promotion-linked retrieval.
|
||||
@@ -206,7 +212,7 @@ type DetailTab =
|
||||
</section>
|
||||
}
|
||||
@case ('replay') {
|
||||
<section class="panel" aria-label="Replay and verify">
|
||||
<section class="panel" aria-label="Replay and verify" *stellaAuditorOnly>
|
||||
<h2>Replay / Verify Decision</h2>
|
||||
<p>Replay and verification are delegated to Evidence and Audit.</p>
|
||||
<a routerLink="/evidence/verify-replay" class="link-sm">Open replay and verify -></a>
|
||||
@@ -262,6 +268,12 @@ type DetailTab =
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.promotion-detail__header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.promotion-detail__title {
|
||||
font-size: 1.375rem;
|
||||
font-weight: 600;
|
||||
|
||||
Reference in New Issue
Block a user