feat(studio): operator-mode pass — Store + audit detail + channel switcher (v0.10.0)
Turns Studio from read-only dashboard into operator console. Pieces: * **New Store page** (sidebar destination #3): browses the hub's bundled store-index with query + category + status filters. Each card has an Install button that prompts for the `.fai` bundle source (URL or local path), ships it to HubAdmin.InstallModule, shows success / error in a progress dialog, refreshes the list. * **Audit drill-down**: every event row is now tappable; opens a modal with all LoggedEvent fields including the new `detail` JSON pretty-printed in a code block. KRITIS forensic story: every audit row → full structured detail in two clicks. * **Approvals payload preview**: shipped already; now pretty-prints JSON when the preview parses as such, plus a clearer `PAYLOAD PREVIEW` label. Reviewer identity defaults to `$USER@studio` instead of the hard-coded `studio-mvp` so the audit trail records who acted. * **Channel switcher in Settings dialog**: the dialog now pulls HubAdmin.ChannelStatus and renders one row per channel (local / dev / beta / production) with port, running indicator, active marker. A "Connect" button on a running channel sets Studio's endpoint to that channel's port and reconnects in one click. Cmd+1..6 keyboard shortcuts updated for the six destinations. Widget test asserts every destination renders. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
18db6ff164
commit
060c8003d5
9 changed files with 990 additions and 12 deletions
|
|
@ -106,6 +106,74 @@ class HubService {
|
|||
);
|
||||
}
|
||||
|
||||
/// Active channel + per-channel daemon status snapshot.
|
||||
Future<ChannelStatusSnapshot> channelStatus() async {
|
||||
final r = await _client.channelStatus();
|
||||
return ChannelStatusSnapshot(
|
||||
active: r.active,
|
||||
channels: r.channels
|
||||
.map(
|
||||
(c) => ChannelInfo(
|
||||
name: c.name,
|
||||
port: c.port,
|
||||
running: c.running,
|
||||
endpoint: c.endpoint,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Browse the bundled store index. All filters optional;
|
||||
/// empty query returns the first [limit] entries.
|
||||
Future<List<StoreItem>> searchStore({
|
||||
String query = '',
|
||||
String category = '',
|
||||
String tag = '',
|
||||
String status = '',
|
||||
int limit = 50,
|
||||
}) async {
|
||||
final entries = await _client.searchStore(
|
||||
query: query,
|
||||
category: category,
|
||||
tag: tag,
|
||||
status: status,
|
||||
limit: limit,
|
||||
);
|
||||
return entries
|
||||
.map(
|
||||
(e) => StoreItem(
|
||||
name: e.name,
|
||||
taglineEn: e.taglineEn,
|
||||
descriptionEn: e.descriptionEn,
|
||||
category: e.category,
|
||||
tags: e.tags,
|
||||
requiresCapabilities: e.requiresCapabilities,
|
||||
requiresServices: e.requiresServices,
|
||||
license: e.license,
|
||||
repository: e.repository,
|
||||
bestVersion: e.bestVersion,
|
||||
status: e.status,
|
||||
installed: e.installed,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Install a module from a `.fai` bundle (URL or local path).
|
||||
/// Returns the installed module's name + version on success;
|
||||
/// throws on hub error (signature mismatch, sha256 fail, etc.).
|
||||
Future<({String name, String version})> installModule({
|
||||
required String source,
|
||||
String expectedSha256 = '',
|
||||
}) async {
|
||||
final r = await _client.installModule(
|
||||
source: source,
|
||||
expectedSha256: expectedSha256,
|
||||
);
|
||||
return (name: r.name, version: r.version);
|
||||
}
|
||||
|
||||
/// Saved flows known to the hub.
|
||||
Future<List<SavedFlow>> listFlows() async {
|
||||
final flows = await _client.listFlows();
|
||||
|
|
@ -163,8 +231,15 @@ class HubService {
|
|||
flowName: e.flowName.isEmpty ? null : e.flowName,
|
||||
stepId: e.stepId.isEmpty ? null : e.stepId,
|
||||
moduleName: e.moduleName.isEmpty ? null : e.moduleName,
|
||||
moduleVersion:
|
||||
e.moduleVersion.isEmpty ? null : e.moduleVersion,
|
||||
invocationId:
|
||||
e.invocationId.isEmpty ? null : e.invocationId,
|
||||
flowExecution:
|
||||
e.flowExecution.isEmpty ? null : e.flowExecution,
|
||||
durationMs: e.durationMs == 0 ? null : e.durationMs.toInt(),
|
||||
error: e.error.isEmpty ? null : e.error,
|
||||
detail: e.detail.isEmpty ? null : e.detail,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
|
@ -378,8 +453,14 @@ class AuditEvent {
|
|||
final String? flowName;
|
||||
final String? stepId;
|
||||
final String? moduleName;
|
||||
final String? moduleVersion;
|
||||
final String? invocationId;
|
||||
final String? flowExecution;
|
||||
final int? durationMs;
|
||||
final String? error;
|
||||
/// Free-form JSON-string carrying event-type-specific fields.
|
||||
/// Surfaced verbatim in the audit drill-down dialog.
|
||||
final String? detail;
|
||||
|
||||
const AuditEvent({
|
||||
required this.eventId,
|
||||
|
|
@ -388,8 +469,12 @@ class AuditEvent {
|
|||
this.flowName,
|
||||
this.stepId,
|
||||
this.moduleName,
|
||||
this.moduleVersion,
|
||||
this.invocationId,
|
||||
this.flowExecution,
|
||||
this.durationMs,
|
||||
this.error,
|
||||
this.detail,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -412,3 +497,59 @@ class PendingApproval {
|
|||
this.expiresAt,
|
||||
});
|
||||
}
|
||||
|
||||
class ChannelInfo {
|
||||
final String name;
|
||||
final int port;
|
||||
final bool running;
|
||||
final String endpoint;
|
||||
|
||||
const ChannelInfo({
|
||||
required this.name,
|
||||
required this.port,
|
||||
required this.running,
|
||||
required this.endpoint,
|
||||
});
|
||||
}
|
||||
|
||||
class ChannelStatusSnapshot {
|
||||
final String active;
|
||||
final List<ChannelInfo> channels;
|
||||
|
||||
const ChannelStatusSnapshot({
|
||||
required this.active,
|
||||
required this.channels,
|
||||
});
|
||||
}
|
||||
|
||||
/// One row from the store-index search.
|
||||
class StoreItem {
|
||||
final String name;
|
||||
final String taglineEn;
|
||||
final String descriptionEn;
|
||||
final String category;
|
||||
final List<String> tags;
|
||||
final List<String> requiresCapabilities;
|
||||
final List<String> requiresServices;
|
||||
final String license;
|
||||
final String repository;
|
||||
final String bestVersion;
|
||||
/// "published", "alpha", "planned", or empty when unknown.
|
||||
final String status;
|
||||
final bool installed;
|
||||
|
||||
const StoreItem({
|
||||
required this.name,
|
||||
required this.taglineEn,
|
||||
required this.descriptionEn,
|
||||
required this.category,
|
||||
required this.tags,
|
||||
required this.requiresCapabilities,
|
||||
required this.requiresServices,
|
||||
required this.license,
|
||||
required this.repository,
|
||||
required this.bestVersion,
|
||||
required this.status,
|
||||
required this.installed,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue