ui: compact workspace overview
This commit is contained in:
parent
816c79290f
commit
7d90cb644d
|
|
@ -52,6 +52,8 @@
|
|||
|
||||
--radius-soft: 8px;
|
||||
--radius-panel: 12px;
|
||||
--interactive-hover: color-mix(in oklch, var(--bg-subtle) 88%, white 12%);
|
||||
--interactive-selected: color-mix(in oklch, var(--bg-subtle) 88%, var(--accent) 12%);
|
||||
--font-sans:
|
||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", sans-serif;
|
||||
|
|
@ -143,7 +145,8 @@
|
|||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-6);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
|
@ -296,18 +299,32 @@
|
|||
display: grid;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
padding: var(--space-2) 0 var(--space-2) var(--space-3);
|
||||
border-left: 2px solid transparent;
|
||||
margin-inline: calc(-1 * var(--space-2));
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: var(--radius-soft);
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
background-color 140ms ease,
|
||||
color 140ms ease;
|
||||
}
|
||||
|
||||
.nav-item.active,
|
||||
.objective-link.active,
|
||||
.nav-item:hover,
|
||||
.objective-link:hover {
|
||||
border-left-color: var(--accent);
|
||||
a.nav-item:hover,
|
||||
a.nav-item:focus-visible,
|
||||
a.objective-link:hover,
|
||||
a.objective-link:focus-visible {
|
||||
background: var(--interactive-hover);
|
||||
}
|
||||
|
||||
a.nav-item.active,
|
||||
a.objective-link.active {
|
||||
background: var(--interactive-selected);
|
||||
}
|
||||
|
||||
a.nav-item.active .item-title,
|
||||
a.objective-link.active .item-title {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.item-title,
|
||||
|
|
@ -322,6 +339,26 @@
|
|||
font-weight: 650;
|
||||
}
|
||||
|
||||
.worker-nav-item {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.worker-title-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, max-content) minmax(0, 1fr);
|
||||
align-items: baseline;
|
||||
gap: var(--space-2);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.worker-task-title {
|
||||
overflow: hidden;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.82rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-meta,
|
||||
.section-note,
|
||||
.section-state,
|
||||
|
|
@ -338,22 +375,107 @@
|
|||
|
||||
.card {
|
||||
min-width: 0;
|
||||
padding: var(--space-5) 0 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.card:first-child {
|
||||
padding-top: 0;
|
||||
border-top: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.runtime-card {
|
||||
padding: var(--space-4) 0 0;
|
||||
border-top: 1px solid var(--line);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.selected-card.selected {
|
||||
border-top-color: var(--accent);
|
||||
.objective-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.objective-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) max-content;
|
||||
align-items: start;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-radius: var(--radius-panel);
|
||||
background: var(--bg-raised);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: background-color 140ms ease;
|
||||
}
|
||||
|
||||
.objective-row:hover,
|
||||
.objective-row:focus-visible {
|
||||
background: var(--interactive-hover);
|
||||
}
|
||||
|
||||
.objective-row.selected {
|
||||
background: var(--interactive-selected);
|
||||
}
|
||||
|
||||
.objective-row.selected .objective-title {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.objective-main {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.objective-title-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--space-2);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.objective-title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--text-strong);
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.state-pill {
|
||||
flex: 0 0 auto;
|
||||
color: var(--success);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.objective-summary {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.objective-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-2);
|
||||
color: var(--text-faint);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.35;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.objective-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.objective-meta {
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.runtime-heading,
|
||||
|
|
@ -384,20 +506,37 @@
|
|||
|
||||
dl {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dl > div {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(6.5rem, 10rem) minmax(0, 1fr);
|
||||
align-items: baseline;
|
||||
gap: var(--space-3);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
color: var(--text-faint);
|
||||
font-size: 0.78rem;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dd small {
|
||||
display: inline;
|
||||
margin-top: 0;
|
||||
margin-left: var(--space-2);
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
ObjectiveDetail,
|
||||
ObjectiveListResponse,
|
||||
RepositoryDetailResponse,
|
||||
RepositoryLogResponse,
|
||||
RepositorySummary,
|
||||
RepositoryTicketsResponse,
|
||||
Worker,
|
||||
|
|
@ -33,7 +32,6 @@
|
|||
{ label: 'Tickets', path: '/api/tickets' },
|
||||
{ label: 'Objectives', path: '/api/objectives' },
|
||||
{ label: 'Repositories', path: '/api/repositories' },
|
||||
{ label: 'Repository log', path: '/api/repositories/local/log' },
|
||||
{ label: 'Repository tickets', path: '/api/repositories/local/tickets' },
|
||||
{ label: 'Runs', path: '/api/runs' },
|
||||
{ label: 'Hosts', path: '/api/hosts' },
|
||||
|
|
@ -44,7 +42,6 @@
|
|||
let hosts = $state<ListResponse<Host> | null>(null);
|
||||
let workers = $state<ListResponse<Worker> | null>(null);
|
||||
let repository = $state<RepositorySummary | null>(null);
|
||||
let repositoryLog = $state<RepositoryLogResponse | null>(null);
|
||||
let repositoryTickets = $state<RepositoryTicketsResponse | null>(null);
|
||||
let objectives = $state<ObjectiveListResponse | null>(null);
|
||||
let objectiveDetail = $state<ObjectiveDetail | null>(null);
|
||||
|
|
@ -53,7 +50,6 @@
|
|||
let hostsError = $state<string | null>(null);
|
||||
let workersError = $state<string | null>(null);
|
||||
let repositoryError = $state<string | null>(null);
|
||||
let repositoryLogError = $state<string | null>(null);
|
||||
let repositoryTicketsError = $state<string | null>(null);
|
||||
let objectivesError = $state<string | null>(null);
|
||||
let objectiveDetailError = $state<string | null>(null);
|
||||
|
|
@ -111,16 +107,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function loadRepositoryLog() {
|
||||
repositoryLogError = null;
|
||||
try {
|
||||
repositoryLog = await getJson<RepositoryLogResponse>('/api/repositories/local/log?limit=10');
|
||||
} catch (error) {
|
||||
repositoryLogError = error instanceof Error ? error.message : String(error);
|
||||
repositoryLog = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRepositoryTickets() {
|
||||
repositoryTicketsError = null;
|
||||
try {
|
||||
|
|
@ -196,16 +182,11 @@
|
|||
return value ?? 'not recorded';
|
||||
}
|
||||
|
||||
function shortHash(hash: string | null | undefined): string {
|
||||
return hash ? hash.slice(0, 12) : 'unknown';
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
void loadWorkspace();
|
||||
void loadHosts();
|
||||
void loadWorkers();
|
||||
void loadRepository();
|
||||
void loadRepositoryLog();
|
||||
void loadRepositoryTickets();
|
||||
void loadObjectives();
|
||||
});
|
||||
|
|
@ -237,116 +218,35 @@
|
|||
|
||||
<main class="shell">
|
||||
{#if route.page === 'repository'}
|
||||
<section class="grid runtime">
|
||||
<div class="card">
|
||||
<h2>Repository summary</h2>
|
||||
{#if repository}
|
||||
<dl>
|
||||
<div>
|
||||
<dt>ID</dt>
|
||||
<dd><code>{repository.id}</code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Kind</dt>
|
||||
<dd>{repository.kind}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Workspace root</dt>
|
||||
<dd><code>{repository.workspace_root}</code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Record authority</dt>
|
||||
<dd>{repository.record_authority}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Git</dt>
|
||||
<dd>{repository.git.status}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{:else if repositoryError}
|
||||
<p class="error">{repositoryError}</p>
|
||||
{:else}
|
||||
<p>Waiting for <code>/api/repositories/local</code>…</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Git summary</h2>
|
||||
{#if repository}
|
||||
{#if repository.git.status === 'available'}
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Root</dt>
|
||||
<dd><code>{repository.git.root ?? 'unknown'}</code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Branch</dt>
|
||||
<dd>{repository.git.branch ?? 'unknown'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>HEAD</dt>
|
||||
<dd><code>{shortHash(repository.git.head)}</code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Dirty</dt>
|
||||
<dd>{repository.git.dirty === null || repository.git.dirty === undefined ? 'unknown' : repository.git.dirty ? 'yes' : 'no'} <small>{repository.git.dirty_scope}</small></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Remote</dt>
|
||||
<dd>
|
||||
{#if repository.git.remote}
|
||||
<code>{repository.git.remote.name}</code> · {repository.git.remote.url}
|
||||
{#if repository.git.remote.redacted}<small>credentials redacted</small>{/if}
|
||||
{:else}
|
||||
not configured
|
||||
{/if}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{:else}
|
||||
<p>Git metadata is unavailable for this local Repository.</p>
|
||||
{/if}
|
||||
{:else if repositoryError}
|
||||
<p class="error">{repositoryError}</p>
|
||||
{:else}
|
||||
<p>Waiting for Git summary…</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Recent Git log</h2>
|
||||
{#if repositoryLog}
|
||||
{#if repositoryLog.items.length === 0}
|
||||
<p>No recent commits are available from the bounded Git log API.</p>
|
||||
{:else}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Commit</th>
|
||||
<th>Subject</th>
|
||||
<th>Author</th>
|
||||
<th>Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each repositoryLog.items as commit (commit.hash)}
|
||||
<tr>
|
||||
<td><code>{shortHash(commit.hash)}</code></td>
|
||||
<td>{commit.subject}</td>
|
||||
<td>{commit.author_name} <small>{commit.author_email}</small></td>
|
||||
<td>{commit.timestamp}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>Repository summary</h2>
|
||||
{#if repository}
|
||||
<dl>
|
||||
<div>
|
||||
<dt>ID</dt>
|
||||
<dd><code>{repository.id}</code></dd>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if repositoryLogError}
|
||||
<p class="error">{repositoryLogError}</p>
|
||||
<div>
|
||||
<dt>Kind</dt>
|
||||
<dd>{repository.kind}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Workspace root</dt>
|
||||
<dd><code>{repository.workspace_root}</code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Record authority</dt>
|
||||
<dd>{repository.record_authority}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Git</dt>
|
||||
<dd>{repository.git.status}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{:else if repositoryError}
|
||||
<p class="error">{repositoryError}</p>
|
||||
{:else}
|
||||
<p>Waiting for <code>/api/repositories/local/log</code>…</p>
|
||||
<p>Waiting for <code>/api/repositories/local</code>…</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
|
|
@ -364,7 +264,7 @@
|
|||
{/if}
|
||||
</section>
|
||||
|
||||
{@const repositoryDiagnostics = diagnosticsFor(repository?.git.diagnostics, repositoryLog?.diagnostics, repositoryTickets?.diagnostics)}
|
||||
{@const repositoryDiagnostics = diagnosticsFor(repository?.git.diagnostics, repositoryTickets?.diagnostics)}
|
||||
{#if repositoryDiagnostics.length > 0}
|
||||
<section class="card diagnostics">
|
||||
<h2>Repository diagnostics</h2>
|
||||
|
|
@ -389,30 +289,22 @@
|
|||
{#if objectives.items.length === 0}
|
||||
<p>No Objective records are present.</p>
|
||||
{:else}
|
||||
<div class="stack">
|
||||
<div class="objective-list">
|
||||
{#each objectives.items as objective (objective.id)}
|
||||
<article class="runtime-card selected-card" class:selected={route.page === 'objective' && route.objectiveId === objective.id}>
|
||||
<div class="runtime-heading">
|
||||
<strong>{objective.title}</strong>
|
||||
<span>{objective.state}</span>
|
||||
<a class="objective-row" class:selected={route.page === 'objective' && route.objectiveId === objective.id} href={`/objectives/${objective.id}`}>
|
||||
<div class="objective-main">
|
||||
<div class="objective-title-row">
|
||||
<strong class="objective-title">{objective.title}</strong>
|
||||
<span class="state-pill">{objective.state}</span>
|
||||
</div>
|
||||
<p class="objective-summary">{objective.summary || 'No summary text is available.'}</p>
|
||||
</div>
|
||||
<p>{objective.summary || 'No summary text is available.'}</p>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>ID</dt>
|
||||
<dd><code>{objective.id}</code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Updated</dt>
|
||||
<dd>{formatDate(objective.updated_at)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Linked tickets</dt>
|
||||
<dd>{objective.linked_tickets?.length ? objective.linked_tickets.join(', ') : 'none'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p><a href={`/objectives/${objective.id}`}>View detail</a></p>
|
||||
</article>
|
||||
<div class="objective-meta" aria-label="Objective metadata">
|
||||
<span>Updated {formatDate(objective.updated_at)}</span>
|
||||
<span>{objective.linked_tickets?.length ? `${objective.linked_tickets.length} linked ticket(s)` : 'No linked tickets'}</span>
|
||||
<code>{objective.id}</code>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,4 @@
|
|||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p class="section-note">
|
||||
Repository authority remains the current workspace root and canonical project records.
|
||||
</p>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -64,10 +64,13 @@
|
|||
{:else}
|
||||
<ul class="nav-list" aria-label="Workers">
|
||||
{#each workers as worker (worker.worker_id)}
|
||||
<li class="nav-item">
|
||||
<span class="item-title">{worker.label}</span>
|
||||
<li class="nav-item worker-nav-item">
|
||||
<span class="worker-title-row">
|
||||
<span class="item-title">{worker.label}</span>
|
||||
<span class="worker-task-title">-</span>
|
||||
</span>
|
||||
<span class="item-meta">
|
||||
{worker.state} · {worker.status}{worker.role ? ` · ${worker.role}` : ''}
|
||||
{worker.role ? `${worker.role} · ` : ''}{worker.state} · {worker.status}
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user