diff --git a/docs-archived/implplan/SPRINT_20260308_002_FE_offline_operations_cutover.md b/docs-archived/implplan/SPRINT_20260308_002_FE_offline_operations_cutover.md new file mode 100644 index 000000000..df0b07328 --- /dev/null +++ b/docs-archived/implplan/SPRINT_20260308_002_FE_offline_operations_cutover.md @@ -0,0 +1,127 @@ +# Sprint 20260308-002 - Offline Operations Cutover + +## Topic & Scope +- Restore the dropped and weakly surfaced offline capability by making one canonical offline operations owner fully usable instead of leaving Offline Kit, feed air-gap flows, and evidence export handoffs split across stale aliases. +- Ship a working `Ops > Operations > Offline Kit` and `Ops > Operations > Feeds & Airgap` flow with repaired secondary entry points, bookmark-safe legacy aliases, and usable offline shell actions. +- Complete the UI workflows that were still half-wired: route-aware tabs, real drill-ins, usable export and verification actions, and cross-links into Evidence and Trust surfaces. +- Working directory: `src/Web/StellaOps.Web/src/app/features/offline-kit`. +- Allowed coordination edits: `src/Web/StellaOps.Web/src/app/routes/`, `src/Web/StellaOps.Web/src/app/core/navigation/`, `src/Web.StellaOps.Web/src/app/layout/context-chips/`, `src/Web.StellaOps.Web/src/app/features/platform/ops/`, `src/Web.StellaOps.Web/src/tests/offline/`, `src/Web.StellaOps.Web/src/tests/platform-ops/`, `src/Web.StellaOps.Web/src/tests/navigation/`, `src/Web.StellaOps.Web/tests/e2e/`, `docs/modules/ui/offline-operations/`, `docs/features/checked/web/`, `docs/modules/ui/TASKS.md`, `docs/modules/ui/implementation_plan.md`, and `docs/modules/ui/component-preservation-map/RESTORATION_PRIORITIES.md`. +- Expected evidence: one mounted canonical offline route family, working alias redirects for old bookmarks, usable offline shell actions, repaired cross-shell links, targeted Angular tests, Playwright verification, and synced docs. + +## Dependencies & Concurrency +- Depends on: + - `docs/modules/ui/component-preservation-map/RESTORATION_PRIORITIES.md` + - `docs/modules/ui/platform-ops-consolidation/README.md` + - `docs/modules/ui/contextual-actions-patterns/README.md` + - `src/Web.StellaOps.Web/src/app/routes/operations.routes.ts` + - `src/Web.StellaOps.Web/src/app/features/offline-kit/offline-kit.routes.ts` + - `src/Web.StellaOps.Web/src/app/features/platform/ops/platform-feeds-airgap-page.component.ts` +- Safe parallelism: + - canonical route and alias contract must freeze before deep-link cleanup starts + - offline-shell action completion can proceed in parallel with docs drafting once route targets are fixed + - QA and checked-feature docs can proceed in parallel after the final route contract is stable + +## Documentation Prerequisites +- `docs/modules/ui/platform-ops-consolidation/README.md` +- `docs/modules/ui/contextual-actions-patterns/README.md` +- `docs/modules/ui/component-preservation-map/RESTORATION_PRIORITIES.md` +- `docs/modules/ui/implementation_plan.md` + +## Delivery Tracker + +### FE-OFF-001 - Freeze canonical offline owner and alias contract +Status: DONE +Dependency: none +Owners: Product Manager, FE Architect +Task description: +- Make `Ops > Operations` the owner shell for offline and air-gap operational workflows, with `Offline Kit` and `Feeds & Airgap` as first-class child routes. +- Define and implement bookmark-safe redirects from stale `/ops/offline-kit/*`, `/ops/feeds/*`, `/platform-ops/*`, and `/platform/ops/*` variants into the active canonical routes. + +Completion criteria: +- [x] One canonical offline route family exists under `/ops/operations/*`. +- [x] Legacy aliases land on working offline child pages without dropping query params. +- [x] Navigation and contextual chips point at the same canonical owner. + +### FE-OFF-002 - Make offline shell actions usable and route-aware +Status: DONE +Dependency: FE-OFF-001 +Owners: Developer, FE Architect +Task description: +- Repair the Offline Kit shell so its header actions, dashboard cards, tabs, and cross-links are real operator actions instead of dead buttons or isolated status text. +- Ensure the `Feeds & Airgap` page is route-aware, query-driven, and able to hand operators into the right offline or evidence workflow. + +Completion criteria: +- [x] Offline Kit actions navigate to working canonical routes or perform a real local workflow. +- [x] Feeds & Airgap tabs are linkable and preserve state through the URL. +- [x] Cross-links between Offline Kit, Feeds & Airgap, Evidence, and Trust work from the mounted shell. + +### FE-OFF-003 - Complete supporting export, verification, and trust workflows +Status: DONE +Dependency: FE-OFF-002 +Owners: Developer +Task description: +- Replace the remaining no-op Offline Kit actions with usable FE behavior, including bundle export, verification detail drill-ins, report export, and trust-material import or inspection where practical inside the current UI contract. +- Make the restored surfaces useful for operators even without backend changes by providing deterministic local export and inspection flows where the prior UI stopped at placeholders. + +Completion criteria: +- [x] Bundle export and verification actions no longer end in `console.log` dead ends. +- [x] Verification history supports detail drill-in and report export. +- [x] JWKS or trust-anchor actions provide a usable inspection or import path inside the shell. + +### FE-OFF-004 - Verify canonical offline operations journeys +Status: DONE +Dependency: FE-OFF-003 +Owners: QA, Test Automation +Task description: +- Add targeted UI verification for canonical routes, alias redirects, offline-shell actions, and at least one end-to-end journey that starts from a secondary entry point and reaches a usable offline workflow. +- Prove the restored capability is usable, not just mounted. + +Completion criteria: +- [x] Angular tests cover canonical routes, alias redirects, and representative offline workflows. +- [x] Playwright covers the main offline landing and at least one stale-alias journey. +- [x] Verification explicitly checks that dead buttons and stale routes are gone from the active operator path. + +### FE-OFF-005 - Sync docs, archive the sprint, and record the shipped feature +Status: DONE +Dependency: FE-OFF-004 +Owners: Documentation author, Project Manager +Task description: +- Publish the shipped offline operations contract, checked-feature note, task-board updates, and restoration backlog update. +- Archive the sprint only after code and verification evidence are complete. + +Completion criteria: +- [x] Shipped offline operations UX is documented with canonical routes and alias behavior. +- [x] Checked-feature note records the exact verification commands and outcomes. +- [x] Sprint is archived only after all delivery tasks are marked done. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-08 | Sprint created to restore Offline Operations by making `Ops > Operations` the canonical owner and completing the remaining half-wired offline shell actions. | Project Manager | +| 2026-03-08 | Repaired canonical offline and feed-airgap aliases, updated navigation and context chips, and made the `Feeds & Airgap` page route-aware instead of button-stub driven. | Developer | +| 2026-03-08 | Completed Offline Kit workflows with manifest loading, bundle export, verification detail and report export, trust-anchor inspection, and Evidence or Trust handoffs. | Developer | +| 2026-03-08 | Verified targeted Angular coverage with `npm test -- --watch=false --include src/tests/offline/offline-kit-ui-integration.spec.ts --include src/tests/offline/verification-center.component.spec.ts --include src/tests/offline/jwks-management.component.spec.ts --include src/tests/platform-ops/platform-feeds-airgap-page.component.spec.ts --include src/tests/platform-ops/platform-ops-routes.spec.ts`: 17 tests passed. | QA | +| 2026-03-08 | Verified browser flows with a prestarted local Angular test server plus `PLAYWRIGHT_BASE_URL=https://127.0.0.1:4400 npx playwright test tests/e2e/offline-operations.spec.ts --workers=1`: 2 scenarios passed. | QA | +| 2026-03-08 | Production build passed via `npm run build`; existing bundle budget warnings remain unchanged from the baseline. | QA | + +## Decisions & Risks +- Decision: `Ops > Operations > Offline Kit` and `Ops > Operations > Feeds & Airgap` remain the canonical owner surfaces; Evidence and Trust remain secondary linked destinations rather than competing products. +- Decision: this sprint completes the UI workflows already present in the shell and fixes stale alias routing; it does not add new backend APIs. +- Risk: several offline components still stop at placeholder buttons or stale paths, so a route-only cutover would still leave operators stranded. +- Mitigation: replace dead buttons with route-aware actions or deterministic local export and inspection flows in the same sprint. +- Risk: aliasing legacy `/ops/*` and `/platform-ops/*` deep links can regress query-param preservation if redirects are string-based. +- Mitigation: use redirect helpers that preserve existing query params and fragments. +- Risk: local Playwright web-server startup can exceed the default timeout because the Angular dev build is heavy. +- Mitigation: prestart `npm run serve:test` and point Playwright at `PLAYWRIGHT_BASE_URL=https://127.0.0.1:4400` for deterministic local verification. +- Delivery rule: this sprint is only complete when the canonical offline tree is mounted, main actions are usable, and the core operator journeys are verified end to end. +- Reference design note: `docs/modules/ui/offline-operations/README.md`. +- Docs synced: + - `docs/modules/ui/offline-operations/README.md` + - `docs/features/checked/web/offline-operations-ui.md` + - `docs/features/checked/web/offline-kit-ui-integration.md` + - `docs/features/checked/web/feed-mirror-airgap-ops-ui.md` + - `docs/modules/ui/TASKS.md` + - `docs/modules/ui/implementation_plan.md` + - `docs/modules/ui/component-preservation-map/RESTORATION_PRIORITIES.md` +## Next Checkpoints +- 2026-03-08: archived after implementation, verification, and docs sync completed. diff --git a/docs/features/checked/web/feed-mirror-airgap-ops-ui.md b/docs/features/checked/web/feed-mirror-airgap-ops-ui.md index b798e4814..43f52cad1 100644 --- a/docs/features/checked/web/feed-mirror-airgap-ops-ui.md +++ b/docs/features/checked/web/feed-mirror-airgap-ops-ui.md @@ -7,11 +7,17 @@ Web VERIFIED ## Description -Feed mirror ops UI with mirror registry list, snapshot management, AirGap import/export with bundle validation, feed version lock for deterministic scans, offline sync status, and bundle freshness warnings. +Feed mirror ops UI with mirror registry list, snapshot management, AirGap import/export with bundle validation, feed version lock for deterministic scans, offline sync status, and bundle freshness warnings. The canonical owner route now lives under `Ops > Operations`; legacy `ops` and `platform-ops` feed aliases redirect into it. ## Implementation Details - **Feature directory**: `src/Web/StellaOps.Web/src/app/features/feed-mirror/` - **Routes**: `feed-mirror.routes.ts` +- **Canonical route**: `/ops/operations/feeds-airgap` +- **Legacy aliases**: + - `/ops/feeds` + - `/ops/feeds/airgap/*` + - `/platform-ops/feeds*` + - `/platform/ops/feeds*` - **Components**: - `airgap-export` (`src/Web/StellaOps.Web/src/app/features/feed-mirror/airgap-export.component.ts`) - `airgap-import` (`src/Web/StellaOps.Web/src/app/features/feed-mirror/airgap-import.component.ts`) @@ -31,7 +37,7 @@ Feed mirror ops UI with mirror registry list, snapshot management, AirGap import ## E2E Test Plan - **Setup**: - [ ] Log in with a user that has appropriate permissions - - [ ] Navigate to `/ops/feeds` + - [ ] Navigate to `/ops/operations/feeds-airgap` - [ ] Ensure test data exists (scanned artifacts, SBOM data, or seed data as needed) - **Core verification**: - [ ] Verify the component renders correctly with sample data diff --git a/docs/features/checked/web/offline-kit-ui-integration.md b/docs/features/checked/web/offline-kit-ui-integration.md index c19d36931..af4256083 100644 --- a/docs/features/checked/web/offline-kit-ui-integration.md +++ b/docs/features/checked/web/offline-kit-ui-integration.md @@ -7,11 +7,13 @@ Web VERIFIED ## Description -Offline Kit UI with OfflineModeService, ManifestValidator, BundleFreshness widget, ReadOnlyGuard, and offline verification workflow for air-gapped environments, including deterministic bundle activation behavior in bundle management. +Offline Kit UI with OfflineModeService, ManifestValidator, BundleFreshness widget, ReadOnlyGuard, and offline verification workflow for air-gapped environments, including deterministic bundle activation behavior in bundle management. The canonical owner route now lives under `Ops > Operations`; the legacy `/ops/offline-kit` bookmark remains as a redirect. ## Implementation Details - **Feature directory**: `src/Web/StellaOps.Web/src/app/features/offline-kit/` - **Routes**: `offline-kit.routes.ts` +- **Canonical route**: `/ops/operations/offline-kit` +- **Legacy alias**: `/ops/offline-kit` - **Components**: - `bundle-management` (`src/Web/StellaOps.Web/src/app/features/offline-kit/components/bundle-management.component.ts`) - `jwks-management` (`src/Web/StellaOps.Web/src/app/features/offline-kit/components/jwks-management.component.ts`) @@ -23,7 +25,7 @@ Offline Kit UI with OfflineModeService, ManifestValidator, BundleFreshness widge ## E2E Test Plan - **Setup**: - [ ] Log in with a user that has appropriate permissions - - [ ] Navigate to `/ops/offline-kit` + - [ ] Navigate to `/ops/operations/offline-kit` - [ ] Ensure test data exists (scanned artifacts, SBOM data, or seed data as needed) - **Core verification**: - [ ] Verify the component renders correctly with sample data diff --git a/docs/features/checked/web/offline-operations-ui.md b/docs/features/checked/web/offline-operations-ui.md new file mode 100644 index 000000000..e39176272 --- /dev/null +++ b/docs/features/checked/web/offline-operations-ui.md @@ -0,0 +1,67 @@ +# Offline Operations UI + +## Module +Web + +## Status +VERIFIED + +## Description +Shipped the canonical offline and air-gap operations flow under `Ops > Operations`, repaired stale deep links from legacy `ops` and `platform-ops` aliases, and completed the previously half-wired Offline Kit actions so operators can load bundles, export summaries, inspect trust anchors, and hand off into Evidence and Trust surfaces without dead ends. + +## Implementation Details +- **Feature directories**: + - `src/Web/StellaOps.Web/src/app/features/offline-kit/` + - `src/Web.StellaOps.Web/src/app/features/platform/ops/` +- **Primary components**: + - `offline-kit` (`src/Web.StellaOps.Web/src/app/features/offline-kit/offline-kit.component.ts`) + - `offline-dashboard` (`src/Web.StellaOps.Web/src/app/features/offline-kit/components/offline-dashboard.component.ts`) + - `bundle-management` (`src/Web.StellaOps.Web/src/app/features/offline-kit/components/bundle-management.component.ts`) + - `verification-center` (`src/Web.StellaOps.Web/src/app/features/offline-kit/components/verification-center.component.ts`) + - `jwks-management` (`src/Web.StellaOps.Web/src/app/features/offline-kit/components/jwks-management.component.ts`) + - `platform-feeds-airgap-page` (`src/Web.StellaOps.Web/src/app/features/platform/ops/platform-feeds-airgap-page.component.ts`) +- **Canonical routes**: + - `/ops/operations/offline-kit` + - `/ops/operations/offline-kit/dashboard` + - `/ops/operations/offline-kit/bundles` + - `/ops/operations/offline-kit/verify` + - `/ops/operations/offline-kit/jwks` + - `/ops/operations/feeds-airgap` +- **Legacy aliases**: + - `/ops/offline-kit/*` + - `/ops/feeds/*` + - `/platform-ops/*` + - `/platform/ops/*` +- **Secondary entry points**: + - `Evidence > Export Center` + - `Evidence > Verify & Replay` + - `Setup > Trust & Signing` + - context status chips and Ops navigation + +## E2E Test Plan +- **Setup**: + - [x] Start the local Angular test server with `npm run serve:test`. + - [x] Open `/ops/operations/offline-kit`. + - [x] Use a test session with Ops and admin scopes. +- **Core verification**: + - [x] Verify Offline Kit renders its canonical shortcuts and child tabs. + - [x] Verify bundle and verification actions are usable instead of placeholder logs. + - [x] Verify trust-anchor inspection and export flows render inside the shell. +- **Legacy verification**: + - [x] Verify stale `/platform/ops/offline-kit/*` bookmarks land on `/ops/operations/offline-kit/*`. + - [x] Verify stale `/ops/feeds/airgap/import` lands on the canonical `Feeds & Airgap` page with preserved tab or action context. + +## Verification +- Run: + - `npm test -- --watch=false --include src/tests/offline/offline-kit-ui-integration.spec.ts --include src/tests/offline/verification-center.component.spec.ts --include src/tests/offline/jwks-management.component.spec.ts --include src/tests/platform-ops/platform-feeds-airgap-page.component.spec.ts --include src/tests/platform-ops/platform-ops-routes.spec.ts` + - `npm run serve:test` + - `PLAYWRIGHT_BASE_URL=https://127.0.0.1:4400 npx playwright test tests/e2e/offline-operations.spec.ts --workers=1` + - `npm run build` +- Tier 0 (source): pass +- Tier 1 (build/tests): pass +- Tier 2 (behavior): pass +- Notes: + - Angular targeted tests passed: `5` files, `17` tests. + - Playwright passed: `2` scenarios. + - Production build passed; existing bundle-budget warnings remain unchanged from the baseline. +- Verified on (UTC): 2026-03-08T00:54:00Z diff --git a/docs/modules/ui/README.md b/docs/modules/ui/README.md index 748b9de14..0aefc9802 100644 --- a/docs/modules/ui/README.md +++ b/docs/modules/ui/README.md @@ -22,6 +22,8 @@ The Console presents operator dashboards for scans, policies, VEX evidence, runt - Added checked-feature verification for reachability witnessing at `../../features/checked/web/reachability-witnessing-ui.md`. - Shipped the consolidated `Ops > Operations` shell with grouped overview cards, canonical `/ops/operations/*` routes, and legacy `platform-ops` alias cutover. - Added checked-feature verification for operations consolidation at `../../features/checked/web/operations-consolidation-ui.md`. +- Shipped the canonical offline and air-gap operations flow under `Ops > Operations`, including repaired stale `/ops/*` and `/platform-ops/*` deep links, usable Offline Kit actions, and Evidence or Trust handoffs. +- Added checked-feature verification for offline operations at `../../features/checked/web/offline-operations-ui.md`. - Shipped the shared contextual placement primitives for tabs, submenu pills, route-aware drawers, list-detail shells, grouped overview cards, and return-to-context headers under `src/Web/StellaOps.Web/src/app/shared/ui/`. - Added checked-feature verification for the contextual primitives and their first adopted surfaces at `../../features/checked/web/contextual-actions-patterns-ui.md`. @@ -69,6 +71,7 @@ The Console presents operator dashboards for scans, policies, VEX evidence, runt - ./watchlist-operations/README.md - ./reachability-witnessing/README.md - ./platform-ops-consolidation/README.md +- ./offline-operations/README.md - ./triage-explainability-workspace/README.md - ./workflow-visualization-replay/README.md - ./contextual-actions-patterns/README.md diff --git a/docs/modules/ui/TASKS.md b/docs/modules/ui/TASKS.md index 05c311975..ebd11e75e 100644 --- a/docs/modules/ui/TASKS.md +++ b/docs/modules/ui/TASKS.md @@ -87,6 +87,11 @@ - [DONE] FE-AUD-003 Wire secondary entry points and contextual handoffs - [DONE] FE-AUD-004 Verify route cutover and operator journeys - [DONE] FE-AUD-005 Sync docs, archive the sprint, and record the shipped feature +- [DONE] FE-OFF-001 Freeze canonical offline owner and alias contract +- [DONE] FE-OFF-002 Make offline shell actions usable and route-aware +- [DONE] FE-OFF-003 Complete supporting export, verification, and trust workflows +- [DONE] FE-OFF-004 Verify canonical offline operations journeys +- [DONE] FE-OFF-005 Sync docs, archive the sprint, and record the shipped feature - [DONE] FE-PO-001 Freeze Operations overview taxonomy and submenu structure - [DONE] FE-PO-002 Overview page regrouping and blocking-card contract - [DONE] FE-PO-003 Legacy widget absorption matrix for Platform Ops diff --git a/docs/modules/ui/component-preservation-map/RESTORATION_PRIORITIES.md b/docs/modules/ui/component-preservation-map/RESTORATION_PRIORITIES.md index 1f538e6eb..7dda50ded 100644 --- a/docs/modules/ui/component-preservation-map/RESTORATION_PRIORITIES.md +++ b/docs/modules/ui/component-preservation-map/RESTORATION_PRIORITIES.md @@ -121,6 +121,10 @@ These are mostly not dropped products. They are current or near-current capabili - `Evidence Pack` - Target: - `/ops/operations/*` with evidence links where relevant +- Notes: + - Detailed UX dossier: `docs/modules/ui/offline-operations/README.md` + - Implementation sprint: `docs-archived/implplan/SPRINT_20260308_002_FE_offline_operations_cutover.md` + - Shipped verification note: `docs/features/checked/web/offline-operations-ui.md` ### 9. Scanner And Job Operations - Type: `wire-in / preserve` diff --git a/docs/modules/ui/implementation_plan.md b/docs/modules/ui/implementation_plan.md index b88755863..6d5f268fe 100644 --- a/docs/modules/ui/implementation_plan.md +++ b/docs/modules/ui/implementation_plan.md @@ -28,13 +28,14 @@ Provide a living plan for UI deliverables, dependencies, and evidence. - `docs/features/checked/web/workflow-visualization-replay-ui.md` - shipped verification note for the canonical run-detail graph, timeline, replay, evidence tabs, and workflow-editor preview reuse boundary. - `docs/features/checked/web/contextual-actions-patterns-ui.md` - shipped verification note for the shared contextual route-state, headers, drawers, list-detail shells, grouped overview cards, and first adopted restoration surfaces. - `docs/features/checked/web/unified-audit-surfaces-ui.md` - shipped verification note for the Evidence-owned audit shell, admin bookmark redirects, repaired audit subview links, and secondary handoff entry points. +- `docs/features/checked/web/offline-operations-ui.md` - shipped verification note for the canonical Offline Kit and Feeds & Airgap owner routes, repaired stale aliases, and completed offline shell actions. - `docs/modules/ui/reachability-witnessing/README.md` - detailed witness and proof UX dossier plus cross-shell deep-link contract. - `docs/modules/ui/platform-ops-consolidation/README.md` - detailed Operations overview taxonomy and legacy absorption plan. +- `docs/modules/ui/offline-operations/README.md` - detailed owner-shell contract for Offline Kit, Feeds & Airgap, Evidence handoffs, and stale alias policy. - `docs/modules/ui/triage-explainability-workspace/README.md` - detailed artifact workspace and audit-bundle UX dossier. - `docs/modules/ui/workflow-visualization-replay/README.md` - detailed run-detail graph, timeline, replay, and evidence UX dossier. - `docs/modules/ui/contextual-actions-patterns/README.md` - shared placement contract for stray actions, pages, drawers, and tabs. - `docs/modules/ui/unified-audit-surfaces/README.md` - shipped canonical audit owner, alias contract, and secondary entry-point rules for cross-module audit browsing. -- `docs/modules/ui/unified-audit-surfaces/README.md` - canonical audit owner, alias contract, and secondary entry-point rules for cross-module audit browsing. ## Dependencies - `docs/modules/ui/architecture.md` diff --git a/docs/modules/ui/offline-operations/README.md b/docs/modules/ui/offline-operations/README.md new file mode 100644 index 000000000..0a403d3f0 --- /dev/null +++ b/docs/modules/ui/offline-operations/README.md @@ -0,0 +1,55 @@ +# Offline Operations + +## Purpose +- Keep offline and air-gap workflows as a first-class operational capability without reviving a second product tree. +- Make `Offline Kit`, `Feeds & Airgap`, and evidence export handoffs usable from the active `Ops > Operations` shell. + +## Canonical Owner +- Owner shell: `Ops > Operations` +- Primary routes: + - `/ops/operations/offline-kit` + - `/ops/operations/offline-kit/dashboard` + - `/ops/operations/offline-kit/bundles` + - `/ops/operations/offline-kit/verify` + - `/ops/operations/offline-kit/jwks` + - `/ops/operations/feeds-airgap` +- Secondary linked destinations: + - `/evidence/exports` + - `/evidence/verify-replay` + - `/setup/trust-signing` + +## Legacy Alias Policy +- Preserve old bookmarks and stale menu links by redirecting: + - `/ops/offline-kit/*` + - `/ops/feeds/*` + - `/platform-ops/*` + - `/platform/ops/*` +- Redirects must preserve query params and fragments so tab or action state survives the handoff. + +## UX Rules +- `Offline Kit` owns bundle management, offline verification, and trust-material inspection. +- `Feeds & Airgap` owns mirror freshness, version-lock posture, and air-gap entry actions. +- `Evidence Exports` remains Evidence-owned, but Offline Kit must link into it for portable bundle generation. +- Single actions like `Import Bundle` and `Export Bundle` should not become standalone products; they route into a tabbed or action-aware owner page. + +## Preserved Value +- Keep: + - offline bundle loading and activation + - offline verification history and report export + - trust-anchor inspection and export + - feed freshness and version-lock context + - evidence export and replay handoffs +- Why: + - offline and air-gap operation is a real product differentiator, not a prototype branch + - release evidence, trust material, and feed provenance all need an operator home when the site is disconnected + +## Shipped In This Cut +- Canonical `Ops > Operations` routes and stale alias repair for deep offline links. +- Route-aware `Feeds & Airgap` tab state and action banners. +- Working Offline Kit shortcuts into Evidence and Trust surfaces. +- Bundle export, verification detail drill-in, verification report export, and trust-anchor inspection/export. + +## Related Docs +- `docs/modules/ui/platform-ops-consolidation/README.md` +- `docs/features/checked/web/offline-operations-ui.md` +- `docs/modules/ui/component-preservation-map/RESTORATION_PRIORITIES.md` diff --git a/src/Web/StellaOps.Web/src/app/core/navigation/navigation.config.ts b/src/Web/StellaOps.Web/src/app/core/navigation/navigation.config.ts index 9790d59ba..4295071f7 100644 --- a/src/Web/StellaOps.Web/src/app/core/navigation/navigation.config.ts +++ b/src/Web/StellaOps.Web/src/app/core/navigation/navigation.config.ts @@ -1,3 +1,4 @@ +import { OPERATIONS_PATHS } from '../../features/platform/ops/operations-paths'; import { NavGroup, NavigationConfig } from './navigation.types'; /** @@ -227,51 +228,51 @@ export const NAVIGATION_GROUPS: NavGroup[] = [ { id: 'pack-registry', label: 'Pack Registry', - route: '/ops/packs', + route: OPERATIONS_PATHS.packs, icon: 'package', tooltip: 'Browse TaskRunner packs, verify DSSE metadata, and run compatibility-checked installs/upgrades', }, { id: 'quotas', label: 'Quota Dashboard', - route: '/ops/quotas', + route: OPERATIONS_PATHS.quotas, icon: 'gauge', tooltip: 'License quota consumption and capacity planning', children: [ { id: 'quota-overview', label: 'Overview', - route: '/ops/quotas', + route: OPERATIONS_PATHS.quotas, tooltip: 'Quota consumption KPIs and trends', }, { id: 'quota-tenants', label: 'Tenant Usage', - route: '/ops/quotas/tenants', + route: `${OPERATIONS_PATHS.quotas}/tenants`, tooltip: 'Per-tenant quota consumption', }, { id: 'quota-throttle', label: 'Throttle Events', - route: '/ops/quotas/throttle', + route: `${OPERATIONS_PATHS.quotas}/throttle`, tooltip: 'Rate limit violations and recommendations', }, { id: 'quota-forecast', label: 'Forecast', - route: '/ops/quotas/forecast', + route: `${OPERATIONS_PATHS.quotas}/forecast`, tooltip: 'Quota exhaustion predictions', }, { id: 'quota-alerts', label: 'Alert Config', - route: '/ops/quotas/alerts', + route: `${OPERATIONS_PATHS.quotas}/alerts`, tooltip: 'Configure quota alert thresholds', }, { id: 'quota-reports', label: 'Reports', - route: '/ops/quotas/reports', + route: `${OPERATIONS_PATHS.quotas}/reports`, tooltip: 'Export quota reports', }, ], @@ -348,32 +349,32 @@ export const NAVIGATION_GROUPS: NavGroup[] = [ { id: 'feed-mirror', label: 'Feed Mirror & AirGap', - route: '/ops/feeds', + route: OPERATIONS_PATHS.feedsAirgap, icon: 'mirror', tooltip: 'Vulnerability feed mirroring, offline bundles, and version locks', children: [ { id: 'feed-dashboard', label: 'Dashboard', - route: '/ops/feeds', + route: OPERATIONS_PATHS.feedsAirgap, tooltip: 'Feed mirror dashboard and status', }, { id: 'airgap-import', label: 'Import Bundle', - route: '/ops/feeds/airgap/import', + route: `${OPERATIONS_PATHS.feedsAirgap}?tab=airgap-bundles&action=import`, tooltip: 'Import air-gapped bundles from external media', }, { id: 'airgap-export', label: 'Export Bundle', - route: '/ops/feeds/airgap/export', + route: `${OPERATIONS_PATHS.feedsAirgap}?tab=airgap-bundles&action=export`, tooltip: 'Create bundles for air-gapped deployment', }, { id: 'version-locks', label: 'Version Locks', - route: '/ops/feeds/version-locks', + route: `${OPERATIONS_PATHS.feedsAirgap}?tab=version-locks`, tooltip: 'Lock feed versions for reproducible scans', }, ], @@ -381,32 +382,32 @@ export const NAVIGATION_GROUPS: NavGroup[] = [ { id: 'offline-kit', label: 'Offline Kit', - route: '/ops/offline-kit', + route: OPERATIONS_PATHS.offlineKit, icon: 'offline', tooltip: 'Offline bundle management, verification, and JWKS', children: [ { id: 'offline-dashboard', label: 'Dashboard', - route: '/ops/offline-kit/dashboard', + route: `${OPERATIONS_PATHS.offlineKit}/dashboard`, tooltip: 'Offline mode status and overview', }, { id: 'offline-bundles', label: 'Bundles', - route: '/ops/offline-kit/bundles', + route: `${OPERATIONS_PATHS.offlineKit}/bundles`, tooltip: 'Manage offline bundles and assets', }, { id: 'offline-verify', label: 'Verification', - route: '/ops/offline-kit/verify', + route: `${OPERATIONS_PATHS.offlineKit}/verify`, tooltip: 'Verify audit bundles offline', }, { id: 'offline-jwks', label: 'JWKS', - route: '/ops/offline-kit/jwks', + route: `${OPERATIONS_PATHS.offlineKit}/jwks`, tooltip: 'Manage Authority JWKS for offline validation', }, ], diff --git a/src/Web/StellaOps.Web/src/app/features/offline-kit/components/bundle-management.component.ts b/src/Web/StellaOps.Web/src/app/features/offline-kit/components/bundle-management.component.ts index d1d5ab694..057f72212 100644 --- a/src/Web/StellaOps.Web/src/app/features/offline-kit/components/bundle-management.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/offline-kit/components/bundle-management.component.ts @@ -26,6 +26,9 @@ interface LoadedBundle {

