---
title: Goals
---

```js
import {API, apiFetch, pollDelay, waitForVisible} from "./components/config.js";
const POLL = 20_000;
```

```js
const goalsState = (async function*() {
  let failures = 0;
  while (true) {
    let domains = [], domainGoals = [], repoGoals = [], repos = [], ok = false;
    try {
      const [rd, rdg, rrg, rr] = await Promise.all([
        apiFetch("/domains/?status=active"),
        apiFetch("/domain-goals/"),
        apiFetch("/repo-goals/"),
        apiFetch("/repos/"),
      ]);
      ok = rd.ok && rdg.ok && rrg.ok && rr.ok;
      if (ok) {
        [domains, domainGoals, repoGoals, repos] = await Promise.all([
          rd.json(), rdg.json(), rrg.json(), rr.json(),
        ]);
      }
    } catch {}
    failures = ok ? 0 : failures + 1;
    yield {domains, domainGoals, repoGoals, repos, ok, ts: new Date()};
    await waitForVisible(pollDelay({ok, base: POLL, failures}));
  }
})();
```

```js
const domains     = goalsState.domains     ?? [];
const domainGoals = goalsState.domainGoals ?? [];
const repoGoals   = goalsState.repoGoals   ?? [];
const repos       = goalsState.repos       ?? [];
const _ok         = goalsState.ok          ?? false;
const _ts         = goalsState.ts;
```

```js
// ── Indexes ────────────────────────────────────────────────────────────────────
const repoById       = Object.fromEntries(repos.map(r => [r.id, r]));
const domainById     = Object.fromEntries(domains.map(d => [d.id, d]));

// Domain goals keyed by domain_id; active first, then superseded, then archived
const goalsByDomain = {};
for (const g of domainGoals) {
  if (!goalsByDomain[g.domain_id]) goalsByDomain[g.domain_id] = [];
  goalsByDomain[g.domain_id].push(g);
}
const STATUS_ORDER = {active: 0, superseded: 1, archived: 2};
for (const id of Object.keys(goalsByDomain)) {
  goalsByDomain[id].sort((a, b) =>
    (STATUS_ORDER[a.status] ?? 9) - (STATUS_ORDER[b.status] ?? 9)
  );
}

// Repo goals keyed by domain_goal_id (primary) and repo_id (for unlinked)
const repoGoalsByDomainGoal = {};
const unlinkedRepoGoals = [];  // active repo goals with no domain_goal_id
for (const rg of repoGoals) {
  if (rg.domain_goal_id) {
    if (!repoGoalsByDomainGoal[rg.domain_goal_id]) repoGoalsByDomainGoal[rg.domain_goal_id] = [];
    repoGoalsByDomainGoal[rg.domain_goal_id].push(rg);
  } else if (rg.status === "active") {
    unlinkedRepoGoals.push(rg);
  }
}
// Sort repo goals within each domain goal by priority asc
for (const id of Object.keys(repoGoalsByDomainGoal)) {
  repoGoalsByDomainGoal[id].sort((a, b) => a.priority - b.priority);
}

// KPI
const domainsWithActiveGoal = domains.filter(d => (goalsByDomain[d.id] ?? []).some(g => g.status === "active"));
const domainsWithoutGoal    = domains.filter(d => !(goalsByDomain[d.id] ?? []).some(g => g.status === "active"));
const totalActiveRepoGoals  = repoGoals.filter(g => g.status === "active").length;
```

# Goals

