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

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import '../data/error_presentation.dart';
import '../data/hub.dart';
import '../data/workspace.dart';
import '../l10n/app_localizations.dart';
import '../theme/theme.dart';
import '../theme/tokens.dart';
@ -38,6 +39,8 @@ class _AuditPageState extends State<AuditPage> {
@override
void initState() {
super.initState();
Workspace.instance.addListener(_onWorkspaceChanged);
Workspace.instance.ensureLoaded();
_refresh();
_poller = Timer.periodic(const Duration(seconds: 2), (_) => _refresh());
_subscribeLive();
@ -45,15 +48,26 @@ class _AuditPageState extends State<AuditPage> {
@override
void dispose() {
Workspace.instance.removeListener(_onWorkspaceChanged);
_poller?.cancel();
_nudgeDebounce?.cancel();
_eventSub?.cancel();
super.dispose();
}
/// The workspace filter is applied hub-side, so both the list
/// query and the live stream have to be re-established.
void _onWorkspaceChanged() {
if (!mounted) return;
_refresh();
_subscribeLive();
}
void _subscribeLive() {
_eventSub?.cancel();
_eventSub = HubService.instance.streamEvents(backfill: 0).listen(
_eventSub = HubService.instance
.streamEvents(backfill: 0, project: Workspace.instance.activeSlug)
.listen(
(_) => _nudge(),
// On a persistent error the 2 s poll keeps the page fresh, so we
// do nothing. On a clean close (hub restart / stream end) try to
@ -108,7 +122,10 @@ class _AuditPageState extends State<AuditPage> {
Future<void> _refresh() async {
try {
final events = await HubService.instance.recentEvents(limit: 100);
final events = await HubService.instance.recentEvents(
limit: 100,
project: Workspace.instance.activeSlug,
);
if (!mounted) return;
setState(() {
_events = events;
@ -136,6 +153,8 @@ class _AuditPageState extends State<AuditPage> {
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.auditTitle),
actions: [
const ChainWorkspaceSwitcher(),
const SizedBox(width: ChainSpace.md),
Padding(
padding: const EdgeInsets.only(right: ChainSpace.lg),
child: _FilterChips(