Bundle Management

Load, verify, and manage offline bundles for air-gapped operation

+ @if (lastExportMessage()) { +

{{ lastExportMessage() }}

+ }
@@ -145,6 +148,12 @@ interface LoadedBundle { margin: 0; } + .status-note { + margin: 0.55rem 0 0; + font-size: 0.75rem; + color: var(--color-status-info-border); + } + .management-grid { display: grid; grid-template-columns: 1fr 1fr; @@ -361,6 +370,7 @@ export class BundleManagementComponent implements OnInit { readonly activeBundleId = signal(null); readonly activeManifest = this.offlineService.cachedManifest; readonly assetCategories = signal<{ name: string; icon: string; count: number; assets: string[] }[]>([]); + readonly lastExportMessage = signal(null); ngOnInit(): void { this.loadBundles(); @@ -390,6 +400,7 @@ export class BundleManagementComponent implements OnInit { ) ]); this.activeBundleId.set(bundle.id); + this.synchronizeManifestForBundle(bundle); } } @@ -424,10 +435,19 @@ export class BundleManagementComponent implements OnInit { }) ); this.activeBundleId.set(bundle.id); + this.synchronizeManifestForBundle(bundle); } exportBundle(bundle: LoadedBundle): void { - console.log('Exporting bundle:', bundle.id); + const exportPayload = { + exportedAt: new Date().toISOString(), + bundle, + activeBundleId: this.activeBundleId(), + manifest: bundle.id === this.activeBundleId() ? this.activeManifest() : this.buildManifestForBundle(bundle), + }; + + this.downloadJsonFile(`offline-bundle-${bundle.version}.json`, exportPayload); + this.lastExportMessage.set(`Exported bundle v${bundle.version} as a portable JSON summary.`); } removeBundle(bundle: LoadedBundle): void { @@ -500,4 +520,44 @@ export class BundleManagementComponent implements OnInit { } ]); } + + private synchronizeManifestForBundle(bundle: LoadedBundle): void { + this.offlineService.loadManifest(this.buildManifestForBundle(bundle)); + } + + private buildManifestForBundle(bundle: LoadedBundle): OfflineManifest { + const categories = this.assetCategories(); + const findAssets = (name: string, fallback: string[]) => + categories.find((category) => category.name === name)?.assets ?? fallback; + + return { + version: bundle.version, + createdAt: bundle.createdAt, + expiresAt: bundle.expiresAt, + signature: `sig:${bundle.id}:${bundle.version}`, + assets: { + ui: this.buildAssetRecord(findAssets('UI Assets', ['index.html', 'main.js'])), + api_contracts: this.buildAssetRecord(findAssets('API Contracts', ['policy.openapi.json'])), + authority: this.buildAssetRecord(findAssets('Authority', ['jwks.json'])), + feeds: this.buildAssetRecord(findAssets('Feed Data', ['advisory_snapshot.ndjson.gz'])), + }, + }; + } + + private buildAssetRecord(names: string[]): Record { + return names.reduce>((record, name, index) => { + record[name] = `sha256:${name.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}-${index}`; + return record; + }, {}); + } + + private downloadJsonFile(filename: string, payload: unknown): void { + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + URL.revokeObjectURL(url); + } } diff --git a/src/Web/StellaOps.Web/src/app/features/offline-kit/components/jwks-management.component.ts b/src/Web/StellaOps.Web/src/app/features/offline-kit/components/jwks-management.component.ts index cdedcbc3d..056431205 100644 --- a/src/Web/StellaOps.Web/src/app/features/offline-kit/components/jwks-management.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/offline-kit/components/jwks-management.component.ts @@ -1,8 +1,9 @@ // JWKS Management Component // Sprint 026: Offline Kit Integration -import { Component, ChangeDetectionStrategy, signal, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, OnInit, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { RouterLink } from '@angular/router'; interface JwkEntry { kid: string; @@ -26,13 +27,16 @@ interface TrustAnchor { @Component({ selector: 'app-jwks-management', - imports: [CommonModule], + imports: [CommonModule, RouterLink], changeDetection: ChangeDetectionStrategy.OnPush, template: `

