fix(doctor): honest module count, temp-path audit warning, connection line
Some checks are pending
Security / Security check (push) Waiting to run

Three doctor-page findings from the usertest panel:

- Module tile no longer counts the hub's built-in 'system'
  pseudo-module — a fresh hub shows 0 modules, matching the
  welcome checklist's definition of an install. Counting is a
  top-level function with unit tests.
- When the audit DB lives in an OS-cleanable temp directory
  (/var/folders, /tmp, Windows Temp), the daemon-files panel
  says so instead of presenting the state as healthy. The
  classifier is a top-level function with unit tests.
- The daemon card states who-talks-to-whom-how in one line:
  endpoint, transport security (TLS / unencrypted-local /
  unencrypted), and whether a bearer token is attached (length
  only) — the auditor's baseline the green dot cannot answer.

Also syncs pubspec.yaml (0.70.0 -> 0.72.0) with kStudioVersion,
which had drifted to 0.71.0 while pubspec stayed behind.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-18 00:56:46 +02:00
parent afa3fa7eae
commit 703d961cec
10 changed files with 303 additions and 3 deletions

View file

@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import '../data/error_presentation.dart';
import '../data/chain_log.dart';
import '../data/hub.dart';
import '../data/hub_auth_token.dart';
import '../data/system_actions.dart';
import '../l10n/app_localizations.dart';
import '../theme/theme.dart';
@ -334,6 +335,24 @@ class _EventLogPanel extends StatelessWidget {
}
}
/// True when [path] points into an OS-managed temporary
/// directory (macOS `/var/folders`, Unix `/tmp`, the Windows
/// user/system temp folders) locations the OS may clean up
/// at any time. The doctor page warns when the audit DB lives
/// in one: an evidence log the OS can silently delete is not
/// evidence. Top-level so the unit test can exercise the
/// classification directly.
bool isVolatilePath(String path) {
if (path.isEmpty) return false;
final p = path.toLowerCase().replaceAll('\\', '/');
return p.startsWith('/tmp/') ||
p.startsWith('/private/tmp/') ||
p.startsWith('/var/folders/') ||
p.startsWith('/private/var/folders/') ||
p.contains('/appdata/local/temp/') ||
p.contains('/windows/temp/');
}
/// Lists the daemon's filesystem paths with one "Open" button
/// per row. Windows operators who never touch a shell still
/// need a way to inspect the audit DB or edit the operator
@ -374,10 +393,33 @@ class _DaemonPathsPanel extends StatelessWidget {
);
}
final theme = Theme.of(context);
return ChainCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isVolatilePath(paths.dbPath)) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
Icons.warning_amber_outlined,
size: 16,
color: ChainColors.warning,
),
const SizedBox(width: ChainSpace.sm),
Expanded(
child: Text(
l.doctorDbVolatileWarning,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
],
),
const SizedBox(height: ChainSpace.sm),
],
for (final e in entries)
_PathRow(label: e.$1, path: e.$2, icon: e.$3, isDirectory: e.$4),
],
@ -512,10 +554,20 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
bool _binaryMissing = false;
ChannelStatusSnapshot? _channels;
/// Trimmed length of `~/.chain/hub-auth-token`, or null when
/// Studio talks to the hub anonymously. Length only the
/// secret itself never reaches the UI.
int? _tokenChars;
@override
void initState() {
super.initState();
_refreshStatus();
HubAuthToken.charCount()
.then((n) {
if (mounted) setState(() => _tokenChars = n);
})
.catchError((_) {});
}
Future<void> _refreshStatus() async {
@ -528,6 +580,21 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
}
}
String _connectionLine(AppLocalizations l) {
final ep = HubService.instance.currentEndpoint;
final isLocal =
ep.host == '127.0.0.1' || ep.host == 'localhost' || ep.host == '::1';
final transport = ep.secure
? l.doctorConnTls
: isLocal
? l.doctorConnPlainLocal
: l.doctorConnPlainRemote;
final auth = _tokenChars == null
? l.doctorConnAnonymous
: l.doctorConnToken(_tokenChars!);
return l.doctorConnLine(ep.toString(), transport, auth);
}
Future<void> _run(
String label,
Future<({bool ok, String stdout, String stderr})> Function() action,
@ -626,6 +693,17 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 2),
// Who-talks-to-whom-how in one line: endpoint,
// transport security, and whether a bearer token is
// attached the auditor's baseline questions the
// green "connected" dot alone cannot answer.
SelectableText(
_connectionLine(l),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: ChainSpace.md),
Wrap(
spacing: ChainSpace.sm,