feat: workspace switcher, per-project filters and run stamping (stage 1)
Some checks failed
Security / Security check (push) Failing after 2s

Multi-project stage 1 against the shared hub (platform design
docs/architecture/projects.md, § Studio):

- ChainWorkspaceSwitcher in the Audit + Approvals AppBars: lists the
  registry (colour dot per project, shield for protected, honesty
  tooltip), 'All projects' stays reachable — a filter, not a jail.
  Selection is persisted and shared via the Workspace notifier.
- Audit page: list query AND live stream re-scoped hub-side on switch.
- Approvals page: pending + history scoped; the sidebar badge counts
  the active workspace's pending approvals.
- Flow runs are stamped with the active workspace; a flow file
  carrying its own project: keeps it (file wins, CLI semantics).
- Data layer: listProjects/ProjectRef; project fields on AuditEvent,
  PendingApproval(+Record), SavedFlow; project params through
  HubService. l10n DE+EN. Widget tests for the switcher contract.

Visual verification (light+dark screenshots) still pending — the
shared desktop was in active use; code paths are covered by
flutter test (26 green).

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-12 13:49:51 +02:00
parent 7a38cd58aa
commit 2592a23cc0
14 changed files with 554 additions and 9 deletions

View file

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import '../data/hub.dart';
import '../data/workspace.dart';
import '../l10n/app_localizations.dart';
import '../theme/theme.dart';
import '../theme/tokens.dart';
@ -63,23 +64,30 @@ class _ApprovalsPageState extends State<ApprovalsPage>
void initState() {
super.initState();
_tab = TabController(length: 2, vsync: this);
Workspace.instance.addListener(_refresh);
Workspace.instance.ensureLoaded();
_refresh();
}
@override
void dispose() {
Workspace.instance.removeListener(_refresh);
_tab.dispose();
super.dispose();
}
void _refresh() {
if (!mounted) return;
final project = Workspace.instance.activeSlug;
setState(() {
_pendingFuture = HubService.instance.listApprovalsRecords(
statuses: const ['pending'],
project: project,
);
_historyFuture = HubService.instance.listApprovalsRecords(
statuses: const ['approved', 'rejected', 'expired'],
limit: 200,
project: project,
);
});
}
@ -256,6 +264,8 @@ class _ApprovalsPageState extends State<ApprovalsPage>
],
),
actions: [
const ChainWorkspaceSwitcher(),
const SizedBox(width: ChainSpace.md),
IconButton(
icon: const Icon(Icons.help_outline, size: 18),
tooltip: AppLocalizations.of(context)!.helpTooltip,