fix(approvals,l10n,theme): usertest findings — localized prompts, no-data confirm, .chain log path, honest wording, AA contrast
Some checks failed
Security / Security check (push) Failing after 2s
Some checks failed
Security / Security check (push) Failing after 2s
- approvals: empty/legacy hub prompts render the localized fallback; approving without show: data asks for conscious confirmation first - chain_log: write to ~/.chain/logs/studio-errors.log (was .fai), one-time best-effort migration of the legacy file + rotation sibling - l10n: 'manipulationssicher' -> 'manipulationserkennend', neutral WORM-1 blurb, doctor pill 'Integritätskette v1', federation hint says the CA authenticates the first connect (DE+EN) - theme: muted text token now >=4.5:1 on canvas, cards and elevated dark surfaces (was 3.7:1 on cards) Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
0fda2600ad
commit
bb606a8b23
10 changed files with 167 additions and 26 deletions
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -5,6 +5,31 @@ version + `kStudioVersion` in `lib/main.dart` stay in lockstep.
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Fixed (usertest 2026-07-10 findings)
|
||||
|
||||
- **Approval prompts localize.** An approval whose flow step gave no
|
||||
`prompt:` (and legacy rows carrying the hub's old baked-in English
|
||||
sentence) now renders the localized fallback "Freigabe für diesen
|
||||
Schritt erforderlich" / "Approval required for this step" — no more
|
||||
English inside the German approvals UI.
|
||||
- **Approving without review data asks first.** When the approval step
|
||||
attached no `show:` payload, "Freigeben" opens a calm confirmation
|
||||
("Ohne Prüfdaten freigeben?") explaining that the flow deliberately
|
||||
attached no data, with an explicit "Trotzdem freigeben".
|
||||
- **Studio error log moved to `~/.chain/logs/`.** Writes went to the
|
||||
pre-rename `~/.fai/logs/studio-errors.log` while the Doctor page and
|
||||
`chain doctor` read `~/.chain/…`. Studio now writes to `.chain` and
|
||||
migrates the old file (+ rotation sibling) over once.
|
||||
- **Honest audit wording (legal review).** DE strings no longer claim
|
||||
"manipulationssicher" — the audit log is *manipulationserkennend*
|
||||
(tamper-evident); "warum WORM-1 für KRITIS reicht" became a neutral
|
||||
what-it-does-and-does-not sentence (EN too); the Doctor chain pill
|
||||
says "Integritätskette v1" instead of "WORM-1"; the federation
|
||||
enrollment hint says the CA *authenticates* the first connect.
|
||||
- **WCAG-AA secondary text on dark.** De-emphasised text was 3.7:1 on
|
||||
cards; the muted token is now ≥ 4.5:1 against canvas, cards and
|
||||
elevated surfaces.
|
||||
|
||||
### Added
|
||||
|
||||
- **Live audit feed.** The Audit page subscribes to `streamEvents` and
|
||||
|
|
|
|||
|
|
@ -51,7 +51,31 @@ class ChainLog {
|
|||
Platform.environment['HOME'] ??
|
||||
Platform.environment['USERPROFILE'] ??
|
||||
'.';
|
||||
return p.join(home, '.fai', 'logs', 'studio-errors.log');
|
||||
final path = p.join(home, '.chain', 'logs', 'studio-errors.log');
|
||||
_migrateLegacyLog(home, path);
|
||||
return path;
|
||||
}
|
||||
|
||||
// Pre-rename installs wrote to `~/.fai/logs/`. Move that file (and
|
||||
// its rotation sibling) over once so the error trail survives the
|
||||
// rename; never overwrite an existing new-path file. Best-effort
|
||||
// and cheap enough to run per access (two stat calls after the
|
||||
// first migration).
|
||||
static void _migrateLegacyLog(String home, String newPath) {
|
||||
try {
|
||||
for (final suffix in const ['', '.1']) {
|
||||
final legacy = File(
|
||||
p.join(home, '.fai', 'logs', 'studio-errors.log$suffix'),
|
||||
);
|
||||
final target = File('$newPath$suffix');
|
||||
if (legacy.existsSync() && !target.existsSync()) {
|
||||
target.parent.createSync(recursive: true);
|
||||
legacy.renameSync(target.path);
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Best-effort: a failed migration must not break logging.
|
||||
}
|
||||
}
|
||||
|
||||
/// Absolute path of the studio-errors log. Public so the
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
"welcomeTrustHeader": "TRUST-POSTURE",
|
||||
"welcomeTrustSandboxTitle": "Sandbox von Anfang an",
|
||||
"welcomeTrustSandboxBody": "Jedes Modul bringt eine explizite Berechtigungsliste mit — Netzwerk-Endpunkte, Dateien, Umgebungsvariablen. Der Hub setzt sie durch; ohne Operator-Freigabe verlässt nichts die Sandbox.",
|
||||
"welcomeTrustAuditTitle": "Manipulationssicheres Audit-Log",
|
||||
"welcomeTrustAuditTitle": "Manipulationserkennendes Audit-Log",
|
||||
"welcomeTrustAuditBody": "Flow-Läufe, Installationen, Deinstallationen, Freigabe-Entscheidungen — alles wird in ein hash-verkettetes Audit-Log geschrieben. Die Diagnose-Seite verifiziert die Kette bei jedem Laden komplett.",
|
||||
"welcomeTrustAirgapTitle": "Air-Gap-tauglich",
|
||||
"welcomeTrustAirgapBody": "Der gesamte Hub steckt in einem einzigen Binary für Linux, macOS und Windows. Sobald ein Modul installiert ist, läuft der Flow, der es nutzt, ohne weiteren Netzwerkzugriff — ideal für regulierte Umgebungen.",
|
||||
|
|
@ -40,8 +40,8 @@
|
|||
"welcomeDocArchitectureBlurb": "Hub, Modul, Flow — und wie die drei zusammenpassen.",
|
||||
"welcomeDocSecurityTitle": "Sandbox-Modell",
|
||||
"welcomeDocSecurityBlurb": "Was ein Modul deklariert, was der Operator deckelt, was der Hub durchsetzt.",
|
||||
"welcomeDocAuditTitle": "Manipulationssicheres Audit-Log",
|
||||
"welcomeDocAuditBlurb": "Wie die Hash-Kette funktioniert und warum WORM-1 für KRITIS reicht.",
|
||||
"welcomeDocAuditTitle": "Manipulationserkennendes Audit-Log",
|
||||
"welcomeDocAuditBlurb": "Wie die Hash-Kette nachträgliche Änderungen erkennbar macht — und was die Integritätsstufe WORM-1 leistet (und was nicht).",
|
||||
"welcomeDocFlowsTitle": "Flow-Komposition",
|
||||
"welcomeDocFlowsBlurb": "YAML-Grundlagen, Templating-Referenz, das Extract→Summarize-Beispiel.",
|
||||
"welcomeDocApprovalsTitle": "Freigaben",
|
||||
|
|
@ -1046,6 +1046,9 @@
|
|||
"approvalsPillExpired": "abgelaufen",
|
||||
"approvalsPayloadPreview": "ZU PRÜFENDE DATEN",
|
||||
"approvalsNoPayload": "Keine Daten zum Prüfen angehängt. Der system.approval-Schritt des Flows bestimmt über sein \"show:\"-Feld, was angezeigt wird — setze es, um die Daten hinter dieser Entscheidung sichtbar zu machen.",
|
||||
"approvalsNoDataConfirmTitle": "Ohne Prüfdaten freigeben?",
|
||||
"approvalsNoDataConfirmBody": "Dieser Flow hat bewusst keine Prüfdaten hinterlegt (kein \"show:\" am Freigabe-Schritt). Du kannst trotzdem freigeben — entscheidest dann aber, ohne die Daten hinter dieser Entscheidung gesehen zu haben.",
|
||||
"approvalsNoDataConfirmAction": "Trotzdem freigeben",
|
||||
"approvalsRequestFallback": "Freigabe für diesen Schritt erforderlich",
|
||||
"approvalsFlowStepMeta": "Flow: {flow} · Schritt: {step}",
|
||||
"approvalsApproveButton": "Freigeben",
|
||||
|
|
@ -1193,7 +1196,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"doctorChainPillOk": "WORM-1",
|
||||
"doctorChainPillOk": "Integritätskette v1",
|
||||
"doctorChainPillTamper": "MANIPULIERT",
|
||||
"doctorVerifyNow": "Jetzt prüfen",
|
||||
"doctorRestart": "Neustart",
|
||||
|
|
@ -1661,5 +1664,5 @@
|
|||
"federationTokenLabel": "Bootstrap-Token (einmalig)",
|
||||
"federationConfigLabel": "Satelliten-Konfiguration (in den Satelliten einfügen)",
|
||||
"federationCopied": "In die Zwischenablage kopiert",
|
||||
"federationEnrollmentHint": "Übergib das Token dem Satelliten-Betreiber über einen sicheren Kanal. Die mitgelieferte CA macht den ersten Connect des Satelliten manipulationssicher."
|
||||
"federationEnrollmentHint": "Übergib das Token dem Satelliten-Betreiber über einen sicheren Kanal. Die mitgelieferte CA authentifiziert den ersten Connect des Satelliten."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
"welcomeDocSecurityTitle": "Sandbox model",
|
||||
"welcomeDocSecurityBlurb": "What a module declares, what the operator caps, what the hub enforces.",
|
||||
"welcomeDocAuditTitle": "Tamper-evident audit log",
|
||||
"welcomeDocAuditBlurb": "How the hash chain works and why WORM-1 is enough for KRITIS.",
|
||||
"welcomeDocAuditBlurb": "How the hash chain makes later edits detectable — and what integrity level WORM-1 does (and does not) provide.",
|
||||
"welcomeDocFlowsTitle": "Flow composition",
|
||||
"welcomeDocFlowsBlurb": "YAML basics, templating reference, the extract→summarize example.",
|
||||
"welcomeDocApprovalsTitle": "Approvals",
|
||||
|
|
@ -1064,6 +1064,9 @@
|
|||
"approvalsPillExpired": "expired",
|
||||
"approvalsPayloadPreview": "DATA TO REVIEW",
|
||||
"approvalsNoPayload": "No data was attached for review. The flow's approval step chooses what to show via its \"show:\" field — set it to surface the data behind this decision.",
|
||||
"approvalsNoDataConfirmTitle": "Approve without review data?",
|
||||
"approvalsNoDataConfirmBody": "This flow deliberately attached no review data (no \"show:\" on its approval step). You can still approve — but you would be deciding without seeing the data behind this decision.",
|
||||
"approvalsNoDataConfirmAction": "Approve anyway",
|
||||
"approvalsRequestFallback": "Approval required for this step",
|
||||
"approvalsFlowStepMeta": "Flow: {flow} · Step: {step}",
|
||||
"@approvalsFlowStepMeta": {
|
||||
|
|
@ -1217,7 +1220,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"doctorChainPillOk": "WORM-1",
|
||||
"doctorChainPillOk": "Integrity chain v1",
|
||||
"doctorChainPillTamper": "TAMPER",
|
||||
"doctorVerifyNow": "Verify now",
|
||||
"doctorRestart": "Restart",
|
||||
|
|
@ -1700,5 +1703,5 @@
|
|||
"federationTokenLabel": "Bootstrap token (single use)",
|
||||
"federationConfigLabel": "Satellite config (paste into the satellite)",
|
||||
"federationCopied": "Copied to clipboard",
|
||||
"federationEnrollmentHint": "Hand the token to the satellite operator over a secure channel. The bundled CA makes the satellite's first connect tamper-proof."
|
||||
"federationEnrollmentHint": "Hand the token to the satellite operator over a secure channel. The bundled CA authenticates the satellite's first connect."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @welcomeDocAuditBlurb.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'How the hash chain works and why WORM-1 is enough for KRITIS.'**
|
||||
/// **'How the hash chain makes later edits detectable — and what integrity level WORM-1 does (and does not) provide.'**
|
||||
String get welcomeDocAuditBlurb;
|
||||
|
||||
/// No description provided for @welcomeDocFlowsTitle.
|
||||
|
|
@ -3254,6 +3254,24 @@ abstract class AppLocalizations {
|
|||
/// **'No data was attached for review. The flow\'s approval step chooses what to show via its \"show:\" field — set it to surface the data behind this decision.'**
|
||||
String get approvalsNoPayload;
|
||||
|
||||
/// No description provided for @approvalsNoDataConfirmTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Approve without review data?'**
|
||||
String get approvalsNoDataConfirmTitle;
|
||||
|
||||
/// No description provided for @approvalsNoDataConfirmBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This flow deliberately attached no review data (no \"show:\" on its approval step). You can still approve — but you would be deciding without seeing the data behind this decision.'**
|
||||
String get approvalsNoDataConfirmBody;
|
||||
|
||||
/// No description provided for @approvalsNoDataConfirmAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Approve anyway'**
|
||||
String get approvalsNoDataConfirmAction;
|
||||
|
||||
/// No description provided for @approvalsRequestFallback.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -3527,7 +3545,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @doctorChainPillOk.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'WORM-1'**
|
||||
/// **'Integrity chain v1'**
|
||||
String get doctorChainPillOk;
|
||||
|
||||
/// No description provided for @doctorChainPillTamper.
|
||||
|
|
@ -4870,7 +4888,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @federationEnrollmentHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Hand the token to the satellite operator over a secure channel. The bundled CA makes the satellite\'s first connect tamper-proof.'**
|
||||
/// **'Hand the token to the satellite operator over a secure channel. The bundled CA authenticates the satellite\'s first connect.'**
|
||||
String get federationEnrollmentHint;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
'Jedes Modul bringt eine explizite Berechtigungsliste mit — Netzwerk-Endpunkte, Dateien, Umgebungsvariablen. Der Hub setzt sie durch; ohne Operator-Freigabe verlässt nichts die Sandbox.';
|
||||
|
||||
@override
|
||||
String get welcomeTrustAuditTitle => 'Manipulationssicheres Audit-Log';
|
||||
String get welcomeTrustAuditTitle => 'Manipulationserkennendes Audit-Log';
|
||||
|
||||
@override
|
||||
String get welcomeTrustAuditBody =>
|
||||
|
|
@ -145,11 +145,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
'Was ein Modul deklariert, was der Operator deckelt, was der Hub durchsetzt.';
|
||||
|
||||
@override
|
||||
String get welcomeDocAuditTitle => 'Manipulationssicheres Audit-Log';
|
||||
String get welcomeDocAuditTitle => 'Manipulationserkennendes Audit-Log';
|
||||
|
||||
@override
|
||||
String get welcomeDocAuditBlurb =>
|
||||
'Wie die Hash-Kette funktioniert und warum WORM-1 für KRITIS reicht.';
|
||||
'Wie die Hash-Kette nachträgliche Änderungen erkennbar macht — und was die Integritätsstufe WORM-1 leistet (und was nicht).';
|
||||
|
||||
@override
|
||||
String get welcomeDocFlowsTitle => 'Flow-Komposition';
|
||||
|
|
@ -1871,6 +1871,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get approvalsNoPayload =>
|
||||
'Keine Daten zum Prüfen angehängt. Der system.approval-Schritt des Flows bestimmt über sein \"show:\"-Feld, was angezeigt wird — setze es, um die Daten hinter dieser Entscheidung sichtbar zu machen.';
|
||||
|
||||
@override
|
||||
String get approvalsNoDataConfirmTitle => 'Ohne Prüfdaten freigeben?';
|
||||
|
||||
@override
|
||||
String get approvalsNoDataConfirmBody =>
|
||||
'Dieser Flow hat bewusst keine Prüfdaten hinterlegt (kein \"show:\" am Freigabe-Schritt). Du kannst trotzdem freigeben — entscheidest dann aber, ohne die Daten hinter dieser Entscheidung gesehen zu haben.';
|
||||
|
||||
@override
|
||||
String get approvalsNoDataConfirmAction => 'Trotzdem freigeben';
|
||||
|
||||
@override
|
||||
String get approvalsRequestFallback =>
|
||||
'Freigabe für diesen Schritt erforderlich';
|
||||
|
|
@ -2043,7 +2053,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get doctorChainPillOk => 'WORM-1';
|
||||
String get doctorChainPillOk => 'Integritätskette v1';
|
||||
|
||||
@override
|
||||
String get doctorChainPillTamper => 'MANIPULIERT';
|
||||
|
|
@ -2860,5 +2870,5 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get federationEnrollmentHint =>
|
||||
'Übergib das Token dem Satelliten-Betreiber über einen sicheren Kanal. Die mitgelieferte CA macht den ersten Connect des Satelliten manipulationssicher.';
|
||||
'Übergib das Token dem Satelliten-Betreiber über einen sicheren Kanal. Die mitgelieferte CA authentifiziert den ersten Connect des Satelliten.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get welcomeDocAuditBlurb =>
|
||||
'How the hash chain works and why WORM-1 is enough for KRITIS.';
|
||||
'How the hash chain makes later edits detectable — and what integrity level WORM-1 does (and does not) provide.';
|
||||
|
||||
@override
|
||||
String get welcomeDocFlowsTitle => 'Flow composition';
|
||||
|
|
@ -1882,6 +1882,16 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get approvalsNoPayload =>
|
||||
'No data was attached for review. The flow\'s approval step chooses what to show via its \"show:\" field — set it to surface the data behind this decision.';
|
||||
|
||||
@override
|
||||
String get approvalsNoDataConfirmTitle => 'Approve without review data?';
|
||||
|
||||
@override
|
||||
String get approvalsNoDataConfirmBody =>
|
||||
'This flow deliberately attached no review data (no \"show:\" on its approval step). You can still approve — but you would be deciding without seeing the data behind this decision.';
|
||||
|
||||
@override
|
||||
String get approvalsNoDataConfirmAction => 'Approve anyway';
|
||||
|
||||
@override
|
||||
String get approvalsRequestFallback => 'Approval required for this step';
|
||||
|
||||
|
|
@ -2052,7 +2062,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get doctorChainPillOk => 'WORM-1';
|
||||
String get doctorChainPillOk => 'Integrity chain v1';
|
||||
|
||||
@override
|
||||
String get doctorChainPillTamper => 'TAMPER';
|
||||
|
|
@ -2863,5 +2873,5 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get federationEnrollmentHint =>
|
||||
'Hand the token to the satellite operator over a secure channel. The bundled CA makes the satellite\'s first connect tamper-proof.';
|
||||
'Hand the token to the satellite operator over a secure channel. The bundled CA authenticates the satellite\'s first connect.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,24 @@ import '../theme/tokens.dart';
|
|||
import '../widgets/widgets.dart';
|
||||
import 'welcome.dart' show showFaiDoc;
|
||||
|
||||
/// The fixed English sentence pre-0.21 hubs baked into stored
|
||||
/// approvals when the flow gave no `prompt:`. Newer hubs store the
|
||||
/// empty prompt verbatim.
|
||||
const _legacyHubPromptDefault =
|
||||
'Please review and approve this step before continuing.';
|
||||
|
||||
/// Reviewer-facing prompt with fallbacks: an empty prompt (the flow
|
||||
/// gave none) renders the localized default, and the legacy English
|
||||
/// default from old hub rows is mapped onto the same localized
|
||||
/// default so it stops showing English inside a German UI.
|
||||
String displayApprovalPrompt(AppLocalizations l, String prompt) {
|
||||
final trimmed = prompt.trim();
|
||||
if (trimmed.isEmpty || trimmed == _legacyHubPromptDefault) {
|
||||
return l.approvalsRequestFallback;
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
class ApprovalsPage extends StatefulWidget {
|
||||
const ApprovalsPage({super.key});
|
||||
|
||||
|
|
@ -68,6 +86,29 @@ class _ApprovalsPageState extends State<ApprovalsPage>
|
|||
|
||||
Future<void> _approve(ApprovalRecord a) async {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
// No `show:` data attached → never approve on a reflex. Calmly
|
||||
// explain and ask for a conscious confirmation first.
|
||||
if (a.payloadPreview == null) {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(l.approvalsNoDataConfirmTitle),
|
||||
content: Text(l.approvalsNoDataConfirmBody),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(l.buttonCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(l.approvalsNoDataConfirmAction),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
}
|
||||
if (!mounted) return;
|
||||
try {
|
||||
await HubService.instance.approve(a.id, _reviewer);
|
||||
_toast(l.approvalsApprovedToast(a.flowName, a.stepId));
|
||||
|
|
@ -77,6 +118,7 @@ class _ApprovalsPageState extends State<ApprovalsPage>
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> _reject(ApprovalRecord a) async {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final reason = await _promptReason(context);
|
||||
|
|
@ -558,9 +600,7 @@ class _ApprovalCard extends StatelessWidget {
|
|||
// step left the prompt empty, so the card is never reduced to
|
||||
// a cryptic flow id.
|
||||
Text(
|
||||
approval.prompt.trim().isEmpty
|
||||
? l.approvalsRequestFallback
|
||||
: approval.prompt,
|
||||
displayApprovalPrompt(l, approval.prompt),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
|
|
@ -850,7 +890,13 @@ class _HistoryDialog extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
SelectableText(record.prompt, style: theme.textTheme.bodyMedium),
|
||||
SelectableText(
|
||||
displayApprovalPrompt(
|
||||
AppLocalizations.of(context)!,
|
||||
record.prompt,
|
||||
),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
if (record.payloadPreview != null) ...[
|
||||
const SizedBox(height: ChainSpace.md),
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -3252,7 +3252,7 @@ const List<TodayStoryData> _kFallbackTodayStories = <TodayStoryData>[
|
|||
badgeEn: 'AUDIT',
|
||||
badgeDe: 'AUDIT',
|
||||
titleEn: 'Tamper-evident hash chain — built in',
|
||||
titleDe: 'Manipulationssicher per Hash-Kette — eingebaut',
|
||||
titleDe: 'Manipulation erkennbar per Hash-Kette — eingebaut',
|
||||
bodyEn:
|
||||
'Every flow run, every install, every approval lands in ~/.chain/audit/ as a hash-chained event log. Any later edit invalidates the chain. CRA-ready out of the box — no compliance product to buy on top.',
|
||||
bodyDe:
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ class ChainColors {
|
|||
static const surface = Color(0xFF18181B); // cards
|
||||
static const surfaceHigh = Color(0xFF27272A); // elevated
|
||||
static const border = Color(0xFF3F3F46); // 1px outlines
|
||||
static const muted = Color(0xFF71717A); // de-emphasised text
|
||||
// De-emphasised text. WCAG AA (≥4.5:1) against canvas, cards AND
|
||||
// elevated surfaces — 0xFF71717A only reached 3.7:1 on cards.
|
||||
static const muted = Color(0xFF8E8E97);
|
||||
static const text = Color(0xFFE4E4E7); // body
|
||||
static const textStrong = Color(0xFFFAFAFA); // headings
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue