feat(ui): project details panel + compact/responsive cards; rename Sentinel→Yerin (red); migrate research_notes
- Project card expands to show description + status + dates (was only the research stub) - Cards compacted + responsive (actions wrap on narrow) - Sentinel renamed Yerin everywhere (#/yerin, red 'Sage of the Endless Sword' theme + red sidebar dot) - void1 importer now carries research_notes/last_researched_at (was dropped) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,12 @@ export async function importVoid1({ sqlitePath, spaceId, dryRun = false }) {
|
||||
if (existing) { projIds.set(pr.id, existing); continue; }
|
||||
n.projects++; if (dryRun) continue;
|
||||
const created = await projects.create({ space_id: spaceId, slug: slugify(pr.name), name: pr.name || 'Project', description: pr.description || null, status: pr.status === 'archived' ? 'archived' : 'active' }, SYS);
|
||||
// Carry Void 1's Eithan research over (research_notes were not in the original mapping).
|
||||
if (pr.research_notes || pr.last_researched_at) {
|
||||
await pool.query(
|
||||
`UPDATE projects SET research_notes=$1, last_researched_at=$2, research_status=$3 WHERE id=$4`,
|
||||
[pr.research_notes || null, pr.last_researched_at ? new Date(pr.last_researched_at) : null, pr.research_notes ? 'done' : 'none', created.id]);
|
||||
}
|
||||
projIds.set(pr.id, created.id);
|
||||
await map.record('void1', `projects:${pr.id}`, 'project', created.id);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ const VIEWS = {
|
||||
search: () => import('./views/search.js'),
|
||||
inbox: () => import('./views/inbox.js'),
|
||||
'sacred-valley': () => import('./views/sacred_valley.js'),
|
||||
sentinel: () => import('./views/sentinel.js'),
|
||||
yerin: () => import('./views/sentinel.js'),
|
||||
'little-blue': () => import('./views/little_blue.js'),
|
||||
terminal: () => import('./views/terminal.js'),
|
||||
settings: () => import('./views/settings.js'),
|
||||
|
||||
@@ -51,6 +51,24 @@ export function projectCard(p, o) {
|
||||
|
||||
if (open) {
|
||||
const panel = el('div', { class: 'proj-panel' });
|
||||
|
||||
// ---- Details ----
|
||||
panel.appendChild(el('div', { class: 'proj-section-h' }, 'Details'));
|
||||
const det = el('div', { class: 'proj-details' });
|
||||
det.appendChild(p.description
|
||||
? el('div', { class: 'proj-detail-desc' }, p.description)
|
||||
: el('div', { class: 'muted', style: { fontSize: '12px' } }, 'No description yet — Edit to add one.'));
|
||||
const dl = el('dl', { class: 'proj-dl' });
|
||||
const addDL = (k, v) => { if (v) { dl.appendChild(el('dt', {}, k)); dl.appendChild(el('dd', {}, v)); } };
|
||||
addDL('Status', p.status);
|
||||
addDL('Created', p.created_at ? new Date(p.created_at).toLocaleDateString() : null);
|
||||
addDL('Updated', p.updated_at ? `${new Date(p.updated_at).toLocaleDateString()} (${ago(p.updated_at)})` : null);
|
||||
if (p.started_at) addDL('Started', new Date(p.started_at).toLocaleDateString());
|
||||
if (p.completed_at) addDL('Completed', new Date(p.completed_at).toLocaleDateString());
|
||||
det.appendChild(dl);
|
||||
panel.appendChild(det);
|
||||
|
||||
// ---- Eithan research ----
|
||||
panel.appendChild(el('div', { class: 'proj-section-h' }, 'Eithan research' + (p.last_researched_at ? ` · ${ago(p.last_researched_at)}` : '')));
|
||||
if (busy) panel.appendChild(el('div', { class: 'muted' }, "Queued for Eithan — he'll fill this in once the agent ships."));
|
||||
else if (p.research_notes) { const n = el('div', { class: 'md-preview' }); n.innerHTML = renderMarkdown(p.research_notes); panel.appendChild(n); }
|
||||
|
||||
@@ -90,7 +90,7 @@ export function renderSidebar(root) {
|
||||
),
|
||||
el('div', { class: 'sb-section' },
|
||||
el('div', { class: 'sb-title' }, 'Agents'),
|
||||
navItem('Sentinel', '/sentinel', { dot: 'ok' }),
|
||||
navItem('Yerin', '/yerin', { dot: 'yerin' }),
|
||||
navItem('Little Blue', '/little-blue', { dot: 'lb' })
|
||||
),
|
||||
el('div', { class: 'sb-section' },
|
||||
|
||||
@@ -21,7 +21,7 @@ const ROUTES = [
|
||||
{ name: 'search', re: /^\/search$/, keys: [] },
|
||||
{ name: 'inbox', re: /^\/inbox$/, keys: [] },
|
||||
{ name: 'sacred-valley', re: /^\/sacred-valley$/, keys: [] },
|
||||
{ name: 'sentinel', re: /^\/sentinel$/, keys: [] },
|
||||
{ name: 'yerin', re: /^\/(yerin|sentinel)$/, keys: [] },
|
||||
{ name: 'little-blue', re: /^\/little-blue$/, keys: [] },
|
||||
{ name: 'terminal', re: /^\/terminal$/, keys: [] },
|
||||
{ name: 'settings', re: /^\/settings$/, keys: [] },
|
||||
|
||||
@@ -284,6 +284,35 @@ button.ghost:hover { color: var(--text); border-color: var(--accent-dim); }
|
||||
.agent-file-label { font-family: var(--font-display); font-size: 10px; text-transform: uppercase; letter-spacing: .12em; color: var(--muted); margin: 10px 0 4px; }
|
||||
.agent-file-content { background: var(--bg); border: 1px solid var(--border); border-radius: 5px; padding: 10px 12px; font-size: 12.5px; line-height: 1.5; white-space: pre-wrap; color: var(--text); max-height: 280px; overflow: auto; }
|
||||
|
||||
/* Project cards — compact + responsive + details panel */
|
||||
.proj-head { padding: 7px 10px; gap: 8px; }
|
||||
.proj-title { font-size: 14px; }
|
||||
.proj-desc { font-size: 12px; }
|
||||
.proj-status { font-size: 11px; padding: 2px 6px; }
|
||||
.proj-btn { font-size: 11px; padding: 3px 8px; }
|
||||
.proj-actions { flex-wrap: wrap; justify-content: flex-end; }
|
||||
.proj-panel { padding: 2px 14px 12px 32px; }
|
||||
.proj-details { margin-bottom: 4px; }
|
||||
.proj-detail-desc { font-size: 13px; color: var(--text); line-height: 1.55; margin-bottom: 8px; }
|
||||
.proj-dl { display: grid; grid-template-columns: max-content 1fr; gap: 3px 14px; margin: 0; font-size: 12px; }
|
||||
.proj-dl dt { color: var(--muted); text-transform: uppercase; letter-spacing: .06em; font-size: 10px; align-self: center; }
|
||||
.proj-dl dd { margin: 0; color: var(--text); }
|
||||
.proj-panel .research-notes, .proj-panel .md-preview { font-size: 13px; }
|
||||
@media (max-width: 620px) {
|
||||
.proj-head { flex-wrap: wrap; }
|
||||
.proj-actions { width: 100%; margin-top: 4px; }
|
||||
.proj-panel { padding-left: 14px; }
|
||||
}
|
||||
|
||||
/* Yerin — classic red (Sage of the Endless Sword) */
|
||||
:root { --yerin: #d23b3b; }
|
||||
.sb-dot.yerin { background: var(--yerin); box-shadow: 0 0 6px var(--yerin); }
|
||||
.sentinel-chat { display: flex; flex-direction: column; height: 70vh; border: 1px solid var(--border); border-radius: 6px; background: var(--panel); overflow: hidden; }
|
||||
.sentinel-chat .sentinel-log { flex: 1; overflow-y: auto; padding: 14px; }
|
||||
.yerin-view .view-h1 { color: var(--yerin); }
|
||||
.yerin-view .view-sub { color: var(--yerin); opacity: .85; font-style: italic; }
|
||||
.yerin-view .sentinel-chat { border-color: var(--yerin); box-shadow: inset 0 2px 0 rgba(210, 59, 59, .25); }
|
||||
|
||||
/* modal */
|
||||
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 100; }
|
||||
.modal {
|
||||
|
||||
@@ -12,10 +12,10 @@ const YERIN_LABELS = {
|
||||
export async function render(main) {
|
||||
const log = el('div', { class: 'rail-log sentinel-log' });
|
||||
const input = el('textarea', { class: 'rail-input', rows: 1, placeholder: 'Ask Yerin about the Void’s security…' });
|
||||
mount(main,
|
||||
el('h1', { class: 'view-h1' }, '◆ Sentinel — Yerin'),
|
||||
el('p', { class: 'view-sub' }, 'Read-only security & observability. She watches, reports, and warns — she never acts.'),
|
||||
el('div', { class: 'sentinel-chat' }, log, el('div', { class: 'rail-inputwrap' }, input)));
|
||||
mount(main, el('div', { class: 'yerin-view' },
|
||||
el('h1', { class: 'view-h1' }, '◆ Yerin'),
|
||||
el('p', { class: 'view-sub' }, 'Sage of the Endless Sword — read-only security & observability. She watches, reports, and warns; she never acts.'),
|
||||
el('div', { class: 'sentinel-chat' }, log, el('div', { class: 'rail-inputwrap' }, input))));
|
||||
const chat = wireAgentChat({
|
||||
logEl: log, inputEl: input,
|
||||
historyUrl: '/api/security/yerin', turnUrl: '/api/security/yerin/turn',
|
||||
|
||||
Reference in New Issue
Block a user