This commit is contained in:
@@ -15,3 +15,4 @@ db/.exports
|
|||||||
coverage
|
coverage
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
assets
|
||||||
|
|||||||
+3
-15
@@ -1,8 +1,11 @@
|
|||||||
|
# Development settings. The installer writes its own .env; see README.md.
|
||||||
|
|
||||||
PORT=7227
|
PORT=7227
|
||||||
|
|
||||||
GOOGLE_CLIENT_ID=
|
GOOGLE_CLIENT_ID=
|
||||||
GOOGLE_CLIENT_SECRET=
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# SMTP host, user and password
|
||||||
EMAIL_DOMAIN=
|
EMAIL_DOMAIN=
|
||||||
EMAIL_USER=
|
EMAIL_USER=
|
||||||
EMAIL_PASS=
|
EMAIL_PASS=
|
||||||
@@ -14,29 +17,14 @@ DEV_EMAIL_RECIPIENT=
|
|||||||
ENCRYPTION_KEY=
|
ENCRYPTION_KEY=
|
||||||
ENCRYPTION_SALT=
|
ENCRYPTION_SALT=
|
||||||
|
|
||||||
SOURCE_DIRECTORY=/home/user/projects/bunext-sqlite
|
|
||||||
DEPLOY_SERVER_IP=192.168.5.27
|
|
||||||
|
|
||||||
DATA_DIR=/data
|
|
||||||
|
|
||||||
# Set to 1 on the one container that should run cron tasks in production
|
|
||||||
MAIN_SERVER=
|
|
||||||
|
|
||||||
# Web Push: generated into Settings > Environment on first start. Set a
|
# Web Push: generated into Settings > Environment on first start. Set a
|
||||||
# fixed pair here only if you want to keep one across installs.
|
# fixed pair here only if you want to keep one across installs.
|
||||||
VAPID_PUBLIC_KEY=
|
VAPID_PUBLIC_KEY=
|
||||||
VAPID_PRIVATE_KEY=
|
VAPID_PRIVATE_KEY=
|
||||||
|
|
||||||
# Optional public envs exposed to the client through __root.server.ts
|
|
||||||
PAYSTACK_PUBLIC_KEY=
|
|
||||||
R2_PUBLIC_DOMAIN=
|
|
||||||
|
|
||||||
# Seed script overrides (bun run seed)
|
# Seed script overrides (bun run seed)
|
||||||
SEED_SUPER_ADMIN_EMAIL=
|
SEED_SUPER_ADMIN_EMAIL=
|
||||||
SEED_SUPER_ADMIN_USERNAME=
|
SEED_SUPER_ADMIN_USERNAME=
|
||||||
SEED_SUPER_ADMIN_FIRST_NAME=
|
SEED_SUPER_ADMIN_FIRST_NAME=
|
||||||
SEED_SUPER_ADMIN_LAST_NAME=
|
SEED_SUPER_ADMIN_LAST_NAME=
|
||||||
SEED_SUPER_ADMIN_PASSWORD=
|
SEED_SUPER_ADMIN_PASSWORD=
|
||||||
|
|
||||||
# Production: sitemap lastmod cache file on the /data volume
|
|
||||||
PAGE_LASTMOD_FILE=
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.4.3
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Check installer syntax
|
||||||
|
run: bash -n install.sh
|
||||||
|
|
||||||
|
- name: Type-check
|
||||||
|
run: bun run typecheck
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: bun run test
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
name: Release image
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: release-image
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to Gitea
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.tben.me
|
||||||
|
username: ${{ secrets.GITEA_USERNAME }}
|
||||||
|
password: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
|
||||||
|
- name: Work out image tags
|
||||||
|
id: tags
|
||||||
|
env:
|
||||||
|
EVENT: ${{ github.event_name }}
|
||||||
|
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
sha="sha-${GITHUB_SHA::12}"
|
||||||
|
if [ "$EVENT" = "release" ] && [ -n "$RELEASE_TAG" ]; then
|
||||||
|
version="$RELEASE_TAG"
|
||||||
|
minor="$(printf '%s' "$version" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/\1.\2/')"
|
||||||
|
tags="$(printf '%s\n' \
|
||||||
|
"ghcr.io/moduletrace/cobble:latest" \
|
||||||
|
"ghcr.io/moduletrace/cobble:${version}" \
|
||||||
|
"ghcr.io/moduletrace/cobble:${minor}" \
|
||||||
|
"ghcr.io/moduletrace/cobble:${sha}" \
|
||||||
|
"git.tben.me/moduletrace/cobble:latest" \
|
||||||
|
"git.tben.me/moduletrace/cobble:${version}" \
|
||||||
|
"git.tben.me/moduletrace/cobble:${minor}" \
|
||||||
|
"git.tben.me/moduletrace/cobble:${sha}")"
|
||||||
|
else
|
||||||
|
tags="$(printf '%s\n' \
|
||||||
|
"ghcr.io/moduletrace/cobble:${sha}" \
|
||||||
|
"git.tben.me/moduletrace/cobble:${sha}")"
|
||||||
|
fi
|
||||||
|
{
|
||||||
|
echo "tags<<EOF"
|
||||||
|
echo "$tags"
|
||||||
|
echo "EOF"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.tags.outputs.tags }}
|
||||||
|
provenance: false
|
||||||
|
sbom: false
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
@@ -111,9 +111,13 @@ Inspect live data with `bun llm-code-assistant/query-db.ts "<sql>"` - a read-onl
|
|||||||
- **Schedules (routines in the UI):** `cron_jobs` (per user and bot, `workspace_id` null for its own chat, `title`, `instructions` (the brief), `schedule` (a `Bun.cron` expression, UTC), `computer_id` (not a foreign key; null runs on the bot's current one), `enabled`, `max_runs` (1 for a one-off), `run_count`, `next_run_at`, `last_run_at`, `last_job_id`, `last_error`). Helpers in `src/functions/backend/db/cron-jobs/` (`create-cron-job`, `update-cron-job`, `delete-cron-job`, `grab-cron-jobs`, `grab-user-cron-job`, `fire-cron-job`), all ownership-checked, and with `bot_id` limited to that bot's own. `nextCronRun` (`src/utils/next-cron-run.ts`, `Bun.cron.parse`) validates a schedule and refuses one that runs more often than `AppData.CronJobMinIntervalMs`; a bot has at most `AppData.CronJobMaxPerBot`. Every bot gets `create_schedule`, `list_schedules`, `update_schedule`, `delete_schedule` (`grabBotTools`, every run; descriptions via `describe-cron-job.ts`). The `run-cron-jobs` cron fires due ones (`fireCronJob`: claims the run atomically by moving `next_run_at`, skips it while the last run's job is still open, queues the brief as a task with `enqueueBotJob` in the bot's active session there, turns off after `max_runs` or when there's no next run, keeps why it couldn't in `last_error`, logs each run under `cron`). A run missed while the server was down runs once, then carries on from now. `/admin/routines` lists them (pause/resume, edit, delete; `api/admin/routines/update-routine`, `delete-routine`); bots create them from chat.
|
- **Schedules (routines in the UI):** `cron_jobs` (per user and bot, `workspace_id` null for its own chat, `title`, `instructions` (the brief), `schedule` (a `Bun.cron` expression, UTC), `computer_id` (not a foreign key; null runs on the bot's current one), `enabled`, `max_runs` (1 for a one-off), `run_count`, `next_run_at`, `last_run_at`, `last_job_id`, `last_error`). Helpers in `src/functions/backend/db/cron-jobs/` (`create-cron-job`, `update-cron-job`, `delete-cron-job`, `grab-cron-jobs`, `grab-user-cron-job`, `fire-cron-job`), all ownership-checked, and with `bot_id` limited to that bot's own. `nextCronRun` (`src/utils/next-cron-run.ts`, `Bun.cron.parse`) validates a schedule and refuses one that runs more often than `AppData.CronJobMinIntervalMs`; a bot has at most `AppData.CronJobMaxPerBot`. Every bot gets `create_schedule`, `list_schedules`, `update_schedule`, `delete_schedule` (`grabBotTools`, every run; descriptions via `describe-cron-job.ts`). The `run-cron-jobs` cron fires due ones (`fireCronJob`: claims the run atomically by moving `next_run_at`, skips it while the last run's job is still open, queues the brief as a task with `enqueueBotJob` in the bot's active session there, turns off after `max_runs` or when there's no next run, keeps why it couldn't in `last_error`, logs each run under `cron`). A run missed while the server was down runs once, then carries on from now. `/admin/routines` lists them (pause/resume, edit, delete; `api/admin/routines/update-routine`, `delete-routine`); bots create them from chat.
|
||||||
- **Not built yet:** task steps (nothing writes `bot_task_steps` yet).
|
- **Not built yet:** task steps (nothing writes `bot_task_steps` yet).
|
||||||
|
|
||||||
### Production container
|
### Installer and release
|
||||||
|
|
||||||
`deploy/run-app-container.sh` builds the image and (re)runs the app's container (`deploy/deploy-prod.sh` calls it on the server); rerun it to update. The app container and every computer container share the `cobble` network. Mounts: `.env` at `/app/.env` (read only; bun loads it), the host data dir (`COBBLE_DATA_DIR`, default `/data/cobble`) at `/data`, and the docker socket. Everything that must survive an update lives on `/data`: media and computers' home dirs (`DATA_DIR=/data`), the database with its backups and exports (`DB_DIR=/data/db`, read by `bun-sqlite.config.ts`; the schema always comes from `db/schema.ts`), and bun-sqlite's live schema record (`entrypoint.sh` links `/app/.bun-sqlite` there). `MAIN_SERVER=1` is set on that one container.
|
`install.sh` is the only supported install and update path: `curl -fsSL https://raw.githubusercontent.com/moduletrace/cobble/main/install.sh | sudo bash`. It is idempotent, so the same script is the updater, and `--uninstall [--purge]` is the way back out. It installs Docker when missing (Linux), creates the `cobble` network, writes `$COBBLE_HOME/install.conf` and a generated `.env` (mode 600), and starts one `cobble` container. Existing secrets are never regenerated; the database is backed up to `$COBBLE_HOME/backups` before a container is replaced. The default home is `/opt/cobble`, configurable with `--home`, and everything that must survive an update lives in its `data/` directory (`db`, `media`, `computers`).
|
||||||
|
|
||||||
|
The container runs with `--network cobble`, `-p BIND:PORT`, `.env` at `/app/.env` read only, the host data directory at `/data`, the docker socket at `/var/run/docker.sock`, `MAIN_SERVER=1` and `PORT`, `--init`, a `unless-stopped` restart policy and bounded json-file logs. `/data` must stay a bind mount: `resolveHostPath` turns paths under the app's own mounts into host paths for computer containers, and a named volume would break them. `DATA_DIR=/data` and `DB_DIR=/data/db` come from the Dockerfile; `entrypoint.sh` keeps bun-sqlite's schema record on the same volume.
|
||||||
|
|
||||||
|
Images are built by GitHub Actions on a published release (`.github/workflows/release.yml`), for `linux/amd64` and `linux/arm64`, and pushed to both `ghcr.io/moduletrace/cobble` and `git.tben.me/moduletrace/cobble` with `latest`, version, `major.minor` and commit-sha tags. `.github/workflows/ci.yml` runs `bun run typecheck` and `bun run test` on every push and pull request, and checks that `install.sh` parses. The Dockerfile pins Bun 1.4.3 to match CI. The repository is mirrored on Gitea, and the installer defaults to the GHCR image with `--image git.tben.me/moduletrace/cobble` as the Gitea alternative.
|
||||||
|
|
||||||
### Envs
|
### Envs
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -1,4 +1,9 @@
|
|||||||
FROM oven/bun:1.3.14-debian
|
FROM oven/bun:1.4.3-debian
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="Cobble" \
|
||||||
|
org.opencontainers.image.description="A self-hosted crew of small AI worker bots." \
|
||||||
|
org.opencontainers.image.source="https://github.com/Moduletrace/cobble" \
|
||||||
|
org.opencontainers.image.licenses="MIT"
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends curl bash nano wget zip unzip \
|
&& apt-get install -y --no-install-recommends curl bash nano wget zip unzip \
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Moduletrace
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,126 +1,167 @@
|
|||||||
# bunext-sqlite
|
# Cobble
|
||||||
|
|
||||||
A preset for [bunext](https://github.com/moduletrace/bunext) running [bun-sqlite](https://github.com/moduletrace/bun-sqlite) as the database management system. Use this as the starting point for new full-stack Bun projects: filesystem routing, SQLite with generated types, password login, an admin CRUD API, WebSockets, cron, audit logging, and email all wired up out of the box.
|
A self-hosted crew of small AI worker bots.
|
||||||
|
|
||||||
## Stack
|
Cobble gives you a coordinator bot and a crew of workers, each with its own
|
||||||
|
job, models, memory and workspace. The crew talks to you in a chat, shares
|
||||||
|
workspaces, runs routines on a schedule, and drives its own computers:
|
||||||
|
Docker sandboxes with a browser, terminal, code editor and desktop that a
|
||||||
|
bot can use and hand back to you when it needs help.
|
||||||
|
|
||||||
| Layer | Choice |
|
It is an install, not a SaaS. One SQLite database, one Docker container, and
|
||||||
| --------- | -------------------------------------------------- |
|
your data on your own machine.
|
||||||
| Runtime | Bun (never npm/npx - use `bun`) |
|
|
||||||
| Framework | `@moduletrace/bunext` (filesystem routing, SSR) |
|
|
||||||
| Database | `@moduletrace/bun-sqlite` (schema + generated types) |
|
|
||||||
| UI | Tailwind CSS v4 + `twui` component library |
|
|
||||||
| Icons | `lucide-react` |
|
|
||||||
| Validation| `zod` (v4) |
|
|
||||||
| Email | `nodemailer` (React-rendered templates) |
|
|
||||||
|
|
||||||
## Requirements
|
- Website: [cobble.moduletrace.org](https://cobble.moduletrace.org)
|
||||||
|
- Source: [GitHub](https://github.com/Moduletrace/cobble) and [Gitea](https://git.tben.me/Moduletrace/cobble)
|
||||||
|
- License: MIT
|
||||||
|
|
||||||
- Bun >= 1.3
|
## Install
|
||||||
- No database server required - `bun-sqlite` manages a local SQLite file at `db/<db_name>` (`db/bsqlite` by default).
|
|
||||||
|
|
||||||
## Local Development
|
On a Linux machine with Docker (the installer adds Docker when it is
|
||||||
|
missing):
|
||||||
|
|
||||||
1. `cp .env.example .env` (if present) or create `.env` with `ENCRYPTION_KEY`/`ENCRYPTION_SALT` values.
|
```bash
|
||||||
2. `bun install`
|
curl -fsSL https://raw.githubusercontent.com/moduletrace/cobble/main/install.sh | sudo bash
|
||||||
3. `bun run db:schema` - creates tables and regenerates `db/types/db.ts`.
|
```
|
||||||
4. `bun run dev` - starts the dev server (default port `7227`).
|
|
||||||
5. Open the app. With no accounts yet, every page sends you to `/auth/signup` to create the super admin (user #1). That is the only time signup is shown.
|
|
||||||
|
|
||||||
Headless alternative to step 5: `SEED_SUPER_ADMIN_PASSWORD=... bun run seed` creates the super admin from the CLI (default `[email protected]`, override with `SEED_SUPER_ADMIN_EMAIL`, `SEED_SUPER_ADMIN_USERNAME`, `SEED_SUPER_ADMIN_FIRST_NAME`, `SEED_SUPER_ADMIN_LAST_NAME`).
|
Open the address it prints, usually `http://<your-ip>:7227`, and create the
|
||||||
|
super admin account. The first signup also creates the coordinator and the
|
||||||
|
main computer, which pulls the sandbox image in the background. Give it a
|
||||||
|
few minutes.
|
||||||
|
|
||||||
Log in at `/auth/login` with your email or username and password.
|
Then open **Settings > Models**, add an OpenRouter key (or another provider)
|
||||||
|
and wait for the model list to sync. This is what the bots think with.
|
||||||
|
|
||||||
## Available Scripts
|
### Requirements
|
||||||
|
|
||||||
| Purpose | Command |
|
- Linux amd64 or arm64, with Docker. The installer installs Docker from
|
||||||
| ----------------------------- | ---------------------------------- |
|
get.docker.com when it is missing.
|
||||||
| Dev server | `bun run dev` |
|
- 2 CPU cores, 8 GB RAM and 20 GB free disk for the app plus one computer.
|
||||||
| Production | `bun run start` |
|
Each computer is its own container with its own memory limit.
|
||||||
| Type-check only | `bun run typecheck` |
|
- Docker Desktop on macOS works for development and small installs, but
|
||||||
| Run tests | `bun run test` |
|
Linux is the supported host.
|
||||||
| Run a single test | `bun test <path/to/file.test.ts>` |
|
- Port 7227, or another port with `--port`.
|
||||||
| Regenerate favicon/brand | `bun run gen:favicon` |
|
- HTTPS in front of the app before you expose it to the internet. Web Push
|
||||||
| DB schema (tables) | `bun run db:schema` |
|
and installable PWA features need it. A Cloudflare Tunnel or Caddy in
|
||||||
| DB admin UI | `bun run db:admin` |
|
front of `127.0.0.1:7227` is enough.
|
||||||
| DB backup | `bun run db:backup` |
|
|
||||||
| Seed super admin user | `bun run seed` |
|
|
||||||
| Query DB (CLI) | `bun llm-code-assistant/query-db.ts "<sql>"` |
|
|
||||||
| Deploy to production | `./deploy/deploy-prod.sh` |
|
|
||||||
|
|
||||||
## App Structure
|
The installer binds to `0.0.0.0` so the app is reachable from your phone on
|
||||||
|
the same network. Use `--bind 127.0.0.1` when a reverse proxy is the only
|
||||||
|
thing that should reach it.
|
||||||
|
|
||||||
| Directory | Purpose |
|
## Update and uninstall
|
||||||
| ------------------------------ | ------------------------------------------------------------- |
|
|
||||||
| `db/schema.ts` | SQLite schema - the source of truth for `db/types/db.ts` |
|
|
||||||
| `src/server.ts` | Entrypoint: `bunextInit()`, `Bun.serve` (media, `/ws` auth'd upgrade), cron startup |
|
|
||||||
| `src/pages/` | Filesystem routes (`__root.server.ts` auth guard, `__root.tsx` layout/context) |
|
|
||||||
| `src/pages/api/` | API routes - incl. the CRUD catch-all at `api/admin/crud/[[...paths]].ts` |
|
|
||||||
| `src/websocket/` | WebSocket handler: upgrade-time auth, room system, event dispatch |
|
|
||||||
| `src/cron/` | Cron tasks - `Bun.cron` schedules, folder-per-task |
|
|
||||||
| `@moduletrace/twui` | The `twui` component library (external package, shared across projects) |
|
|
||||||
| `src/components/general/` | Shared page components built on twui |
|
|
||||||
| `src/functions/backend/` | Server-only logic: auth, DB queries, email, PDF generation, media, logger |
|
|
||||||
| `src/functions/frontend/` | Client-safe: CRUD/fetch handlers, media upload helpers, document actions |
|
|
||||||
| `src/hooks/` | Shared React hooks (`use-admin-crud-get`, `use-form-init`, ...) |
|
|
||||||
| `src/schemas/` | zod request schemas for public/auth endpoints |
|
|
||||||
| `src/utils/` | Utility modules: cookies, crypt, safe-compare, ejson, money |
|
|
||||||
| `src/utils/rbac/` | RBAC engine: pass-through users + declarative assignment matrix |
|
|
||||||
| `src/dict/` | Dictionaries: user types (+ assignment matrices), media types, document statuses, websocket rooms, log categories |
|
|
||||||
| `src/data/` | Static data: `site-data.ts` (brand literals), `app-data.ts` (constants) |
|
|
||||||
| `src/scripts/` | CLI scripts: brand asset generator, seed |
|
|
||||||
| `llm-code-assistant/` | `query-db.ts` - read-only SQL CLI for coding assistants |
|
|
||||||
|
|
||||||
## Architecture
|
The same script is the updater. It keeps the database, media, computers'
|
||||||
|
home directories and generated secrets, backs up the database before
|
||||||
|
replacing the container, and does nothing when the image has not changed.
|
||||||
|
|
||||||
### Auth
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/moduletrace/cobble/main/install.sh | sudo bash
|
||||||
|
```
|
||||||
|
|
||||||
- **Auth guard:** `src/pages/__root.server.ts` - runs per request. There are no public pages: anything outside `/admin` and `/auth` redirects to `/admin`. With no accounts, everything redirects to `/auth/signup`; once one exists, signup redirects away. `/admin/*` requires login; `/auth/*` redirects logged-in users.
|
```bash
|
||||||
- **Session:** encrypted cookie (`AES-256-GCM`, PBKDF2-derived key) + a matching CSRF cookie, verified constant-time on every request (`src/functions/backend/auth/user-auth.ts`). 60-day expiry.
|
# Stop the app and keep the data
|
||||||
- **Login:** email or username plus password (`api/auth/password-login.ts`), no email step. Set/change password at `/admin/settings/change-password`. Google login (`api/auth/google-login.ts`) works for existing accounts only when `GOOGLE_CLIENT_ID` is set; it never creates accounts.
|
curl -fsSL https://raw.githubusercontent.com/moduletrace/cobble/main/install.sh | sudo bash -s -- --uninstall
|
||||||
- **Signup:** first-run only (`api/auth/signup.tsx`): creates the super admin with a password and logs them in. Refused once any user exists.
|
|
||||||
- **RBAC:** `src/utils/rbac/` - `super_admin` passes everything; other grants/revocations are governed by `UserTypeAssignmentMatrix`/`UserTypeDeletionMatrix` in `src/dict/user-types-dict.ts`.
|
|
||||||
- **Audit trail:** every auth event (signup, login, failure, logout) is written to the `logs` table via `src/functions/backend/logger.ts`.
|
|
||||||
|
|
||||||
### Admin CRUD API
|
# Stop the app and delete the data
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/moduletrace/cobble/main/install.sh | sudo bash -s -- --uninstall --purge
|
||||||
|
```
|
||||||
|
|
||||||
`api/admin/crud/[[...paths]].ts` dispatches `GET/POST/PUT/DELETE` by table name. Tables without a custom handler fall through to generic CRUD (super admin only). Customize a table by adding a folder under `src/pages/api/admin/crud/(tables)/<table>/` with per-method authorization.
|
### Options
|
||||||
|
|
||||||
### Documents (Letters / Emails / Invoices / Quotes)
|
| Option | Meaning |
|
||||||
|
| --------------------- | -------------------------------------------------------------- |
|
||||||
|
| `--home DIR` | Install directory, default `/opt/cobble` |
|
||||||
|
| `--data-dir DIR` | Data directory mounted at `/data`, default `HOME/data` |
|
||||||
|
| `--port PORT` | Host port, default `7227` |
|
||||||
|
| `--bind ADDRESS` | Host address to publish on, default `0.0.0.0` |
|
||||||
|
| `--image IMAGE` | Container image, default `ghcr.io/moduletrace/cobble` |
|
||||||
|
| `--version TAG` | Image tag, default `latest` |
|
||||||
|
| `--extra-network NET` | Join another Docker network, for a tunnel or reverse proxy |
|
||||||
|
| `--force` | Recreate the container even when it is current |
|
||||||
|
| `--no-pull` | Use an image that is already on the machine |
|
||||||
|
| `--skip-backup` | Update without a database backup |
|
||||||
|
| `--no-docker-install` | Fail instead of installing Docker |
|
||||||
|
| `--uninstall` | Stop and remove the app container |
|
||||||
|
| `--purge` | With `--uninstall`, also delete the data directory |
|
||||||
|
|
||||||
A complete document suite ships on top of the generic CRUD: `letters`, `emails`, `email_attachments`, `invoices`, `invoice_items`, `quotes`, `quote_items`, `invoice_quotes` tables with admin sections at `/admin/{letters,emails,invoices,quotes}`. Invoices and quotes get auto-generated numbers (from `SiteData["SiteSlug"]`), sortable line items, quote-to-invoice linking, branded PDF generation (`src/functions/backend/pdf/`), and sending via React-rendered HTML email (`src/functions/backend/email/`, shell in `src/email/root.tsx`). See `AGENTS.md` for the full map.
|
## Data and settings
|
||||||
|
|
||||||
### Backups
|
Everything that must survive an update lives under the install directory:
|
||||||
|
|
||||||
Super admins can create, list, download, and restore database snapshots from `/admin/backups` (`src/functions/backend/backups/`, API under `api/admin/backups/`). Backups are written to `db/.backups`; restore accepts a listed snapshot or an uploaded SQLite file. Every action is audit-logged.
|
```
|
||||||
|
/opt/cobble/
|
||||||
|
.env # generated once, mode 600
|
||||||
|
install.conf # installer state
|
||||||
|
backups/ # pre-update database snapshots
|
||||||
|
data/
|
||||||
|
db/cobble
|
||||||
|
db/.backups/
|
||||||
|
media/
|
||||||
|
computers/<id>/ # each computer's home directory
|
||||||
|
```
|
||||||
|
|
||||||
### Settings
|
`.env` holds the port and the encryption keys. The keys encrypt sessions and
|
||||||
|
settings, so keep a copy and do not rotate them without re-entering provider
|
||||||
|
keys and other secrets. Provider keys, MCP servers, tools and app settings
|
||||||
|
are managed in the app, not in `.env`.
|
||||||
|
|
||||||
`/admin/settings` redirects to `/admin/settings/general` (theme: System, Light or Dark, saved per device). `/admin/settings/profile` is the current user's profile (editable by every logged-in user, including `read_only`). `/admin/settings/change-password` sets or changes the user's own password. The sub pages are listed in the rail (`src/dict/settings-pages-dict.ts`).
|
Edit `.env` and run `docker restart cobble` to apply changes. The installer
|
||||||
|
only appends missing values and keeps what is already there.
|
||||||
|
|
||||||
### WebSockets
|
The app container is started with the Docker socket mounted and
|
||||||
|
`MAIN_SERVER=1`, so it can create computer containers and run cron, the
|
||||||
|
queue and Web Push.
|
||||||
|
|
||||||
`/ws` upgrades only after `userAuth` succeeds; the user session is frozen into the socket. Rooms are declared in `src/dict/websocket-rooms.ts` and managed by the dual-index store in `src/websocket/(data)/rooms.ts`. Client events dispatch through `src/websocket/(functions)/handle-message.ts`; add per-event handlers as `src/websocket/client-requests/<event>/index.ts`. The twui `useWebSocket` hook is the client counterpart.
|
## Images
|
||||||
|
|
||||||
### Cron
|
Every release publishes a multi-arch image to both registries:
|
||||||
|
|
||||||
`src/cron/index.ts` registers tasks (gated to the `MAIN_SERVER` container in production). Each task is a folder with a default-exported async function scheduled with `Bun.cron` through `registerCronJob` (`src/cron/register-cron-job.ts`), which logs before each run and catches errors. Schedules are interpreted in UTC. The bundled heartbeat task doubles as a DB-liveness probe.
|
- `ghcr.io/moduletrace/cobble`
|
||||||
|
- `git.tben.me/moduletrace/cobble`
|
||||||
|
|
||||||
## UI Conventions
|
Tags are `latest`, the release version, `major.minor` and the commit sha.
|
||||||
|
Install from Gitea with
|
||||||
|
`--image git.tben.me/moduletrace/cobble`, or build from source with
|
||||||
|
`docker build -t cobble:local .` and `--image cobble --version local --no-pull`.
|
||||||
|
|
||||||
- Build with `twui` components (`Section`, `Container`, `Row`, `Stack`, `Card`, `Button`, ...) and `src/components/general/` - never raw HTML for layout.
|
## Development
|
||||||
- Themable tokens live only in `src/styles/main.css` (`@theme inline`). No hardcoded hex colors or radii in components.
|
|
||||||
- Brand name/slug live only in `src/data/site-data.ts`; the logo mark only in `public/logos/logo.svg`.
|
|
||||||
|
|
||||||
## Page Directory Rules
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# set ENCRYPTION_KEY and ENCRYPTION_SALT (openssl rand -hex 32)
|
||||||
|
bun install
|
||||||
|
bun run db:schema
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
Route-local code under `src/pages` uses non-routed meta folders: `(sections)`, `(partials)`, `(data)`, `(functions)`, `(utils)`, `(hooks)`. Keep route files thin. Dynamic routes use `[param]`; catch-alls use `[[...paths]]`. Server functions live in `index.server.ts` beside `index.tsx`.
|
Open `http://localhost:7227`. With no users, every page goes to
|
||||||
|
`/auth/signup` to create the super admin. For a headless setup:
|
||||||
|
|
||||||
## Verification
|
```bash
|
||||||
|
SEED_SUPER_ADMIN_PASSWORD=... bun run seed
|
||||||
|
```
|
||||||
|
|
||||||
`bun run typecheck` and `bun run test` are the verification steps (no linter/formatter configured). CI runs both on every push and pull request (`.github/workflows/ci.yml`). Unit tests are colocated `*.test.ts` files run individually with `bun test <file>`.
|
| Command | Purpose |
|
||||||
|
| -------------------- | ------------------------------------ |
|
||||||
|
| `bun run dev` | Development server |
|
||||||
|
| `bun run start` | Production server on the host |
|
||||||
|
| `bun run typecheck` | Type-check |
|
||||||
|
| `bun run test` | Run the test suite |
|
||||||
|
| `bun run seed` | Create the super admin from the CLI |
|
||||||
|
| `bun run seed:bots` | Add a sample crew |
|
||||||
|
| `bun run db:schema` | Sync the schema and types |
|
||||||
|
| `bun run gen:favicon` | Regenerate favicons and PWA assets |
|
||||||
|
|
||||||
## Maintainer Notes
|
The app is Bun, [`@moduletrace/bunext`](https://github.com/Moduletrace/bunext),
|
||||||
|
SQLite via [`@moduletrace/bun-sqlite`](https://github.com/Moduletrace/bun-sqlite),
|
||||||
|
Tailwind CSS and the `@moduletrace/twui` component library. See
|
||||||
|
[AGENTS.md](AGENTS.md) for the full architecture, routes, queue, harness and
|
||||||
|
data model.
|
||||||
|
|
||||||
- `.env` and `secrets/` are gitignored. Never commit them.
|
CI runs type-check and tests on every push and pull request. Releases are
|
||||||
- Don't patch `@moduletrace/twui` in `node_modules` - it is a shared publishable library. Appearance overrides belong in `src/styles/main.css`.
|
built by GitHub Actions and pushed to both registries.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT. See [LICENSE](LICENSE).
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
ENV_FILE="$PROJECT_ROOT/.env"
|
|
||||||
|
|
||||||
read_env_var() {
|
|
||||||
local key="$1" line value
|
|
||||||
line="$(grep -E "^[[:space:]]*(export[[:space:]]+)?${key}=" "$ENV_FILE" | tail -n 1 || true)"
|
|
||||||
value="${line#*=}"
|
|
||||||
value="${value%$'\r'}"
|
|
||||||
if [[ "$value" =~ ^\"(.*)\"$ ]] || [[ "$value" =~ ^\'(.*)\'$ ]]; then
|
|
||||||
value="${BASH_REMATCH[1]}"
|
|
||||||
fi
|
|
||||||
printf '%s' "$value"
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
|
||||||
echo "Error: $ENV_FILE not found." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
DEPLOY_NAME="cobble"
|
|
||||||
SOURCE_PATH="${SOURCE_DIRECTORY:-$(read_env_var SOURCE_DIRECTORY)}"
|
|
||||||
SERVER_IP="${DEPLOY_SERVER_IP:-$(read_env_var DEPLOY_SERVER_IP)}"
|
|
||||||
SSH_USER="${SSH_USER:-$(read_env_var SSH_USER)}"
|
|
||||||
SSH_USER="${SSH_USER:-root}"
|
|
||||||
SERVER_PATH="/docker/$DEPLOY_NAME"
|
|
||||||
SSH_KEY="$HOME/.ssh/tben"
|
|
||||||
SSH_OPTS=(-i "$SSH_KEY")
|
|
||||||
REMOTE="$SSH_USER@$SERVER_IP"
|
|
||||||
|
|
||||||
if [ ! -d "$SOURCE_PATH" ] || [ "$(realpath -m "$SOURCE_PATH")" = "/" ]; then
|
|
||||||
echo "Error: SOURCE_DIRECTORY is invalid, missing, or set to root path ('/')." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$SERVER_IP" ]; then
|
|
||||||
echo "Error: DEPLOY_SERVER_IP is not set (shell env or $ENV_FILE)." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "$SOURCE_PATH/secrets/prod.env" ]; then
|
|
||||||
echo "Error: $SOURCE_PATH/secrets/prod.env not found." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd "$SOURCE_PATH"
|
|
||||||
|
|
||||||
rsync -avz -e "ssh -i $SSH_KEY" --delete \
|
|
||||||
--exclude='secrets/' --exclude='.env' \
|
|
||||||
--filter=':- .gitignore' \
|
|
||||||
"$SOURCE_PATH/" "$REMOTE:$SERVER_PATH/"
|
|
||||||
|
|
||||||
rsync -avz -e "ssh -i $SSH_KEY" --chmod=F600 \
|
|
||||||
"$SOURCE_PATH/secrets/prod.env" "$REMOTE:$SERVER_PATH/.env"
|
|
||||||
|
|
||||||
ssh "${SSH_OPTS[@]}" "$REMOTE" bash -s -- "$DEPLOY_NAME" << 'EOF'
|
|
||||||
set -euo pipefail
|
|
||||||
DEPLOY_NAME="$1"
|
|
||||||
|
|
||||||
cd "/docker/$DEPLOY_NAME"
|
|
||||||
COBBLE_DATA_DIR="/data/$DEPLOY_NAME/.data" COBBLE_EXTRA_NETWORK=cloudflare \
|
|
||||||
./deploy/run-app-container.sh
|
|
||||||
EOF
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Builds the app's image and (re)starts its container. Rerun it to update:
|
|
||||||
# the database, media and computers' home directories (the data dir) and
|
|
||||||
# .env stay on the host. The container needs the docker socket, since it
|
|
||||||
# creates every computer as a container on the cobble network.
|
|
||||||
#
|
|
||||||
# COBBLE_DATA_DIR host data dir, mounted at /data (default /data/cobble)
|
|
||||||
# COBBLE_BIND host address the app is published on (default 127.0.0.1)
|
|
||||||
# COBBLE_EXTRA_NETWORK another docker network to join, e.g. a tunnel's
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
ENV_FILE="$PROJECT_ROOT/.env"
|
|
||||||
NAME="cobble"
|
|
||||||
NETWORK="cobble"
|
|
||||||
DATA_PATH="${COBBLE_DATA_DIR:-/data/cobble}"
|
|
||||||
BIND="${COBBLE_BIND:-127.0.0.1}"
|
|
||||||
DOCKER_SOCKET="/var/run/docker.sock"
|
|
||||||
|
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
|
||||||
echo "Error: $ENV_FILE not found. Copy .env.example and set ENCRYPTION_KEY and ENCRYPTION_SALT." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -S "$DOCKER_SOCKET" ]; then
|
|
||||||
echo "Error: no docker socket at $DOCKER_SOCKET. Computers run in docker, so the app needs it." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
PORT="$(grep -E '^[[:space:]]*PORT=' "$ENV_FILE" | tail -n 1 | cut -d= -f2- | tr -d "\"' \r")"
|
|
||||||
PORT="${PORT:-7227}"
|
|
||||||
|
|
||||||
mkdir -p "$DATA_PATH"
|
|
||||||
|
|
||||||
docker network inspect "$NETWORK" >/dev/null 2>&1 || docker network create "$NETWORK" >/dev/null
|
|
||||||
|
|
||||||
docker build -t "$NAME" "$PROJECT_ROOT"
|
|
||||||
|
|
||||||
docker stop -t 30 "$NAME" >/dev/null 2>&1 || true
|
|
||||||
docker rm -f "$NAME" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
docker run --name "$NAME" -d \
|
|
||||||
--network "$NETWORK" \
|
|
||||||
-p "$BIND:$PORT:$PORT" \
|
|
||||||
-v "$ENV_FILE:/app/.env:ro" \
|
|
||||||
-v "$DATA_PATH:/data" \
|
|
||||||
-v "$DOCKER_SOCKET:$DOCKER_SOCKET" \
|
|
||||||
-e MAIN_SERVER=1 \
|
|
||||||
--cpus 1 --memory 1g \
|
|
||||||
--restart unless-stopped \
|
|
||||||
"$NAME:latest"
|
|
||||||
|
|
||||||
if [ -n "${COBBLE_EXTRA_NETWORK:-}" ]; then
|
|
||||||
docker network connect "$COBBLE_EXTRA_NETWORK" "$NAME"
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker image prune -f >/dev/null
|
|
||||||
|
|
||||||
echo "Cobble is starting on http://$BIND:$PORT (data in $DATA_PATH)"
|
|
||||||
+706
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,6 @@
|
|||||||
"db:backup": "bunx bun-sqlite backup",
|
"db:backup": "bunx bun-sqlite backup",
|
||||||
"seed": "bun src/scripts/seed-super-admin.tsx",
|
"seed": "bun src/scripts/seed-super-admin.tsx",
|
||||||
"seed:bots": "bun src/scripts/seed-bots.ts",
|
"seed:bots": "bun src/scripts/seed-bots.ts",
|
||||||
"deploy:prod": "./deploy/deploy-prod.sh",
|
|
||||||
"start": "NODE_ENV=production bun src/server.ts",
|
"start": "NODE_ENV=production bun src/server.ts",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
|
|||||||
Reference in New Issue
Block a user