```js
import {injectTocTop} from "./components/toc-sidebar.js";
import {withDocHelp}  from "./components/doc-overlay.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");

// ── KPI sidebar card ────────────────────────────────────────────────────────────
const _kpiBox = html`<div class="kpi-infobox">
  <div class="kpi-infobox-title">Goals</div>
  <div class="kpi-row">
    <span class="kpi-row-label">domains with active goal</span>
    <div class="kpi-row-right">
      <div class="kpi-row-value">${domainsWithActiveGoal.length} / ${domains.length}</div>
    </div>
  </div>
  <div class="kpi-row">
    <span class="kpi-row-label">active repo goals</span>
    <div class="kpi-row-right">
      <div class="kpi-row-value">${totalActiveRepoGoals}</div>
    </div>
  </div>
  ${domainsWithoutGoal.length > 0 ? html`
  <div class="kpi-block" style="border-top:1px solid var(--theme-foreground-faint,#eee);padding-top:0.4rem;margin-top:0.1rem">
    <div class="kpi-row-label" style="color:#b45309;margin-bottom:0.25rem">no active goal</div>
    <div class="kpi-slug-list">${domainsWithoutGoal.map(d => html`<div class="kpi-slug-item">${d.slug}</div>`)}</div>
  </div>` : ""}
</div>`;

injectTocTop("goals-kpi-box", _kpiBox);
injectTocTop("live-indicator", _liveEl);

const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/goals"); }
```

Strategic intent, organised by domain. Each domain has one **active** goal at a time; prior goals are retained as history. Repository goals inherit from a domain goal and refine it into actionable scope for a specific repo.

---

```js
// ── Repo goal card renderer ────────────────────────────────────────────────────
function renderRepoGoalCard(rg) {
  const repo   = repoById[rg.repo_id];
  const STATUS_COLORS = {
    active:    {border: "#3b82f6", badge_bg: "#dbeafe", badge_fg: "#1e40af"},
    paused:    {border: "#f59e0b", badge_bg: "#fef3c7", badge_fg: "#92400e"},
    completed: {border: "#22c55e", badge_bg: "#dcfce7", badge_fg: "#166534"},
    archived:  {border: "#94a3b8", badge_bg: "#f1f5f9", badge_fg: "#475569"},
  };
  const c = STATUS_COLORS[rg.status] ?? STATUS_COLORS.archived;
  return html`<div class="repo-goal-card" style="border-left-color:${c.border}">
    <div class="rg-header">
      <span class="rg-priority" title="Priority (lower = higher priority)">#${rg.priority}</span>
      <span class="rg-repo">${repo?.slug ?? rg.repo_id.slice(0,8)}</span>
      <span class="status-badge" style="background:${c.badge_bg};color:${c.badge_fg}">${rg.status}</span>
    </div>
    <div class="rg-title">${rg.title}</div>
    <div class="rg-desc">${rg.description}</div>
    <div class="rg-meta">goal id: <code>${rg.id.slice(0,8)}…</code></div>
  </div>`;
}

// ── Domain section renderer ────────────────────────────────────────────────────
function renderDomainSection(domain) {
  const goals = goalsByDomain[domain.id] ?? [];
  const activeGoal     = goals.find(g => g.status === "active");
  const secondaryGoals = goals.filter(g => g.status !== "active");

  return html`<section class="domain-section">
    <div class="domain-header">
      <span class="domain-chip">${domain.slug}</span>
      <span class="domain-name">${domain.name}</span>
    </div>

    ${activeGoal ? html`
    <!-- ── Active domain goal ─────────────────────────────────────────── -->
    <div class="domain-goal-card dg-active">
      <div class="dg-header">
        <span class="dg-level-label">Domain Goal</span>
        <span class="status-badge badge-active">active</span>
      </div>
      <div class="dg-title">${activeGoal.title}</div>
      <div class="dg-desc">${activeGoal.description}</div>
      <div class="dg-meta">
        goal id: <code>${activeGoal.id.slice(0,8)}…</code> ·
        set ${new Date(activeGoal.created_at).toLocaleDateString()}
      </div>

      ${(repoGoalsByDomainGoal[activeGoal.id] ?? []).length > 0 ? html`
      <div class="rg-section">
        <div class="rg-section-label">Repository Goals</div>
        ${(repoGoalsByDomainGoal[activeGoal.id] ?? []).map(renderRepoGoalCard)}
      </div>` : html`
      <div class="rg-section rg-empty">No repository goals linked to this domain goal yet.</div>`}
    </div>
    ` : html`
    <div class="dg-empty">
      <span class="dg-no-goal-label">No active goal set for this domain.</span>
    </div>`}

    ${secondaryGoals.length > 0 ? html`
    <!-- ── Secondary goals (superseded / archived) ────────────────────── -->
    <details class="secondary-goals-details">
      <summary class="secondary-goals-summary">
        ${secondaryGoals.length} secondary goal${secondaryGoals.length === 1 ? "" : "s"}
        (${[...new Set(secondaryGoals.map(g => g.status))].join(", ")})
      </summary>
      <div class="secondary-goals-list">
        ${secondaryGoals.map(g => html`
        <div class="domain-goal-card dg-secondary">
          <div class="dg-header">
            <span class="dg-level-label">Domain Goal</span>
            <span class="status-badge badge-${g.status}">${g.status}</span>
          </div>
          <div class="dg-title">${g.title}</div>
          <div class="dg-desc">${g.description}</div>
          <div class="dg-meta">
            goal id: <code>${g.id.slice(0,8)}…</code> ·
            set ${new Date(g.created_at).toLocaleDateString()}
          </div>
          ${(repoGoalsByDomainGoal[g.id] ?? []).length > 0 ? html`
          <div class="rg-section rg-section-secondary">
            <div class="rg-section-label">Repository Goals</div>
            ${(repoGoalsByDomainGoal[g.id] ?? []).map(renderRepoGoalCard)}
          </div>` : ""}
        </div>`)}
      </div>
    </details>` : ""}

  </section>`;
}

// ── Main render ────────────────────────────────────────────────────────────────
if (!_ok) {
  display(html`<p class="dim">API offline — run <code>make api</code> from state-hub/.</p>`);
} else if (domains.length === 0) {
  display(html`<p class="dim">No active domains found.</p>`);
} else {
  // Domains with active goal first, then those without
  const sorted = [
    ...domainsWithActiveGoal.sort((a, b) => a.slug.localeCompare(b.slug)),
    ...domainsWithoutGoal.sort((a, b) => a.slug.localeCompare(b.slug)),
  ];
  display(html`<div class="goals-root">${sorted.map(renderDomainSection)}</div>`);
}
```

