feat: F∆I Studio MVP scaffold (Tier-2 desktop GUI)

Initial scaffold for the F∆I Platform Tier-2 generic GUI client.
Flutter Desktop (macOS, Linux, Windows). Three MVP pages with
mock data, sharing one navigation shell:

- Modules — installed modules with capabilities, declared
  permissions and required services.
- Audit — event-stream view with type filter and tone-coded
  rows (started / completed / failed).
- Approvals — pending system.approval@^0 reviews with prompt,
  payload preview, and approve/reject buttons.

Live gRPC connection arrives in the next iteration via
fai_dart_sdk (sibling repo, currently a typed stub).

Future Forgejo path: fai/studio. Local layout matches existing
fai_platform/ convention.

Background: see docs/architecture/client.md in the platform
repo. The tier-2 client was previously called "Stage" — renamed
to "Studio" on 2026-05-05 to avoid confusion with
"staging environment".

flutter analyze: clean. flutter test: 2/2.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-05 14:19:39 +02:00
commit a173eec1d0
68 changed files with 4277 additions and 0 deletions

165
lib/data/mock.dart Normal file
View file

@ -0,0 +1,165 @@
// Mock data for the MVP. Replaced by live gRPC calls via
// fai_dart_sdk in the next iteration.
class ModuleSummary {
final String name;
final String version;
final List<String> capabilities;
final List<String> permissions;
final List<String> requiresServices;
const ModuleSummary({
required this.name,
required this.version,
required this.capabilities,
required this.permissions,
required this.requiresServices,
});
}
class AuditEvent {
final String eventId;
final DateTime timestamp;
final String type;
final String? flowName;
final String? stepId;
final String? moduleName;
final int? durationMs;
final String? error;
const AuditEvent({
required this.eventId,
required this.timestamp,
required this.type,
this.flowName,
this.stepId,
this.moduleName,
this.durationMs,
this.error,
});
}
class PendingApproval {
final String id;
final String flowName;
final String stepId;
final String prompt;
final String? showPreview;
final DateTime createdAt;
final DateTime? expiresAt;
const PendingApproval({
required this.id,
required this.flowName,
required this.stepId,
required this.prompt,
this.showPreview,
required this.createdAt,
this.expiresAt,
});
}
const mockModules = <ModuleSummary>[
ModuleSummary(
name: 'echo',
version: '0.1.0',
capabilities: ['debug.echo@0.1.0'],
permissions: [],
requiresServices: [],
),
ModuleSummary(
name: 'text-extract',
version: '0.1.1',
capabilities: ['text.extract@0.1.0'],
permissions: [],
requiresServices: [],
),
ModuleSummary(
name: 'llm-chat',
version: '0.1.0',
capabilities: ['llm.chat@0.1.0'],
permissions: [
'net: localhost',
'net: 127.0.0.1',
'net: api.openai.com',
'net: api.anthropic.com',
],
requiresServices: ['ollama'],
),
ModuleSummary(
name: 'text-translate',
version: '0.1.0',
capabilities: ['text.translate@0.1.0'],
permissions: [
'net: localhost',
'net: 127.0.0.1',
],
requiresServices: ['ollama'],
),
ModuleSummary(
name: 'text-summarize',
version: '0.1.0',
capabilities: ['text.summarize@0.1.0'],
permissions: [
'net: localhost',
'net: 127.0.0.1',
],
requiresServices: ['ollama'],
),
];
final mockEvents = <AuditEvent>[
AuditEvent(
eventId: '01926f8a-7c34-7b2c-9e4f-a8d3c1f9e2b4',
timestamp: DateTime.now().subtract(const Duration(seconds: 2)),
type: 'flow.completed',
flowName: 'extract-translate-summarize',
durationMs: 8421,
),
AuditEvent(
eventId: '01926f8a-7b00-7000-aaaa-bbbbcccc0042',
timestamp: DateTime.now().subtract(const Duration(seconds: 3)),
type: 'step.completed',
flowName: 'extract-translate-summarize',
stepId: 'summarize',
moduleName: 'text-summarize',
durationMs: 3204,
),
AuditEvent(
eventId: '01926f8a-7a90-7000-aaaa-bbbbcccc0041',
timestamp: DateTime.now().subtract(const Duration(seconds: 9)),
type: 'step.completed',
flowName: 'extract-translate-summarize',
stepId: 'review',
moduleName: 'system.approval',
durationMs: 5102,
),
AuditEvent(
eventId: '01926f8a-7a00-7000-aaaa-bbbbcccc0040',
timestamp: DateTime.now().subtract(const Duration(seconds: 14)),
type: 'step.completed',
flowName: 'extract-translate-summarize',
stepId: 'translate',
moduleName: 'text-translate',
durationMs: 4815,
),
AuditEvent(
eventId: '01926f8a-7800-7000-aaaa-bbbbcccc003f',
timestamp: DateTime.now().subtract(const Duration(seconds: 19)),
type: 'flow.started',
flowName: 'extract-translate-summarize',
),
];
final mockApprovals = <PendingApproval>[
PendingApproval(
id: '01926f8a-9000-7b2c-9e4f-aaaa00000001',
flowName: 'extract-translate-summarize',
stepId: 'review',
prompt: 'Review the German→English translation before summarisation.',
showPreview:
'{ "translated": "The quarterly report shows..." }',
createdAt: DateTime.now().subtract(const Duration(minutes: 2)),
expiresAt: DateTime.now().add(const Duration(minutes: 28)),
),
];