Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions PATTERN_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,6 @@ chapters use all of this.

![A pattern's detail page](docs/images/pattern-guide/10-pattern-detail.png)

One more thing in the top nav — **Workshop** is where directions get discussed
and decided. It's under heavy construction, so this guide leaves it alone for
now.

---

## 3. Putting someone else's pattern on your device
Expand Down
6 changes: 3 additions & 3 deletions web/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The `web/` app is the Patternflow site at [patternflow.work](https://patternflow
| `/guide` · `/guide/build` · `/guide/play` · `/guide/make` · `/guide/audio` (each + `/ko`) | The interactive guide: a hub and four guides on one continuous 3D stage (`components/guide/`), with the firmware's input logic and screens ported to `lib/guide/` and the real console and audio editor in windows (`public/guide/console-demo/`, `audio-editor/`, generated by `firmware/toolchain/console_demo.py` and `editor_demo.py`). Words in `components/guide/copy.ts` and `copy/`; old `/guide#flash`-style anchors land on `/guide/play` (`legacy.ts`) |
| `/inside/[build]` | One community build's page |
| `/pattern-lab` | The pattern workspace — see below |
| `/community/**` | The community: feed, pattern pages (`/p/[id]`), profiles (`/u/[username]`), decks (`/d/[id]`, `/decks`), the Workshop (`/workshop/[code]`), territories and the atlas, notifications, featured, reports. Renders a pointer to the community host unless `COMMUNITY_ENABLED=1` |
| `/community/**` | The community: feed, pattern pages (`/p/[id]`), profiles (`/u/[username]`), decks (`/d/[id]`, `/decks`), the atlas, notifications, featured, reports. The workshop came down on 26 October 2026, and its old addresses redirect to `/community` (`next.config.ts`). Renders a pointer to the community host unless `COMMUNITY_ENABLED=1` |
| `/editions` | The edition shelf: every firmware you can put on a panel, official and community, with one-click install. `/variants` (the URL until 2026-09, baked into shipped console pages) redirects here permanently |
| `/features` | The catalogue: every feature on its own — what it needs, which firmware carries it, and a reel of it running (Instagram embeds by permalink, lazy). The other axis of the shelf; hand-curated in `features-data.ts`. A feature that is not on the shelf can carry a frozen try-out image (`tryOut`), installed the way the shelf installs |
| `/update` | The device's firmware-update handoff: the browser downloads an image and POSTs it to the panel over the LAN, because the panel cannot fetch over TLS |
Expand All @@ -23,7 +23,7 @@ The `web/` app is the Patternflow site at [patternflow.work](https://patternflow
| `/roadmap` | Roadmap rendered from `roadmap-data.ts` plus GitHub issue progress via `/api/roadmap` |
| `/compliance` | Printed on the box — never move, redirect or noindex it |
| `/terms` · `/business` · `/contact` | Static pages |
| `/api/community/**` | The community's JSON API (30-odd routes: patterns, decks, posts, comments, builds, workshop, moderation…). Every route guards on `communityEnabled()`, and `src/proxy.ts` closes the whole prefix with a 404 on a deployment without the community, so a route that forgets is still closed |
| `/api/community/**` | The community's JSON API (20-odd routes: patterns, decks, comments, builds, the atlas, moderation…). Every route guards on `communityEnabled()`, and `src/proxy.ts` closes the whole prefix with a 404 on a deployment without the community, so a route that forgets is still closed |
| `/api/auth/[...all]` | Better Auth |
| `/api/variant-bin/[...path]` | Serves hosted edition images from `VARIANT_BIN_DIR` with the CORS headers `/update` needs |
| `/api/roadmap` | Open issues + sub-issue progress (10 min revalidate; optional `GITHUB_TOKEN`) |
Expand Down Expand Up @@ -81,7 +81,7 @@ Exports flatten the visible stack to one standalone pattern (`flatten.ts`); `hEx

## Community (`src/lib/community/` + `src/components/community/`)

SQLite via Drizzle (`server/schema.ts`; migrations in `web/drizzle/`, applied on first open by `server/db.ts`), Better Auth (`server/auth.ts`), reads in `server/queries/` — one file per domain (patterns, decks, workshop, moderation, users, notifications, plus the shared sub-selects), re-exported by `server/queries.ts` — attachments and thumbnails on disk under `communityDataDir()` (default `web/data/`, gitignored — set `COMMUNITY_DB_PATH` on a real deployment). **`lib/community/server/` is the server side** — everything that touches SQLite, the filesystem or a session — and ESLint refuses a value import of it from `components/` or a `*Client.tsx` (type imports are fine; a value crosses through an API route). The rest of `lib/community/` is either `"use client"` (`auth-client`, `deviceHost`, `download`, `handoff`, `knobs`, `thumbs`) or pure and usable anywhere (`validate`, `license`, `deck`, `workshop`, `cors`, …).
SQLite via Drizzle (`server/schema.ts`; migrations in `web/drizzle/`, applied on first open by `server/db.ts`), Better Auth (`server/auth.ts`), reads in `server/queries/` — one file per domain (patterns, decks, atlas, moderation, users, notifications, plus the shared sub-selects), re-exported by `server/queries.ts` — attachments and thumbnails on disk under `communityDataDir()` (default `web/data/`, gitignored — set `COMMUNITY_DB_PATH` on a real deployment). **`lib/community/server/` is the server side** — everything that touches SQLite, the filesystem or a session — and ESLint refuses a value import of it from `components/` or a `*Client.tsx` (type imports are fine; a value crosses through an API route). The rest of `lib/community/` is either `"use client"` (`auth-client`, `deviceHost`, `download`, `handoff`, `knobs`, `thumbs`) or pure and usable anywhere (`validate`, `license`, `deck`, `cors`, …).

Patterns render in a sandboxed iframe (`public/pattern-sandbox.html`, driven by `components/community/SandboxPreview.tsx`). Pattern Lab hands off to the community through `handoff.ts` (fork vs. edit-in-place), and publishes through `components/community/PublishModal.tsx`.

Expand Down
10 changes: 10 additions & 0 deletions web/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ const nextConfig: NextConfig = {
// to the guide's Flash chapter, where the button and the steps are. Only
// the exact path: /flash/manifest.json and /flash/bin/* are untouched.
{ source: "/flash", destination: "/guide/play#flash", permanent: false },
// The community's workshop came down on 26 October 2026: the map of
// directions, its threads, and the moderators' territory editor. Its
// rows stay in the database. A link to any of it, from an old alert, a
// post elsewhere or a bookmark, lands on the community's front page. One
// line per page that was there.
{ source: "/community/workshop", destination: "/community", permanent: false },
{ source: "/community/workshop/:code", destination: "/community", permanent: false },
{ source: "/community/workshop/:code/t/:id", destination: "/community", permanent: false },
{ source: "/community/t/:id", destination: "/community", permanent: false },
{ source: "/community/territories", destination: "/community", permanent: false },
// From the journal's first post on 29 April 2026 until 11 May, its own
// links asked for English with ?lang=en. English moved to /en paths that
// day (c44b0d7a), and these two send the old links there. The query goes
Expand Down
3 changes: 1 addition & 2 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
"check:notify": "tsx scripts/notify-smoke.ts",
"check:ports": "tsx scripts/ports-smoke.ts",
"seed:map": "tsx scripts/seed-territories.ts",
"check:workshop": "tsx scripts/workshop-smoke.ts",
"check:sandbox": "tsx scripts/sandbox-smoke.ts",
"check:capture": "tsx scripts/lab-capture-smoke.ts",
"check:thumbs": "tsx scripts/thumbs-smoke.ts",
Expand All @@ -43,7 +42,7 @@
"check:pfstvectors": "tsx scripts/pfst-v2-vectors-smoke.ts",
"check:ramp": "tsx scripts/ramp-oklab-smoke.ts",
"check:panels": "vitest run",
"check:ci": "npm run check:license && npm run check:moderation && npm run check:headermod && npm run check:hidemod && npm run check:retention && npm run check:deck && npm run check:pack && npm run check:sharing && npm run check:publish && npm run check:notify && npm run check:ports && npm run check:workshop && npm run check:sandbox && npm run check:capture && npm run check:thumbs && npm run check:flatten && npm run check:labedit && npm run check:performance && npm run check:pixelcode && npm run check:director && npm run check:pfstvectors && npm run check:ramp && npm run check:panels && npm run check:sandbox-sync && npm run check:modcache && npm run check:drift",
"check:ci": "npm run check:license && npm run check:moderation && npm run check:headermod && npm run check:hidemod && npm run check:retention && npm run check:deck && npm run check:pack && npm run check:sharing && npm run check:publish && npm run check:notify && npm run check:ports && npm run check:sandbox && npm run check:capture && npm run check:thumbs && npm run check:flatten && npm run check:labedit && npm run check:performance && npm run check:pixelcode && npm run check:director && npm run check:pfstvectors && npm run check:ramp && npm run check:panels && npm run check:sandbox-sync && npm run check:modcache && npm run check:drift",
"build:sandbox": "tsx scripts/build-sandbox.ts",
"check:sandbox-sync": "tsx scripts/build-sandbox.ts --check",
"gen:shared": "tsx scripts/gen-shared.ts",
Expand Down
4 changes: 3 additions & 1 deletion web/scripts/moderation-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@ check("a non-string is rejected", cleanModerationReason(42), undefined);

console.log("\n── report input ──");
check("pattern is a valid target", isReportTargetType("pattern"), true);
check("post is a valid target", isReportTargetType("post"), true);
// A workshop thread was a target until the workshop came down on 26 October 2026.
check("post is no longer a target", isReportTargetType("post"), false);
check("deck is a valid target", isReportTargetType("deck"), true);
check("comment is a valid target", isReportTargetType("comment"), true);
check("user is not a target", isReportTargetType("user"), false);
check("non-string is not a target", isReportTargetType(42), false);
Expand Down
50 changes: 24 additions & 26 deletions web/scripts/notify-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ async function main() {
updatedAt: ago(10),
},
]);
// Threads live on the map now, so the fixture needs somewhere to put one.
// The workshop came down on 26 October 2026 and its tables stayed, so the
// fixture keeps a thread for the alert below that still points at one.
await db.insert(schema.territories).values({
id: "z1",
code: "A1",
Expand All @@ -102,7 +103,6 @@ async function main() {
createdAt: NOW,
});
await notify.notifyCommentAdded({
on: "pattern",
targetId: "p1",
targetTitle: "Alice One",
ownerId: "alice",
Expand Down Expand Up @@ -132,28 +132,26 @@ async function main() {
check("thread rows say so", bobRows.map((row) => row.type), ["thread", "thread"]);
check("and carry the snippet", bobRows[0]?.snippet, "thanks both");

console.log("\n── post comments use the same path ──");
await db.insert(schema.postComments).values({
id: "pc1",
postId: "t1",
userId: "bob",
body: "post reply",
createdAt: NOW,
});
await notify.notifyCommentAdded({
on: "post",
console.log("\n── a workshop alert stays off the list ──");
await db.insert(schema.notifications).values({
id: "n-post",
userId: "cara",
type: "thread",
actorId: "bob",
targetType: "post",
targetId: "t1",
targetTitle: "Alice Thread",
ownerId: "alice",
actorId: "bob",
commentId: "pc1",
body: "post reply",
createdAt: NOW,
});
check("the post's author hears it", await queries.countUnreadNotifications("alice"), 3);
check(
"the row points at the post",
(await queries.listNotifications("alice"))[0]?.targetType,
"post",
"a row about a workshop thread is not listed",
(await queries.listNotifications("cara")).map((row) => row.targetType),
["pattern"],
);
check(
"not for a moderator either",
(await queries.listNotifications("cara", { moderator: true })).map((row) => row.targetType),
["pattern"],
);

console.log("\n── forks ──");
Expand All @@ -178,23 +176,23 @@ async function main() {
forkVisibility: "public",
actorId: "bob",
});
check("a public fork tells the parent's author", await queries.countUnreadNotifications("alice"), 4);
check("a public fork tells the parent's author", await queries.countUnreadNotifications("alice"), 3);
await notify.notifyForkPublished({
parentOwnerId: "alice",
parentTitle: "Alice One",
forkId: "p-fork2",
forkVisibility: "private",
actorId: "bob",
});
check("a private fork tells nobody — its page would 404", await queries.countUnreadNotifications("alice"), 4);
check("a private fork tells nobody — its page would 404", await queries.countUnreadNotifications("alice"), 3);
await notify.notifyForkPublished({
parentOwnerId: "alice",
parentTitle: "Alice One",
forkId: "p-fork3",
forkVisibility: "public",
actorId: "alice",
});
check("forking your own work tells nobody", await queries.countUnreadNotifications("alice"), 4);
check("forking your own work tells nobody", await queries.countUnreadNotifications("alice"), 3);

console.log("\n── deck inclusion ──");
await db.insert(schema.decks).values({
Expand All @@ -215,20 +213,20 @@ async function main() {
actorId: "bob",
patterns: deckPatterns,
});
check("the pattern's author hears it", await queries.countUnreadNotifications("alice"), 5);
check("the pattern's author hears it", await queries.countUnreadNotifications("alice"), 4);
check("the deck owner's own pattern tells nobody", await queries.countUnreadNotifications("bob"), 2);
await notify.notifyDeckInclusion({
deckId: "d1",
deckTitle: "Bob's Shelf",
actorId: "bob",
patterns: deckPatterns,
});
check("repeating the event while unread is absorbed", await queries.countUnreadNotifications("alice"), 5);
check("repeating the event while unread is absorbed", await queries.countUnreadNotifications("alice"), 4);

console.log("\n── reading ──");
await queries.markNotificationsRead("alice");
check("opening the page reads everything", await queries.countUnreadNotifications("alice"), 0);
check("the rows themselves stay", (await queries.listNotifications("alice")).length, 5);
check("the rows themselves stay", (await queries.listNotifications("alice")).length, 4);

console.log("\n── the read path respects visibility ──");
await db.update(schema.patterns).set({ visibility: "private" }).where(eq(schema.patterns.id, "p1"));
Expand Down
Loading
Loading