feat(studio): Approvals page gains History tab

Pending tab keeps the existing card layout for actionable
approvals. New History tab shows decided rows (approved /
rejected / expired) as compact one-liners, color-coded by
status with reviewer + decided-at metadata. Click expands to
the full record (prompt, payload preview, reason, ids).

Closes the visibility gap from the recent fix: rejected
approvals were correctly written to the hash-chained audit
log but invisible to operators on the Approvals page itself.
HubService gains `listApprovalsRecords(statuses:)` returning
the decided-side fields the Studio wire previously dropped.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-07 17:33:00 +02:00
parent d82738e17e
commit d9aabfd00a
2 changed files with 448 additions and 53 deletions

View file

@ -461,6 +461,42 @@ class HubService {
.toList();
}
/// List approvals filtered by status. Empty [statuses] returns
/// every row (pending + decided + expired). Returned records
/// carry the decided-side fields so the History tab can render
/// "rejected by alice@studio at … because …".
Future<List<ApprovalRecord>> listApprovalsRecords({
List<String> statuses = const [],
int limit = 200,
}) async {
final entries = await _client.listApprovals(
statuses: statuses,
limit: limit,
);
return entries
.map(
(e) => ApprovalRecord(
id: e.approvalId,
flowName: e.flowName,
stepId: e.stepId,
prompt: e.prompt,
payloadPreview:
e.payloadPreview.isEmpty ? null : e.payloadPreview,
createdAt: DateTime.tryParse(e.createdAt) ?? DateTime.now(),
expiresAt: e.expiresAt.isEmpty
? null
: DateTime.tryParse(e.expiresAt),
status: e.status,
decidedAt: e.decidedAt.isEmpty
? null
: DateTime.tryParse(e.decidedAt),
decidedBy: e.decidedBy,
reason: e.reason,
),
)
.toList();
}
Future<void> approve(String id, String reviewer) =>
_client.approve(approvalId: id, reviewer: reviewer);
@ -695,6 +731,41 @@ class PendingApproval {
});
}
/// Full approval record including decided-side fields. Used by
/// the History tab so operators can review what they (or the
/// system) decided.
class ApprovalRecord {
final String id;
final String flowName;
final String stepId;
final String prompt;
final String? payloadPreview;
final DateTime createdAt;
final DateTime? expiresAt;
/// One of: pending / approved / rejected / expired.
final String status;
final DateTime? decidedAt;
/// Reviewer handle (e.g. "alice@studio") or "system" for
/// auto-expired approvals. Empty until decided.
final String decidedBy;
/// Reject reason empty for approve / pending / expired.
final String reason;
const ApprovalRecord({
required this.id,
required this.flowName,
required this.stepId,
required this.prompt,
required this.payloadPreview,
required this.createdAt,
required this.expiresAt,
required this.status,
required this.decidedAt,
required this.decidedBy,
required this.reason,
});
}
class SystemAiStatus {
final bool enabled;
final String provider;