From c253f5d23ecdbe203c12feed4a67d389cb46cf1d Mon Sep 17 00:00:00 2001 From: flemming-it Date: Fri, 17 Jul 2026 23:40:21 +0200 Subject: [PATCH] =?UTF-8?q?feat(settings):=20About=20section=20=E2=80=94?= =?UTF-8?q?=20vendor,=20versions,=20license,=20contact?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Procurement personas found no vendor, version, license, or support information anywhere in the app (a hard checklist fail for regulated buyers). Settings gains an About category: product name, Studio + running-hub version, vendor Flemming.AI, author, Apache 2.0 license, contact address, and the docs URL — every value selectable and one-click copyable. Studio version constant moved to data/about_info.dart so sidebar tag and About can never drift. Bumps Studio to 0.71.0. Signed-off-by: flemming-it --- lib/data/about_info.dart | 14 ++++ lib/data/hub.dart | 16 ++++ lib/l10n/app_de.arb | 15 ++++ lib/l10n/app_en.arb | 15 ++++ lib/l10n/app_localizations.dart | 90 +++++++++++++++++++++ lib/l10n/app_localizations_de.dart | 46 +++++++++++ lib/l10n/app_localizations_en.dart | 46 +++++++++++ lib/main.dart | 8 +- lib/widgets/chain_settings_dialog.dart | 107 ++++++++++++++++++++++++- 9 files changed, 352 insertions(+), 5 deletions(-) create mode 100644 lib/data/about_info.dart diff --git a/lib/data/about_info.dart b/lib/data/about_info.dart new file mode 100644 index 0000000..5e8fef3 --- /dev/null +++ b/lib/data/about_info.dart @@ -0,0 +1,14 @@ +// Product identity shown in Settings → About and the sidebar +// version tag. One place to bump so procurement-facing facts +// (vendor, license, contact) can never drift between surfaces. + +/// Studio's own build version. Bump on every UI release so the +/// running app self-identifies. +const String kStudioVersion = '0.71.0'; + +const String kProductName = 'Ch∆In Studio'; +const String kVendorName = 'Flemming.AI (F∆I)'; +const String kAuthorName = 'Dr. Stefan Flemming'; +const String kLicenseName = 'Apache 2.0'; +const String kContactEmail = 'chain@flemming.ai'; +const String kDocsUrl = 'https://chain.flemming.ai'; diff --git a/lib/data/hub.dart b/lib/data/hub.dart index 992e9af..f51dc49 100644 --- a/lib/data/hub.dart +++ b/lib/data/hub.dart @@ -1187,6 +1187,22 @@ class HubService { } /// Composite "doctor" snapshot. One round-trip per piece, run + /// Version string of the RUNNING hub (from CheckUpdate's + /// local_version — a local RPC field; the manifest fetch result + /// is ignored). Empty when the hub is unreachable. Used by + /// Settings → About. + Future hubVersion() async { + try { + final r = await _client.checkUpdate().timeout( + const Duration(seconds: 2), + onTimeout: () => CheckUpdateResponse(), + ); + return r.localVersion; + } catch (_) { + return ''; + } + } + /// in parallel so the page populates fast. Future doctor() async { final results = await Future.wait([ diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 2de43fe..21990d5 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1549,6 +1549,21 @@ "doctorPathStudioErrors": "Studio-Fehler", "settingsCategoryGeneral": "Allgemein", "settingsCategoryAppearance": "Darstellung", + "settingsCategoryAbout": "Über", + "aboutPanelTitle": "Über Ch∆In", + "aboutPanelBody": "Produkt-, Hersteller- und Lizenzangaben dieser Installation — alle Werte sind kopierbar.", + "aboutProduct": "Produkt", + "aboutStudioVersion": "Studio-Version", + "aboutHubVersion": "Hub-Version", + "aboutHubVersionUnknown": "nicht verbunden", + "aboutVendor": "Hersteller", + "aboutAuthor": "Autor", + "aboutLicense": "Lizenz", + "aboutContact": "Kontakt", + "aboutDocs": "Dokumentation", + "aboutOpenLink": "Im Browser öffnen", + "aboutCopyTooltip": "Kopieren", + "aboutCopiedToast": "In die Zwischenablage kopiert.", "sidebarSearchLabel": "Suchen & Befehle", "settingsSidebarPinnedTitle": "Navigation immer ausgeklappt", "settingsSidebarPinnedBody": "Zeigt die Beschriftungen der Seitenleiste dauerhaft an, statt sie nur beim Überfahren mit der Maus einzublenden.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7d57dc1..106f3db 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1573,6 +1573,21 @@ "doctorPathStudioErrors": "Studio errors", "settingsCategoryGeneral": "General", "settingsCategoryAppearance": "Appearance", + "settingsCategoryAbout": "About", + "aboutPanelTitle": "About Ch∆In", + "aboutPanelBody": "Product, vendor, and license details of this installation — every value is copyable.", + "aboutProduct": "Product", + "aboutStudioVersion": "Studio version", + "aboutHubVersion": "Hub version", + "aboutHubVersionUnknown": "not connected", + "aboutVendor": "Vendor", + "aboutAuthor": "Author", + "aboutLicense": "License", + "aboutContact": "Contact", + "aboutDocs": "Documentation", + "aboutOpenLink": "Open in browser", + "aboutCopyTooltip": "Copy", + "aboutCopiedToast": "Copied to the clipboard.", "sidebarSearchLabel": "Search & commands", "settingsSidebarPinnedTitle": "Keep the navigation expanded", "settingsSidebarPinnedBody": "Shows the sidebar labels permanently instead of only while hovering with the mouse.", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index fc9fc81..eca27bd 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4544,6 +4544,96 @@ abstract class AppLocalizations { /// **'Appearance'** String get settingsCategoryAppearance; + /// No description provided for @settingsCategoryAbout. + /// + /// In en, this message translates to: + /// **'About'** + String get settingsCategoryAbout; + + /// No description provided for @aboutPanelTitle. + /// + /// In en, this message translates to: + /// **'About Ch∆In'** + String get aboutPanelTitle; + + /// No description provided for @aboutPanelBody. + /// + /// In en, this message translates to: + /// **'Product, vendor, and license details of this installation — every value is copyable.'** + String get aboutPanelBody; + + /// No description provided for @aboutProduct. + /// + /// In en, this message translates to: + /// **'Product'** + String get aboutProduct; + + /// No description provided for @aboutStudioVersion. + /// + /// In en, this message translates to: + /// **'Studio version'** + String get aboutStudioVersion; + + /// No description provided for @aboutHubVersion. + /// + /// In en, this message translates to: + /// **'Hub version'** + String get aboutHubVersion; + + /// No description provided for @aboutHubVersionUnknown. + /// + /// In en, this message translates to: + /// **'not connected'** + String get aboutHubVersionUnknown; + + /// No description provided for @aboutVendor. + /// + /// In en, this message translates to: + /// **'Vendor'** + String get aboutVendor; + + /// No description provided for @aboutAuthor. + /// + /// In en, this message translates to: + /// **'Author'** + String get aboutAuthor; + + /// No description provided for @aboutLicense. + /// + /// In en, this message translates to: + /// **'License'** + String get aboutLicense; + + /// No description provided for @aboutContact. + /// + /// In en, this message translates to: + /// **'Contact'** + String get aboutContact; + + /// No description provided for @aboutDocs. + /// + /// In en, this message translates to: + /// **'Documentation'** + String get aboutDocs; + + /// No description provided for @aboutOpenLink. + /// + /// In en, this message translates to: + /// **'Open in browser'** + String get aboutOpenLink; + + /// No description provided for @aboutCopyTooltip. + /// + /// In en, this message translates to: + /// **'Copy'** + String get aboutCopyTooltip; + + /// No description provided for @aboutCopiedToast. + /// + /// In en, this message translates to: + /// **'Copied to the clipboard.'** + String get aboutCopiedToast; + /// No description provided for @sidebarSearchLabel. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index f05a2af..58a845d 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2666,6 +2666,52 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settingsCategoryAppearance => 'Darstellung'; + @override + String get settingsCategoryAbout => 'Über'; + + @override + String get aboutPanelTitle => 'Über Ch∆In'; + + @override + String get aboutPanelBody => + 'Produkt-, Hersteller- und Lizenzangaben dieser Installation — alle Werte sind kopierbar.'; + + @override + String get aboutProduct => 'Produkt'; + + @override + String get aboutStudioVersion => 'Studio-Version'; + + @override + String get aboutHubVersion => 'Hub-Version'; + + @override + String get aboutHubVersionUnknown => 'nicht verbunden'; + + @override + String get aboutVendor => 'Hersteller'; + + @override + String get aboutAuthor => 'Autor'; + + @override + String get aboutLicense => 'Lizenz'; + + @override + String get aboutContact => 'Kontakt'; + + @override + String get aboutDocs => 'Dokumentation'; + + @override + String get aboutOpenLink => 'Im Browser öffnen'; + + @override + String get aboutCopyTooltip => 'Kopieren'; + + @override + String get aboutCopiedToast => 'In die Zwischenablage kopiert.'; + @override String get sidebarSearchLabel => 'Suchen & Befehle'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 761dbe2..84ff859 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2668,6 +2668,52 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settingsCategoryAppearance => 'Appearance'; + @override + String get settingsCategoryAbout => 'About'; + + @override + String get aboutPanelTitle => 'About Ch∆In'; + + @override + String get aboutPanelBody => + 'Product, vendor, and license details of this installation — every value is copyable.'; + + @override + String get aboutProduct => 'Product'; + + @override + String get aboutStudioVersion => 'Studio version'; + + @override + String get aboutHubVersion => 'Hub version'; + + @override + String get aboutHubVersionUnknown => 'not connected'; + + @override + String get aboutVendor => 'Vendor'; + + @override + String get aboutAuthor => 'Author'; + + @override + String get aboutLicense => 'License'; + + @override + String get aboutContact => 'Contact'; + + @override + String get aboutDocs => 'Documentation'; + + @override + String get aboutOpenLink => 'Open in browser'; + + @override + String get aboutCopyTooltip => 'Copy'; + + @override + String get aboutCopiedToast => 'Copied to the clipboard.'; + @override String get sidebarSearchLabel => 'Search & commands'; diff --git a/lib/main.dart b/lib/main.dart index f2e1244..fb2f5a8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart' show defaultTargetPlatform; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'data/about_info.dart'; import 'data/chain_log.dart'; import 'data/error_presentation.dart'; import 'data/hub.dart'; @@ -33,10 +34,9 @@ import 'theme/tokens.dart'; import 'widgets/chain_search_palette.dart'; import 'widgets/widgets.dart'; -/// Studio's own build version. Bump on every UI commit so the -/// running app self-identifies — visible in the sidebar header -/// and quick-glance proof that you're seeing the current build. -const String kStudioVersion = '0.70.0'; +/// Studio's own build version — canonical value lives in +/// data/about_info.dart (shared with Settings → About). +export 'data/about_info.dart' show kStudioVersion; /// Run one pre-frame restore step, absorbing any failure. Everything /// before `runApp` is a black window waiting to happen: an exception diff --git a/lib/widgets/chain_settings_dialog.dart b/lib/widgets/chain_settings_dialog.dart index 24263d6..7fdf1d5 100644 --- a/lib/widgets/chain_settings_dialog.dart +++ b/lib/widgets/chain_settings_dialog.dart @@ -4,7 +4,9 @@ import 'package:chain_client_sdk/chain_client_sdk.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show Clipboard, ClipboardData; +import '../data/about_info.dart'; import '../data/error_presentation.dart'; import '../data/hub.dart'; import '../data/hub_auth_token.dart'; @@ -41,7 +43,15 @@ class ChainSettingsDialog extends StatefulWidget { /// Sidebar categories — every Settings panel belongs to exactly /// one. Order here drives the sidebar order and the /// first-opened category. -enum _Category { general, appearance, ai, integrations, security, maintenance } +enum _Category { + general, + appearance, + ai, + integrations, + security, + maintenance, + about, +} class _FaiSettingsDialogState extends State { late final TextEditingController _host; @@ -70,6 +80,10 @@ class _FaiSettingsDialogState extends State { /// auth-policy panel below re-queries with the new credentials. int _authPolicyReloadTick = 0; + /// Running hub's version for the About panel. Null while + /// loading, empty string when the hub is unreachable. + String? _hubVersion; + @override void initState() { super.initState(); @@ -83,6 +97,13 @@ class _FaiSettingsDialogState extends State { _loadN8nEndpoints(); _loadRegistryToken(); _loadHubAuthToken(); + _loadHubVersion(); + } + + Future _loadHubVersion() async { + final v = await HubService.instance.hubVersion(); + if (!mounted) return; + setState(() => _hubVersion = v); } Future _loadHubAuthToken() async { @@ -392,6 +413,7 @@ class _FaiSettingsDialogState extends State { _Category.integrations => _integrationsPanel(l, theme), _Category.security => _securityPanel(l, theme), _Category.maintenance => _maintenancePanel(l, theme), + _Category.about => _aboutPanel(l, theme), }; return SingleChildScrollView( padding: const EdgeInsets.fromLTRB( @@ -621,6 +643,39 @@ class _FaiSettingsDialogState extends State { ]; } + List _aboutPanel(AppLocalizations l, ThemeData theme) { + // Procurement-facing identity: who makes this, which + // versions run, under which license, whom to contact. + // Every value is selectable AND has a copy affordance. + return [ + _panelTitle(l.aboutPanelTitle, l.aboutPanelBody, theme), + _AboutRow(label: l.aboutProduct, value: kProductName), + _AboutRow(label: l.aboutStudioVersion, value: kStudioVersion), + _AboutRow( + label: l.aboutHubVersion, + value: _hubVersion == null + ? '…' + : _hubVersion!.isEmpty + ? l.aboutHubVersionUnknown + : _hubVersion!, + ), + _AboutRow(label: l.aboutVendor, value: kVendorName), + _AboutRow(label: l.aboutAuthor, value: kAuthorName), + _AboutRow(label: l.aboutLicense, value: kLicenseName), + _AboutRow(label: l.aboutContact, value: kContactEmail), + _AboutRow( + label: l.aboutDocs, + value: kDocsUrl, + trailing: IconButton( + icon: const Icon(Icons.open_in_new, size: 16), + tooltip: l.aboutOpenLink, + visualDensity: VisualDensity.compact, + onPressed: () => SystemActions.openInOs(kDocsUrl), + ), + ), + ]; + } + List _aiPanel(AppLocalizations l, ThemeData theme) { return [ _panelTitle( @@ -779,6 +834,55 @@ class _FaiSettingsDialogState extends State { } } +/// Label/value row of the About panel. The value is selectable +/// and carries an explicit copy button (procurement reviewers +/// paste these into checklists). +class _AboutRow extends StatelessWidget { + final String label; + final String value; + final Widget? trailing; + + const _AboutRow({required this.label, required this.value, this.trailing}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; + return Padding( + padding: const EdgeInsets.symmetric(vertical: ChainSpace.xs), + child: Row( + children: [ + SizedBox( + width: 150, + child: Text( + label, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + Expanded( + child: SelectableText(value, style: theme.textTheme.bodyMedium), + ), + IconButton( + icon: const Icon(Icons.copy_outlined, size: 14), + tooltip: l.aboutCopyTooltip, + visualDensity: VisualDensity.compact, + onPressed: () async { + await Clipboard.setData(ClipboardData(text: value)); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l.aboutCopiedToast)), + ); + }, + ), + ?trailing, + ], + ), + ); + } +} + class _ChannelRow extends StatelessWidget { final ChannelInfo channel; final bool active; @@ -2635,6 +2739,7 @@ class _Sidebar extends StatelessWidget { Icons.build_outlined, l.settingsCategoryMaintenance, ), + (_Category.about, Icons.info_outline, l.settingsCategoryAbout), ]; return SizedBox( width: 220,