PotterDoc is the external product name for this app.
The repository, internal code identifiers, and some contributor documentation still use glaze as the internal project name during the transition.
A pottery workflow tracking application. Log pieces and record state transitions as work moves through throwing, bisque firing, glazing, and finishing.
- Backend API (
api/) - Django, DRF, public libraries, auth flows, and data isolation. - Frontend Client (
web/) - React components, Vite configuration, and frontend conventions. - Common Tests (
tests/) - Structural tests for the workflow state machine. - Tools (
tools/) - Standalone utilities, Modal crop offloading, and Glaze import tool. - Pages (
pages/) - Static published pages. - CI / CD Infrastructure (
docs/) - GitHub Actions workflows, deployment pipelines, and environment variables. - Agent Development Guide (
docs/agents/) - Shell bootstrap, worktree navigation, and agent workflow context.
This guide assumes you already know the tools listed below and are familiar with separation of concerns and abstraction as design principles; if any term is unfamiliar, click the linked docs to catch up quickly.
- Django is the Python web framework that owns the backend (
backend/,api/). Separation of concerns keeps unrelated responsibilities apart so each layer stays simpler to reason about—for example,api/models.pydefines the data schema,api/serializers.pytranslates between ORM objects and JSON payloads, andapi/views.pywires those serializers into/api/...endpoints that enforce workflow rules fromworkflow.yml. That split keeps the REST API (powered by Django REST Framework, DRF) resilient even when one layer needs to change, while returning consistent data/validation to all clients. - React (web/src/) renders the SPA (Single Page Application) and consumes shared types/API helpers from
web/src/util/types.tsandweb/src/util/api.ts. React follows a component-based paradigm where functions or classes receive props (inputs) and return HTML that the browser can render. - Vite (web tooling) bundles the React app. It provides fast dev reloads (hot module replacement) so UI changes appear immediately while you work, runs the local dev server that powers our web workbench, and produces optimized production builds (tree shaking, minification) so the deployed bundle is as small and performant as possible.
- Material UI supplies the component library used everywhere in the UI for forms, dialogs, buttons, and layout.
- Axios is the HTTP client library we use in the web to talk to REST APIs; it keeps things simple by handling the details of sending and receiving JSON so the UI code does not have to repeat that work. Benefits of Axios over raw
fetchinclude centralized configuration of base URLs and headers, automatic JSON parsing/serialization, and built-in hooks for handling errors, cancellations, and retries. In this project that meansWorkflowState.tsxcan rely on helpers likeupdateCurrentState/updatePieceinstead of duplicating URLs or JSON logic, and we have a single place for surfaces errors before they hit the UI. - A client library is a reusable set of functions that wraps low-level protocols (like HTTP) so developers can interact with remote services using clean function calls, in their programming language of choice, instead of handling bytes, headers, or parsing manually.
While the UI is similar at a surface level to other craft journaling applications, the main differences are under the hood:
- Customizable, potentially non-linear workflows. For some pieces you'll carve first, for others you'll slip first. For others, there might be multiple rounds of each.
- Opinionated data model with immutable stage data for your piece's unique journey and your growth-minded journey as a potter. You can't change the past, so keep moving forward. (Administrative bulk data cleaning is still allowed! And when you find a photo from an earlier stage later, the rewind feature lets you navigate back to that historical state to attach it — without altering the piece's actual history.)
- Data normalization around every piece's history for richer and more reliable single piece and multi-piece analysis.
- Systematically answer questions like "How many pieces do I lose in the firing stage by glaze type?" or "How often do I ruin a piece during trimming?"
Before cloning, ensure the following are installed on your system:
| Tool | Required | Install |
|---|---|---|
| OS | Ubuntu 22.04+ or Debian 12+ (WSL2 on Windows works; macOS untested) | — |
| Bazelisk | Yes — aliased as bazel; downloads Bazel 8.5.1 automatically via .bazelversion |
Bazelisk releases or brew install bazelisk |
curl |
Yes — used by the lazy shell bootstrap to install RTK when needed | apt install curl |
git |
Yes | apt install git |
Python (3.12) and Node (22) are managed hermetically by Bazel — no manual installs needed once Bazelisk is present.
VS Code users: install Docker Desktop (Windows/macOS) or Docker Engine (Linux) and the Dev Containers extension, then open the repo and choose Reopen in Container — the devcontainer pre-installs all prerequisites automatically.
The devcontainer pre-forwards backend ports 8080–8087 and Vite ports 5173–5180. These ranges match the authorized origins registered in the Google OAuth client, and support up to 8 simultaneous worktree dev stacks. Running more than 8 concurrent gz_start instances inside the container is not supported — use the host environment instead if you need more.
This section is for folks who just want to fire up the whole stack quickly and start poking around the app.
source env.sh
gz_start # starts backend + web via the Bazel-run launcherenv.sh, which lazily bootstraps any missing local shell state on first load. Package edits return to healthy via gz_sync, .env* edits return via gz_reload, and gz_start launches the stack from healthy.
Use these shortcuts once you've sourced env.sh; they wrap common CLI sequences so you can focus on implementing features instead of hunting for the right flags. The env.sh script sets up Python/Node paths, loads useful aliases (gz_start, etc.), and keeps environment-specific tweaks (like log rotation and virtualenv activation) centralized, so every developer runs commands against the same configuration without manually sourcing multiple files. On a fresh checkout, source env.sh also materializes any missing local bootstrap state needed for a healthy shell.
Source the file to load all shortcuts into your shell:
source env.shVS Code / Cursor: the repo ships terminal profiles in .vscode/settings.json for Linux and macOS that automatically source env.sh in every new integrated terminal. Linux uses bash; macOS uses zsh with a repo-owned .vscode/.zshrc. The venv is activated and gz_* helpers are available from the moment the terminal opens.
AI coding agents (Claude Code, Codex, Cursor agent): a companion script env-agent.sh provides a silent, lightweight bootstrap (venv activation + current-checkout .env/.env.local loading) for non-interactive shells. Claude Code picks it up via .claude/settings.json; Codex and other agents inherit it through BASH_ENV when launched from an env.sh-sourced terminal. Prefer repo-local worktrees under .agent-worktrees/... instead of /tmp; the bootstrap detects the active git worktree root automatically and expects the worktree to own its .env/.env.local files and materialized dependency environment. env.sh will lazily create the minimal local bootstrap state needed for a healthy shell on first load, and gz_reload refreshes the current shell after shell/bootstrap/env-file edits so the new PATH is visible immediately. Keep repo-local Codex-specific config in .agent-config/codex/ rather than .codex, which may be reserved by the local Codex installation. See docs/agents/dev.md for details.
Use the package managers you already know to edit dependency manifests, then hand the result back to the repo’s Bazel-aware workflow.
Python
- Use the repo-local
uvwrapper from the repo root to edit Python dependencies.source env.shprepends the wrapper directory toPATH, and the wrapper dispatches to the Bazel-managed toolchain. - Typical commands:
uv add,uv remove,uv lock,uv sync. - After changing Python packages, run
gz_syncso the materialized environment and Bazel view stay aligned.
Web
- Use the repo-local
npmwrapper insideweb/to edit JavaScript dependencies.source env.shprepends the wrapper directory toPATH, and the wrapper dispatches to the Bazel-managed toolchain. - After
npm install, regenerateweb/pnpm-lock.yamlfromweb/package-lock.jsonwithpnpm import. - The reconciliation step should use the repo-local
pnpmwrapper so thepnpmside stays aligned with CI and Bazel. - After changing web packages, run
gz_syncso the current shell and repo locks stay in sync.
When to use which helper
- Run
gz_syncafter nativeuvornpmdependency changes. - Run
gz_reloadwhen shell bootstrap files or env files changed and you only need the current terminal to pick up the newPATHimmediately.
Glaze uses a high-level orchestration workflow inspired by the Get Shit Done (GSD) philosophy, but adapted for non-developer QoL and safety with non-frontier models:
/dream: High-level vision and milestone orchestration. Use this to describe a broad feature or user story. The agent will use Plan Mode to break the vision into logical sub-tasks, create a GitHub Milestone, and spawn sub-agents to author specific specs./spec: Detail-oriented issue authoring. Each sub-task from the dream is turned into a precise GitHub issue with problem motivation, proposed solution, and acceptance criteria./do: Execution. When you want an agent to implement an issue or start a PR-sized change, use the explicit issue flow:/do #292./deps: Bazel dependency audit. Use this when you want an agent to inspect the rules_oci image target plus test and lint graphs for unexpected dependencies before planning cleanup work./pm: Session-level communication mode. Use this when the main audience is less technical contributors and you want the assistant to explain choices in terms of user value, trade-offs, and constraints instead of file-by-file edits.
- Normative Content in GitHub: Unlike some agent-first workflows that store state in local markdown files, Glaze keeps all normative requirements and status in GitHub Issues, Milestones, and PRs. This minimizes hallucinations from non-frontier models by using GitHub as the source of truth and ensures that the project remains accessible to human non-developer contributors via the standard GitHub UI.
- Flexible Verification: Verification can happen synchronously (as part of the
/docycle where the agent runs tests before pushing) or asynchronously in bulk using the/audit(performance/flakiness),/cover(coverage), and/deps(Bazel dependency graph audit) skills.
When you run /do #292, the agent will create a branch like issue/292-vibe-coding-flow and a repo-local worktree like .agent-worktrees/codex/issue-292-vibe-coding-flow before it analyzes or edits anything.
The agent should immediately print a copy-friendly line:
Worktree: /home/phil/code/glaze/.agent-worktrees/codex/issue-292-vibe-coding-flow
Open a dedicated terminal tab for that worktree and jump into it with:
gz_cd 292From there, use the normal helpers:
gz_startEach agent gets its own worktree under .agent-worktrees/<agent>/..., and each
issue gets its own branch. Keep one terminal per worktree so gz_start,
gz_stop, logs, and port files stay scoped to the right code checkout.
After the PR is merged or abandoned, stop any servers from that worktree's terminal and clean up the local checkout:
gz_stop
git worktree remove .agent-worktrees/codex/issue-292-vibe-coding-flow
git worktree prune
git branch -d issue/292-vibe-coding-flowUse git branch -D only for an abandoned unmerged branch after confirming the
work is no longer needed.
Keep local-only settings in .env.local files; they are gitignored by default:
.env.local(repo-wide defaults)web/.env.local(web-only overrides)
source env.sh automatically loads both (in that order) so you can inject Cloudinary/API config without committing secrets.
Use the checked-in root template, and create web/.env.local manually if you
need web-only overrides:
cp .env.example .env.localEach variable in .env.example has an inline comment explaining what it enables and what degrades gracefully when it is absent. Keep those comments current whenever a variable is added, removed, or renamed — the file is the primary reference for onboarding and for debugging "why isn't this feature working in dev."
| Command | Description |
|---|---|
gz_sync |
Reconcile native package-manager edits with the Bazel-aware workflow and refresh the current shell. Use this after uv or npm dependency changes. |
gz_reload |
Re-source env.sh in the current shell after changing shell bootstrap, env files, or freshly materialized tools that should appear on PATH right now. |
| Command | Description |
|---|---|
gz_start |
Start backend and web via the Bazel-run launcher. Rotates old logs before starting. |
gz_stop |
Stop both servers. |
gz_status |
Show whether backend and web are running. |
gz_logs [backend|web] |
Tail logs. Omit argument to tail both. |
Logs are written to .dev-logs/ and rotated with a timestamp on each gz_start.
When changing large download endpoints, run the app locally with gz_start,
trigger the same download three times in the browser, and watch the backend RSS:
BACKEND_PID=$(pgrep -f "uvicorn.*$(cat .dev-pids/backend.port)")
watch -n 1 "ps -o pid,rss,vsz,cmd -p ${BACKEND_PID}"RSS may stay at a high-water mark after the first run, but repeated same-size
downloads should plateau rather than ratchet upward. For ASGI production parity,
repeat the check against Docker/staging and watch the Gunicorn/Uvicorn worker
RSS; large StreamingHttpResponse bodies should use async iterators.
| Command | Description |
|---|---|
gz_test |
Run all tests via Bazel (bazel test --test_output=errors //...) — CI-aligned, incremental. |
| Command | Description |
|---|---|
gz_lint |
Run all linters via Bazel (bazel build --config=lint //...) — CI-aligned. |
Prefer Python for standalone dev tooling when the dependency graph allows it. Use the JS tool path under web/scripts/ when the tool is naturally coupled to the web dependency graph or when the needed package exists in npm but not pip. Wire those scripts through web/BUILD.bazel with js_binary and add a vitest_test when you want the tool itself covered by tests. web/scripts/generate-types.mjs and web/scripts/coverage-audit.mjs are the current examples.
Run gz_help to print the full list of shortcuts at any time.
If you prefer to install dependencies and run servers yourself, follow these explicit commands instead of relying on the helper script.
# Backend
bazel run @uv//:uv -- sync
bazel run @uv//:uv -- run python manage.py migrate
uvicorn backend.asgi:application --port 8080 --reload
# Web (separate terminal)
cd web
bazel run @nodejs_linux_amd64//:npm -- install
bazel run @nodejs_linux_amd64//:npm -- run devUse gz_test for the full suite. The package READMEs above describe each area in more detail.
Before committing — auto-fix Python formatting and fixable lint issues:
source env.sh && gz_format
# equivalent to: ruff format . && ruff check --fix .Glaze uses agent workflows for planning, implementation, offline analysis, and documentation. Use the skill that matches the task:
/dream: define a broad feature or product direction and let the agent break it into milestones and sub-issues./spec: turn a specific idea into a focused issue with scope, motivation, and acceptance criteria./do #<issue>: implement a scoped issue on a repo-local worktree and produce the code change./audit: run the performance and flakiness audit workflow for tests./cover: run the coverage audit workflow./deps: inspect Bazel dependency graphs for unexpected or overly broad dependencies./docs: assess and update human-facing READMEs to keep them aligned with the codebase./stories: auto-generate and update Storybook stories for frontend components.
Use /dream and /spec when the work is still being shaped. Use /do when the task is ready to implement. Use the asynchronous audit skills when you want analysis without blocking the main development loop.
Interactive component stories are published to GitHub Pages via Storybook:
https://shaoster.github.io/glaze/storybook/
Run locally with gz_story. See web/README.md for details.
Deployment details, GitHub Actions workflows, and environment variables live in docs/ci-cd.md.
The longer-term Helm/k3s migration checklist is tracked in issue #547.
The hourly database backup CronJob uses Dropbox as its off-cluster backup store.
Create a Dropbox app with scoped access, App folder access, and the
files.content.write and files.metadata.read scopes, then create an offline
refresh token for the app.
gh secret set DROPBOX_APP_KEY --env glaze-droplet --body "<dropbox-app-key>"
gh secret set DROPBOX_APP_SECRET --env glaze-droplet --body "<dropbox-app-secret>"
gh secret set DROPBOX_REFRESH_TOKEN --env glaze-droplet --body "<dropbox-refresh-token>"If you are bootstrapping the cluster manually, add the same values to the
glaze-secrets Kubernetes secret:
kubectl create secret generic glaze-secrets \
--from-literal=DROPBOX_APP_KEY=<dropbox-app-key> \
--from-literal=DROPBOX_APP_SECRET=<dropbox-app-secret> \
--from-literal=DROPBOX_REFRESH_TOKEN=<dropbox-refresh-token>backend/ Django project settings, root URL config
api/ Models, serializers, views, tests
model_factories.py Auto-generates GlobalModel subclasses from workflow.yml
web/
src/
util/
generated-types.ts Auto-generated OpenAPI types (gitignored)
types.ts Domain types/constants derived from generated-types.ts
api.ts HTTP calls; wire-type → domain-type mapping
workflow.ts Workflow helpers loaded from workflow.yml
components/ React components
App.tsx Root component with MUI dark theme
workflow.yml Source of truth for piece states and valid transitions
env.sh Development shell helpers
docker-compose.yml Production stack: web + Postgres
docker-entrypoint.sh Container startup: exec Gunicorn
tools/ensure_cluster.sh Cluster infrastructure convergence (k3s, ESO, probe timeouts)
tools/helm_deploy.sh Helm upgrade with retry and failure diagnostics
The workflow state machine and all valid transitions are defined in workflow.yml. Both the backend and web derive state names and transition rules from this file — nothing is hardcoded elsewhere.
workflow.yml also contains two optional sections beyond the state list:
globals— named domain types backed by Django models. Each entry drives both the backend and frontend:api/model_factories.pyauto-generates the Django model class at import time (amakemigrationsrun is all that is needed to add a new global), and the frontend reads the same declaration to render pickers and resolve display fields. Setfactory: falsefor globals whose model is hand-written (currently onlypiece).custom_fields(per-state) — state-specific fields declared using the embedded DSL. Seeapi/README.mdandweb/README.mdfor more details.