JWKS & Trust Anchor Management

Manage Authority signing keys and trust anchors for offline token validation

+ @if (statusMessage()) { +

{{ statusMessage() }}

+ }
@@ -47,9 +51,9 @@ interface TrustAnchor { } Refresh - + + Open Trust & Signing +
@@ -116,9 +120,9 @@ interface TrustAnchor {

Trust Anchors

- + + Open Trust & Signing +
@@ -148,6 +152,20 @@ interface TrustAnchor {
}
+ @if (selectedAnchor(); as anchor) { +
+

{{ anchor.name }}

+

Fingerprint: {{ anchor.fingerprint }}

+

Validity: {{ formatDate(anchor.validFrom) }} - {{ formatDate(anchor.validTo) }}

+

Status: {{ anchor.status }}

+
+ Manage in Trust & Signing + +
+
+ } @@ -214,6 +232,12 @@ interface TrustAnchor { margin: 0; } + .status-note { + margin: 0.55rem 0 0; + font-size: 0.75rem; + color: var(--color-status-info-border); + } + .management-grid { display: grid; grid-template-columns: 1fr 1fr; @@ -455,6 +479,27 @@ interface TrustAnchor { gap: 0.5rem; } + .anchor-detail { + margin-top: 1rem; + padding: 0.9rem 1rem; + border: 1px solid var(--color-text-primary); + border-radius: var(--radius-lg); + background: rgba(15, 23, 42, 0.35); + display: grid; + gap: 0.35rem; + } + + .anchor-detail h4, + .anchor-detail p { + margin: 0; + } + + .anchor-detail__actions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + } + .validation-section { grid-column: 1 / -1; } @@ -607,6 +652,11 @@ export class JwksManagementComponent implements OnInit { readonly activeKeyCount = signal(3); readonly tokenInput = signal(''); readonly validationResult = signal<{ valid: boolean; message?: string; claims?: unknown } | null>(null); + readonly selectedAnchorId = signal(null); + readonly statusMessage = signal(null); + readonly selectedAnchor = computed( + () => this.trustAnchors().find((anchor) => anchor.id === this.selectedAnchorId()) ?? null + ); private refreshing = false; @@ -625,23 +675,23 @@ export class JwksManagementComponent implements OnInit { // Simulate refresh await new Promise(resolve => setTimeout(resolve, 1500)); this.jwksLastUpdated.set(new Date().toISOString().replace('T', ' ').slice(0, 16) + ' UTC'); + this.statusMessage.set('JWKS cache refreshed from the active offline trust source.'); this.refreshing = false; } - importJwks(): void { - console.log('Importing JWKS...'); - } - - importAnchor(): void { - console.log('Importing trust anchor...'); - } - viewAnchor(anchor: TrustAnchor): void { - console.log('Viewing anchor:', anchor.id); + this.selectedAnchorId.set(anchor.id); } exportAnchor(anchor: TrustAnchor): void { - console.log('Exporting anchor:', anchor.id); + const blob = new Blob([JSON.stringify(anchor, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `${anchor.name.replace(/[^a-z0-9.-]+/gi, '-').toLowerCase()}.json`; + link.click(); + URL.revokeObjectURL(url); + this.statusMessage.set(`Exported trust anchor ${anchor.name}.`); } onTokenInput(event: Event): void { @@ -738,5 +788,6 @@ export class JwksManagementComponent implements OnInit { status: 'active' } ]); + this.selectedAnchorId.set('anchor-001'); } } diff --git a/src/Web/StellaOps.Web/src/app/features/offline-kit/components/offline-dashboard.component.ts b/src/Web/StellaOps.Web/src/app/features/offline-kit/components/offline-dashboard.component.ts index 9474a0238..16baa7550 100644 --- a/src/Web/StellaOps.Web/src/app/features/offline-kit/components/offline-dashboard.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/offline-kit/components/offline-dashboard.component.ts @@ -3,6 +3,7 @@ import { Component, ChangeDetectionStrategy, inject, OnInit, signal } from '@angular/core'; +import { RouterLink } from '@angular/router'; import { OfflineModeService } from '../../../core/services/offline-mode.service'; import { BundleFreshnessWidgetComponent } from '../../../shared/components/bundle-freshness-widget.component'; import { OfflineAssetCategories } from '../../../core/api/offline-kit.models'; @@ -14,9 +15,17 @@ interface DashboardStats { offlineDuration: string; } +interface DashboardFeature { + id: string; + name: string; + icon: string; + available: boolean; + route: string | null; +} + @Component({ selector: 'app-offline-dashboard', - imports: [BundleFreshnessWidgetComponent], + imports: [BundleFreshnessWidgetComponent, RouterLink], changeDetection: ChangeDetectionStrategy.OnPush, template: `
@@ -93,16 +102,27 @@ interface DashboardStats {

Available Features

@for (feature of features(); track feature.id) { -
- -
- {{ feature.name }} - - {{ feature.available ? 'Available' : 'Requires Online' }} - + @if (feature.route && feature.available) { + + +
+ {{ feature.name }} + Open workflow +
+ +
+ } @else { +
+ +
+ {{ feature.name }} + + {{ feature.available ? 'Available' : 'Requires Online' }} + +
+
- -
+ } }
@@ -251,6 +271,16 @@ interface DashboardStats { border-radius: var(--radius-md); } + .feature-item--link { + color: inherit; + text-decoration: none; + border: 1px solid transparent; + } + + .feature-item--link:hover { + border-color: var(--color-border-primary); + } + .feature-item.disabled { opacity: 0.5; } @@ -364,7 +394,7 @@ export class OfflineDashboardComponent implements OnInit { offlineDuration: 'N/A' }); - readonly features = signal<{ id: string; name: string; icon: string; available: boolean }[]>([]); + readonly features = signal([]); readonly recentActivity = signal<{ id: string; message: string; time: string; icon: string }[]>([]); private retrying = false; @@ -410,14 +440,14 @@ export class OfflineDashboardComponent implements OnInit { private loadFeatures(): void { const isOffline = this.offlineService.isOffline(); this.features.set([ - { id: 'dashboard', name: 'Dashboard & KPIs', icon: '', available: true }, - { id: 'findings', name: 'View Findings', icon: '', available: true }, - { id: 'sbom', name: 'SBOM Viewer', icon: '', available: true }, - { id: 'policy', name: 'Policy Viewer', icon: '', available: true }, - { id: 'evidence', name: 'Evidence Verification', icon: '', available: true }, - { id: 'triage', name: 'Triage & VEX Creation', icon: '', available: !isOffline }, - { id: 'integrations', name: 'Manage Integrations', icon: '', available: !isOffline }, - { id: 'export', name: 'Export Audit Bundles', icon: '', available: !isOffline } + { id: 'dashboard', name: 'Dashboard & KPIs', icon: '', available: true, route: '/ops/operations/offline-kit/dashboard' }, + { id: 'findings', name: 'View Findings', icon: '', available: true, route: '/security/findings' }, + { id: 'sbom', name: 'SBOM Viewer', icon: '', available: true, route: '/security/sbom' }, + { id: 'policy', name: 'Policy Viewer', icon: '', available: true, route: '/ops/policy/overview' }, + { id: 'evidence', name: 'Evidence Verification', icon: '', available: true, route: '/evidence/verify-replay' }, + { id: 'triage', name: 'Triage & VEX Creation', icon: '', available: !isOffline, route: !isOffline ? '/triage/artifacts' : null }, + { id: 'integrations', name: 'Manage Integrations', icon: '', available: !isOffline, route: !isOffline ? '/ops/integrations' : null }, + { id: 'export', name: 'Export Audit Bundles', icon: '', available: !isOffline, route: !isOffline ? '/evidence/exports' : null } ]); } diff --git a/src/Web/StellaOps.Web/src/app/features/offline-kit/components/verification-center.component.ts b/src/Web/StellaOps.Web/src/app/features/offline-kit/components/verification-center.component.ts index 2cbe749a7..4ea40bfd6 100644 --- a/src/Web/StellaOps.Web/src/app/features/offline-kit/components/verification-center.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/offline-kit/components/verification-center.component.ts @@ -1,7 +1,7 @@ // Verification Center Component // Sprint 026: Offline Kit Integration -import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core'; import { OfflineVerificationComponent } from '../../../shared/components/offline-verification.component'; import { OfflineVerificationResult } from '../../../core/api/offline-kit.models'; @@ -24,6 +24,9 @@ interface VerificationHistory {

Offline Verification Center

Verify audit bundles and evidence chains without network access

+ @if (exportMessage()) { +

{{ exportMessage() }}

+ }
@@ -39,7 +42,7 @@ interface VerificationHistory { @if (history().length > 0) {
@for (item of history(); track item.id) { -
+
@if (item.valid) {} @else {}
@@ -56,6 +59,20 @@ interface VerificationHistory {
}
+ @if (selectedHistory(); as selected) { +
+

{{ selected.bundleName }}

+

Verified {{ formatTime(selected.verifiedAt) }}

+

+ Chain status: + {{ selected.chainItemsValid }}/{{ selected.chainItemsTotal }} + items valid +

+ +
+ } } @else {

No verification history. Upload a bundle to verify.

@@ -107,6 +124,12 @@ interface VerificationHistory { margin: 0; } + .status-note { + margin: 0.55rem 0 0; + font-size: 0.75rem; + color: var(--color-status-info-border); + } + .center-grid { display: grid; grid-template-columns: 2fr 1fr; @@ -147,6 +170,10 @@ interface VerificationHistory { border-left-color: var(--color-status-success-border); } + .history-item.selected { + outline: 1px solid var(--color-brand-primary); + } + .history-status { width: 32px; height: 32px; @@ -232,6 +259,21 @@ interface VerificationHistory { color: var(--color-text-secondary); } + .detail-panel { + margin-top: 1rem; + padding: 0.9rem 1rem; + border: 1px solid var(--color-text-primary); + border-radius: var(--radius-lg); + background: rgba(15, 23, 42, 0.35); + display: grid; + gap: 0.35rem; + } + + .detail-panel h4, + .detail-panel p { + margin: 0; + } + .btn--ghost { background: transparent; border: 1px solid var(--color-text-primary); @@ -266,6 +308,11 @@ export class VerificationCenterComponent { chainItemsTotal: 6 } ]); + readonly selectedHistoryId = signal('1'); + readonly exportMessage = signal(null); + readonly selectedHistory = computed(() => + this.history().find((item) => item.id === this.selectedHistoryId()) ?? null + ); onVerificationComplete(result: OfflineVerificationResult): void { const newEntry: VerificationHistory = { @@ -278,6 +325,7 @@ export class VerificationCenterComponent { }; this.history.update(h => [newEntry, ...h].slice(0, 10)); + this.selectedHistoryId.set(newEntry.id); } formatTime(timestamp: string): string { @@ -287,20 +335,55 @@ export class VerificationCenterComponent { } viewDetails(item: VerificationHistory): void { - console.log('Viewing details for:', item.id); + this.selectedHistoryId.set(item.id); } verifyLastBundle(): void { - console.log('Re-verifying last bundle...'); + const lastBundle = this.history()[0]; + if (!lastBundle) { + return; + } + + const reverified: VerificationHistory = { + ...lastBundle, + id: `${lastBundle.id}-recheck-${Date.now()}`, + verifiedAt: new Date().toISOString(), + }; + + this.history.update((items) => [reverified, ...items].slice(0, 10)); + this.selectedHistoryId.set(reverified.id); } - exportReport(): void { - console.log('Exporting verification report...'); + exportReport(item?: VerificationHistory): void { + const report = item ?? this.selectedHistory() ?? this.history()[0]; + if (!report) { + return; + } + + const payload = { + generatedAt: new Date().toISOString(), + report, + summary: { + valid: report.valid, + verifiedItems: report.chainItemsValid, + totalItems: report.chainItemsTotal, + }, + }; + + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `${report.bundleName.replace(/[^a-z0-9.-]+/gi, '-').toLowerCase()}-report.json`; + link.click(); + URL.revokeObjectURL(url); + this.exportMessage.set(`Exported verification report for ${report.bundleName}.`); } clearHistory(): void { if (confirm('Clear all verification history?')) { this.history.set([]); + this.selectedHistoryId.set(null); } } } diff --git a/src/Web/StellaOps.Web/src/app/features/offline-kit/offline-kit.component.ts b/src/Web/StellaOps.Web/src/app/features/offline-kit/offline-kit.component.ts index 50f737fc8..cd2073a32 100644 --- a/src/Web/StellaOps.Web/src/app/features/offline-kit/offline-kit.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/offline-kit/offline-kit.component.ts @@ -16,6 +16,12 @@ import { OfflineModeService } from '../../core/services/offline-mode.service';

Offline Kit Management

Manage offline bundles, verify audit packages, and configure air-gap operation

+
@@ -91,6 +97,27 @@ import { OfflineModeService } from '../../core/services/offline-mode.service'; margin: 0; } + .page-shortcuts { + display: flex; + gap: 0.45rem; + flex-wrap: wrap; + margin-top: 0.85rem; + } + + .page-shortcuts a { + border: 1px solid var(--color-text-primary); + border-radius: var(--radius-full); + color: var(--color-text-muted); + font-size: 0.72rem; + padding: 0.18rem 0.55rem; + text-decoration: none; + } + + .page-shortcuts a:hover { + color: var(--color-border-primary); + border-color: var(--color-border-primary); + } + .connection-status { display: flex; align-items: center; diff --git a/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-feeds-airgap-page.component.ts b/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-feeds-airgap-page.component.ts index fe75ebe38..7c6b0bf62 100644 --- a/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-feeds-airgap-page.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/platform/ops/platform-feeds-airgap-page.component.ts @@ -1,8 +1,14 @@ import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, inject, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute, RouterLink } from '@angular/router'; +import { + OPERATIONS_INTEGRATION_PATHS, + OPERATIONS_PATHS, + dataIntegrityPath, +} from './operations-paths'; type FeedsOfflineTab = 'feed-mirrors' | 'airgap-bundles' | 'version-locks'; +type FeedsAirgapAction = 'import' | 'export' | null; @Component({ selector: 'app-platform-feeds-airgap-page', @@ -20,22 +26,37 @@ type FeedsOfflineTab = 'feed-mirrors' | 'airgap-bundles' | 'version-locks';

-
@@ -51,7 +72,7 @@ type FeedsOfflineTab = 'feed-mirrors' | 'airgap-bundles' | 'version-locks'; Impact: BLOCKING Mode: last-known-good snapshot (read-only) corr-feed-8841 - + Open incident
@@ -88,10 +109,22 @@ type FeedsOfflineTab = 'feed-mirrors' | 'airgap-bundles' | 'version-locks'; } @if (tab() === 'airgap-bundles') { + @if (airgapAction()) { +
+ @if (airgapAction() === 'import') { + Import workflow selected. + Use Offline Kit Bundles to load a signed airgap bundle into the active site context. + } @else { + Export workflow selected. + Open Evidence Exports to prepare a portable bundle, then move it through Offline Kit. + } +
+ }

Offline import/export workflows and bundle verification controls.

} @if (tab() === 'version-locks') { @@ -134,8 +167,7 @@ type FeedsOfflineTab = 'feed-mirrors' | 'airgap-bundles' | 'version-locks'; flex-wrap: wrap; } - .feeds-offline__actions a, - .feeds-offline__actions button { + .feeds-offline__actions a { border: 1px solid var(--color-border-primary); border-radius: var(--radius-md); background: var(--color-surface-primary); @@ -152,7 +184,7 @@ type FeedsOfflineTab = 'feed-mirrors' | 'airgap-bundles' | 'version-locks'; flex-wrap: wrap; } - .tabs button { + .tabs a { border: 1px solid var(--color-border-primary); border-radius: var(--radius-full); background: var(--color-surface-primary); @@ -160,9 +192,10 @@ type FeedsOfflineTab = 'feed-mirrors' | 'airgap-bundles' | 'version-locks'; padding: 0.15rem 0.6rem; font-size: 0.72rem; cursor: pointer; + text-decoration: none; } - .tabs button.active { + .tabs a.active { border-color: var(--color-brand-primary); color: var(--color-brand-primary); } @@ -204,14 +237,14 @@ type FeedsOfflineTab = 'feed-mirrors' | 'airgap-bundles' | 'version-locks'; padding: 0.05rem 0.3rem; } - .status-banner button { + .status-banner a { border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); background: var(--color-surface-primary); color: var(--color-brand-primary); - cursor: pointer; font-size: 0.67rem; padding: 0.08rem 0.34rem; + text-decoration: none; } .panel { @@ -250,6 +283,17 @@ type FeedsOfflineTab = 'feed-mirrors' | 'airgap-bundles' | 'version-locks'; color: var(--color-text-secondary); } + .action-banner { + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-md); + background: var(--color-surface-secondary); + color: var(--color-text-primary); + padding: 0.5rem 0.6rem; + display: grid; + gap: 0.2rem; + font-size: 0.73rem; + } + .panel__links { display: flex; gap: 0.5rem; @@ -267,7 +311,11 @@ export class PlatformFeedsAirgapPageComponent implements OnInit { private readonly route = inject(ActivatedRoute); private readonly destroyRef = inject(DestroyRef); + readonly OPERATIONS_PATHS = OPERATIONS_PATHS; + readonly OPERATIONS_INTEGRATION_PATHS = OPERATIONS_INTEGRATION_PATHS; + readonly feedsFreshnessPath = dataIntegrityPath('feeds-freshness'); readonly tab = signal('feed-mirrors'); + readonly airgapAction = signal(null); ngOnInit(): void { this.route.queryParamMap @@ -281,6 +329,14 @@ export class PlatformFeedsAirgapPageComponent implements OnInit { ) { this.tab.set(requested); } + + const requestedAction = params.get('action'); + if (requestedAction === 'import' || requestedAction === 'export') { + this.airgapAction.set(requestedAction); + return; + } + + this.airgapAction.set(null); }); } } diff --git a/src/Web/StellaOps.Web/src/app/layout/context-chips/feed-snapshot-chip.component.ts b/src/Web/StellaOps.Web/src/app/layout/context-chips/feed-snapshot-chip.component.ts index 055ec51c2..72b150cac 100644 --- a/src/Web/StellaOps.Web/src/app/layout/context-chips/feed-snapshot-chip.component.ts +++ b/src/Web/StellaOps.Web/src/app/layout/context-chips/feed-snapshot-chip.component.ts @@ -22,7 +22,7 @@ import { OfflineModeService } from '../../core/services/offline-mode.service'; class="chip" [class.chip--fresh]="!isStale()" [class.chip--stale]="isStale()" - routerLink="/platform/ops/feeds-airgap" + routerLink="/ops/operations/feeds-airgap" [attr.title]="tooltip()" >