style(realism-ui): adopt the persona-page design language
Both pages now layer on DeckyFleet.css + PersonaGeneration.css and use the project's house vocabulary — fleet-root shell, page-header with title-group + actions, btn / btn.violet / btn.ghost, info-banner with the violet left rule, and the dim/matrix/alert text accents. RealismConfig: inputs are flush-styled weight-input fields with a violet focus ring; section heads carry a TOTAL badge; canary rows get the project's amber accent; canary probability lives in a panel-bordered slider row. SyntheticFiles: the inline-styled table is now a styled .files-table with the standard hover affordance, the filter-row uses tweak-group label+select pairs, the drawer carries .drawer-eyebrow / .drawer-title / .meta-grid in the same style as the canary token drawer, and pager buttons share the .btn.ghost.small treatment. No behavioural change.
This commit is contained in:
122
decnet_web/src/components/RealismConfig/RealismConfig.css
Normal file
122
decnet_web/src/components/RealismConfig/RealismConfig.css
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/* Realism Config — layered on DeckyFleet.css.
|
||||||
|
Adds: weight tables (label + numeric input + percent share),
|
||||||
|
canary-class accent, canary-probability slider row. */
|
||||||
|
|
||||||
|
.realism-config-root .mono { font-family: var(--font-mono); }
|
||||||
|
|
||||||
|
.realism-config-root .info-banner {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-left: 3px solid var(--violet);
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.realism-config-root .info-banner em { color: var(--matrix); font-style: normal; }
|
||||||
|
|
||||||
|
/* Section heading above each weight table. */
|
||||||
|
.realism-config-root .section-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
color: var(--dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
.realism-config-root .section-head .total {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--matrix);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.realism-config-root .section-help {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
opacity: 0.45;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin: -4px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Weight table: class label · raw enum · weight input · percent share. */
|
||||||
|
.realism-config-root .weight-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
.realism-config-root .weight-table tr {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.realism-config-root .weight-table tr:last-child { border-bottom: none; }
|
||||||
|
.realism-config-root .weight-table td {
|
||||||
|
padding: 8px 14px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.realism-config-root .weight-table td.cls { width: 50%; }
|
||||||
|
.realism-config-root .weight-table td.cls .cls-label { font-weight: 600; }
|
||||||
|
.realism-config-root .weight-table td.cls .cls-enum {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--dim);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.realism-config-root .weight-table td.cls.canary .cls-label {
|
||||||
|
color: var(--amber, #f59e0b);
|
||||||
|
}
|
||||||
|
.realism-config-root .weight-table td.weight {
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
.realism-config-root .weight-table input.weight-input {
|
||||||
|
width: 90px;
|
||||||
|
background: var(--bg-elev, rgba(0, 0, 0, 0.3));
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--matrix);
|
||||||
|
padding: 5px 9px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.realism-config-root .weight-table input.weight-input:focus {
|
||||||
|
border-color: var(--violet);
|
||||||
|
box-shadow: 0 0 0 1px var(--violet);
|
||||||
|
}
|
||||||
|
.realism-config-root .weight-table td.share {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--dim);
|
||||||
|
width: 80px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Canary probability slider row. */
|
||||||
|
.realism-config-root .prob-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.realism-config-root .prob-row input[type="range"] {
|
||||||
|
flex: 1;
|
||||||
|
accent-color: var(--violet);
|
||||||
|
}
|
||||||
|
.realism-config-root .prob-row .prob-value {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--matrix);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
min-width: 70px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.realism-config-root .footer-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { useToast } from '../Toasts/useToast';
|
import { useToast } from '../Toasts/useToast';
|
||||||
import { Sliders, Save, RotateCcw } from '../../icons';
|
import { Save, RotateCcw, AlertTriangle } from '../../icons';
|
||||||
import { contentClassLabel, isCanaryClass } from '../../realism/labels';
|
import { contentClassLabel, isCanaryClass } from '../../realism/labels';
|
||||||
|
// Reuse the DeckyFleet shell (page-header / btn / fleet-* / dim / mono) and
|
||||||
|
// the persona-page tweaks (info-banner, .input) so the realism config panel
|
||||||
|
// reads the same as the rest of the realism nav group.
|
||||||
|
import '../DeckyFleet.css';
|
||||||
|
import '../PersonaGeneration.css';
|
||||||
|
import './RealismConfig.css';
|
||||||
|
|
||||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -43,73 +49,61 @@ const DEFAULTS: ConfigPayload = {
|
|||||||
canary_probability: 0.03,
|
canary_probability: 0.03,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function pct(weights: WeightEntry[], idx: number): string {
|
|
||||||
const total = weights.reduce((s, w) => s + Math.max(0, w.weight), 0);
|
|
||||||
if (total === 0) return '—';
|
|
||||||
return `${((weights[idx].weight / total) * 100).toFixed(1)}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Subcomponent ────────────────────────────────────────────────────────────
|
// ─── Subcomponent ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const WeightTable: React.FC<{
|
const WeightTable: React.FC<{
|
||||||
title: string;
|
title: string;
|
||||||
|
help: string;
|
||||||
weights: WeightEntry[];
|
weights: WeightEntry[];
|
||||||
onChange: (next: WeightEntry[]) => void;
|
onChange: (next: WeightEntry[]) => void;
|
||||||
}> = ({ title, weights, onChange }) => {
|
}> = ({ title, help, weights, onChange }) => {
|
||||||
const total = weights.reduce((s, w) => s + Math.max(0, w.weight), 0);
|
const total = weights.reduce((s, w) => s + Math.max(0, w.weight), 0);
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: '20px' }}>
|
<>
|
||||||
<div style={{
|
<div className="section-head">
|
||||||
fontSize: '0.7rem', color: 'var(--dim-color)', letterSpacing: '0.1em',
|
<span>{title}</span>
|
||||||
marginBottom: '8px',
|
<span className="total">TOTAL {total}</span>
|
||||||
}}>
|
|
||||||
{title} · TOTAL {total}
|
|
||||||
</div>
|
</div>
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}>
|
<div className="section-help">{help}</div>
|
||||||
|
<table className="weight-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
{weights.map((w, i) => (
|
{weights.map((w, i) => {
|
||||||
<tr key={w.content_class} style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
|
const canary = isCanaryClass(w.content_class);
|
||||||
<td style={{ padding: '6px 12px', width: '45%' }}>
|
const share =
|
||||||
<span style={{ color: isCanaryClass(w.content_class) ? '#ffaa66' : 'inherit' }}>
|
total === 0
|
||||||
{contentClassLabel(w.content_class)}
|
? '—'
|
||||||
</span>
|
: `${((Math.max(0, w.weight) / total) * 100).toFixed(1)}%`;
|
||||||
<span className="mono" style={{
|
return (
|
||||||
marginLeft: 8, fontSize: '0.7rem', color: 'var(--dim-color)',
|
<tr key={w.content_class}>
|
||||||
}}>
|
<td className={`cls${canary ? ' canary' : ''}`}>
|
||||||
{w.content_class}
|
<span className="cls-label">{contentClassLabel(w.content_class)}</span>
|
||||||
</span>
|
<span className="cls-enum">{w.content_class}</span>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: '6px 12px', width: '30%' }}>
|
<td className="weight">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
step={1}
|
step={1}
|
||||||
value={w.weight}
|
className="weight-input"
|
||||||
onChange={(e) => {
|
value={w.weight}
|
||||||
const next = weights.slice();
|
onChange={(e) => {
|
||||||
const v = parseInt(e.target.value, 10);
|
const v = parseInt(e.target.value, 10);
|
||||||
next[i] = { ...next[i], weight: Number.isFinite(v) ? Math.max(0, v) : 0 };
|
const next = weights.slice();
|
||||||
onChange(next);
|
next[i] = {
|
||||||
}}
|
...next[i],
|
||||||
style={{
|
weight: Number.isFinite(v) ? Math.max(0, v) : 0,
|
||||||
width: '80px',
|
};
|
||||||
backgroundColor: 'rgba(255,255,255,0.03)',
|
onChange(next);
|
||||||
color: 'var(--text-color)',
|
}}
|
||||||
border: '1px solid rgba(255,255,255,0.1)',
|
/>
|
||||||
padding: '4px 8px', fontFamily: 'inherit',
|
</td>
|
||||||
}}
|
<td className="share">{share}</td>
|
||||||
/>
|
</tr>
|
||||||
</td>
|
);
|
||||||
<td style={{ padding: '6px 12px', color: 'var(--dim-color)', fontVariantNumeric: 'tabular-nums' }}>
|
})}
|
||||||
{pct(weights, i)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,98 +150,110 @@ const RealismConfig: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
if (!window.confirm('Reset to baked-in defaults? This will overwrite the current saved config on next save.')) return;
|
if (!window.confirm(
|
||||||
|
'Reset to baked-in defaults? This will overwrite the saved config on next save.',
|
||||||
|
)) return;
|
||||||
setConfig(DEFAULTS);
|
setConfig(DEFAULTS);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const totals = useMemo(() => ({
|
||||||
<div style={{ padding: '24px', color: 'var(--text-color)', maxWidth: '900px' }}>
|
user: config.user_class_weights.reduce((s, w) => s + Math.max(0, w.weight), 0),
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
system: config.system_class_weights.reduce((s, w) => s + Math.max(0, w.weight), 0),
|
||||||
<Sliders size={18} />
|
canary: config.canary_class_weights.reduce((s, w) => s + Math.max(0, w.weight), 0),
|
||||||
<h2 style={{ margin: 0, fontSize: '1.1rem', letterSpacing: '0.05em' }}>REALISM CONFIG</h2>
|
}), [config]);
|
||||||
</div>
|
|
||||||
<p style={{ color: 'var(--dim-color)', fontSize: '0.85rem', marginTop: 0 }}>
|
|
||||||
Operator-tuned planner weights. The orchestrator refreshes from the DB
|
|
||||||
every ~5 minutes; saved changes land within one refresh window.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{error && <div style={{ color: '#ff5555', marginBottom: '12px' }}>{error}</div>}
|
return (
|
||||||
|
<div className="fleet-root realism-config-root">
|
||||||
|
<div className="page-header">
|
||||||
|
<div className="page-title-group">
|
||||||
|
<h1>REALISM CONFIG</h1>
|
||||||
|
<span className="page-sub">
|
||||||
|
USER {totals.user} · SYSTEM {totals.system} · CANARY {totals.canary} ·
|
||||||
|
{' '}CANARY PROB {(config.canary_probability * 100).toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="actions">
|
||||||
|
<button
|
||||||
|
className="btn ghost"
|
||||||
|
onClick={handleReset}
|
||||||
|
disabled={saving || loading}
|
||||||
|
title="Reset form fields to baked-in defaults (does not save until you press SAVE)"
|
||||||
|
>
|
||||||
|
<RotateCcw size={12} /> RESET
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn violet"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving || loading}
|
||||||
|
title="Persist current values to realism_config; orchestrator picks them up within one refresh tick (~5 min)."
|
||||||
|
>
|
||||||
|
<Save size={12} /> {saving ? 'SAVING…' : 'SAVE'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="info-banner">
|
||||||
|
<div>
|
||||||
|
<strong>Scope:</strong> tunes the orchestrator's <em>realism planner</em>
|
||||||
|
{' '}— how often each kind of synthetic file lands on a decky, and
|
||||||
|
how rare canary plants are. Persisted in the{' '}
|
||||||
|
<span className="mono matrix-text">realism_config</span> table; the
|
||||||
|
orchestrator refreshes from the DB every ~5 minutes.
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<div className="info-line alert-text" style={{ marginTop: 8 }}>
|
||||||
|
<AlertTriangle size={12} /> {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div style={{ opacity: 0.6 }}>Loading…</div>
|
<div className="dim" style={{ padding: '24px 0' }}>Loading…</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<WeightTable
|
<WeightTable
|
||||||
title="USER CLASS WEIGHTS · written by personas during work hours"
|
title="User Class Weights"
|
||||||
|
help="Files written by personas during their work hours. The realism win when a persona looks busy."
|
||||||
weights={config.user_class_weights}
|
weights={config.user_class_weights}
|
||||||
onChange={(next) => setConfig({ ...config, user_class_weights: next })}
|
onChange={(next) => setConfig({ ...config, user_class_weights: next })}
|
||||||
/>
|
/>
|
||||||
<WeightTable
|
<WeightTable
|
||||||
title="SYSTEM CLASS WEIGHTS · plausible OS-side filler"
|
title="System Class Weights"
|
||||||
|
help="Plausible OS-side filler — rotated logs, daemon noise, ephemeral cache."
|
||||||
weights={config.system_class_weights}
|
weights={config.system_class_weights}
|
||||||
onChange={(next) => setConfig({ ...config, system_class_weights: next })}
|
onChange={(next) => setConfig({ ...config, system_class_weights: next })}
|
||||||
/>
|
/>
|
||||||
<WeightTable
|
<WeightTable
|
||||||
title="CANARY CLASS WEIGHTS · uniform across generators by default"
|
title="Canary Class Weights"
|
||||||
|
help="Callback-bearing artifacts. Uniform across generators by default; raise one to bias toward a specific bait flavour."
|
||||||
weights={config.canary_class_weights}
|
weights={config.canary_class_weights}
|
||||||
onChange={(next) => setConfig({ ...config, canary_class_weights: next })}
|
onChange={(next) => setConfig({ ...config, canary_class_weights: next })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={{ marginBottom: '24px' }}>
|
<div className="section-head">
|
||||||
<div style={{
|
<span>Canary Probability</span>
|
||||||
fontSize: '0.7rem', color: 'var(--dim-color)', letterSpacing: '0.1em',
|
<span className="total">{(config.canary_probability * 100).toFixed(1)}%</span>
|
||||||
marginBottom: '8px',
|
|
||||||
}}>
|
|
||||||
CANARY PROBABILITY · share of file picks that materialise a canary
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.005}
|
|
||||||
value={config.canary_probability}
|
|
||||||
onChange={(e) => setConfig({
|
|
||||||
...config,
|
|
||||||
canary_probability: parseFloat(e.target.value),
|
|
||||||
})}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
|
||||||
<span className="mono" style={{ minWidth: '60px', textAlign: 'right' }}>
|
|
||||||
{(config.canary_probability * 100).toFixed(1)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="section-help">
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
Share of file picks that materialise a canary. Each plant
|
||||||
<button
|
creates a real canary token row + DNS slug or HTTP URL —
|
||||||
className="action-btn"
|
keeping this rare prevents a noisy alert surface.
|
||||||
onClick={handleSave}
|
</div>
|
||||||
disabled={saving}
|
<div className="prob-row">
|
||||||
style={{
|
<input
|
||||||
padding: '8px 16px',
|
type="range"
|
||||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
min={0}
|
||||||
color: 'var(--matrix)',
|
max={1}
|
||||||
borderColor: 'var(--matrix)',
|
step={0.005}
|
||||||
opacity: saving ? 0.5 : 1,
|
value={config.canary_probability}
|
||||||
}}
|
onChange={(e) => setConfig({
|
||||||
title="Persist current values to realism_config; orchestrator picks them up within one refresh tick."
|
...config,
|
||||||
>
|
canary_probability: parseFloat(e.target.value),
|
||||||
<Save size={12} />
|
})}
|
||||||
{saving ? 'SAVING…' : 'SAVE'}
|
/>
|
||||||
</button>
|
<span className="prob-value">
|
||||||
<button
|
{(config.canary_probability * 100).toFixed(1)}%
|
||||||
className="action-btn"
|
</span>
|
||||||
onClick={handleReset}
|
|
||||||
style={{
|
|
||||||
padding: '8px 16px',
|
|
||||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
|
||||||
}}
|
|
||||||
title="Reset form fields to baked-in defaults (does not save until you press SAVE)"
|
|
||||||
>
|
|
||||||
<RotateCcw size={12} />
|
|
||||||
RESET TO DEFAULTS
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
214
decnet_web/src/components/SyntheticFiles/SyntheticFiles.css
Normal file
214
decnet_web/src/components/SyntheticFiles/SyntheticFiles.css
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
/* Synthetic Files — layered on DeckyFleet.css.
|
||||||
|
Adds: filter row of label+select pairs, the file table itself, the
|
||||||
|
right-side detail drawer, and a TRUNCATED chip for capped bodies. */
|
||||||
|
|
||||||
|
.synthetic-files-root .mono { font-family: var(--font-mono); }
|
||||||
|
|
||||||
|
.synthetic-files-root .info-banner {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-left: 3px solid var(--violet);
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.synthetic-files-root .info-banner em { color: var(--matrix); font-style: normal; }
|
||||||
|
|
||||||
|
/* Filter row — three label+select pairs. */
|
||||||
|
.synthetic-files-root .filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
.synthetic-files-root .filter-group { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.synthetic-files-root .filter-group label {
|
||||||
|
font-size: 0.62rem;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
color: var(--dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.synthetic-files-root select.filter-input {
|
||||||
|
background: var(--bg-elev, rgba(0, 0, 0, 0.3));
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 180px;
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
.synthetic-files-root select.filter-input:focus {
|
||||||
|
border-color: var(--violet);
|
||||||
|
box-shadow: 0 0 0 1px var(--violet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File table. */
|
||||||
|
.synthetic-files-root .files-table-wrap {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--panel);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.synthetic-files-root .files-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
.synthetic-files-root .files-table thead tr {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.synthetic-files-root .files-table th {
|
||||||
|
padding: 10px 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
color: var(--dim);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.synthetic-files-root .files-table tbody tr {
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
.synthetic-files-root .files-table tbody tr:hover {
|
||||||
|
background: var(--matrix-tint-10, rgba(0, 255, 65, 0.04));
|
||||||
|
}
|
||||||
|
.synthetic-files-root .files-table td {
|
||||||
|
padding: 8px 12px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.synthetic-files-root .files-table td.path { font-family: var(--font-mono); word-break: break-all; }
|
||||||
|
.synthetic-files-root .files-table td.cls.canary { color: var(--amber, #f59e0b); }
|
||||||
|
.synthetic-files-root .files-table td.hash {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--dim);
|
||||||
|
}
|
||||||
|
.synthetic-files-root .files-table td.dim-time {
|
||||||
|
color: var(--dim);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.synthetic-files-root .files-table .empty-row td {
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination row. */
|
||||||
|
.synthetic-files-root .pager {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.synthetic-files-root .pager .page-counter {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--dim);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drawer — right-side detail panel. */
|
||||||
|
.synthetic-files-root .drawer-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.synthetic-files-root .drawer {
|
||||||
|
width: min(720px, 100%);
|
||||||
|
height: 100%;
|
||||||
|
background: var(--bg-color, #0d1117);
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.synthetic-files-root .drawer-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.synthetic-files-root .drawer-eyebrow {
|
||||||
|
font-size: 0.62rem;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
color: var(--dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.synthetic-files-root .drawer-title {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-top: 4px;
|
||||||
|
word-break: break-all;
|
||||||
|
color: var(--matrix);
|
||||||
|
}
|
||||||
|
.synthetic-files-root .drawer-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drawer meta grid — label / value rows. */
|
||||||
|
.synthetic-files-root .meta-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 140px 1fr;
|
||||||
|
row-gap: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.synthetic-files-root .meta-grid .label {
|
||||||
|
color: var(--dim);
|
||||||
|
font-size: 0.62rem;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
.synthetic-files-root .meta-grid .value-canary { color: var(--amber, #f59e0b); }
|
||||||
|
|
||||||
|
/* Body preview block. */
|
||||||
|
.synthetic-files-root .body-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
color: var(--dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.synthetic-files-root .body-pre {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.synthetic-files-root .truncated-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border: 1px solid var(--amber, #f59e0b);
|
||||||
|
color: var(--amber, #f59e0b);
|
||||||
|
font-size: 0.6rem;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
@@ -2,8 +2,13 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { useEscapeKey } from '../../hooks/useEscapeKey';
|
import { useEscapeKey } from '../../hooks/useEscapeKey';
|
||||||
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||||
import { X, FileText } from '../../icons';
|
import { X } from '../../icons';
|
||||||
import { contentClassLabel, isCanaryClass } from '../../realism/labels';
|
import { contentClassLabel, isCanaryClass } from '../../realism/labels';
|
||||||
|
// Reuse the DeckyFleet shell + the persona-page tweaks so this page reads
|
||||||
|
// the same as the rest of the realism nav group.
|
||||||
|
import '../DeckyFleet.css';
|
||||||
|
import '../PersonaGeneration.css';
|
||||||
|
import './SyntheticFiles.css';
|
||||||
|
|
||||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -43,8 +48,7 @@ const PAGE_SIZE = 50;
|
|||||||
// available without a typo path failing silently.
|
// available without a typo path failing silently.
|
||||||
const CONTENT_CLASSES = [
|
const CONTENT_CLASSES = [
|
||||||
'note', 'todo', 'draft', 'script',
|
'note', 'todo', 'draft', 'script',
|
||||||
'log_cron', 'log_daemon',
|
'log_cron', 'log_daemon', 'cache_tmp',
|
||||||
'cache_tmp', 'config_local',
|
|
||||||
'canary_aws_creds', 'canary_env_file', 'canary_git_config',
|
'canary_aws_creds', 'canary_env_file', 'canary_git_config',
|
||||||
'canary_ssh_key', 'canary_honeydoc', 'canary_honeydoc_docx',
|
'canary_ssh_key', 'canary_honeydoc', 'canary_honeydoc_docx',
|
||||||
'canary_honeydoc_pdf', 'canary_mysql_dump',
|
'canary_honeydoc_pdf', 'canary_mysql_dump',
|
||||||
@@ -100,89 +104,74 @@ const SyntheticFileDrawer: React.FC<DrawerProps> = ({ uuid, deckies, onClose })
|
|||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [uuid]);
|
}, [uuid]);
|
||||||
|
|
||||||
|
const canary = row ? isCanaryClass(row.content_class) : false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
className="drawer-backdrop"
|
||||||
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
||||||
style={{
|
|
||||||
position: 'fixed', inset: 0,
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
||||||
display: 'flex', justifyContent: 'flex-end',
|
|
||||||
zIndex: 1000,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<div ref={panelRef} role="dialog" aria-modal="true" className="drawer">
|
||||||
ref={panelRef}
|
<div className="drawer-head">
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
style={{
|
|
||||||
width: 'min(720px, 100%)', height: '100%',
|
|
||||||
backgroundColor: 'var(--bg-color, #0d1117)',
|
|
||||||
borderLeft: '1px solid var(--border-color, #30363d)',
|
|
||||||
padding: '24px', overflowY: 'auto',
|
|
||||||
color: 'var(--text-color)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '0.7rem', color: 'var(--dim-color)', letterSpacing: '0.1em' }}>
|
<div className="drawer-eyebrow">
|
||||||
SYNTHETIC FILE {row ? `· ${deckyLabel(row.decky_uuid, deckies)}` : ''}
|
SYNTHETIC FILE{row ? ` · ${deckyLabel(row.decky_uuid, deckies)}` : ''}
|
||||||
</div>
|
|
||||||
<div className="mono" style={{ fontSize: '0.95rem', fontWeight: 'bold', marginTop: '4px', wordBreak: 'break-all' }}>
|
|
||||||
{row?.path ?? uuid}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="drawer-title">{row?.path ?? uuid}</div>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose} aria-label="Close" style={{ background: 'none', border: 'none', color: 'var(--text-color)', cursor: 'pointer' }}>
|
<button onClick={onClose} aria-label="Close" className="drawer-close">
|
||||||
<X size={18} />
|
<X size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading && <div style={{ opacity: 0.6 }}>Loading…</div>}
|
{loading && <div className="dim">Loading…</div>}
|
||||||
{error && <div style={{ color: '#ff5555' }}>{error}</div>}
|
{error && <div className="alert-text">{error}</div>}
|
||||||
|
|
||||||
{row && (
|
{row && (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '140px 1fr', rowGap: '6px', fontSize: '0.85rem', marginBottom: '16px' }}>
|
<div className="meta-grid">
|
||||||
<div style={{ color: 'var(--dim-color)' }}>PERSONA</div><div>{row.persona}</div>
|
<div className="label">Persona</div>
|
||||||
<div style={{ color: 'var(--dim-color)' }}>CONTENT CLASS</div>
|
<div>{row.persona}</div>
|
||||||
|
|
||||||
|
<div className="label">Content Class</div>
|
||||||
<div>
|
<div>
|
||||||
<span style={{ color: isCanaryClass(row.content_class) ? '#ffaa66' : 'inherit' }}>
|
<span className={canary ? 'value-canary' : ''}>
|
||||||
{contentClassLabel(row.content_class)}
|
{contentClassLabel(row.content_class)}
|
||||||
</span>
|
</span>
|
||||||
<span className="mono" style={{ marginLeft: 8, fontSize: '0.75rem', color: 'var(--dim-color)' }}>
|
<span className="mono dim" style={{ marginLeft: 8, fontSize: '0.75rem' }}>
|
||||||
{row.content_class}
|
{row.content_class}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: 'var(--dim-color)' }}>EDIT COUNT</div><div>{row.edit_count}</div>
|
|
||||||
<div style={{ color: 'var(--dim-color)' }}>CREATED</div><div>{fmt(row.created_at)}</div>
|
<div className="label">Edit Count</div>
|
||||||
<div style={{ color: 'var(--dim-color)' }}>LAST MODIFIED</div><div>{fmt(row.last_modified)}</div>
|
<div className="mono">{row.edit_count}</div>
|
||||||
<div style={{ color: 'var(--dim-color)' }}>CONTENT HASH</div>
|
|
||||||
<div className="mono" style={{ wordBreak: 'break-all' }}>{row.content_hash}</div>
|
<div className="label">Created</div>
|
||||||
|
<div className="mono dim">{fmt(row.created_at)}</div>
|
||||||
|
|
||||||
|
<div className="label">Last Modified</div>
|
||||||
|
<div className="mono">{fmt(row.last_modified)}</div>
|
||||||
|
|
||||||
|
<div className="label">Content Hash</div>
|
||||||
|
<div className="mono dim" style={{ wordBreak: 'break-all' }}>
|
||||||
|
{row.content_hash}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ borderTop: '1px solid rgba(255,255,255,0.05)', paddingTop: '12px' }}>
|
<div className="body-head">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
<span>BODY PREVIEW · {(row.last_body?.length ?? 0).toLocaleString()} BYTES</span>
|
||||||
<span style={{ fontSize: '0.7rem', color: 'var(--dim-color)', letterSpacing: '0.1em' }}>
|
{row.truncated && (
|
||||||
BODY PREVIEW ({(row.last_body?.length ?? 0).toLocaleString()} bytes)
|
<span
|
||||||
|
className="truncated-chip"
|
||||||
|
title="Body is at the 64KB cap; the decky filesystem holds the canonical bytes."
|
||||||
|
>
|
||||||
|
TRUNCATED
|
||||||
</span>
|
</span>
|
||||||
{row.truncated && (
|
)}
|
||||||
<span
|
|
||||||
className="chip dim-chip"
|
|
||||||
title="Body is at the 64KB cap; the decky filesystem holds the canonical bytes."
|
|
||||||
>
|
|
||||||
TRUNCATED
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<pre className="mono" style={{
|
|
||||||
whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.03)',
|
|
||||||
border: '1px solid rgba(255,255,255,0.05)',
|
|
||||||
padding: '12px', fontSize: '0.78rem',
|
|
||||||
maxHeight: '60vh', overflowY: 'auto',
|
|
||||||
}}>
|
|
||||||
{row.last_body || <span style={{ opacity: 0.4 }}>—</span>}
|
|
||||||
</pre>
|
|
||||||
</div>
|
</div>
|
||||||
|
<pre className="body-pre">
|
||||||
|
{row.last_body || <span className="dim">—</span>}
|
||||||
|
</pre>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -245,116 +234,131 @@ const SyntheticFiles: React.FC = () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
||||||
|
const filtersActive = !!(deckyFilter || personaFilter || classFilter);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '24px', color: 'var(--text-color)' }}>
|
<div className="fleet-root synthetic-files-root">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '16px' }}>
|
<div className="page-header">
|
||||||
<FileText size={18} />
|
<div className="page-title-group">
|
||||||
<h2 style={{ margin: 0, fontSize: '1.1rem', letterSpacing: '0.05em' }}>SYNTHETIC FILES</h2>
|
<h1>SYNTHETIC FILES</h1>
|
||||||
<span style={{ marginLeft: 'auto', color: 'var(--dim-color)', fontSize: '0.8rem' }}>
|
<span className="page-sub">
|
||||||
{total} total
|
{total} TOTAL · PAGE {page + 1} / {totalPages}
|
||||||
</span>
|
{filtersActive ? ' · FILTERED' : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="actions filters">
|
||||||
|
<div className="filter-group">
|
||||||
|
<label>Decky</label>
|
||||||
|
<select
|
||||||
|
className="filter-input"
|
||||||
|
value={deckyFilter}
|
||||||
|
onChange={(e) => { setDeckyFilter(e.target.value); setPage(0); }}
|
||||||
|
>
|
||||||
|
<option value="">All</option>
|
||||||
|
{deckies.map((d) => (
|
||||||
|
<option key={d.uuid} value={d.uuid}>{d.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="filter-group">
|
||||||
|
<label>Persona</label>
|
||||||
|
<select
|
||||||
|
className="filter-input"
|
||||||
|
value={personaFilter}
|
||||||
|
onChange={(e) => { setPersonaFilter(e.target.value); setPage(0); }}
|
||||||
|
>
|
||||||
|
<option value="">All</option>
|
||||||
|
{personaOptions.map((p) => (
|
||||||
|
<option key={p} value={p}>{p}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="filter-group">
|
||||||
|
<label>Content Class</label>
|
||||||
|
<select
|
||||||
|
className="filter-input"
|
||||||
|
value={classFilter}
|
||||||
|
onChange={(e) => { setClassFilter(e.target.value); setPage(0); }}
|
||||||
|
>
|
||||||
|
<option value="">All</option>
|
||||||
|
{CONTENT_CLASSES.map((c) => (
|
||||||
|
<option key={c} value={c}>{contentClassLabel(c)}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '12px', marginBottom: '16px', flexWrap: 'wrap' }}>
|
<div className="info-banner">
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.8rem' }}>
|
<div>
|
||||||
<span style={{ color: 'var(--dim-color)' }}>Decky:</span>
|
<strong>Scope:</strong> read-only inventory of files the realism
|
||||||
<select
|
worker has grown across the fleet. The orchestrator is the sole
|
||||||
value={deckyFilter}
|
writer; rows persist in the{' '}
|
||||||
onChange={(e) => { setDeckyFilter(e.target.value); setPage(0); }}
|
<span className="mono matrix-text">synthetic_files</span> table.
|
||||||
>
|
Click any row for the body preview and lineage detail.
|
||||||
<option value="">All</option>
|
</div>
|
||||||
{deckies.map((d) => (
|
{error && (
|
||||||
<option key={d.uuid} value={d.uuid}>{d.name}</option>
|
<div className="info-line alert-text" style={{ marginTop: 8 }}>{error}</div>
|
||||||
))}
|
)}
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.8rem' }}>
|
|
||||||
<span style={{ color: 'var(--dim-color)' }}>Persona:</span>
|
|
||||||
<select
|
|
||||||
value={personaFilter}
|
|
||||||
onChange={(e) => { setPersonaFilter(e.target.value); setPage(0); }}
|
|
||||||
>
|
|
||||||
<option value="">All</option>
|
|
||||||
{personaOptions.map((p) => (
|
|
||||||
<option key={p} value={p}>{p}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.8rem' }}>
|
|
||||||
<span style={{ color: 'var(--dim-color)' }}>Class:</span>
|
|
||||||
<select
|
|
||||||
value={classFilter}
|
|
||||||
onChange={(e) => { setClassFilter(e.target.value); setPage(0); }}
|
|
||||||
>
|
|
||||||
<option value="">All</option>
|
|
||||||
{CONTENT_CLASSES.map((c) => (
|
|
||||||
<option key={c} value={c}>{contentClassLabel(c)}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <div style={{ color: '#ff5555', marginBottom: '12px' }}>{error}</div>}
|
<div className="files-table-wrap">
|
||||||
|
<table className="files-table">
|
||||||
<div style={{ overflowX: 'auto', border: '1px solid rgba(255,255,255,0.05)' }}>
|
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}>
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ textAlign: 'left', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
<tr>
|
||||||
<th style={{ padding: '8px 12px', color: 'var(--dim-color)', fontSize: '0.7rem', letterSpacing: '0.1em' }}>DECKY</th>
|
<th>Decky</th>
|
||||||
<th style={{ padding: '8px 12px', color: 'var(--dim-color)', fontSize: '0.7rem', letterSpacing: '0.1em' }}>PATH</th>
|
<th>Path</th>
|
||||||
<th style={{ padding: '8px 12px', color: 'var(--dim-color)', fontSize: '0.7rem', letterSpacing: '0.1em' }}>PERSONA</th>
|
<th>Persona</th>
|
||||||
<th style={{ padding: '8px 12px', color: 'var(--dim-color)', fontSize: '0.7rem', letterSpacing: '0.1em' }}>CLASS</th>
|
<th>Class</th>
|
||||||
<th style={{ padding: '8px 12px', color: 'var(--dim-color)', fontSize: '0.7rem', letterSpacing: '0.1em' }}>LAST MODIFIED</th>
|
<th>Last Modified</th>
|
||||||
<th style={{ padding: '8px 12px', color: 'var(--dim-color)', fontSize: '0.7rem', letterSpacing: '0.1em' }}>EDITS</th>
|
<th>Edits</th>
|
||||||
<th style={{ padding: '8px 12px', color: 'var(--dim-color)', fontSize: '0.7rem', letterSpacing: '0.1em' }}>HASH</th>
|
<th>Hash</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{loading && (
|
{loading && (
|
||||||
<tr><td colSpan={7} style={{ padding: '20px', textAlign: 'center', opacity: 0.6 }}>Loading…</td></tr>
|
<tr className="empty-row"><td colSpan={7}>Loading…</td></tr>
|
||||||
)}
|
)}
|
||||||
{!loading && rows.length === 0 && (
|
{!loading && rows.length === 0 && (
|
||||||
<tr><td colSpan={7} style={{ padding: '20px', textAlign: 'center', opacity: 0.6 }}>
|
<tr className="empty-row"><td colSpan={7}>
|
||||||
No files match the current filters.
|
No files match the current filters.
|
||||||
</td></tr>
|
</td></tr>
|
||||||
)}
|
)}
|
||||||
{!loading && rows.map((r) => (
|
{!loading && rows.map((r) => {
|
||||||
<tr
|
const canary = isCanaryClass(r.content_class);
|
||||||
key={r.uuid}
|
return (
|
||||||
className="clickable"
|
<tr key={r.uuid} onClick={() => setSelectedUuid(r.uuid)}>
|
||||||
onClick={() => setSelectedUuid(r.uuid)}
|
<td>{deckyLabel(r.decky_uuid, deckies)}</td>
|
||||||
style={{ cursor: 'pointer', borderBottom: '1px solid rgba(255,255,255,0.03)' }}
|
<td className="path">{r.path}</td>
|
||||||
>
|
<td>{r.persona}</td>
|
||||||
<td style={{ padding: '8px 12px' }}>{deckyLabel(r.decky_uuid, deckies)}</td>
|
<td className={`cls${canary ? ' canary' : ''}`}>
|
||||||
<td className="mono" style={{ padding: '8px 12px', wordBreak: 'break-all' }}>{r.path}</td>
|
{contentClassLabel(r.content_class)}
|
||||||
<td style={{ padding: '8px 12px' }}>{r.persona}</td>
|
</td>
|
||||||
<td style={{
|
<td className="dim-time">{fmt(r.last_modified)}</td>
|
||||||
padding: '8px 12px',
|
<td className="mono">{r.edit_count}</td>
|
||||||
color: isCanaryClass(r.content_class) ? '#ffaa66' : 'inherit',
|
<td className="hash">{r.content_hash.slice(0, 12)}…</td>
|
||||||
}}>
|
</tr>
|
||||||
{contentClassLabel(r.content_class)}
|
);
|
||||||
</td>
|
})}
|
||||||
<td style={{ padding: '8px 12px' }}>{fmt(r.last_modified)}</td>
|
|
||||||
<td style={{ padding: '8px 12px' }}>{r.edit_count}</td>
|
|
||||||
<td className="mono" style={{ padding: '8px 12px', opacity: 0.7 }}>{r.content_hash.slice(0, 12)}…</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '8px', marginTop: '12px', alignItems: 'center' }}>
|
<div className="pager">
|
||||||
<button onClick={() => setPage((p) => Math.max(0, p - 1))} disabled={page === 0}>
|
<button
|
||||||
← Prev
|
className="btn ghost small"
|
||||||
|
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||||
|
disabled={page === 0}
|
||||||
|
>
|
||||||
|
← PREV
|
||||||
</button>
|
</button>
|
||||||
<span style={{ fontSize: '0.8rem', color: 'var(--dim-color)' }}>
|
<span className="page-counter">PAGE {page + 1} / {totalPages}</span>
|
||||||
Page {page + 1} / {totalPages}
|
<button
|
||||||
</span>
|
className="btn ghost small"
|
||||||
<button onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))} disabled={page >= totalPages - 1}>
|
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||||
Next →
|
disabled={page >= totalPages - 1}
|
||||||
|
>
|
||||||
|
NEXT →
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user