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: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| --- | --- |
Expand Down
37 changes: 36 additions & 1 deletion apps/node_backend/src/routes/channelSiteHost.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof import('node:fs/promises')>();
const readFile = vi.fn((...args: Parameters<typeof actual.readFile>) => {
Comment on lines +19 to +21
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;
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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');
});
});
22 changes: 19 additions & 3 deletions apps/node_backend/src/routes/channelSiteHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
try {
const site = await getChannelSiteBySlug(slug);
Expand All @@ -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;
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
6 changes: 3 additions & 3 deletions docs/code_maps/feature_map.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -634,9 +634,9 @@ products:
- main push 后 production app 重新部署并继续使用 `/home/bricks/data/production/sandboxes`。
- PR open/synchronize 后创建或更新 `bricks-preview-<branch-slug>`,域名为 `<branch-slug>.craft-dev.bricks.cool`。
- Dokku preview deploy 成功后发布 GitHub Deployment status,PR timeline 的 `View deployment` 按钮指向 preview 域名。
- 每个 preview app mount 到 `/home/bricks/data/previews/<branch-slug>/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 域名。
1 change: 1 addition & 0 deletions docs/code_maps/logic_map.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 8 additions & 2 deletions docs/deploy/dokku.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<branch-slug>/sandboxes
/home/bricks/data/production/sandboxes
```

## Production App
Expand Down Expand Up @@ -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.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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-<slug>.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://<branch-slug>.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.
Loading