refactor(decnet_web/CanaryTokens): wire hook + bump coverage floor

Final integration step. The page shell is now a thin composition
of useCanaryTokens + the previously-extracted children:

- CanaryTokens.tsx: 1,334 -> 210 LOC. Page owns only the
  pure-UI state (tab, search/state/scope filters, modal
  visibility, drawer selection, local fileDrops log) and the
  thin handlers that translate hook results into confirm/alert
  prompts. Initial parallel fetch + deleteBlob mutation moved
  to useCanaryTokens in the prior commit.
- Modals plug directly into the hook's optimistic helpers
  (prependToken / prependBlob / markTokenRevoked) so the page
  doesn't reach into the data shape.

Coverage floor bumped after the split:

  lines       11 -> 14
  functions   10 -> 13
  branches     8 -> 11
  statements  11 -> 13

Phase 3 final scoreboard: 28 test files, 131 tests, all green.
This commit is contained in:
2026-05-09 05:17:52 -04:00
parent c5cbe084cb
commit 6ba12cc571
2 changed files with 61 additions and 307 deletions

View File

@@ -1,74 +1,46 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Plus, Upload, Search, Target } from '../icons';
import api from '../utils/api';
import React, { useEffect, useState } from 'react';
import { Plus, Upload, Target } from '../icons';
import CanaryTokenDrawer from './CanaryTokenDrawer';
import type { CanaryTokenRow } from './CanaryTokenDrawer';
import {
STATE_COLOR,
type BlobRow, type DeckyOption, type TopologyOption,
} from './CanaryTokens/types';
import { extractError, fmt, fmtBytes } from './CanaryTokens/helpers';
import { INPUT_STYLE, Stat } from './CanaryTokens/ui';
import { STATE_COLOR } from './CanaryTokens/types';
import { Stat } from './CanaryTokens/ui';
import { extractError } from './CanaryTokens/helpers';
import { useCanaryTokens } from './CanaryTokens/useCanaryTokens';
import { CreateTokenModal } from './CanaryTokens/CreateTokenModal';
import { UploadModal } from './CanaryTokens/UploadModal';
import {
FileDropModal, loadFileDrops, saveFileDrops,
type FileDropEntry,
} from './CanaryTokens/FileDropModal';
import {
TokenListView,
type StateFilter, type ScopeFilter,
} from './CanaryTokens/TokenListView';
import { BlobListView } from './CanaryTokens/BlobListView';
import { FileDropListView } from './CanaryTokens/FileDropListView';
// ─── MAIN PAGE ─────────────────────────────────────────────────────────────
type Tab = 'tokens' | 'blobs' | 'filedrops';
const CanaryTokens: React.FC = () => {
const [tokens, setTokens] = useState<CanaryTokenRow[]>([]);
const [blobs, setBlobs] = useState<BlobRow[]>([]);
const [deckies, setDeckies] = useState<DeckyOption[]>([]);
const [topologies, setTopologies] = useState<TopologyOption[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [tab, setTab] = useState<'tokens' | 'blobs' | 'filedrops'>('tokens');
const [fileDrops, setFileDrops] = useState<FileDropEntry[]>(() => loadFileDrops());
const [showFileDrop, setShowFileDrop] = useState(false);
const [filter, setFilter] = useState('');
const [stateFilter, setStateFilter] = useState<'all' | 'planted' | 'revoked' | 'failed'>('all');
const [scopeFilter, setScopeFilter] = useState<'all' | 'fleet' | 'topology'>('all');
const {
tokens, blobs, deckies, topologies, loading, error,
prependToken, prependBlob, markTokenRevoked, deleteBlob,
} = useCanaryTokens();
// Pure-UI state. The local fileDrops log lives entirely in the
// browser; the server doesn't persist it.
const [tab, setTab] = useState<Tab>('tokens');
const [fileDrops, setFileDrops] = useState<FileDropEntry[]>(() => loadFileDrops());
const [filter, setFilter] = useState('');
const [stateFilter, setStateFilter] = useState<StateFilter>('all');
const [scopeFilter, setScopeFilter] = useState<ScopeFilter>('all');
const [showCreate, setShowCreate] = useState(false);
const [showUpload, setShowUpload] = useState(false);
const [showFileDrop, setShowFileDrop] = useState(false);
const [drawerToken, setDrawerToken] = useState<CanaryTokenRow | null>(null);
const loadAll = async () => {
setLoading(true);
setError(null);
try {
const [t, b, d, topos] = await Promise.all([
api.get('/canary/tokens'),
api.get('/canary/blobs').catch(() => ({ data: { blobs: [] } })), // viewers can't list blobs
api.get<DeckyOption[]>('/deckies').catch(() => ({ data: [] })),
// Active topologies only — planting on a torn-down or pending
// topology would 422/404 anyway. Endpoint shape: { data: [...] }
// Trailing slash matters: FastAPI's slash-redirect issues a 307
// and the browser re-fires without the Authorization header,
// landing as 401 on the redirected URL. Hit the canonical
// path (/topologies/) directly.
api.get('/topologies/?status=active').catch(() => ({ data: { data: [] } })),
]);
setTokens(t.data.tokens || []);
setBlobs(b.data.blobs || []);
setDeckies(Array.isArray(d.data) ? d.data : []);
const topoRows: Array<{ id: string; name: string; status: string }> =
topos.data?.data ?? [];
setTopologies(topoRows.map((r) => ({ id: r.id, name: r.name, status: r.status })));
} catch (err) {
setError(extractError(err, 'Failed to load canary tokens.'));
} finally {
setLoading(false);
}
};
useEffect(() => { loadAll(); }, []);
// Alt+C / Alt+D — open create-token / drop-file modals (per
// feedback_linux_meta_key — never Meta/⌘ on Linux).
// Alt+C / Alt+D — open create-token / drop-file modals
// (per feedback_linux_meta_key — never Meta/⌘ on Linux).
useEffect(() => {
const handler = (e: KeyboardEvent) => {
const anyModalOpen = showCreate || showUpload || showFileDrop || drawerToken;
@@ -85,41 +57,25 @@ const CanaryTokens: React.FC = () => {
return () => window.removeEventListener('keydown', handler);
}, [showCreate, showUpload, showFileDrop, drawerToken]);
const visibleTokens = useMemo(() => {
return tokens.filter((t) => {
if (stateFilter !== 'all' && t.state !== stateFilter) return false;
if (scopeFilter === 'fleet' && t.topology_id) return false;
if (scopeFilter === 'topology' && !t.topology_id) return false;
if (!filter) return true;
const f = filter.toLowerCase();
return (
t.decky_name.toLowerCase().includes(f) ||
t.placement_path.toLowerCase().includes(f) ||
t.callback_token.toLowerCase().includes(f) ||
(t.generator || '').toLowerCase().includes(f) ||
(t.instrumenter || '').toLowerCase().includes(f) ||
(t.topology_id || '').toLowerCase().includes(f)
);
});
}, [tokens, filter, stateFilter, scopeFilter]);
const counts = useMemo(() => {
const counts = (() => {
const c = { planted: 0, revoked: 0, failed: 0, hits: 0 };
for (const t of tokens) {
c[t.state] += 1;
c.hits += t.trigger_count;
}
return c;
}, [tokens]);
})();
const handleDeleteBlob = async (uuid: string) => {
if (!window.confirm('Delete this blob? Refused if any token still references it.')) return;
try {
await api.delete(`/canary/blobs/${encodeURIComponent(uuid)}`);
setBlobs((prev) => prev.filter((b) => b.uuid !== uuid));
} catch (err) {
alert(extractError(err, 'Delete failed.'));
}
const r = await deleteBlob(uuid);
if (!r.ok) alert(extractError(r.reason, 'Delete failed.'));
};
const handleClearFileDrops = () => {
if (!window.confirm('Clear local file drop history? This does not delete dropped files.')) return;
setFileDrops([]);
saveFileDrops([]);
};
return (
@@ -178,226 +134,26 @@ const CanaryTokens: React.FC = () => {
</div>
{tab === 'tokens' && (
<>
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px', alignItems: 'center', flexWrap: 'wrap' }}>
<div style={{ position: 'relative', flex: '1 1 300px' }}>
<Search size={14} style={{ position: 'absolute', left: '10px', top: '50%', transform: 'translateY(-50%)', opacity: 0.5 }} />
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter by decky / path / slug / generator…"
style={{ ...INPUT_STYLE, paddingLeft: '32px', marginBottom: 0 }}
/>
</div>
<select
value={stateFilter}
onChange={(e) => setStateFilter(e.target.value as typeof stateFilter)}
style={{ ...INPUT_STYLE, marginBottom: 0, width: 'auto' }}
>
<option value="all">all states</option>
<option value="planted">planted</option>
<option value="revoked">revoked</option>
<option value="failed">failed</option>
</select>
<select
value={scopeFilter}
onChange={(e) => setScopeFilter(e.target.value as typeof scopeFilter)}
style={{ ...INPUT_STYLE, marginBottom: 0, width: 'auto' }}
>
<option value="all">all scopes</option>
<option value="fleet">fleet only</option>
<option value="topology">topology only</option>
</select>
</div>
{loading && <div style={{ opacity: 0.6 }}>loading</div>}
{error && <div style={{ color: '#ff5555', marginBottom: '16px' }}>{error}</div>}
{!loading && visibleTokens.length === 0 && (
<div style={{ textAlign: 'center', padding: '40px', opacity: 0.6, fontSize: '0.85rem' }}>
{tokens.length === 0
? 'No canary tokens yet. Click NEW TOKEN to plant one, or UPLOAD ARTIFACT to start with an operator-supplied document.'
: 'No tokens match the current filter.'}
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
{visibleTokens.map((t) => (
<button
key={t.uuid}
onClick={() => setDrawerToken(t)}
style={{
display: 'grid',
gridTemplateColumns: '110px 80px 140px 1fr 100px 110px 80px',
alignItems: 'center', gap: '12px',
padding: '10px 14px',
border: '1px solid var(--border-color, #30363d)',
background: 'var(--matrix-tint-5)',
color: 'var(--text-color)',
cursor: 'pointer',
textAlign: 'left',
fontSize: '0.8rem',
}}
>
<span style={{
color: STATE_COLOR[t.state], fontFamily: 'monospace',
fontSize: '0.7rem', letterSpacing: '0.05em',
}}>
{t.state.toUpperCase()}
</span>
<span
title={t.topology_id ? `topology ${t.topology_id}` : 'fleet'}
style={{
fontSize: '0.65rem', letterSpacing: '0.05em',
padding: '2px 6px',
border: `1px solid ${t.topology_id ? 'var(--accent-color, #00ff88)' : 'var(--dim-color)'}`,
color: t.topology_id ? 'var(--accent-color, #00ff88)' : 'var(--dim-color)',
textAlign: 'center',
textTransform: 'uppercase',
}}
>
{t.topology_id ? 'topology' : 'fleet'}
</span>
<span style={{ fontFamily: 'monospace' }}>{t.decky_name}</span>
<span style={{ fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{t.placement_path}
</span>
<span style={{ fontSize: '0.7rem', opacity: 0.7 }}>
{t.kind === 'aws_passive' ? 'aws-passive' : t.kind}
</span>
<span style={{ fontSize: '0.7rem', opacity: 0.7, fontFamily: 'monospace' }}>
{t.generator || t.instrumenter || '?'}
</span>
<span style={{ textAlign: 'right', fontFamily: 'monospace', color: t.trigger_count > 0 ? '#00ff88' : 'var(--dim-color)' }}>
{t.trigger_count} {t.trigger_count === 1 ? 'hit' : 'hits'}
</span>
</button>
))}
</div>
</>
<TokenListView
tokens={tokens}
loading={loading}
error={error}
filter={filter}
setFilter={setFilter}
stateFilter={stateFilter}
setStateFilter={setStateFilter}
scopeFilter={scopeFilter}
setScopeFilter={setScopeFilter}
onPick={setDrawerToken}
/>
)}
{tab === 'blobs' && (
<>
{blobs.length === 0 && (
<div style={{ textAlign: 'center', padding: '40px', opacity: 0.6, fontSize: '0.85rem' }}>
No uploaded artifacts. Click UPLOAD ARTIFACT to add one.
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
{blobs.map((b) => (
<div
key={b.uuid}
style={{
display: 'grid',
gridTemplateColumns: '1fr 220px 90px 100px 80px',
alignItems: 'center', gap: '12px',
padding: '10px 14px',
border: '1px solid var(--border-color, #30363d)',
background: 'var(--matrix-tint-5)',
fontSize: '0.8rem',
}}
>
<span style={{ fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{b.filename}
</span>
<span style={{ fontSize: '0.7rem', opacity: 0.7, fontFamily: 'monospace' }}>{b.content_type}</span>
<span style={{ fontSize: '0.7rem', opacity: 0.7 }}>{fmtBytes(b.size_bytes)}</span>
<span style={{ fontSize: '0.7rem', opacity: 0.7 }}>{fmt(b.uploaded_at)}</span>
<button
onClick={() => handleDeleteBlob(b.uuid)}
disabled={b.token_count > 0}
title={b.token_count > 0 ? `${b.token_count} token(s) still reference this blob` : 'Delete'}
style={{
background: 'transparent', color: b.token_count > 0 ? 'var(--dim-color)' : '#ff5555',
border: `1px solid ${b.token_count > 0 ? 'var(--dim-color)' : '#ff5555'}`,
padding: '4px 8px', fontSize: '0.7rem',
cursor: b.token_count > 0 ? 'not-allowed' : 'pointer',
opacity: b.token_count > 0 ? 0.4 : 1,
}}
>
{b.token_count > 0 ? `${b.token_count} REFS` : 'DELETE'}
</button>
</div>
))}
</div>
</>
<BlobListView blobs={blobs} onDelete={handleDeleteBlob} />
)}
{tab === 'filedrops' && (
<>
<div style={{
display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '12px',
justifyContent: 'space-between',
}}>
<div style={{ fontSize: '0.75rem', opacity: 0.6 }}>
Local log only the server doesn't persist file drops.
Cleared when you clear browser storage.
</div>
{fileDrops.length > 0 && (
<button
onClick={() => {
if (window.confirm('Clear local file drop history? This does not delete dropped files.')) {
setFileDrops([]);
saveFileDrops([]);
}
}}
style={{
padding: '4px 10px',
border: '1px solid var(--dim-color)',
background: 'transparent', color: 'var(--dim-color)',
fontSize: '0.7rem', cursor: 'pointer',
textTransform: 'uppercase',
}}
>
CLEAR LIST
</button>
)}
</div>
{fileDrops.length === 0 && (
<div style={{ textAlign: 'center', padding: '40px', opacity: 0.6, fontSize: '0.85rem' }}>
No file drops in this browser yet. Click DROP FILE to send bytes to a decky.
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
{fileDrops.map((fd) => (
<div
key={fd.id}
style={{
display: 'grid',
gridTemplateColumns: '80px 140px 1fr 90px 80px 140px',
alignItems: 'center', gap: '12px',
padding: '10px 14px',
border: '1px solid var(--border-color, #30363d)',
background: 'var(--matrix-tint-5)',
fontSize: '0.8rem',
}}
>
<span
title={fd.topology_id ? `topology ${fd.topology_id}` : 'fleet'}
style={{
fontSize: '0.65rem', letterSpacing: '0.05em',
padding: '2px 6px',
border: `1px solid ${fd.topology_id ? 'var(--accent-color, #00ff88)' : 'var(--dim-color)'}`,
color: fd.topology_id ? 'var(--accent-color, #00ff88)' : 'var(--dim-color)',
textAlign: 'center',
textTransform: 'uppercase',
}}
>
{fd.topology_id ? 'topology' : 'fleet'}
</span>
<span style={{ fontFamily: 'monospace' }}>{fd.decky_name}</span>
<span
title={`${fd.filename} → ${fd.path}`}
style={{ fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
>
{fd.path}
</span>
<span style={{ fontSize: '0.7rem', opacity: 0.7 }}>{fmtBytes(fd.size_bytes)}</span>
<span style={{ fontSize: '0.7rem', opacity: 0.7, fontFamily: 'monospace' }}>{fd.mode.toString(8)}</span>
<span style={{ fontSize: '0.7rem', opacity: 0.7 }}>{fmt(fd.dropped_at)}</span>
</div>
))}
</div>
</>
<FileDropListView fileDrops={fileDrops} onClear={handleClearFileDrops} />
)}
{showCreate && (
@@ -407,7 +163,7 @@ const CanaryTokens: React.FC = () => {
topologies={topologies}
onClose={() => setShowCreate(false)}
onCreated={(t) => {
setTokens((prev) => [t, ...prev]);
prependToken(t);
setShowCreate(false);
}}
/>
@@ -416,7 +172,7 @@ const CanaryTokens: React.FC = () => {
<UploadModal
onClose={() => setShowUpload(false)}
onUploaded={(b) => {
setBlobs((prev) => prev.some((x) => x.uuid === b.uuid) ? prev : [b, ...prev]);
prependBlob(b);
setShowUpload(false);
}}
/>
@@ -426,9 +182,7 @@ const CanaryTokens: React.FC = () => {
token={drawerToken}
onClose={() => setDrawerToken(null)}
onRevoked={(uuid) => {
setTokens((prev) => prev.map((t) =>
t.uuid === uuid ? { ...t, state: 'revoked' } : t,
));
markTokenRevoked(uuid);
setDrawerToken(null);
}}
/>

View File

@@ -15,14 +15,14 @@ export default defineConfig({
include: ['src/**/*.{ts,tsx}'],
exclude: ['src/**/*.d.ts', 'src/test/**', 'src/main.tsx'],
// Baseline floors. Each refactor PR raises these; never lower.
// Phase 2 (DeckyFleet split): page shell down from 1,674 to 274
// LOC; hook + 6 children, 28 new tests. Suite: 21 files, 98 tests,
// 11.95% lines / 8.78% branches.
// Phase 3 (CanaryTokens split): page shell down from 1,334 to 210
// LOC; hook + 3 modals + 3 list views + ui/types/helpers, 33 new
// tests. Suite: 28 files, 131 tests, 14.51% lines / 11.43% branches.
thresholds: {
lines: 11,
functions: 10,
branches: 8,
statements: 11,
lines: 14,
functions: 13,
branches: 11,
statements: 13,
},
},
},