Compare commits
No commits in common. "bb606a8b23be6b379c0dd9bcd361c7568b56e4b6" and "07432055a880c76423940bebf7b3eabf48c6d155" have entirely different histories.
bb606a8b23
...
07432055a8
11 changed files with 51 additions and 508 deletions
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -5,31 +5,6 @@ version + `kStudioVersion` in `lib/main.dart` stay in lockstep.
|
||||||
|
|
||||||
## Unreleased
|
## 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
|
### Added
|
||||||
|
|
||||||
- **Live audit feed.** The Audit page subscribes to `streamEvents` and
|
- **Live audit feed.** The Audit page subscribes to `streamEvents` and
|
||||||
|
|
|
||||||
|
|
@ -51,31 +51,7 @@ class ChainLog {
|
||||||
Platform.environment['HOME'] ??
|
Platform.environment['HOME'] ??
|
||||||
Platform.environment['USERPROFILE'] ??
|
Platform.environment['USERPROFILE'] ??
|
||||||
'.';
|
'.';
|
||||||
final path = p.join(home, '.chain', 'logs', 'studio-errors.log');
|
return p.join(home, '.fai', '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
|
/// Absolute path of the studio-errors log. Public so the
|
||||||
|
|
|
||||||
|
|
@ -787,8 +787,6 @@ class HubService {
|
||||||
provider: e.provider,
|
provider: e.provider,
|
||||||
sourceKind: e.sourceKind,
|
sourceKind: e.sourceKind,
|
||||||
source: e.source,
|
source: e.source,
|
||||||
canonicalCategory: e.canonicalCategory,
|
|
||||||
canonicalCategoryLabel: e.canonicalCategoryLabel,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
@ -1802,20 +1800,6 @@ class StoreItem {
|
||||||
/// label and group modules by where they came from.
|
/// label and group modules by where they came from.
|
||||||
final String source;
|
final String source;
|
||||||
|
|
||||||
/// Normalized category slug from the hub (e.g. "data",
|
|
||||||
/// "studio-themes"). The whole catalogue is mapped onto a fixed
|
|
||||||
/// taxonomy by capability namespace, so different stores' free-form
|
|
||||||
/// labels collapse into one bucket. Studio groups + filters by this,
|
|
||||||
/// not [category]. Empty from a pre-0.21 hub → fall back to [category].
|
|
||||||
final String canonicalCategory;
|
|
||||||
|
|
||||||
/// Human-readable label for [canonicalCategory] (e.g. "Data & Formats").
|
|
||||||
final String canonicalCategoryLabel;
|
|
||||||
|
|
||||||
/// True iff this entry is a Studio plugin/theme rather than a flow
|
|
||||||
/// module — drives the "Modules | Studio & Themes" store segment.
|
|
||||||
bool get isStudioPlugin => canonicalCategory == 'studio-themes';
|
|
||||||
|
|
||||||
bool get isFederated => kind == 'federated';
|
bool get isFederated => kind == 'federated';
|
||||||
|
|
||||||
const StoreItem({
|
const StoreItem({
|
||||||
|
|
@ -1841,7 +1825,5 @@ class StoreItem {
|
||||||
required this.provider,
|
required this.provider,
|
||||||
this.sourceKind = '',
|
this.sourceKind = '',
|
||||||
this.source = '',
|
this.source = '',
|
||||||
this.canonicalCategory = '',
|
|
||||||
this.canonicalCategoryLabel = '',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"welcomeTrustHeader": "TRUST-POSTURE",
|
"welcomeTrustHeader": "TRUST-POSTURE",
|
||||||
"welcomeTrustSandboxTitle": "Sandbox von Anfang an",
|
"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.",
|
"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": "Manipulationserkennendes Audit-Log",
|
"welcomeTrustAuditTitle": "Manipulationssicheres 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.",
|
"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",
|
"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.",
|
"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.",
|
"welcomeDocArchitectureBlurb": "Hub, Modul, Flow — und wie die drei zusammenpassen.",
|
||||||
"welcomeDocSecurityTitle": "Sandbox-Modell",
|
"welcomeDocSecurityTitle": "Sandbox-Modell",
|
||||||
"welcomeDocSecurityBlurb": "Was ein Modul deklariert, was der Operator deckelt, was der Hub durchsetzt.",
|
"welcomeDocSecurityBlurb": "Was ein Modul deklariert, was der Operator deckelt, was der Hub durchsetzt.",
|
||||||
"welcomeDocAuditTitle": "Manipulationserkennendes Audit-Log",
|
"welcomeDocAuditTitle": "Manipulationssicheres Audit-Log",
|
||||||
"welcomeDocAuditBlurb": "Wie die Hash-Kette nachträgliche Änderungen erkennbar macht — und was die Integritätsstufe WORM-1 leistet (und was nicht).",
|
"welcomeDocAuditBlurb": "Wie die Hash-Kette funktioniert und warum WORM-1 für KRITIS reicht.",
|
||||||
"welcomeDocFlowsTitle": "Flow-Komposition",
|
"welcomeDocFlowsTitle": "Flow-Komposition",
|
||||||
"welcomeDocFlowsBlurb": "YAML-Grundlagen, Templating-Referenz, das Extract→Summarize-Beispiel.",
|
"welcomeDocFlowsBlurb": "YAML-Grundlagen, Templating-Referenz, das Extract→Summarize-Beispiel.",
|
||||||
"welcomeDocApprovalsTitle": "Freigaben",
|
"welcomeDocApprovalsTitle": "Freigaben",
|
||||||
|
|
@ -327,17 +327,6 @@
|
||||||
"storeCategoryChannel": "Kanäle",
|
"storeCategoryChannel": "Kanäle",
|
||||||
"storeCategoryWeb": "Web",
|
"storeCategoryWeb": "Web",
|
||||||
"storeCategoryOrchestrator": "Orchestrierung",
|
"storeCategoryOrchestrator": "Orchestrierung",
|
||||||
"storeCatDocuments": "Dokumente",
|
|
||||||
"storeCatTextLanguage": "Text & Sprache",
|
|
||||||
"storeCatData": "Daten & Formate",
|
|
||||||
"storeCatAiLlm": "KI & LLM",
|
|
||||||
"storeCatWebApi": "Web & APIs",
|
|
||||||
"storeCatAnalysisDomain": "Analyse & Domäne",
|
|
||||||
"storeCatStudioThemes": "Studio & Themes",
|
|
||||||
"storeCatExamplesDev": "Beispiele & Dev",
|
|
||||||
"storeCatOther": "Sonstiges",
|
|
||||||
"storeSegmentModules": "Module",
|
|
||||||
"storeSegmentStudio": "Studio & Themes",
|
|
||||||
"storeNResults": "{n} Treffer",
|
"storeNResults": "{n} Treffer",
|
||||||
"@storeNResults": {
|
"@storeNResults": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
@ -1046,9 +1035,6 @@
|
||||||
"approvalsPillExpired": "abgelaufen",
|
"approvalsPillExpired": "abgelaufen",
|
||||||
"approvalsPayloadPreview": "ZU PRÜFENDE DATEN",
|
"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.",
|
"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",
|
"approvalsRequestFallback": "Freigabe für diesen Schritt erforderlich",
|
||||||
"approvalsFlowStepMeta": "Flow: {flow} · Schritt: {step}",
|
"approvalsFlowStepMeta": "Flow: {flow} · Schritt: {step}",
|
||||||
"approvalsApproveButton": "Freigeben",
|
"approvalsApproveButton": "Freigeben",
|
||||||
|
|
@ -1196,7 +1182,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"doctorChainPillOk": "Integritätskette v1",
|
"doctorChainPillOk": "WORM-1",
|
||||||
"doctorChainPillTamper": "MANIPULIERT",
|
"doctorChainPillTamper": "MANIPULIERT",
|
||||||
"doctorVerifyNow": "Jetzt prüfen",
|
"doctorVerifyNow": "Jetzt prüfen",
|
||||||
"doctorRestart": "Neustart",
|
"doctorRestart": "Neustart",
|
||||||
|
|
@ -1664,5 +1650,5 @@
|
||||||
"federationTokenLabel": "Bootstrap-Token (einmalig)",
|
"federationTokenLabel": "Bootstrap-Token (einmalig)",
|
||||||
"federationConfigLabel": "Satelliten-Konfiguration (in den Satelliten einfügen)",
|
"federationConfigLabel": "Satelliten-Konfiguration (in den Satelliten einfügen)",
|
||||||
"federationCopied": "In die Zwischenablage kopiert",
|
"federationCopied": "In die Zwischenablage kopiert",
|
||||||
"federationEnrollmentHint": "Übergib das Token dem Satelliten-Betreiber über einen sicheren Kanal. Die mitgelieferte CA authentifiziert den ersten Connect des Satelliten."
|
"federationEnrollmentHint": "Übergib das Token dem Satelliten-Betreiber über einen sicheren Kanal. Die mitgelieferte CA macht den ersten Connect des Satelliten manipulationssicher."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
"welcomeDocSecurityTitle": "Sandbox model",
|
"welcomeDocSecurityTitle": "Sandbox model",
|
||||||
"welcomeDocSecurityBlurb": "What a module declares, what the operator caps, what the hub enforces.",
|
"welcomeDocSecurityBlurb": "What a module declares, what the operator caps, what the hub enforces.",
|
||||||
"welcomeDocAuditTitle": "Tamper-evident audit log",
|
"welcomeDocAuditTitle": "Tamper-evident audit log",
|
||||||
"welcomeDocAuditBlurb": "How the hash chain makes later edits detectable — and what integrity level WORM-1 does (and does not) provide.",
|
"welcomeDocAuditBlurb": "How the hash chain works and why WORM-1 is enough for KRITIS.",
|
||||||
"welcomeDocFlowsTitle": "Flow composition",
|
"welcomeDocFlowsTitle": "Flow composition",
|
||||||
"welcomeDocFlowsBlurb": "YAML basics, templating reference, the extract→summarize example.",
|
"welcomeDocFlowsBlurb": "YAML basics, templating reference, the extract→summarize example.",
|
||||||
"welcomeDocApprovalsTitle": "Approvals",
|
"welcomeDocApprovalsTitle": "Approvals",
|
||||||
|
|
@ -335,17 +335,6 @@
|
||||||
"storeCategoryChannel": "Channels",
|
"storeCategoryChannel": "Channels",
|
||||||
"storeCategoryWeb": "Web",
|
"storeCategoryWeb": "Web",
|
||||||
"storeCategoryOrchestrator": "Orchestration",
|
"storeCategoryOrchestrator": "Orchestration",
|
||||||
"storeCatDocuments": "Documents",
|
|
||||||
"storeCatTextLanguage": "Text & Language",
|
|
||||||
"storeCatData": "Data & Formats",
|
|
||||||
"storeCatAiLlm": "AI & LLM",
|
|
||||||
"storeCatWebApi": "Web & APIs",
|
|
||||||
"storeCatAnalysisDomain": "Analysis & Domain",
|
|
||||||
"storeCatStudioThemes": "Studio & Themes",
|
|
||||||
"storeCatExamplesDev": "Examples & Dev",
|
|
||||||
"storeCatOther": "Other",
|
|
||||||
"storeSegmentModules": "Modules",
|
|
||||||
"storeSegmentStudio": "Studio & Themes",
|
|
||||||
"storeNResults": "{n} result{n, plural, =1{} other{s}}",
|
"storeNResults": "{n} result{n, plural, =1{} other{s}}",
|
||||||
"@storeNResults": {
|
"@storeNResults": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
@ -1064,9 +1053,6 @@
|
||||||
"approvalsPillExpired": "expired",
|
"approvalsPillExpired": "expired",
|
||||||
"approvalsPayloadPreview": "DATA TO REVIEW",
|
"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.",
|
"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",
|
"approvalsRequestFallback": "Approval required for this step",
|
||||||
"approvalsFlowStepMeta": "Flow: {flow} · Step: {step}",
|
"approvalsFlowStepMeta": "Flow: {flow} · Step: {step}",
|
||||||
"@approvalsFlowStepMeta": {
|
"@approvalsFlowStepMeta": {
|
||||||
|
|
@ -1220,7 +1206,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"doctorChainPillOk": "Integrity chain v1",
|
"doctorChainPillOk": "WORM-1",
|
||||||
"doctorChainPillTamper": "TAMPER",
|
"doctorChainPillTamper": "TAMPER",
|
||||||
"doctorVerifyNow": "Verify now",
|
"doctorVerifyNow": "Verify now",
|
||||||
"doctorRestart": "Restart",
|
"doctorRestart": "Restart",
|
||||||
|
|
@ -1703,5 +1689,5 @@
|
||||||
"federationTokenLabel": "Bootstrap token (single use)",
|
"federationTokenLabel": "Bootstrap token (single use)",
|
||||||
"federationConfigLabel": "Satellite config (paste into the satellite)",
|
"federationConfigLabel": "Satellite config (paste into the satellite)",
|
||||||
"federationCopied": "Copied to clipboard",
|
"federationCopied": "Copied to clipboard",
|
||||||
"federationEnrollmentHint": "Hand the token to the satellite operator over a secure channel. The bundled CA authenticates the satellite's first connect."
|
"federationEnrollmentHint": "Hand the token to the satellite operator over a secure channel. The bundled CA makes the satellite's first connect tamper-proof."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @welcomeHeroSubtitle.
|
/// No description provided for @welcomeHeroSubtitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Deterministic workflow engine for AI-assisted data processing in regulated environments.'**
|
/// **'Deterministic workflow engine for AI-assisted document processing in regulated environments.'**
|
||||||
String get welcomeHeroSubtitle;
|
String get welcomeHeroSubtitle;
|
||||||
|
|
||||||
/// No description provided for @welcomePillarHubTitle.
|
/// No description provided for @welcomePillarHubTitle.
|
||||||
|
|
@ -347,7 +347,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @welcomeDocAuditBlurb.
|
/// No description provided for @welcomeDocAuditBlurb.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'How the hash chain makes later edits detectable — and what integrity level WORM-1 does (and does not) provide.'**
|
/// **'How the hash chain works and why WORM-1 is enough for KRITIS.'**
|
||||||
String get welcomeDocAuditBlurb;
|
String get welcomeDocAuditBlurb;
|
||||||
|
|
||||||
/// No description provided for @welcomeDocFlowsTitle.
|
/// No description provided for @welcomeDocFlowsTitle.
|
||||||
|
|
@ -1526,72 +1526,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Orchestration'**
|
/// **'Orchestration'**
|
||||||
String get storeCategoryOrchestrator;
|
String get storeCategoryOrchestrator;
|
||||||
|
|
||||||
/// No description provided for @storeCatDocuments.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Documents'**
|
|
||||||
String get storeCatDocuments;
|
|
||||||
|
|
||||||
/// No description provided for @storeCatTextLanguage.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Text & Language'**
|
|
||||||
String get storeCatTextLanguage;
|
|
||||||
|
|
||||||
/// No description provided for @storeCatData.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Data & Formats'**
|
|
||||||
String get storeCatData;
|
|
||||||
|
|
||||||
/// No description provided for @storeCatAiLlm.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'AI & LLM'**
|
|
||||||
String get storeCatAiLlm;
|
|
||||||
|
|
||||||
/// No description provided for @storeCatWebApi.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Web & APIs'**
|
|
||||||
String get storeCatWebApi;
|
|
||||||
|
|
||||||
/// No description provided for @storeCatAnalysisDomain.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Analysis & Domain'**
|
|
||||||
String get storeCatAnalysisDomain;
|
|
||||||
|
|
||||||
/// No description provided for @storeCatStudioThemes.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Studio & Themes'**
|
|
||||||
String get storeCatStudioThemes;
|
|
||||||
|
|
||||||
/// No description provided for @storeCatExamplesDev.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Examples & Dev'**
|
|
||||||
String get storeCatExamplesDev;
|
|
||||||
|
|
||||||
/// No description provided for @storeCatOther.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Other'**
|
|
||||||
String get storeCatOther;
|
|
||||||
|
|
||||||
/// No description provided for @storeSegmentModules.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Modules'**
|
|
||||||
String get storeSegmentModules;
|
|
||||||
|
|
||||||
/// No description provided for @storeSegmentStudio.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Studio & Themes'**
|
|
||||||
String get storeSegmentStudio;
|
|
||||||
|
|
||||||
/// No description provided for @storeNResults.
|
/// No description provided for @storeNResults.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -3254,24 +3188,6 @@ 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.'**
|
/// **'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;
|
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.
|
/// No description provided for @approvalsRequestFallback.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -3545,7 +3461,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @doctorChainPillOk.
|
/// No description provided for @doctorChainPillOk.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Integrity chain v1'**
|
/// **'WORM-1'**
|
||||||
String get doctorChainPillOk;
|
String get doctorChainPillOk;
|
||||||
|
|
||||||
/// No description provided for @doctorChainPillTamper.
|
/// No description provided for @doctorChainPillTamper.
|
||||||
|
|
@ -4888,7 +4804,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @federationEnrollmentHint.
|
/// No description provided for @federationEnrollmentHint.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Hand the token to the satellite operator over a secure channel. The bundled CA authenticates the satellite\'s first connect.'**
|
/// **'Hand the token to the satellite operator over a secure channel. The bundled CA makes the satellite\'s first connect tamper-proof.'**
|
||||||
String get federationEnrollmentHint;
|
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.';
|
'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
|
@override
|
||||||
String get welcomeTrustAuditTitle => 'Manipulationserkennendes Audit-Log';
|
String get welcomeTrustAuditTitle => 'Manipulationssicheres Audit-Log';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get welcomeTrustAuditBody =>
|
String get welcomeTrustAuditBody =>
|
||||||
|
|
@ -145,11 +145,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
'Was ein Modul deklariert, was der Operator deckelt, was der Hub durchsetzt.';
|
'Was ein Modul deklariert, was der Operator deckelt, was der Hub durchsetzt.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get welcomeDocAuditTitle => 'Manipulationserkennendes Audit-Log';
|
String get welcomeDocAuditTitle => 'Manipulationssicheres Audit-Log';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get welcomeDocAuditBlurb =>
|
String get welcomeDocAuditBlurb =>
|
||||||
'Wie die Hash-Kette nachträgliche Änderungen erkennbar macht — und was die Integritätsstufe WORM-1 leistet (und was nicht).';
|
'Wie die Hash-Kette funktioniert und warum WORM-1 für KRITIS reicht.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get welcomeDocFlowsTitle => 'Flow-Komposition';
|
String get welcomeDocFlowsTitle => 'Flow-Komposition';
|
||||||
|
|
@ -820,39 +820,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get storeCategoryOrchestrator => 'Orchestrierung';
|
String get storeCategoryOrchestrator => 'Orchestrierung';
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatDocuments => 'Dokumente';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatTextLanguage => 'Text & Sprache';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatData => 'Daten & Formate';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatAiLlm => 'KI & LLM';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatWebApi => 'Web & APIs';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatAnalysisDomain => 'Analyse & Domäne';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatStudioThemes => 'Studio & Themes';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatExamplesDev => 'Beispiele & Dev';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatOther => 'Sonstiges';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeSegmentModules => 'Module';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeSegmentStudio => 'Studio & Themes';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String storeNResults(int n) {
|
String storeNResults(int n) {
|
||||||
return '$n Treffer';
|
return '$n Treffer';
|
||||||
|
|
@ -1871,16 +1838,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get approvalsNoPayload =>
|
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.';
|
'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
|
@override
|
||||||
String get approvalsRequestFallback =>
|
String get approvalsRequestFallback =>
|
||||||
'Freigabe für diesen Schritt erforderlich';
|
'Freigabe für diesen Schritt erforderlich';
|
||||||
|
|
@ -2053,7 +2010,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get doctorChainPillOk => 'Integritätskette v1';
|
String get doctorChainPillOk => 'WORM-1';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get doctorChainPillTamper => 'MANIPULIERT';
|
String get doctorChainPillTamper => 'MANIPULIERT';
|
||||||
|
|
@ -2870,5 +2827,5 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get federationEnrollmentHint =>
|
String get federationEnrollmentHint =>
|
||||||
'Übergib das Token dem Satelliten-Betreiber über einen sicheren Kanal. Die mitgelieferte CA authentifiziert den ersten Connect des Satelliten.';
|
'Übergib das Token dem Satelliten-Betreiber über einen sicheren Kanal. Die mitgelieferte CA macht den ersten Connect des Satelliten manipulationssicher.';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get welcomeDocAuditBlurb =>
|
String get welcomeDocAuditBlurb =>
|
||||||
'How the hash chain makes later edits detectable — and what integrity level WORM-1 does (and does not) provide.';
|
'How the hash chain works and why WORM-1 is enough for KRITIS.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get welcomeDocFlowsTitle => 'Flow composition';
|
String get welcomeDocFlowsTitle => 'Flow composition';
|
||||||
|
|
@ -833,39 +833,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get storeCategoryOrchestrator => 'Orchestration';
|
String get storeCategoryOrchestrator => 'Orchestration';
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatDocuments => 'Documents';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatTextLanguage => 'Text & Language';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatData => 'Data & Formats';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatAiLlm => 'AI & LLM';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatWebApi => 'Web & APIs';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatAnalysisDomain => 'Analysis & Domain';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatStudioThemes => 'Studio & Themes';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatExamplesDev => 'Examples & Dev';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeCatOther => 'Other';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeSegmentModules => 'Modules';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storeSegmentStudio => 'Studio & Themes';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String storeNResults(int n) {
|
String storeNResults(int n) {
|
||||||
String _temp0 = intl.Intl.pluralLogic(
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
|
@ -1882,16 +1849,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get approvalsNoPayload =>
|
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.';
|
'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
|
@override
|
||||||
String get approvalsRequestFallback => 'Approval required for this step';
|
String get approvalsRequestFallback => 'Approval required for this step';
|
||||||
|
|
||||||
|
|
@ -2062,7 +2019,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get doctorChainPillOk => 'Integrity chain v1';
|
String get doctorChainPillOk => 'WORM-1';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get doctorChainPillTamper => 'TAMPER';
|
String get doctorChainPillTamper => 'TAMPER';
|
||||||
|
|
@ -2873,5 +2830,5 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get federationEnrollmentHint =>
|
String get federationEnrollmentHint =>
|
||||||
'Hand the token to the satellite operator over a secure channel. The bundled CA authenticates the satellite\'s first connect.';
|
'Hand the token to the satellite operator over a secure channel. The bundled CA makes the satellite\'s first connect tamper-proof.';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,24 +10,6 @@ import '../theme/tokens.dart';
|
||||||
import '../widgets/widgets.dart';
|
import '../widgets/widgets.dart';
|
||||||
import 'welcome.dart' show showFaiDoc;
|
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 {
|
class ApprovalsPage extends StatefulWidget {
|
||||||
const ApprovalsPage({super.key});
|
const ApprovalsPage({super.key});
|
||||||
|
|
||||||
|
|
@ -86,29 +68,6 @@ class _ApprovalsPageState extends State<ApprovalsPage>
|
||||||
|
|
||||||
Future<void> _approve(ApprovalRecord a) async {
|
Future<void> _approve(ApprovalRecord a) async {
|
||||||
final l = AppLocalizations.of(context)!;
|
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 {
|
try {
|
||||||
await HubService.instance.approve(a.id, _reviewer);
|
await HubService.instance.approve(a.id, _reviewer);
|
||||||
_toast(l.approvalsApprovedToast(a.flowName, a.stepId));
|
_toast(l.approvalsApprovedToast(a.flowName, a.stepId));
|
||||||
|
|
@ -118,7 +77,6 @@ class _ApprovalsPageState extends State<ApprovalsPage>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<void> _reject(ApprovalRecord a) async {
|
Future<void> _reject(ApprovalRecord a) async {
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
final reason = await _promptReason(context);
|
final reason = await _promptReason(context);
|
||||||
|
|
@ -600,7 +558,9 @@ class _ApprovalCard extends StatelessWidget {
|
||||||
// step left the prompt empty, so the card is never reduced to
|
// step left the prompt empty, so the card is never reduced to
|
||||||
// a cryptic flow id.
|
// a cryptic flow id.
|
||||||
Text(
|
Text(
|
||||||
displayApprovalPrompt(l, approval.prompt),
|
approval.prompt.trim().isEmpty
|
||||||
|
? l.approvalsRequestFallback
|
||||||
|
: approval.prompt,
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
|
|
@ -890,13 +850,7 @@ class _HistoryDialog extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
SelectableText(
|
SelectableText(record.prompt, style: theme.textTheme.bodyMedium),
|
||||||
displayApprovalPrompt(
|
|
||||||
AppLocalizations.of(context)!,
|
|
||||||
record.prompt,
|
|
||||||
),
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
if (record.payloadPreview != null) ...[
|
if (record.payloadPreview != null) ...[
|
||||||
const SizedBox(height: ChainSpace.md),
|
const SizedBox(height: ChainSpace.md),
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,6 @@ class _StorePageState extends State<StorePage> {
|
||||||
String _category = '';
|
String _category = '';
|
||||||
String _status = '';
|
String _status = '';
|
||||||
|
|
||||||
/// Store segment: `false` shows flow Modules, `true` shows Studio
|
|
||||||
/// plugins + themes. A theme extends the GUI, a module runs in a
|
|
||||||
/// flow — mixing them in one grid was a big part of the clutter, so
|
|
||||||
/// they live under a top segment toggle instead.
|
|
||||||
bool _showStudio = false;
|
|
||||||
|
|
||||||
/// Source filter: '' (all), 'native', 'mcp', 'n8n'. Applied
|
/// Source filter: '' (all), 'native', 'mcp', 'n8n'. Applied
|
||||||
/// client-side after the hub returns results — the search RPC
|
/// client-side after the hub returns results — the search RPC
|
||||||
/// has no source field.
|
/// has no source field.
|
||||||
|
|
@ -361,32 +355,6 @@ class _StorePageState extends State<StorePage> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Modules vs Studio plugins/themes — a theme
|
|
||||||
// extends the GUI, a module runs in a flow.
|
|
||||||
Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.only(bottom: ChainSpace.md),
|
|
||||||
child: SegmentedButton<bool>(
|
|
||||||
segments: [
|
|
||||||
ButtonSegment(
|
|
||||||
value: false,
|
|
||||||
label: Text(l.storeSegmentModules),
|
|
||||||
icon: const Icon(Icons.extension_outlined,
|
|
||||||
size: 16),
|
|
||||||
),
|
|
||||||
ButtonSegment(
|
|
||||||
value: true,
|
|
||||||
label: Text(l.storeSegmentStudio),
|
|
||||||
icon:
|
|
||||||
const Icon(Icons.palette_outlined, size: 16),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
selected: {_showStudio},
|
|
||||||
showSelectedIcon: false,
|
|
||||||
onSelectionChanged: (s) =>
|
|
||||||
setState(() => _showStudio = s.first),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_aiThinking ||
|
if (_aiThinking ||
|
||||||
_aiAnswer != null ||
|
_aiAnswer != null ||
|
||||||
_aiError != null) ...[
|
_aiError != null) ...[
|
||||||
|
|
@ -528,10 +496,6 @@ class _StorePageState extends State<StorePage> {
|
||||||
/// card and the grid stay coherent.
|
/// card and the grid stay coherent.
|
||||||
List<StoreItem> _applyAllFilters(List<StoreItem> items) {
|
List<StoreItem> _applyAllFilters(List<StoreItem> items) {
|
||||||
var out = _applySourceFilter(items);
|
var out = _applySourceFilter(items);
|
||||||
// Store segment: flow modules vs Studio plugins/themes. Federated
|
|
||||||
// entries (MCP/n8n bridges) are never Studio plugins, so they stay
|
|
||||||
// in the Modules segment.
|
|
||||||
out = out.where((e) => e.isStudioPlugin == _showStudio).toList();
|
|
||||||
final ai = _aiMatchedNames;
|
final ai = _aiMatchedNames;
|
||||||
if (ai != null) {
|
if (ai != null) {
|
||||||
out = out.where((e) => ai.contains(e.name)).toList();
|
out = out.where((e) => ai.contains(e.name)).toList();
|
||||||
|
|
@ -801,34 +765,6 @@ String _sourceForItem(StoreItem i) {
|
||||||
/// data model — only the display side gets localised. Unknown
|
/// data model — only the display side gets localised. Unknown
|
||||||
/// ids fall through unchanged so newly-added categories don't
|
/// ids fall through unchanged so newly-added categories don't
|
||||||
/// vanish until we update this map.
|
/// vanish until we update this map.
|
||||||
/// Localized label for a canonical category slug (from the hub). Falls
|
|
||||||
/// back to the hub's English label / raw category for an unknown slug.
|
|
||||||
String _canonicalCatLabel(BuildContext ctx, String slug, String fallback) {
|
|
||||||
final l = AppLocalizations.of(ctx)!;
|
|
||||||
switch (slug) {
|
|
||||||
case 'documents':
|
|
||||||
return l.storeCatDocuments;
|
|
||||||
case 'text-language':
|
|
||||||
return l.storeCatTextLanguage;
|
|
||||||
case 'data':
|
|
||||||
return l.storeCatData;
|
|
||||||
case 'ai-llm':
|
|
||||||
return l.storeCatAiLlm;
|
|
||||||
case 'web-api':
|
|
||||||
return l.storeCatWebApi;
|
|
||||||
case 'analysis-domain':
|
|
||||||
return l.storeCatAnalysisDomain;
|
|
||||||
case 'studio-themes':
|
|
||||||
return l.storeCatStudioThemes;
|
|
||||||
case 'examples-dev':
|
|
||||||
return l.storeCatExamplesDev;
|
|
||||||
case 'other':
|
|
||||||
return l.storeCatOther;
|
|
||||||
default:
|
|
||||||
return fallback.isNotEmpty ? fallback : slug;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _categoryDisplayName(BuildContext ctx, String wire) {
|
String _categoryDisplayName(BuildContext ctx, String wire) {
|
||||||
final l = AppLocalizations.of(ctx)!;
|
final l = AppLocalizations.of(ctx)!;
|
||||||
switch (wire) {
|
switch (wire) {
|
||||||
|
|
@ -1593,116 +1529,36 @@ class _StoreGrid extends StatelessWidget {
|
||||||
required this.onInstall,
|
required this.onInstall,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Fixed display order of the canonical categories (mirrors the
|
|
||||||
/// hub's `Category::all_ordered`) — flow-module categories first,
|
|
||||||
/// Studio + Other last, so the grouped store reads top-to-bottom
|
|
||||||
/// like an app store's category rows.
|
|
||||||
static const List<String> _order = [
|
|
||||||
'documents',
|
|
||||||
'text-language',
|
|
||||||
'data',
|
|
||||||
'ai-llm',
|
|
||||||
'web-api',
|
|
||||||
'analysis-domain',
|
|
||||||
'examples-dev',
|
|
||||||
'studio-themes',
|
|
||||||
'other',
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
const minCardWidth = 360.0;
|
const minCardWidth = 360.0;
|
||||||
final cols = (constraints.maxWidth / minCardWidth).floor().clamp(1, 4);
|
final cols = (constraints.maxWidth / minCardWidth).floor().clamp(1, 4);
|
||||||
|
// shrinkWrap + NeverScrollable lets the grid sit inside
|
||||||
// Group the flat result set by canonical category so the
|
// the page-level SingleChildScrollView so editorial
|
||||||
// store reads like an app store — a labelled section per
|
// chrome and the grid scroll as one continuous surface
|
||||||
// category instead of one jumbled grid. Falls back to the
|
// (App-Store / Play-Store behaviour). Without this, the
|
||||||
// raw `category` label for a pre-0.21 hub that sends no
|
// inner grid claims its own scroll viewport and the
|
||||||
// canonical slug.
|
// outer Column overflows on small windows.
|
||||||
final groups = <String, List<StoreItem>>{};
|
return GridView.builder(
|
||||||
final labels = <String, String>{};
|
padding: EdgeInsets.zero,
|
||||||
for (final it in items) {
|
shrinkWrap: true,
|
||||||
final slug = it.canonicalCategory.isNotEmpty
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
? it.canonicalCategory
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
: (it.category.isNotEmpty ? it.category : 'other');
|
crossAxisCount: cols,
|
||||||
groups.putIfAbsent(slug, () => []).add(it);
|
mainAxisSpacing: ChainSpace.md,
|
||||||
labels[slug] = it.canonicalCategoryLabel.isNotEmpty
|
crossAxisSpacing: ChainSpace.md,
|
||||||
? it.canonicalCategoryLabel
|
mainAxisExtent: 168,
|
||||||
: (it.category.isNotEmpty ? it.category : 'Other');
|
),
|
||||||
}
|
itemCount: items.length,
|
||||||
final slugs = groups.keys.toList()
|
itemBuilder: (context, i) => _StoreCard(
|
||||||
..sort((a, b) {
|
item: items[i],
|
||||||
final ia = _order.indexOf(a);
|
locale: locale,
|
||||||
final ib = _order.indexOf(b);
|
installedVersion: installedVersions[items[i].name],
|
||||||
// Unknown slugs sort after the known order, alphabetically.
|
onTap: () => onTap(items[i]),
|
||||||
if (ia == -1 && ib == -1) return a.compareTo(b);
|
onInstall: () => onInstall(items[i]),
|
||||||
if (ia == -1) return 1;
|
),
|
||||||
if (ib == -1) return -1;
|
|
||||||
return ia.compareTo(ib);
|
|
||||||
});
|
|
||||||
|
|
||||||
Widget grid(List<StoreItem> gi) => GridView.builder(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: cols,
|
|
||||||
mainAxisSpacing: ChainSpace.md,
|
|
||||||
crossAxisSpacing: ChainSpace.md,
|
|
||||||
mainAxisExtent: 168,
|
|
||||||
),
|
|
||||||
itemCount: gi.length,
|
|
||||||
itemBuilder: (context, i) => _StoreCard(
|
|
||||||
item: gi[i],
|
|
||||||
locale: locale,
|
|
||||||
installedVersion: installedVersions[gi[i].name],
|
|
||||||
onTap: () => onTap(gi[i]),
|
|
||||||
onInstall: () => onInstall(gi[i]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// A single category (e.g. the store is already filtered to
|
|
||||||
// one) renders without a redundant header.
|
|
||||||
if (slugs.length <= 1) {
|
|
||||||
return grid(items);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
for (final slug in slugs) ...[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: ChainSpace.lg,
|
|
||||||
bottom: ChainSpace.sm,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
_canonicalCatLabel(context, slug, labels[slug] ?? ''),
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.titleSmall
|
|
||||||
?.copyWith(fontWeight: FontWeight.w700),
|
|
||||||
),
|
|
||||||
const SizedBox(width: ChainSpace.sm),
|
|
||||||
Text(
|
|
||||||
'${groups[slug]!.length}',
|
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onSurfaceVariant,
|
|
||||||
fontFeatures: const [FontFeature.tabularFigures()],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
grid(groups[slug]!),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -3252,7 +3108,7 @@ const List<TodayStoryData> _kFallbackTodayStories = <TodayStoryData>[
|
||||||
badgeEn: 'AUDIT',
|
badgeEn: 'AUDIT',
|
||||||
badgeDe: 'AUDIT',
|
badgeDe: 'AUDIT',
|
||||||
titleEn: 'Tamper-evident hash chain — built in',
|
titleEn: 'Tamper-evident hash chain — built in',
|
||||||
titleDe: 'Manipulation erkennbar per Hash-Kette — eingebaut',
|
titleDe: 'Manipulationssicher per Hash-Kette — eingebaut',
|
||||||
bodyEn:
|
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.',
|
'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:
|
bodyDe:
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,7 @@ class ChainColors {
|
||||||
static const surface = Color(0xFF18181B); // cards
|
static const surface = Color(0xFF18181B); // cards
|
||||||
static const surfaceHigh = Color(0xFF27272A); // elevated
|
static const surfaceHigh = Color(0xFF27272A); // elevated
|
||||||
static const border = Color(0xFF3F3F46); // 1px outlines
|
static const border = Color(0xFF3F3F46); // 1px outlines
|
||||||
// De-emphasised text. WCAG AA (≥4.5:1) against canvas, cards AND
|
static const muted = Color(0xFF71717A); // de-emphasised text
|
||||||
// elevated surfaces — 0xFF71717A only reached 3.7:1 on cards.
|
|
||||||
static const muted = Color(0xFF8E8E97);
|
|
||||||
static const text = Color(0xFFE4E4E7); // body
|
static const text = Color(0xFFE4E4E7); // body
|
||||||
static const textStrong = Color(0xFFFAFAFA); // headings
|
static const textStrong = Color(0xFFFAFAFA); // headings
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue