generated from coulomb/repo-seed
Implement RecentlyOnScope domain digest
This commit is contained in:
@@ -62,6 +62,18 @@ One card per domain showing:
|
||||
|
||||
---
|
||||
|
||||
## RecentlyOnScope
|
||||
|
||||
The `Domains / RecentlyOnScope` page generates deterministic Markdown digests
|
||||
for a selected domain. The range parameter defaults to `1h` and accepts compact
|
||||
durations such as `15m`, `6h`, or `1d`.
|
||||
|
||||
Generated reports are written under the configured State Hub report directory,
|
||||
defaulting to `reports/recently-on-scope/<domain_slug>/`. The dashboard lists
|
||||
those Markdown files and previews the raw report content.
|
||||
|
||||
---
|
||||
|
||||
## Managing domains
|
||||
|
||||
Via MCP:
|
||||
|
||||
128
dashboard/src/domains/recently-on-scope.md
Normal file
128
dashboard/src/domains/recently-on-scope.md
Normal file
@@ -0,0 +1,128 @@
|
||||
---
|
||||
title: RecentlyOnScope
|
||||
---
|
||||
|
||||
```js
|
||||
import {apiFetch} from "../components/config.js";
|
||||
```
|
||||
|
||||
```js
|
||||
const domainsResp = await apiFetch("/domains/?status=all");
|
||||
const domains = domainsResp.ok ? await domainsResp.json() : [];
|
||||
const domainOptions = domains.map(d => d.slug).sort();
|
||||
const defaultDomain = domainOptions.includes("custodian") ? "custodian" : domainOptions[0];
|
||||
```
|
||||
|
||||
# RecentlyOnScope
|
||||
|
||||
```js
|
||||
import {injectTocTop} from "../components/toc-sidebar.js";
|
||||
import {withDocHelp} from "../components/doc-overlay.js";
|
||||
|
||||
const _liveEl = html`<div class="live-indicator">
|
||||
<span style="color:${domainsResp.ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
|
||||
${domainsResp.ok
|
||||
? `Live · ${domains.length} domains`
|
||||
: html`<span style="color:red">Offline — run: <code>make api</code></span>`}
|
||||
</div>`;
|
||||
withDocHelp(_liveEl, "/docs/domains");
|
||||
injectTocTop("live-indicator", _liveEl);
|
||||
```
|
||||
|
||||
```js
|
||||
const selectedDomain = view(Inputs.select(domainOptions, {label: "Domain", value: defaultDomain}));
|
||||
const selectedRange = view(Inputs.text({label: "Range", value: "1h", placeholder: "15m, 1h, 6h, 1d"}));
|
||||
|
||||
const generated = view(Inputs.button("Generate", {
|
||||
reduce: async () => {
|
||||
if (!selectedDomain) return {ok: false, error: "No domain selected"};
|
||||
const resp = await apiFetch(`/domains/${encodeURIComponent(selectedDomain)}/recently-on-scope/`, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({range: selectedRange || "1h"}),
|
||||
timeout: 30_000,
|
||||
});
|
||||
if (!resp.ok) return {ok: false, error: await resp.text()};
|
||||
return {ok: true, report: await resp.json()};
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
```js
|
||||
if (generated?.ok) {
|
||||
display(html`<div class="notice success">Generated <code>${generated.report.id}</code></div>`);
|
||||
} else if (generated?.error) {
|
||||
display(html`<div class="notice error">${generated.error}</div>`);
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
generated;
|
||||
const reportsResp = selectedDomain
|
||||
? await apiFetch(`/domains/${encodeURIComponent(selectedDomain)}/recently-on-scope/`)
|
||||
: {ok: false};
|
||||
const reports = reportsResp.ok ? await reportsResp.json() : [];
|
||||
```
|
||||
|
||||
## Reports
|
||||
|
||||
```js
|
||||
function fmtDate(value) {
|
||||
if (!value) return "—";
|
||||
return new Date(value).toLocaleString();
|
||||
}
|
||||
|
||||
if (!selectedDomain) {
|
||||
display(html`<p class="dim">No domains registered.</p>`);
|
||||
} else if (reports.length === 0) {
|
||||
display(html`<p class="dim">No reports for <code>${selectedDomain}</code>.</p>`);
|
||||
} else {
|
||||
display(html`<div class="report-list">${reports.map(report => html`<article class="report-row">
|
||||
<div>
|
||||
<a class="report-id" href=${`#${report.id}`}>${report.id}</a>
|
||||
<div class="report-window">${fmtDate(report.since)} -> ${fmtDate(report.until)}</div>
|
||||
</div>
|
||||
<div class="report-counts">
|
||||
<span>${report.source_counts.progress_events} progress</span>
|
||||
<span>${report.source_counts.decisions} decisions</span>
|
||||
<span>${report.source_counts.tasks} tasks</span>
|
||||
<span>${report.source_counts.attention_items} attention</span>
|
||||
</div>
|
||||
</article>`)}</div>`);
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
const reportIds = reports.map(report => report.id);
|
||||
const selectedReport = reportIds.length > 0
|
||||
? view(Inputs.select(reportIds, {label: "Preview", value: reportIds[0]}))
|
||||
: null;
|
||||
```
|
||||
|
||||
```js
|
||||
if (selectedDomain && selectedReport) {
|
||||
const markdownResp = await apiFetch(`/domains/${encodeURIComponent(selectedDomain)}/recently-on-scope/${encodeURIComponent(selectedReport)}`);
|
||||
const markdown = markdownResp.ok ? await markdownResp.text() : "";
|
||||
display(html`<pre class="markdown-preview">${markdown}</pre>`);
|
||||
}
|
||||
```
|
||||
|
||||
<style>
|
||||
.live-indicator { font-size: 0.8rem; color: gray; position: relative; padding: 0.55rem 1.8rem 0.55rem 0.7rem; margin-bottom: 0.75rem; }
|
||||
.notice { border-radius: 6px; padding: 0.55rem 0.75rem; margin: 0.75rem 0; font-size: 0.85rem; }
|
||||
.notice.success { border: 1px solid #86efac; background: #f0fdf4; color: #166534; }
|
||||
.notice.error { border: 1px solid #fecaca; background: #fef2f2; color: #991b1b; white-space: pre-wrap; }
|
||||
.report-list { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.report-row { display: grid; grid-template-columns: minmax(220px, 1.2fr) minmax(260px, 1fr); gap: 1rem; align-items: center; border: 1px solid var(--theme-foreground-faint, #ddd); border-radius: 6px; padding: 0.7rem 0.85rem; background: var(--theme-background-alt); }
|
||||
.report-id { font-family: var(--mono); font-size: 0.86rem; font-weight: 700; text-decoration: none; color: var(--theme-foreground-focus); }
|
||||
.report-id:hover { text-decoration: underline; }
|
||||
.report-window { font-size: 0.78rem; color: var(--theme-foreground-muted); margin-top: 0.2rem; }
|
||||
.report-counts { display: flex; flex-wrap: wrap; gap: 0.35rem; justify-content: flex-end; }
|
||||
.report-counts span { border: 1px solid var(--theme-foreground-faint, #ddd); border-radius: 999px; padding: 0.12rem 0.45rem; font-size: 0.72rem; background: var(--theme-background); color: var(--theme-foreground-muted); }
|
||||
.markdown-preview { white-space: pre-wrap; overflow-x: auto; margin-top: 1rem; padding: 0.85rem; border: 1px solid var(--theme-foreground-faint, #ddd); border-radius: 6px; background: var(--theme-background-alt); font-size: 0.82rem; line-height: 1.45; }
|
||||
.dim { color: gray; font-style: italic; }
|
||||
@media (max-width: 720px) {
|
||||
.report-row { grid-template-columns: 1fr; }
|
||||
.report-counts { justify-content: flex-start; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user