```js
// ── Unlinked active repo goals ─────────────────────────────────────────────────
if (unlinkedRepoGoals.length > 0) {
  display(html`
    <hr/>
    <h2>Unlinked Repository Goals</h2>
    <p class="dim" style="margin-bottom:1rem">Active repo goals not yet associated with a domain goal.</p>
    <div class="rg-list-unlinked">${unlinkedRepoGoals.map(renderRepoGoalCard)}</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 infobox ──────────────────────────────────────────────────────────── */
.kpi-row { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: 0.3rem 0; }
.kpi-row + .kpi-row { border-top: 1px solid var(--theme-foreground-faint, #eee); }
.kpi-row-label { font-size: 0.8rem; color: var(--theme-foreground-muted, #666); white-space: nowrap; }
.kpi-row-right { text-align: right; }
.kpi-row-value { font-size: 1.25rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.1; }
.kpi-block { }
.kpi-slug-list { display: flex; flex-direction: column; gap: 0.15rem; }
.kpi-slug-item { font-family: monospace; font-size: 0.78rem; color: #b45309; }

/* ── Layout ───────────────────────────────────────────────────────────────── */
.goals-root { display: flex; flex-direction: column; gap: 2rem; }

/* ── Domain section ───────────────────────────────────────────────────────── */
.domain-section { border: 1px solid var(--theme-foreground-faint, #e0e0e0); border-radius: 12px; overflow: hidden; }
.domain-header { display: flex; align-items: center; gap: 0.75rem; padding: 0.65rem 1.1rem; background: var(--theme-background-alt); border-bottom: 1px solid var(--theme-foreground-faint, #e0e0e0); }
.domain-chip { font-family: monospace; font-size: 0.8rem; background: var(--theme-background); border: 1px solid var(--theme-foreground-faint, #ddd); border-radius: 4px; padding: 0.1rem 0.5rem; color: var(--theme-foreground-muted); }
.domain-name { font-size: 0.95rem; font-weight: 700; }

/* ── Domain goal card ─────────────────────────────────────────────────────── */
.domain-goal-card { padding: 1rem 1.2rem; }
.dg-active  { border-left: 4px solid #22c55e; background: var(--theme-background); }
.dg-secondary { border-left: 4px solid #94a3b8; background: var(--theme-background-alt); opacity: 0.85; }
.dg-empty { padding: 0.8rem 1.2rem; color: var(--theme-foreground-muted); font-style: italic; font-size: 0.85rem; }
.dg-no-goal-label { }

.dg-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.4rem; }
.dg-level-label { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: var(--theme-foreground-muted); background: var(--theme-background-alt); border: 1px solid var(--theme-foreground-faint, #ddd); border-radius: 4px; padding: 0.1rem 0.4rem; }
.dg-title { font-size: 1.05rem; font-weight: 700; margin-bottom: 0.35rem; line-height: 1.3; }
.dg-desc { font-size: 0.85rem; color: var(--theme-foreground-muted); line-height: 1.5; margin-bottom: 0.5rem; }
.dg-meta { font-size: 0.72rem; color: var(--theme-foreground-faint); }

/* ── Status badges ────────────────────────────────────────────────────────── */
.status-badge { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 8px; font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; }
.badge-active     { background: #dcfce7; color: #166534; }
.badge-superseded { background: #f3e8ff; color: #6b21a8; }
.badge-archived   { background: #f1f5f9; color: #475569; }
.badge-paused     { background: #fef3c7; color: #92400e; }
.badge-completed  { background: #dcfce7; color: #166534; }

/* ── Repo goal section ────────────────────────────────────────────────────── */
.rg-section { border-top: 1px dashed var(--theme-foreground-faint, #ddd); margin-top: 0.8rem; padding-top: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; }
.rg-section-secondary { opacity: 0.85; }
.rg-section-label { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: var(--theme-foreground-muted); margin-bottom: 0.35rem; }
.rg-empty { font-size: 0.8rem; font-style: italic; color: var(--theme-foreground-faint); }

/* ── Repo goal card ───────────────────────────────────────────────────────── */
.repo-goal-card { border-left: 3px solid #3b82f6; border-radius: 0 6px 6px 0; background: var(--theme-background-alt); padding: 0.55rem 0.9rem; }
.rg-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.3rem; font-size: 0.75rem; }
.rg-priority { font-weight: 700; color: var(--theme-foreground-muted); font-family: monospace; min-width: 2rem; }
.rg-repo { font-family: monospace; font-size: 0.75rem; background: var(--theme-background); border: 1px solid var(--theme-foreground-faint, #ddd); border-radius: 3px; padding: 0.05rem 0.35rem; color: var(--theme-foreground-muted); }
.rg-title { font-size: 0.9rem; font-weight: 700; margin-bottom: 0.25rem; }
.rg-desc { font-size: 0.8rem; color: var(--theme-foreground-muted); line-height: 1.45; margin-bottom: 0.3rem; }
.rg-meta { font-size: 0.7rem; color: var(--theme-foreground-faint); }

/* ── Secondary goals accordion ───────────────────────────────────────────── */
.secondary-goals-details { border-top: 1px solid var(--theme-foreground-faint, #eee); }
.secondary-goals-summary { padding: 0.6rem 1.2rem; font-size: 0.8rem; color: var(--theme-foreground-muted); cursor: pointer; list-style: none; user-select: none; }
.secondary-goals-summary::-webkit-details-marker { display: none; }
.secondary-goals-summary::before { content: "▶ "; font-size: 0.65rem; }
details[open] .secondary-goals-summary::before { content: "▼ "; }
.secondary-goals-list { padding: 0 0 0.5rem 0; display: flex; flex-direction: column; gap: 0.5rem; }

/* ── Unlinked repo goals ──────────────────────────────────────────────────── */
.rg-list-unlinked { display: flex; flex-direction: column; gap: 0.5rem; max-width: 720px; }

/* ── Utility ─────────────────────────────────────────────────────────────── */
.dim { color: gray; font-style: italic; }
</style>
