Skip to content

Commit 2c01741

Browse files
authored
Merge pull request #58 from alanshurafa/contrib/alanshurafa/default-visible-nav
[dashboards] Default-visible CRM + wiki nav with setup states
2 parents 316beac + 550477f commit 2c01741

9 files changed

Lines changed: 294 additions & 41 deletions

File tree

‎dashboards/open-brain-dashboard-pro/.env.example‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,19 @@ SESSION_SECRET=
1212
# column on the thoughts table). Leave unset to hide the lock/unlock toggle.
1313
# Generate with: echo -n "your-passphrase" | shasum -a 256
1414
# RESTRICTED_PASSPHRASE_HASH=
15+
16+
# Optional: controls whether the Contacts/Proposals (CRM) and Wiki nav entries
17+
# show up before their backend schema is installed. Leave unset (or any value
18+
# other than "auto") for the default: always show them, with a setup card
19+
# explaining how to enable the surface. Set to "auto" to hide each entry until
20+
# its schema/routes are detected, matching this dashboard's pre-milestone
21+
# behavior. NEXT_PUBLIC_* vars are inlined at build time — changing this
22+
# requires a rebuild, not just a restart.
23+
# NEXT_PUBLIC_OPTIONAL_NAV=
24+
25+
# Optional: base URL for the schema-folder links on the setup cards (e.g.
26+
# https://github.com/YOUR-ORG/YOUR-REPO/tree/main). Defaults to the
27+
# alanshurafa/OB1 fork, where all the optional schemas currently live. Set this
28+
# to your own repo/mirror if you maintain one. Build-time, like the other
29+
# NEXT_PUBLIC_* vars above.
30+
# NEXT_PUBLIC_SCHEMA_REPO_BASE=

