diff --git a/README.md b/README.md index 70f2f4d9..d1cb5fe9 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,9 @@ Bricks keeps useful information available after the chat moves on. ## Showcase -![Bricks code agent builds a website and returns a public URL](docs/assets/showcase/code-agent-website-public-url.png) +| Vibe Coding: Every Channel Gets a Workspace, Domain, and Live Site | +| --- | +| ![Bricks code agent builds a website and returns a public URL](docs/assets/showcase/code-agent-website-public-url.jpg) | | Topic-based agent work | Saved highlights and notes | | --- | --- | diff --git a/apps/node_backend/src/routes/channelSiteHost.test.ts b/apps/node_backend/src/routes/channelSiteHost.test.ts index cf15fee9..1dd3f80d 100644 --- a/apps/node_backend/src/routes/channelSiteHost.test.ts +++ b/apps/node_backend/src/routes/channelSiteHost.test.ts @@ -4,15 +4,37 @@ import os from 'node:os'; import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -const { serviceMock } = vi.hoisted(() => ({ +const { serviceMock, fsMockState } = vi.hoisted(() => ({ serviceMock: { getChannelSiteBySlug: vi.fn(), publicSiteDomain: vi.fn(() => 'craft-spaces.bricks.cool'), webDistPath: vi.fn(), }, + fsMockState: { + readFileError: null as NodeJS.ErrnoException | null, + }, })); vi.mock('../services/channelSiteService.js', () => serviceMock); +vi.mock('node:fs/promises', async (importOriginal) => { + const actual = await importOriginal(); + const readFile = vi.fn((...args: Parameters) => { + if (fsMockState.readFileError) { + const error = fsMockState.readFileError; + fsMockState.readFileError = null; + return Promise.reject(error); + } + return actual.readFile(...args); + }); + return { + ...actual, + readFile, + default: { + ...actual, + readFile, + }, + }; +}); describe('channelSiteHost route', () => { let tempDir: string; @@ -54,6 +76,7 @@ describe('channelSiteHost route', () => { await rm(tempDir, { recursive: true, force: true }); serviceMock.getChannelSiteBySlug.mockReset(); serviceMock.webDistPath.mockReset(); + fsMockState.readFileError = null; }); it('serves static files for craft-spaces slug hosts with noindex headers', async () => { @@ -106,4 +129,16 @@ describe('channelSiteHost route', () => { expect(response.headers.get('x-robots-tag')).toBe('noindex'); expect(await response.text()).toBe('Site not published'); }); + + it('returns not published instead of 500 when an asset disappears during read', async () => { + fsMockState.readFileError = Object.assign(new Error('missing asset'), { code: 'ENOENT' }); + + const response = await fetch(`${baseUrl}/assets/app.js`, { + headers: { 'X-Forwarded-Host': 's-abc123.craft-spaces.bricks.cool' }, + }); + + expect(response.status).toBe(404); + expect(response.headers.get('x-robots-tag')).toBe('noindex'); + expect(await response.text()).toBe('Site not published'); + }); }); diff --git a/apps/node_backend/src/routes/channelSiteHost.ts b/apps/node_backend/src/routes/channelSiteHost.ts index 6d540562..0e8d5915 100644 --- a/apps/node_backend/src/routes/channelSiteHost.ts +++ b/apps/node_backend/src/routes/channelSiteHost.ts @@ -78,6 +78,11 @@ function sendSiteNotPublished(res: Response): void { res.status(404).send('Site not published'); } +function isMissingFileError(error: unknown): boolean { + return (error as NodeJS.ErrnoException).code === 'ENOENT' || + (error as NodeJS.ErrnoException).code === 'ENOTDIR'; +} + async function serveChannelSiteBySlug(slug: string, req: Request, res: Response, next: NextFunction): Promise { try { const site = await getChannelSiteBySlug(slug); @@ -101,17 +106,28 @@ async function serveChannelSiteBySlug(slug: string, req: Request, res: Response, try { const stat = await fs.stat(target); if (stat.isFile()) { - await sendStaticFile(res, target); + try { + await sendStaticFile(res, target); + } catch (error) { + if (isMissingFileError(error)) { + sendSiteNotPublished(res); + return; + } + throw error; + } return; } - } catch { + } catch (error) { + if (!isMissingFileError(error)) { + throw error; + } // Fall through to SPA fallback. } try { await sendStaticFile(res, path.join(distRoot, 'index.html')); } catch (error) { - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + if (isMissingFileError(error)) { sendSiteNotPublished(res); return; } diff --git a/docs/assets/showcase/code-agent-website-public-url.jpg b/docs/assets/showcase/code-agent-website-public-url.jpg new file mode 100644 index 00000000..9ec89466 Binary files /dev/null and b/docs/assets/showcase/code-agent-website-public-url.jpg differ diff --git a/docs/assets/showcase/code-agent-website-public-url.png b/docs/assets/showcase/code-agent-website-public-url.png deleted file mode 100644 index 3e950d88..00000000 Binary files a/docs/assets/showcase/code-agent-website-public-url.png and /dev/null differ diff --git a/docs/code_maps/feature_map.yaml b/docs/code_maps/feature_map.yaml index e945f94a..9cef1c4a 100644 --- a/docs/code_maps/feature_map.yaml +++ b/docs/code_maps/feature_map.yaml @@ -622,7 +622,7 @@ products: features: - feature_id: dokku_channel_deployment name: Dokku 主站与 PR 预览部署 - user_value: main 分支自动部署到 `craft.bricks.cool`,PR 分支自动部署到独立预览域名和独立频道磁盘根目录。 + user_value: main 分支自动部署到 `craft.bricks.cool`,PR 分支自动部署到独立预览域名;preview 与 production 共享数据库和 production sandbox workspace root。 entry_paths: - route: https://craft.bricks.cool route_hint: .github/workflows/dokku_production_deploy.yml @@ -634,9 +634,9 @@ products: - main push 后 production app 重新部署并继续使用 `/home/bricks/data/production/sandboxes`。 - PR open/synchronize 后创建或更新 `bricks-preview-`,域名为 `.craft-dev.bricks.cool`。 - Dokku preview deploy 成功后发布 GitHub Deployment status,PR timeline 的 `View deployment` 按钮指向 preview 域名。 - - 每个 preview app mount 到 `/home/bricks/data/previews//sandboxes`,不共享 production 或其他 preview 的 sandbox 文件。 + - 每个 preview app mount 到 `/home/bricks/data/production/sandboxes`,并设置 `BRICKS_SANDBOX_RUNNER_ROOT_SEGMENTS=production,sandboxes`,与 production 共享 channel workspace 和 generated site build output。 - Turso credentials, JWT signing secret, and encryption key stay in Dokku server config; preview apps copy them from the production app on the server instead of reading GitHub Actions secrets. - Production CSP must allow Flutter Web bootstrap scripts so `craft.bricks.cool` renders the login app instead of a blank page. - Flutter Web app shell files such as `index.html`, `main.dart.js`, `flutter.js`, and `flutter_service_worker.js` must return no-store/no-cache headers so preview deploys do not serve stale UI bundles. - - PR close/merge 后对应 preview app 被 destroy,preview data root 被删除。 + - PR close/merge 后对应 preview app 被 destroy;当 `DATA_ROOT=/home/bricks/data/production/sandboxes` 时 cleanup 必须保留共享 production data root。 - GitHub OAuth callback 固定为 `https://craft.bricks.cool/api/callback`,preview 登录完成后可 redirect 回 preview 域名。 diff --git a/docs/code_maps/logic_map.yaml b/docs/code_maps/logic_map.yaml index a8c681b9..008c8258 100644 --- a/docs/code_maps/logic_map.yaml +++ b/docs/code_maps/logic_map.yaml @@ -472,6 +472,7 @@ index: - Publish status is commit-based only in the current architecture; if future flows need uncommitted workspace detection, dirty-state checks must be added separately without breaking the published commit contract. - "`site_build` must create a git snapshot before building so `published_commit_sha` identifies the workspace tree that produced the live artifact." - A `succeeded` build record with a missing `web/dist/index.html` must return a controlled not-published response instead of leaking a backend 500. + - Static asset reads can race with build/deploy replacement of `web/dist`; missing files after `stat` must return the controlled not-published response instead of leaking ENOENT as a backend 500. - Public slug lookup must not expose user_id or channel_id, and `craft-spaces` responses must include `X-Robots-Tag: noindex`. - PR preview environments should use `BRICKS_PUBLIC_SITE_BASE_URL` path URLs and Vite starter builds must use relative asset paths; otherwise generated `craft-spaces` links can fail before production wildcard origin TLS is configured or assets can resolve to the Bricks app root. - Generated site responses need a more permissive CSP than the Bricks app shell; otherwise user-authored apps that reference CDN scripts/styles such as Tailwind, UMD React, Babel, analytics, fonts, or media fail at runtime. diff --git a/docs/deploy/dokku.md b/docs/deploy/dokku.md index 89e533d0..8596b203 100644 --- a/docs/deploy/dokku.md +++ b/docs/deploy/dokku.md @@ -13,10 +13,11 @@ Production host data: /home/bricks/data/production/sandboxes ``` -Preview host data: +Preview apps intentionally use the same production host data root while they +share the production database: ```text -/home/bricks/data/previews//sandboxes +/home/bricks/data/production/sandboxes ``` ## Production App @@ -101,3 +102,8 @@ validated preview `return_to` URL. Preview apps copy `TURSO_DATABASE_URL`, `TURSO_AUTH_TOKEN`, `JWT_SECRET`, and `ENCRYPTION_KEY` from the production Dokku app on the server during deployment, so those values do not leave the server through GitHub Actions. + +When a pull request is closed, the preview workflow destroys the matching +`bricks-preview-*` Dokku app. Because preview apps mount +`/home/bricks/data/production/sandboxes`, cleanup must preserve that shared +production data root. diff --git a/docs/tasks/backlog/2026-06-30-18-35-CST-preview-site-storage-db-isolation.md b/docs/tasks/backlog/2026-06-30-18-35-CST-preview-site-storage-db-isolation.md deleted file mode 100644 index 64c08dd1..00000000 --- a/docs/tasks/backlog/2026-06-30-18-35-CST-preview-site-storage-db-isolation.md +++ /dev/null @@ -1,62 +0,0 @@ -# Preview Site Storage and Database Isolation - -## Background - -Channel-generated websites currently store site metadata in the shared database -while website files live on the filesystem mounted for the running Dokku app. -Production and each PR preview app use isolated filesystem roots, but preview -apps currently share the same Turso database configuration copied from -production. - -This creates a mismatch for generated site preview URLs. A preview app can write -a `channel_sites` row and generate a public slug, but a different app resolving -the same database row cannot necessarily read the corresponding `web/dist` -files, because those files exist only under the filesystem root of the app that -built them. - -The visible symptom is that a production-style URL such as: - -```text -https://s-.craft-spaces.bricks.cool -``` - -cannot safely resolve preview content unless the request is routed to the exact -preview app whose filesystem contains that slug's build output. The slug alone -does not encode the preview app identity, so a global `*.craft-spaces.bricks.cool` -host cannot know which preview filesystem to use. - -## Requirement - -Design and implement a product-level solution for preview generated-site -hosting that keeps production and PR preview data isolated across both database -metadata and filesystem build outputs. - -The solution must make it possible to resolve a generated site's metadata and -its files from the same environment boundary, instead of reading slug metadata -from one shared DB and trying to serve files from another app's isolated disk. - -## Acceptance Criteria - -- Production generated sites continue to support stable public URLs under - `*.craft-spaces.bricks.cool`. -- PR preview generated sites have an environment-aware URL and routing model - that can find the matching preview filesystem. -- A preview app must not read or serve production generated-site files. -- Production must not accidentally serve preview-only build outputs. -- The database strategy is explicit: either preview uses an isolated DB/branch, - or site metadata records include enough environment identity to route to the - correct storage boundary. -- The filesystem strategy is explicit: either a central gateway can access the - correct build output safely, or each preview site URL routes directly to the - preview app that owns the files. -- A stale or cross-environment slug must fail closed rather than serving another - environment's content. -- The chosen design documents how `channel_sites.public_slug`, `web/dist`, and - PR preview app identity relate to each other. - -## Non-Goals - -- Do not implement this in the current slice. -- Do not rely on manually adding one Dokku domain per generated preview slug as - the long-term solution. -- Do not make preview apps share the production filesystem root. diff --git a/docs/tasks/backlog/2026-07-03-15-47-CST-editable-todo-and-note-resources.md b/docs/tasks/backlog/2026-07-03-15-47-CST-editable-todo-and-note-resources.md new file mode 100644 index 00000000..48c78a23 --- /dev/null +++ b/docs/tasks/backlog/2026-07-03-15-47-CST-editable-todo-and-note-resources.md @@ -0,0 +1,16 @@ +# Editable Todo and Note Resources + +## Background + +Bricks needs lightweight editable resource types that users can keep inside their working context. The first two resource types are todo and note. + +## Requirement + +Todo and note resources must be editable. A todo resource lets the user mark an item done or undone. A note resource lets the user type and edit text using an embedded mature open-source lightweight Markdown editor. + +## Acceptance Criteria + +- Given a todo resource, when the user marks it done, then the todo visibly changes to the done state. +- Given a done todo resource, when the user marks it undone, then the todo visibly returns to the undone state. +- Given a note resource, when the user types into the note editor, then the note content is editable in place. +- Given a note resource, when the note editor is shown, then it uses an embedded mature open-source lightweight Markdown editing experience. diff --git a/docs/tasks/done/2026-06-30-18-35-CST-preview-site-storage-db-isolation.md b/docs/tasks/done/2026-06-30-18-35-CST-preview-site-storage-db-isolation.md new file mode 100644 index 00000000..8ed6eaec --- /dev/null +++ b/docs/tasks/done/2026-06-30-18-35-CST-preview-site-storage-db-isolation.md @@ -0,0 +1,66 @@ +# Preview Site Shared Storage and Production Readiness + +## Background + +Channel-generated websites currently store site metadata in the shared database +while website files live under the shared production sandbox filesystem mounted +for the running Dokku app. + +Production and each PR preview app intentionally share the same Turso database +configuration and the same sandbox workspace root: + +```text +/home/bricks/data/production/sandboxes +``` + +This keeps `channel_sites.public_slug`, channel workspace files, and `web/dist` +build output inside the same operational boundary for both production and PR +preview apps. + +The visible risk is not environment isolation. The risk is that deployment +configuration, cleanup, and static serving must match the shared-root design: + +```text +https://s-.craft-spaces.bricks.cool +``` + +must resolve through the shared `channel_sites` row and the shared `web/dist` +files without transient static asset failures surfacing as backend 500s. + +## Requirement + +Verify and harden the production/preview deployment model for generated site +hosting: + +- Dokku app/config/data root values must match the intentional shared database + and shared workspace design. +- Preview close/merge cleanup must remove the preview Dokku app without deleting + the shared production sandbox data root. +- Production and preview GitHub OAuth return flows must remain valid with the + production callback and preview `return_to` redirect. +- Generated site static serving must fail closed with a controlled not-published + response rather than a backend 500 when a dist asset disappears during a build + or deploy race. + +## Acceptance Criteria + +- Production generated sites continue to support stable public URLs under + `*.craft-spaces.bricks.cool`. +- PR preview apps use the production Turso credentials and mount + `/home/bricks/data/production/sandboxes` into `/app/data/sandboxes`. +- PR preview apps set `BRICKS_SANDBOX_RUNNER_ROOT_SEGMENTS=production,sandboxes` + so shell/build commands operate on the shared workspace root. +- PR close/merge cleanup destroys only the matching `bricks-preview-*` Dokku app + when `DATA_ROOT=/home/bricks/data/production/sandboxes`. +- Production keeps `GITHUB_CALLBACK_URL=https://craft.bricks.cool/api/callback` + and preview return URLs under `https://.craft-dev.bricks.cool` + remain allowlisted. +- A generated site request for an asset that disappears during read returns + `404 Site not published` with `X-Robots-Tag: noindex`, not a backend 500. +- Code maps and deployment docs describe the shared-root production/preview + model accurately. + +## Non-Goals + +- Do not split preview DB or workspace storage in this slice. +- Do not delete production shared workspace data when cleaning up PR previews.