EP catalogue (all domains): - EP-RAIL-001 ep_id patched (schema fix: add ep_id to EPUpdate) - EP-RAIL-003 (git bare-repo mirrors) and EP-RAIL-004 (offsite secondary backup) registered from railiance-cluster/docs/backup-restore.md - EP-CUST-003..007 ep_ids assigned to existing custodian EPs - EP-CUST-008 (State Hub API auth) and EP-CUST-009 (update_workstream MCP tool) registered as new custodian extension points TD catalogue (railiance — first 5 items): - TD-RAIL-001: backup cron runs as root without audit trail (high/security) - TD-RAIL-002: k3s kubeconfig world-readable mode 644 (medium/security) - TD-RAIL-003: no Ansible role unit tests (medium/test) - TD-RAIL-004: age key extracted via awk — fragile (medium/impl) - TD-RAIL-005: etcd snapshot retention uncoordinated (low/impl) Dashboard (T08 + T10): - Extract API URL and POLL to src/components/config.js; all 15 pages now import from the shared module (contributions/goals keep custom POLL) - Shared .kpi-infobox, .filter-bar, .filter-search/.filter-owner CSS moved to observablehq.config.js head <style> block; removed from 9 pages - Build: 0 errors, 0 warnings API (T09): - progress.py: limit param now Query(100, le=1000) — prevents unbounded list requests; closes TD-CUST-004 for the only endpoint that had limit CUST-WP-0004 marked completed (all 10 tasks done). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
154 lines
7.7 KiB
Markdown
154 lines
7.7 KiB
Markdown
---
|
|
title: Domains
|
|
---
|
|
|
|
```js
|
|
import {API, POLL} from "./components/config.js";
|
|
```
|
|
|
|
```js
|
|
const domainsState = (async function*() {
|
|
while (true) {
|
|
let domains = [], repos = [], ok = false;
|
|
try {
|
|
const [rd, rr] = await Promise.all([
|
|
fetch(`${API}/domains/?status=all`),
|
|
fetch(`${API}/repos/`),
|
|
]);
|
|
ok = rd.ok && rr.ok;
|
|
if (ok) {
|
|
[domains, repos] = await Promise.all([rd.json(), rr.json()]);
|
|
}
|
|
} catch {}
|
|
yield {domains, repos, ok, ts: new Date()};
|
|
await new Promise(res => setTimeout(res, POLL));
|
|
}
|
|
})();
|
|
```
|
|
|
|
```js
|
|
const domains = domainsState.domains ?? [];
|
|
const repos = domainsState.repos ?? [];
|
|
const _ok = domainsState.ok ?? false;
|
|
const _ts = domainsState.ts;
|
|
```
|
|
|
|
# Domains
|
|
|
|
```js
|
|
import {injectTocTop} from "./components/toc-sidebar.js";
|
|
import {withDocHelp} from "./components/doc-overlay.js";
|
|
import {openEntityModal} from "./components/entity-modal.js";
|
|
|
|
// ── Live indicator ─────────────────────────────────────────────────────────────
|
|
const _liveEl = html`<div class="live-indicator">
|
|
<span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
|
|
${_ok
|
|
? `Live · updated ${_ts?.toLocaleTimeString()}`
|
|
: html`<span style="color:red">Offline — run: <code>make api</code></span>`}
|
|
</div>`;
|
|
withDocHelp(_liveEl, "/docs/live-data");
|
|
injectTocTop("live-indicator", _liveEl);
|
|
|
|
const _h1 = document.querySelector("#observablehq-main h1");
|
|
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/domains"); }
|
|
|
|
// ── KPI row ────────────────────────────────────────────────────────────────────
|
|
const activeDomains = domains.filter(d => d.status === "active");
|
|
const archivedDomains = domains.filter(d => d.status === "archived");
|
|
const newestDomain = [...domains].sort((a, b) => b.created_at?.localeCompare(a.created_at ?? "") ?? 0)[0];
|
|
|
|
display(html`<div class="kpi-row-top">
|
|
<div class="kpi-card">
|
|
<div class="kpi-card-value">${domains.length}</div>
|
|
<div class="kpi-card-label">total domains</div>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<div class="kpi-card-value">${activeDomains.length}</div>
|
|
<div class="kpi-card-label">active</div>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<div class="kpi-card-value">${repos.length}</div>
|
|
<div class="kpi-card-label">total repos</div>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<div class="kpi-card-value">${newestDomain?.name ?? "—"}</div>
|
|
<div class="kpi-card-label">newest domain</div>
|
|
</div>
|
|
</div>`);
|
|
```
|
|
|
|
## Domain Cards
|
|
|
|
```js
|
|
// Build repo index by domain_id
|
|
const reposByDomain = {};
|
|
for (const repo of repos) {
|
|
if (!reposByDomain[repo.domain_id]) reposByDomain[repo.domain_id] = [];
|
|
reposByDomain[repo.domain_id].push(repo);
|
|
}
|
|
|
|
if (domains.length === 0) {
|
|
display(html`<p class="dim">No domains found. API may be offline.</p>`);
|
|
} else {
|
|
display(html`<div class="domain-grid">${domains.map(d => {
|
|
const domainRepos = reposByDomain[d.id] ?? [];
|
|
return html`<div class="domain-card domain-status-${d.status} entity-row"
|
|
title="Click to view details">
|
|
<div class="domain-card-header">
|
|
<span class="domain-slug">${d.slug}</span>
|
|
<span class="domain-status-badge domain-status-badge-${d.status}">${d.status}</span>
|
|
</div>
|
|
<div class="domain-name">${d.name}</div>
|
|
${d.description ? html`<div class="domain-desc">${d.description.slice(0, 160)}${d.description.length > 160 ? " …" : ""}</div>` : ""}
|
|
<div class="domain-repos">
|
|
${domainRepos.length === 0
|
|
? html`<span class="no-repos">no repos registered</span>`
|
|
: domainRepos.map(r => html`<div class="repo-row">
|
|
<span class="repo-name">${r.name}</span>
|
|
${r.local_path ? html`<code class="repo-path">${r.local_path}</code>` : ""}
|
|
${r.remote_url ? html`<a class="repo-url" href=${r.remote_url} target="_blank">${r.remote_url.replace(/^https?:\/\//, "")}</a>` : ""}
|
|
</div>`)
|
|
}
|
|
</div>
|
|
</div>`;
|
|
})}</div>`);
|
|
}
|
|
```
|
|
|
|
<style>
|
|
/* ── Live indicator ───────────────────────────────────────────────────────── */
|
|
.live-indicator { font-size: 0.8rem; color: gray; position: relative; padding: 0.55rem 1.8rem 0.55rem 0.7rem; margin-bottom: 0.75rem; }
|
|
|
|
/* ── KPI row ─────────────────────────────────────────────────────────────── */
|
|
.kpi-row-top { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 1.5rem; }
|
|
.kpi-card { background: var(--theme-background-alt); border: 1px solid var(--theme-foreground-faint, #e0e0e0); border-radius: 10px; padding: 0.75rem 1.25rem; min-width: 120px; text-align: center; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
|
.kpi-card-value { font-size: 1.6rem; font-weight: 700; line-height: 1.2; }
|
|
.kpi-card-label { font-size: 0.72rem; color: var(--theme-foreground-muted, #888); text-transform: uppercase; letter-spacing: 0.06em; margin-top: 0.2rem; }
|
|
|
|
/* ── Domain grid ─────────────────────────────────────────────────────────── */
|
|
.domain-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem; }
|
|
.domain-card { border: 1px solid var(--theme-foreground-faint, #e0e0e0); border-radius: 10px; padding: 1rem 1.2rem; background: var(--theme-background-alt); }
|
|
.domain-card.entity-row { cursor: default; }
|
|
.domain-status-archived { opacity: 0.6; }
|
|
.domain-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.3rem; }
|
|
.domain-slug { font-family: monospace; font-size: 0.8rem; color: var(--theme-foreground-muted); background: var(--theme-background); border: 1px solid var(--theme-foreground-faint, #ddd); border-radius: 4px; padding: 0.1rem 0.4rem; }
|
|
.domain-status-badge { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; padding: 0.1rem 0.45rem; border-radius: 8px; letter-spacing: 0.04em; }
|
|
.domain-status-badge-active { background: #dcfce7; color: #166534; }
|
|
.domain-status-badge-archived { background: #f1f5f9; color: #64748b; }
|
|
.domain-name { font-size: 1.1rem; font-weight: 700; margin-bottom: 0.3rem; }
|
|
.domain-desc { font-size: 0.82rem; color: var(--theme-foreground-muted); line-height: 1.4; margin-bottom: 0.6rem; }
|
|
|
|
/* ── Repo list ───────────────────────────────────────────────────────────── */
|
|
.domain-repos { border-top: 1px solid var(--theme-foreground-faint, #eee); padding-top: 0.5rem; margin-top: 0.5rem; display: flex; flex-direction: column; gap: 0.4rem; }
|
|
.no-repos { font-size: 0.78rem; color: var(--theme-foreground-faint); font-style: italic; }
|
|
.repo-row { display: flex; flex-direction: column; gap: 0.1rem; }
|
|
.repo-name { font-size: 0.85rem; font-weight: 600; }
|
|
.repo-path { font-size: 0.72rem; color: var(--theme-foreground-muted); }
|
|
.repo-url { font-size: 0.72rem; color: var(--theme-foreground-focus); text-decoration: none; }
|
|
.repo-url:hover { text-decoration: underline; }
|
|
|
|
/* ── Utility ─────────────────────────────────────────────────────────────── */
|
|
.dim { color: gray; font-style: italic; }
|
|
</style>
|