‎dashboards/open-brain-dashboard-pro/README.md‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ If your brain runs the CRM truth layer (the `crm-core` + `crm-engagement` schema
2727
| **Contact** (`/contacts/:id`) | Editable fact panel with per-field origin, locks, and evidence; contact methods and aliases; this contact's open proposals with accept/reject; a notes / tasks / important-dates panel; and an activity view with a merged timeline and the raw change log. |
2828
| **Proposals** (`/proposals`) | Inbox of machine-suggested field changes filtered by status, with per-row accept/reject and bulk accept/reject for a whole import run. |
2929

30-
The Contacts and Proposals nav entries, and the open-proposals badge on the sidebar, appear **only when the brain exposes the `/crm` surface**. The dashboard probes for it once at login and caches the result in the session, so a brain without the CRM layer never shows these entries and behaves exactly as before. Each CRM read also degrades on its own: if an individual route is missing or errors, that panel renders empty instead of blanking the page.
30+
The Contacts and Proposals nav entries appear **by default, whether or not the brain has the CRM layer installed yet**. Visiting either page on a brain without it shows a setup card: which schema(s) to apply (`schemas/crm-core` + `schemas/crm-engagement`), where to run them (your Supabase project's SQL editor), a link to the schema README, a reminder that `open-brain-rest` needs the `/crm/*` route group deployed, and a **Re-check now** button that re-probes the brain and refreshes the page — no sign-out required. The setup card's schema links point at the `alanshurafa/OB1` fork by default because the optional schemas haven't landed upstream yet; override with `NEXT_PUBLIC_SCHEMA_REPO_BASE` (see [Configuration](#configuration)). The open-proposals badge only ever renders a real count, so it stays hidden until CRM is actually enabled. Each CRM read also degrades on its own: if an individual route is missing or errors after CRM is enabled, that panel renders empty instead of blanking the page.
31+
32+
Prefer the old hide-until-detected behavior? Set `NEXT_PUBLIC_OPTIONAL_NAV=auto` (see [Configuration](#configuration)) and rebuild — the dashboard probes for `/crm` once at login, caches the result in the session, and only then shows the Contacts/Proposals entries.
3133

3234
## Wiki (optional)
3335

@@ -38,7 +40,9 @@ If your brain runs the persistent-wiki layer (the `schemas/wiki-pages` schema an
3840
| **Wiki** (`/wiki`) | Page list filtered by kind (topic / entity / autobiography / custom), with section counts and a "new page" form. |
3941
| **Wiki page** (`/wiki/:slug`) | Sections in display order, each with its markdown body rendered sanitized, an origin chip (yours / generated), a lock toggle, and an evidence chip listing supporting thought ids. Edit a section inline (your edit takes ownership); add a section; archive the page. When a machine writer proposes an update to a section you own, a review panel shows the current body against the proposed draft with **accept / reject**. |
4042

41-
Wiki pages are reachable at `/wiki` (a dedicated sidebar entry lands in a later change). The dashboard probes for the `/wiki` surface once at login and caches the result in the session. When the schema is absent — the probe comes back negative, or `GET /wiki/pages` returns 404 — the page renders a short inline notice ("Wiki schema not installed — apply `schemas/wiki-pages` to enable") instead of an error, so a brain without the wiki layer behaves exactly as before.
43+
The Wiki nav entry appears **by default, whether or not the brain has the wiki layer installed yet**. Visiting `/wiki` on a brain without it — the login-time probe came back negative, a re-check still says no, or `GET /wiki/pages` returns 404 — shows a setup card: apply `schemas/wiki-pages`, confirm `open-brain-rest` exposes the `/wiki/*` route group, and a **Re-check now** button that re-probes the brain and refreshes the page without a sign-out.
44+
45+
Prefer the old hide-until-detected behavior? Set `NEXT_PUBLIC_OPTIONAL_NAV=auto` (see [Configuration](#configuration)) and rebuild — the dashboard probes for `/wiki` once at login, caches the result in the session, and only then shows the Wiki entry.
4246

4347
Archived pages are hidden from the list but stay fetchable — and editable — by slug; unarchive is a future gateway addition.
4448

@@ -64,6 +68,8 @@ All configuration is through environment variables. **The app refuses to start i
6468
| `NEXT_PUBLIC_API_URL` | Yes | Base URL of your Open Brain REST API, typically `https://YOUR-PROJECT-REF.supabase.co/functions/v1/open-brain-rest`. |
6569
| `SESSION_SECRET` | Yes | 32+ character secret used by `iron-session` to encrypt the session cookie. Generate with `openssl rand -hex 32`. |
6670
| `RESTRICTED_PASSPHRASE_HASH` | No | SHA-256 hash of a passphrase that unlocks restricted/sensitive content. Only meaningful if your brain has a `sensitivity_tier` column on `public.thoughts`. There is no official sensitivity-tiers primitive upstream yet — either add your own migration (see PR #192 for pattern) or wait for the primitive to land. On stock OB1, this dashboard's restricted-content toggle is hidden at startup. Generate with `echo -n "your-passphrase" \| shasum -a 256`. |
71+
| `NEXT_PUBLIC_OPTIONAL_NAV` | No | Controls whether the Contacts/Proposals (CRM) and Wiki nav entries show before their schema is installed. Default (unset, or any value other than `auto`): always show them, with a setup card on the page itself. Set to `auto` to restore hide-until-enabled behavior, gated on the login-time probe cached in the session. This is a `NEXT_PUBLIC_*` var, so it's inlined at **build** time — changing it needs a rebuild, not just a restart. |
72+
| `NEXT_PUBLIC_SCHEMA_REPO_BASE` | No | Base URL for the schema-folder links on the setup cards, e.g. `https://github.com/YOUR-ORG/YOUR-REPO/tree/main`. Defaults to the `alanshurafa/OB1` fork, where all the optional schemas currently live (they haven't landed upstream yet). Set it to your own repo/mirror if you maintain one. Build-time, like the other `NEXT_PUBLIC_*` vars. |
6773

6874
Copy `.env.example` to `.env.local` (gitignored) and fill it in.
6975

@@ -112,8 +118,8 @@ The dashboard calls these endpoints on your Open Brain REST gateway (all authent
112118
| `/thought/:id/connections` | GET | Detail page connections panel | Optional — panel hides if it errors |
113119
| `/duplicates`, `/duplicates/resolve` | GET / POST | Duplicates page | Optional — page shows an error otherwise |
114120
| `/ingest`, `/ingestion-jobs`, `/ingestion-jobs/:id`, `/ingestion-jobs/:id/execute` | POST / GET | Ingest page | Optional — page still loads without jobs |
115-
| `/crm/*` (contacts, proposals, notes, tasks, important-dates, timeline, history, …) | GET / POST / PATCH | Contacts, Proposals, contact detail panels | Optional — CRM surface is hidden unless `/crm` is detected at login |
116-
| `/wiki/*` (pages, pages/:slug, sections/:id/accept-pending, reject-pending, lock, …) | GET / POST / PUT / DELETE | Wiki list, wiki page, section edit / lock / draft review | Optional — wiki surface degrades to an inline notice unless `/wiki` is detected at login |
121+
| `/crm/*` (contacts, proposals, notes, tasks, important-dates, timeline, history, …) | GET / POST / PATCH | Contacts, Proposals, contact detail panels | Optional — nav entries show by default; the pages themselves render a setup card until `/crm` is detected |
122+
| `/wiki/*` (pages, pages/:slug, sections/:id/accept-pending, reject-pending, lock, …) | GET / POST / PUT / DELETE | Wiki list, wiki page, section edit / lock / draft review | Optional — nav entry shows by default; the page itself renders a setup card until `/wiki` is detected |
117123

118124
> **On `/reflections/*`:** The ExoCortex upstream dashboard staged a reflections feature. This fork does not yet ship a reflections UI surface, but the architecture is ready: if you add a reflection panel later and your gateway doesn't serve `/reflections/*`, expect a 404 that the UI should swallow. The existing optional endpoints already degrade this way — the Connections panel, Duplicates page, and Ingest history all swallow fetch errors and render an empty/neutral state instead of crashing.
119125
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { NextResponse } from "next/server";
2+
import { requireSession, getSession, AuthError } from "@/lib/auth";
3+
import { crmAvailable, wikiAvailable } from "@/lib/api";
4+
5+
/**
6+
* POST /api/capabilities/recheck — re-probes both optional surfaces (CRM,
7+
* wiki) and writes the results into the session. Lets a user who just applied
8+
* a schema pick it up immediately via the "Re-check now" button instead of
9+
* signing out and back in (the only other way to re-run the login-time probe).
10+
*/
11+
export async function POST() {
12+
let apiKey: string;
13+
try {
14+
({ apiKey } = await requireSession());
15+
} catch (err) {
16+
if (err instanceof AuthError)
17+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
18+
throw err;
19+
}
20+
21+
let crmEnabled: boolean;
22+
let wikiEnabled: boolean;
23+
try {
24+
[crmEnabled, wikiEnabled] = await Promise.all([
25+
crmAvailable(apiKey),
26+
wikiAvailable(apiKey),
27+
]);
28+
} catch (err) {
29+
// crmAvailable/wikiAvailable already swallow their own upstream errors and
30+
// resolve to false — this catch only guards against something unexpected
31+
// (e.g. a thrown non-ApiError) so the route never 500s on a re-check.
32+
console.error("[capabilities/recheck]", err);
33+
return NextResponse.json(
34+
{ error: "Failed to re-check capabilities" },
35+
{ status: 500 }
36+
);
37+
}
38+
39+
const session = await getSession();
40+
session.crmEnabled = crmEnabled;
41+
session.wikiEnabled = wikiEnabled;
42+
await session.save();
43+
44+
return NextResponse.json({ crmEnabled, wikiEnabled });
45+
}

‎dashboards/open-brain-dashboard-pro/app/contacts/page.tsx‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Link from "next/link";
22
import { fetchCrmContacts, ApiError } from "@/lib/api";
33
import { requireSessionOrRedirect, getSession } from "@/lib/auth";
44
import { FormattedDate } from "@/components/FormattedDate";
5+
import { SetupState } from "@/components/SetupState";
56

67
export const dynamic = "force-dynamic";
78

@@ -34,6 +35,15 @@ export default async function ContactsPage({
3435
const q = params.q || "";
3536
const tier = params.privacy_tier || "";
3637

38+
// If we already know from login (or a re-check) the brain has no CRM
39+
// surface, short-circuit to the setup state without a doomed fetch.
40+
// `crmEnabled === false` is the only negative signal we trust; `undefined`
41+
// (old cookie, pre-dating this field) falls through to the fetch below,
42+
// whose 404 handling covers the schema-missing case anyway.
43+
if (session.crmEnabled === false) {
44+
return <SetupState surface="crm" />;
45+
}
46+
3747
let data;
3848
try {
3949
data = await fetchCrmContacts(apiKey, {
@@ -46,6 +56,11 @@ export default async function ContactsPage({
4656
} catch (err) {
4757
if (err instanceof ApiError) {
4858
console.error("[contacts] upstream", err.status, err.upstreamBody);
59+
// 404 → the brain doesn't have the crm-core schema/routes. Show the
60+
// same setup state as the cached-negative case.
61+
if (err.status === 404) {
62+
return <SetupState surface="crm" />;
63+
}
4964
} else {
5065
console.error("[contacts]", err);
5166
}

‎dashboards/open-brain-dashboard-pro/app/layout.tsx‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,14 @@ export default async function RootLayout({
3232
process.env.RESTRICTED_PASSPHRASE_HASH &&
3333
process.env.RESTRICTED_PASSPHRASE_HASH.length > 0
3434
);
35-
// Feature detection: the CRM sidebar entry only appears when the brain
36-
// exposed the /crm surface at login (probed and cached in the session).
35+
// Feature detection, cached at login (and refreshable via the "Re-check
36+
// now" button on each surface's setup state — see
37+
// app/api/capabilities/recheck/route.ts). Nav visibility itself defaults to
38+
// always-on; these flags only drive the open-proposals badge here and the
39+
// NEXT_PUBLIC_OPTIONAL_NAV=auto hide-until-enabled branch inside Sidebar.
3740
const session = await getSession();
3841
const crmEnabled = session.crmEnabled === true;
42+
const wikiEnabled = session.wikiEnabled === true;
3943

4044
// Open-proposals badge: a best-effort read that never blocks the shell. An
4145
// older brain without the count route (or any transient error) degrades to no
@@ -60,7 +64,12 @@ export default async function RootLayout({
6064
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
6165
>
6266
<body className="min-h-screen flex bg-bg-primary text-text-primary">
63-
<Sidebar restrictedConfigured={restrictedConfigured} crmEnabled={crmEnabled} openProposals={openProposals} />
67+
<Sidebar
68+
restrictedConfigured={restrictedConfigured}
69+
crmEnabled={crmEnabled}
70+
wikiEnabled={wikiEnabled}
71+
openProposals={openProposals}
72+
/>
6473
<main className="flex-1 ml-56 min-h-screen">
6574
<div className="max-w-6xl mx-auto px-8 py-8">
6675
{children}

‎dashboards/open-brain-dashboard-pro/app/proposals/page.tsx‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import Link from "next/link";
22
import { revalidatePath } from "next/cache";
33
import { fetchCrmProposals, resolveCrmProposal, resolveCrmProposalsByRun, ApiError } from "@/lib/api";
4-
import { requireSessionOrRedirect } from "@/lib/auth";
4+
import { requireSessionOrRedirect, getSession } from "@/lib/auth";
55
import { FormattedDate } from "@/components/FormattedDate";
66
import { ProposalDecision } from "./ProposalDecision";
77
import type { ResolveResult } from "./ProposalDecision";
88
import { RunBulkActions } from "./RunBulkActions";
9+
import { SetupState } from "@/components/SetupState";
910

1011
export const dynamic = "force-dynamic";
1112

@@ -99,17 +100,32 @@ export default async function ProposalsPage({
99100
searchParams: Promise<Record<string, string | undefined>>;
100101
}) {
101102
const { apiKey } = await requireSessionOrRedirect();
103+
const session = await getSession();
102104
const params = await searchParams;
103105
const page = parseInt(params.page || "1", 10);
104106
const status = params.status || "open";
105107
const runId = params.run_id || undefined;
106108

109+
// If we already know from login (or a re-check) the brain has no CRM
110+
// surface, short-circuit to the setup state without a doomed fetch.
111+
// `crmEnabled === false` is the only negative signal we trust; `undefined`
112+
// (old cookie, pre-dating this field) falls through to the fetch below,
113+
// whose 404 handling covers the schema-missing case anyway.
114+
if (session.crmEnabled === false) {
115+
return <SetupState surface="crm" />;
116+
}
117+
107118
let data;
108119
try {
109120
data = await fetchCrmProposals(apiKey, { page, per_page: 25, status, run_id: runId });
110121
} catch (err) {
111122
if (err instanceof ApiError) {
112123
console.error("[proposals] upstream", err.status, err.upstreamBody);
124+
// 404 → the brain doesn't have the crm-core schema/routes. Show the
125+
// same setup state as the cached-negative case.
126+
if (err.status === 404) {
127+
return <SetupState surface="crm" />;
128+
}
113129
} else {
114130
console.error("[proposals]", err);
115131
}

‎dashboards/open-brain-dashboard-pro/app/wiki/page.tsx‎

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { fetchWikiPages, ApiError } from "@/lib/api";
33
import { requireSessionOrRedirect, getSession } from "@/lib/auth";
44
import { FormattedDate } from "@/components/FormattedDate";
55
import { NewPageForm } from "./NewPageForm";
6+
import { SetupState } from "@/components/SetupState";
67
import type { WikiPageKind } from "@/lib/types";
78

89
export const dynamic = "force-dynamic";
@@ -17,26 +18,6 @@ function KindBadge({ kind }: { kind: string }) {
1718
);
1819
}
1920

20-
// Shared inline empty-state shown when the wiki schema/routes are absent. Kept
21-
// deliberately minimal and self-contained: a later PR replaces this with a
22-
// shared setup-state component, so there is nothing here to migrate but one <p>.
23-
function WikiNotInstalled() {
24-
return (
25-
<div className="space-y-6">
26-
<div>
27-
<h1 className="text-2xl font-semibold mb-1">Wiki</h1>
28-
</div>
29-
<div className="bg-bg-surface border border-border rounded-lg p-8">
30-
<p className="text-text-secondary text-sm max-w-prose">
31-
Wiki schema not installed — apply <code className="font-mono text-xs px-1 py-0.5 rounded bg-bg-elevated border border-border text-text-primary">schemas/wiki-pages</code>{" "}
32-
to your brain and deploy the <code className="font-mono text-xs px-1 py-0.5 rounded bg-bg-elevated border border-border text-text-primary">/wiki</code>{" "}
33-
gateway routes to enable persistent wiki pages. See the repo README for the setup steps.
34-
</p>
35-
</div>
36-
</div>
37-
);
38-
}
39-
4021
export default async function WikiPage({
4122
searchParams,
4223
}: {
@@ -54,7 +35,7 @@ export default async function WikiPage({
5435
// negative signal we trust; `undefined` (old cookie) falls through to the fetch,
5536
// whose 404 handling covers the schema-missing case anyway.
5637
if (session.wikiEnabled === false) {
57-
return <WikiNotInstalled />;
38+
return <SetupState surface="wiki" />;
5839
}
5940

6041
let data;
@@ -68,9 +49,9 @@ export default async function WikiPage({
6849
if (err instanceof ApiError) {
6950
console.error("[wiki] upstream", err.status, err.upstreamBody);
7051
// 404 → the brain doesn't have the wiki schema/routes. Show the same
71-
// inline empty-state as the cached-negative case.
52+
// setup state as the cached-negative case.
7253
if (err.status === 404) {
73-
return <WikiNotInstalled />;
54+
return <SetupState surface="wiki" />;
7455
}
7556
} else {
7657
console.error("[wiki]", err);

0 commit comments

Comments
 (0)