Compare commits

..

10 Commits

Author SHA1 Message Date
78e3c34d8d add README
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:57:45 -04:00
2e631136c6 pin build deps in requirements.txt for native host build
The handbook is served natively (no Docker) on the Gitea box: a python
venv runs mkdocs build, and the reverse proxy serves the static site/ at
docs.rethinkstudios.io. Pin the build deps so the host venv reproduces the
build exactly:

  pip install -r requirements.txt && mkdocs build

Verified the host rebuild flow from scratch: fresh venv + clean
pip install -r requirements.txt + mkdocs build --strict builds all 6 pages
with no errors. Static output uses relative URLs and site_url is the docs
subdomain, so it serves correctly behind the proxy at the subdomain root.
site/ and .venv/ stay gitignored.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:56:50 -04:00
613d8f28ea add Virtual environments page (project isolation + pyenv)
New docs/environments.md, nav 'Virtual environments' (before Deploy):
- the rule: never touch system Python (danger callout)
- project-based isolation as Local .venv / Makefile-driven / Docker tabs,
  each with a runnable snippet; Docker ties back to the deploy standard
- local dev with pyenv: why, official install link, shell init with
  annotated lines, everyday use, per-project .python-version
- shell quality-of-life extras (flake8 alias, .local/bin + npm-global PATH),
  cross-ref'd to Standards and Workflow

workflow.md pyenv bullet now points at the new page; index.md gains a card.

Verified in-browser: tabs switch (Local/Makefile/Docker), annotations and
admonitions render; mkdocs build --strict clean (cross-ref anchors resolve).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:54:31 -04:00
e684ac2853 update deploy guide per spec + add licensing standard
deploy.md:
- fix logs path to /srv/logs/<dev>/<project>
- reframe permissions as a deployer-side heads-up (bind-mount ownership is
  handled at deploy time; 'if your bot won't start or logs vanish, flag us')
  instead of a dev task / heavy footgun
- git in the image only when the container needs it (host always has git)
- NEW: layer caching (requirements before code copy)
- NEW: subprocess/browser workloads — init:true (tini + PID-1 shell-wrapper
  gotcha), shm_size 2gb, mem_limit; with code annotations and a warning
- refresh the compose-needs checklist accordingly

standards.md:
- NEW: licensing — no per-file headers; single top-level LICENSE only when a
  repo is for outside use

Verified: mkdocs build --strict clean; new deploy sections rendered.
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:51:07 -04:00
4fd19bf620 rename to 'rethink development', brighten lib table, drop stale note
- Rename site 'a rethink development' -> 'rethink development' (site_name,
  landing H1); copyright -> 'rethink development (handbook)'.
- libraries.md: remove the 'if the live list is empty' admonition now that
  Gitea CORS is fixed.
- extra.css: improve Library-column readability — brighter #cfe6ff code
  chips on a cyan-tinted bg, more weight, roomier cell padding (0.7/1em),
  nowrap lib names, accent-colored links.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:35:16 -04:00
e5ca1c0ada style the live libraries table + add Language column
- libraries.md: render the live lib list as a 3-column table (Library /
  What it does / Language), wrapped in .rt-lib-table so it picks up
  explicit styling (Material only auto-styles markdown tables, not
  innerHTML-injected ones — that's why it rendered bare before).
- extra.css: branded table (navy header, blue borders, hover rows,
  code-styled linked names) and a blue Language badge.

Verified in-browser against real Gitea API data (14 repos fetched,
handbook denylisted -> 13 libs): table renders sorted with language
badges, matches the theme. Live load on prod still needs Gitea CORS
(app.ini [cors] ALLOW_DOMAIN = the docs origin).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:26:11 -04:00
8fc81fc4f4 add Gitea-style admonitions, code annotations, callouts
Make the things that matter stand out, matching the Gitea callout look:
- extra.css: brand-orange warning admonitions (#f57c00, the lambda orange),
  hotter danger (#e8590c), blue note/info and cyan tip/example, plus blue
  code-annotation markers.
- deploy.md: footgun -> danger callout, secrets -> orange warning, eligible
  -> tip; Dockerfile and compose gain numbered code annotations explaining
  each magic line; paths/mounts as a table; a restart snippet for rotation.
- workflow.md: warning on setting per-repo git identity before first commit;
  tip elevating verify-by-executing.

Verified in-browser: computed border colors match (warning #f57c00,
danger #e8590c), 4 annotation markers render in brand blue, admonition
icons + tinted headers match the Gitea style. mkdocs build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:16:02 -04:00
142a0dbff6 add lambda logo, fold sections, add python/error/log snippets
- Pull the rethink lambda logo (assets/img/logo.svg) into docs/assets and
  wire it as theme.logo + favicon — matches the Gitea brand mark.
- standards.md: fold six thin sections into three fuller ones (Files and
  style / Documentation / Quality and error handling), each illustrated
  with python snippets, a flake8 output block, Do/Don't tabbed examples,
  a traceback, and a log line. Admonitions for the run-it-locally tip and
  the lib-logging note.
- libraries.md: add a collapsible 'using a library' example (pyproject
  pin + import/usage python snippet).

Verified in-browser: logo renders in the header, snippets/tabs/traceback/
log blocks render against the dark theme, libraries example expands.
mkdocs build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:09:30 -04:00
da47923088 theme handbook to match rethink Gitea + rename to 'a rethink development'
Mirror the rethink Gitea theme (data-theme=rethink) on the docs site:
- docs/stylesheets/extra.css maps the exact palette onto Material's slate
  scheme: body #0a0f1f, nav #061541, text #eef1f6, primary #569bcc,
  accent #55bbff, borders #294274; square corners and blue card/table
  borders echoing the Gitea repo panels.
- Force dark-only (drop the light/dark toggle; Gitea is dark-only).
- Rename site to lowercase 'a rethink development' (site_name, copyright,
  landing H1).

Verified: mkdocs build --strict clean; rendered landing + content pages
in-browser against the live Gitea page — colors, header bar, links, and
cards match.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:05:39 -04:00
5e787edbb0 add workflow page — dev-flow centerpiece
New docs/workflow.md covering how we actually work day to day:
- why git / why WSL2 (our reasons, links out for generic install)
- signing up on our Gitea (self-serve register, SSH + deploy-key model)
- our git vs public git: per-repo identity, signed commits, develop->main
- our-flavored git basics (everyday loop, commit small/often, gl graph)
- the dev workflow: plan-in-chat / build-in-Claude-Code, tmux split,
  claudedo voice control, the .claude/ project convention, AI-assist
  habits (git diff | clip.exe)
- recommended setup (WSL + VS Code + Claude Code, pyenv, flake8)
- handy .zshrc/.bashrc snippets with prose on the high-value ones

Claude-first but soft: any AI works, we recommend Claude, and the
.claude/ structure is built on that preference. Sanitized with
placeholders throughout. Nav: Home / Libraries / Standards / Workflow /
Deploy; landing card grid updated.

Verified: mkdocs build --strict clean.
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:00:12 -04:00
11 changed files with 956 additions and 97 deletions

36
README.md Normal file
View File

@ -0,0 +1,36 @@
# handbook
The Rethink Studios handbook — our public reference for the shared library suite,
coding standards, the dev workflow, Python environments, and how to deploy on our
network.
Live at **[docs.rethinkstudios.io](https://docs.rethinkstudios.io)**.
## What this is
A static documentation site built with [MkDocs](https://www.mkdocs.org/) +
[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/). Markdown lives
under `docs/`, builds to a static `site/`, and is served by the reverse proxy at
the subdomain — no app process, just static HTML/CSS/JS.
The **Libraries** page is the one dynamic part: it fetches the `rethink-public`
repo list from the Gitea API client-side at view time, so new libraries appear on
the next page load with no rebuild. Everything else is static markdown.
## Build
```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
mkdocs build # -> static site/
mkdocs serve # local preview at http://127.0.0.1:8000
```
## Contributing
- One topic per page, grouped under `docs/`, wired into the nav in `mkdocs.yml`.
- **Public — sanitize:** no real hostnames, internal IPs, secrets, or exact
topology. Use placeholders (`<dev>`, `<project>`, `/srv/...`).
- Markdown: trailing newline, no trailing whitespace, LF line endings.
- Commits signed (`git commit -s`).

10
docs/assets/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -4,76 +4,120 @@
> **rethink-net** — our fleet of Ubuntu 26.x servers, ready to host whatever > **rethink-net** — our fleet of Ubuntu 26.x servers, ready to host whatever
> you've built. > you've built.
**Eligible:** APIs, websites, applets, bots, monitors. !!! tip "Eligible"
APIs, websites, applets, bots, monitors. The whole network runs on a few
The whole network runs on a few simple, consistent rules. Get your container to simple, consistent rules — get your container to follow them and deploying is
follow them and deploying is mostly handing us a `compose.yaml`. mostly handing us a `compose.yaml`.
## Docker — the services account ## Docker — the services account
Every service runs containerized as the shared **`services`** account: Every service runs containerized as the shared **`services`** account:
**uid/gid 1337**, fixed fleet-wide. Build your image to be **uid-agnostic** so it **uid/gid 1337**, fixed fleet-wide. Build your image to be **uid-agnostic** so it
runs cleanly as that account: runs cleanly as that account.
- `user: "1337:1337"` in compose.
- `chmod -R a+rwX /app` in the Dockerfile (covers non-mounted dirs — see the
bind-mount note below).
- `HOME=/tmp`.
- **No** in-container `user`/`useradd` — don't bake a user into the image.
```dockerfile ```dockerfile
FROM python:3.12-slim FROM python:3.12-slim
ENV HOME=/tmp ENV HOME=/tmp # (1)!
WORKDIR /app WORKDIR /app
# git in the build if you pip-install from git RUN apt-get update \
RUN apt-get update && apt-get install -y --no-install-recommends git \ && apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/* # (2)!
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt # (3)!
COPY . . COPY . .
RUN pip install --no-cache-dir . \ RUN chmod -R a+rwX /app # (4)!
&& chmod -R a+rwX /app
CMD ["python", "-m", "yourapp"] CMD ["python", "-m", "yourapp"]
``` ```
1. `HOME=/tmp` — the `services` account has no home dir; anything writing to
`$HOME` (caches, configs) needs a writable target.
2. Include `git` **only if the container itself needs it** — e.g. you
`pip install` from git, or the app shells out to git at runtime. The build and
host always have git; this line is about what's *inside* the image.
3. Install deps **before** copying the code (see [layer
caching](#layer-caching)).
4. `chmod -R a+rwX /app` makes the app tree writable by **any** uid — that's what
"uid-agnostic" means.
```yaml ```yaml
services: services:
yourapp: yourapp:
build: . build: .
user: "1337:1337" user: "1337:1337" # (1)!
environment: environment:
HOME: /tmp HOME: /tmp
volumes: volumes:
- /srv/configs/<project>:/app/config:ro # host-managed, read-only - /srv/configs/<project>:/app/config:ro # (2)!
- /srv/<dev>/<project>:/app/logs # live + rolled logs - /srv/logs/<dev>/<project>:/app/logs # (3)!
- yourapp-data:/app/data # named volume — the rest - yourapp-data:/app/data # (4)!
volumes: volumes:
yourapp-data: yourapp-data:
``` ```
1. Run as the shared account. **No** in-container `user`/`useradd` — don't bake a
user into the image; set it here.
2. Configs: host-managed bind mount, mounted **read-only**.
3. Logs: bind mount — live and rolled, scraped for monitoring.
4. Everything else: a **named volume**. Docker owns it, so there are no host
permissions to fiddle with.
## Paths and mounts ## Paths and mounts
- **Configs**`/srv/configs/<project>/` — bind mount, host-managed. | What | Where | How |
- **Logs**`/srv/<dev>/<project>/` — bind mount; live and rolled, scraped for | --- | --- | --- |
monitoring. | Configs | `/srv/configs/<project>/` | bind mount, host-managed, read-only |
- **Everything else** (caches, browser profiles, scratch) → **named volumes**. | Logs | `/srv/logs/<dev>/<project>/` | bind mount; live + rolled, scraped |
Docker manages ownership, so there are no host permissions to fiddle with. | Caches, profiles, scratch | named volume | Docker manages ownership |
## Permissions — the bind-mount footgun !!! note "If your service won't start or its logs aren't persisting"
That's usually a host-side bind-mount **ownership** thing — the kind of detail
**we sort out at deploy time**, not something you need to chown or provision.
If a bot won't come up or logs/caches keep vanishing, flag it and we'll fix
the mount perms. Stick to a clean `compose.yaml` and let us handle the host.
`docker compose up` does **not** create bind-mount directories as you. If a ## Layer caching
bind-mount source is missing, the Docker daemon (**root**) creates it **as
root** — and your container (**1337**) then can't write it. Logs fall back to
console-only, caches re-download every run.
So: Copy `requirements.txt` and `pip install` **before** `COPY . .`. Docker caches
layers in order, so deps only reinstall when `requirements.txt` changes — not on
every code edit. Get this backwards and every one-line change triggers a full
dependency reinstall.
- **Bind-mount sources (configs, logs) must EXIST and be 1337-owned _before_ ```dockerfile
`up`.** This is handled at provisioning, not a per-deploy chown hook. COPY requirements.txt .
- **Named volumes avoid this entirely** — use them for anything that doesn't need RUN pip install --no-cache-dir -r requirements.txt # cached until deps change
host visibility. COPY . . # changes every build
- The Dockerfile `chmod -R a+rwX /app` only covers **non-mounted** dirs. A bind ```
mount overrides the image directory with the host directory, so for mounted
paths the **host-side ownership wins**. ## Subprocess and browser workloads
Bots that spawn Chrome, Xvfb, ffmpeg, or other child processes need three extra
knobs in compose:
```yaml
services:
yourbot:
build: .
user: "1337:1337"
init: true # (1)!
shm_size: "2gb" # (2)!
mem_limit: "4g" # (3)!
```
1. Runs **tini** as PID 1 to reap zombie subprocesses and forward signals.
Without it, spawned Chrome/Xvfb processes leak as zombies.
2. Chrome and most headless browsers crash on Docker's default **64 MB**
`/dev/shm`. Bump it for any browser workload.
3. Bound memory — especially when each worker spawns a browser. Raise it as
worker count grows.
!!! warning "The PID-1 gotcha with shell-wrapper CMDs"
If your `CMD` is a shell-script wrapper (e.g. `xvfb-run ...`), it must **not**
be PID 1, or the real process dies on startup. `init: true` is exactly what
fixes this — tini takes PID 1, and your wrapper runs as a normal child.
## What your compose / Dockerfile needs ## What your compose / Dockerfile needs
@ -83,16 +127,21 @@ So:
- secrets bind-mounted **`:ro`** - secrets bind-mounted **`:ro`**
- `HOME=/tmp` - `HOME=/tmp`
- `chmod -R a+rwX /app` - `chmod -R a+rwX /app`
- `git` in the build if you `pip install` from git - deps installed **before** the code copy (layer caching)
- `git` in the image **if the container needs it**
- for browser/subprocess workloads: `init: true`, `shm_size`, `mem_limit`
## Secrets ## Secrets
We do **not** commit secrets (usually, lol). The rule: !!! warning "Secrets never go in the image"
We do **not** commit secrets (usually, lol). They stay **gitignored**, live on
- Secrets stay **gitignored**. the host at `/srv/configs/<project>/`, and are bind-mounted **read-only** at
- They're placed on the host at `/srv/configs/<project>/`. runtime. Add them to `.dockerignore` so a `COPY . .` can't sweep them into a
- They're bind-mounted **read-only** at runtime. layer.
- **Never** baked into the image — add them to `.dockerignore` so `COPY . .`
can't grab them.
Rotating a secret = edit the host file and restart. No rebuild. Rotating a secret = edit the host file and restart. No rebuild.
```bash
vim /srv/configs/<project>/secrets.env # edit on the host
docker compose restart yourapp # pick up the change — no rebuild
```

145
docs/environments.md Normal file
View File

@ -0,0 +1,145 @@
# Virtual environments
How we keep a project's Python isolated — from the system Python and from every
other project. The rule underneath all of it: **never install into, or upgrade,
the system Python.** The OS depends on it; a project must never touch it.
!!! danger "Leave the system Python alone"
No `sudo pip install`, no upgrading the system interpreter for a project. If a
project needs a different version or a package, that goes in a **virtual
environment** — never the system one. Breaking system Python can break the OS.
## Project-based isolation
Every project runs against its own isolated environment. There are three ways that
happens, depending on where the project runs:
=== "Local `.venv`"
A virtualenv living in the repo. Simplest for day-to-day local work.
```bash
python -m venv .venv # create it (once)
source .venv/bin/activate # activate for this shell
pip install -r requirements.txt
```
Keep `.venv/` **gitignored** — it's per-machine, never committed.
=== "Makefile-driven"
Wrap the venv in a `make` target so every dev (and CI) sets up identically —
no "did you activate it?" drift.
```makefile
VENV := .venv
PY := $(VENV)/bin/python
$(VENV): requirements.txt
python -m venv $(VENV)
$(PY) -m pip install -r requirements.txt
.PHONY: run
run: $(VENV)
$(PY) -m yourapp
```
`make run` creates the venv if missing, installs deps, and runs — all against
the isolated interpreter, no manual activation.
=== "Docker"
The container **is** the isolation — its own filesystem, its own interpreter,
nothing shared with the host. You don't need a `.venv` inside an image; install
straight into the container's Python.
```dockerfile
FROM python:3.12-slim
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
```
This is how things run in production — see the [Deploy guide](deploy.md) for the
full container standard (uid 1337, mounts, layer caching).
!!! tip "Which one?"
**Local `.venv`** for quick iteration, **Makefile** when you want repeatable
setup across the team, **Docker** for anything that ships. They're not
exclusive — a project often has a `.venv` for local dev *and* a Dockerfile for
deploy.
## Local dev with pyenv
For local work you also need the right **Python version**, not just isolated deps.
We target **Python 3.10+**, and [pyenv](https://github.com/pyenv/pyenv) installs and
switches versions per-project without touching the system Python.
- **Per-project pinning:** a `.python-version` file in a repo makes pyenv
auto-select that interpreter when you `cd` in — everyone on the project runs the
same one.
- **Pairs with venvs:** combined with `pyenv-virtualenv`, each project gets both an
isolated version *and* isolated deps.
### Install
Follow the [official pyenv installation](https://github.com/pyenv/pyenv#installation)
for the installer and build dependencies — no point reproducing it here. Then add
the shell init below to your `~/.zshrc` (or `~/.bashrc`) and restart your shell.
### Shell init
Without these lines pyenv's shims aren't on `PATH`, so `pyenv` and
auto-version-switching won't work:
```bash
# pyenv — Python version management
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)" # (1)!
eval "$(pyenv init -)" # (2)!
eval "$(pyenv virtualenv-init -)" # (3)!
```
1. Puts the **shims** dir on `PATH`, so `python` resolves to the pyenv-selected
version instead of the system one.
2. Shell integration — command rehashing and completion.
3. Auto-activates a project's virtualenv on `cd`**only** if you use
`pyenv-virtualenv`. Drop this line if you don't.
### Everyday use
```bash
pyenv install 3.10.14 # install a version (one-time)
pyenv install --list # see available versions
cd <project>
pyenv local 3.10.14 # writes .python-version -> auto-selects here
python --version # confirms the pinned version
```
- **`pyenv local <ver>`** per project — commit the `.python-version` so the team
matches.
- **`pyenv global <ver>`** for your default outside any project.
## Shell quality-of-life extras
A couple of optional lines worth having alongside pyenv:
```bash
# flake8 with our shared config (max line 120)
alias flake8='flake8 --config ~/.config/flake8'
# local bins on PATH (pip --user installs, npm globals)
export PATH="$HOME/.local/bin:$PATH"
export PATH="$HOME/.npm-global/bin:$PATH"
```
- The **flake8 alias** keeps everyone linting with the same config (our 120
max-line, etc. — see [Standards](standards.md#files-and-style)).
- The **`.local/bin` / npm-global** PATH lines stop "command not found" after a
`pip install --user` or a global npm install.
!!! note "More shell setup"
This is the Python-env slice. The fuller dev shell setup — the WSL/Windows
clipboard bridges, per-repo git-identity aliases, and the `gl` graph log — is on
the [Workflow](workflow.md#handy-shell-setup) page.

View File

@ -1,4 +1,4 @@
# Rethink Studios Handbook # rethink development
The public reference for building and shipping with Rethink Studios: our shared The public reference for building and shipping with Rethink Studios: our shared
libraries, our coding standards, and how to deploy a project on our network. libraries, our coding standards, and how to deploy a project on our network.
@ -25,6 +25,20 @@ out of it; examples use placeholders like `<dev>`, `<project>`, and `/srv/...`.
House coding standards — file hygiene, docstrings, type hints, linting, and House coding standards — file hygiene, docstrings, type hints, linting, and
how we handle errors. how we handle errors.
- :material-sitemap: __[Workflow](workflow.md)__
---
How we actually work day to day — our Gitea, git habits, and the
plan-in-chat / build-in-Claude-Code flow, plus shell setup.
- :material-language-python: __[Virtual environments](environments.md)__
---
Project-based Python isolation — local `.venv`, Makefile, or Docker — and
local version management with pyenv.
- :material-rocket-launch: __[Deploy](deploy.md)__ - :material-rocket-launch: __[Deploy](deploy.md)__
--- ---

View File

@ -11,10 +11,44 @@ the repo, where the README and tags live.
## Install ## Install
Pin a tag in your dependencies — never an unpinned branch:
``` ```
<lib> @ git+https://git.rethinkstudios.io/rethink-public/<lib>.git@<tag> <lib> @ git+https://git.rethinkstudios.io/rethink-public/<lib>.git@<tag>
``` ```
??? example "Using a library — install, import, go"
Add it to your project's deps (e.g. `pyproject.toml`):
```toml
[project]
dependencies = [
"aioweb @ git+https://git.rethinkstudios.io/rethink-public/aioweb.git@v0.3.1",
]
```
Then use it — the README in each repo has the real surface; this is the shape:
```python
import asyncio
from aioweb import Session
async def main() -> None:
"""fetch a page through the shared async http session"""
async with Session() as web:
resp = await web.get("https://example.test")
print(resp.status, len(resp.content))
asyncio.run(main())
```
Need to bump a lib? Change the `@<tag>` and reinstall — versions live with the
lib, not in these docs.
<div id="lib-list" markdown="0"> <div id="lib-list" markdown="0">
<p class="lib-status">Loading libraries from Gitea…</p> <p class="lib-status">Loading libraries from Gitea…</p>
</div> </div>
@ -55,12 +89,17 @@ the repo, where the README and tags live.
var rows = libs.map(function (r) { var rows = libs.map(function (r) {
var url = r.html_url || (REPO_BASE + "/" + r.name); var url = r.html_url || (REPO_BASE + "/" + r.name);
var desc = r.description ? escapeHtml(r.description) : "<em>No description.</em>"; var desc = r.description ? escapeHtml(r.description) : "<em>No description.</em>";
var lang = r.language
? '<span class="rt-lang">' + escapeHtml(r.language) + "</span>"
: '<span class="rt-lang rt-lang--none"></span>';
return '<tr><td><a href="' + escapeHtml(url) + '"><code>' + return '<tr><td><a href="' + escapeHtml(url) + '"><code>' +
escapeHtml(r.name) + '</code></a></td><td>' + desc + '</td></tr>'; escapeHtml(r.name) + "</code></a></td><td>" + desc +
"</td><td>" + lang + "</td></tr>";
}).join(""); }).join("");
el.innerHTML = el.innerHTML =
'<table><thead><tr><th>Library</th><th>What it does</th></tr></thead>' + '<div class="rt-lib-table"><table>' +
'<tbody>' + rows + '</tbody></table>'; "<thead><tr><th>Library</th><th>What it does</th><th>Language</th></tr></thead>" +
"<tbody>" + rows + "</tbody></table></div>";
} }
function fail() { function fail() {
@ -84,11 +123,3 @@ the repo, where the README and tags live.
} }
})(); })();
</script> </script>
!!! info "If the live list is empty or stale"
The fetch runs in your browser against the Gitea API and needs the org repos
to be readable unauthenticated (they are — it's a public org) and CORS to
allow the docs domain. If the list won't load, browse the org directly at
[git.rethinkstudios.io/rethink-public](https://git.rethinkstudios.io/rethink-public).
The fallback for a disabled live fetch is a webhook rebuild — a push to any
`rethink-public` repo triggers a site rebuild.

View File

@ -3,40 +3,129 @@
The house standards every Rethink Studios project follows. They keep our code The house standards every Rethink Studios project follows. They keep our code
consistent, readable, and predictable across the whole suite. consistent, readable, and predictable across the whole suite.
## File hygiene ## Files and style
- Files end with a **single trailing newline** (LF / Unix line endings). - Files end with a **single trailing newline** (LF / Unix line endings), and
- **No trailing whitespace** on any line. carry **no trailing whitespace** on any line.
- **4 spaces** for indentation by default. Respect language norms — TS/JS use
**2 spaces**, Go uses **tabs** — and when editing an existing file, follow that
file's existing indentation.
- **flake8 clean**, max line length **120**. **Type hints** on public functions.
## Indentation A well-formed module — module docstring, type hints, lowercase-start docstring:
- **4 spaces** by default. ```python
- Respect language norms: TS/JS use **2 spaces**, Go uses **tabs**. """async key/value store backed by a single json file"""
- When editing an existing file, follow that file's existing indentation.
## Docstrings and comments from pathlib import Path
- Public functions get a **docstring**. Style: **lowercase-start, no trailing
period**. def load_state(path: Path) -> dict[str, str]:
- Keep inline comments minimal — prefer docstrings. Use an inline comment only """read the json state file, returning an empty dict if it's missing"""
if not path.exists():
return {}
return _read_json(path)
```
flake8 tells you the moment you drift — keep the tree clean:
```text
$ flake8
./aiokv/store.py:14:80: E501 line too long (96 > 79 characters)
./aiokv/store.py:22:1: F401 'json' imported but unused
./aiokv/store.py:31:5: E303 too many blank lines (3)
```
!!! tip "Run it locally"
Wire the shared config into an alias so every project lints the same way:
`alias flake8='flake8 --config ~/.config/flake8'` (max line 120). See the
[Workflow](workflow.md#handy-shell-setup) page.
## Documentation
- Public functions get a **docstring****lowercase-start, no trailing period**.
- Keep inline comments minimal; prefer docstrings. Use an inline comment only
where the code is genuinely complex. where the code is genuinely complex.
- Each module/file states its scope and purpose via a **module docstring** - Each module/file states its scope and purpose via a **module docstring**
(header string) — **not** a license or copyright header. (header string) — **not** a license or copyright header.
- Every library and project has a **README** with the install line, what it does,
and a usage example.
## READMEs !!! note "Licensing — no per-file headers"
Don't prepend license/copyright boilerplate to source files. If a repo needs a
license, it's a single top-level **`LICENSE`** file — never repeated per file.
Most internal repos don't carry one; add it only when a repo is meant for
outside use and the terms are decided.
Every library and project has a defined **README** covering: === "Do"
- the install line, ```python
- what it does, def mask_secret(value: str, keep: int = 4) -> str:
- a usage example. """mask all but the last ``keep`` characters of a secret"""
if len(value) <= keep:
return "*" * len(value)
return "*" * (len(value) - keep) + value[-keep:]
```
## Linting and types === "Don't"
- **flake8 clean**, max line length **120**. ```python
- **Type hints** on public functions. # Masks a secret. <- license-header-style noise, capitalized, trailing period
def mask_secret(value, keep=4): # no type hints
# loop over the chars and hide them
return "*" * (len(value) - keep) + value[-keep:] # crashes if short
```
## Error handling ## Quality and error handling
- **Fail loud** — never swallow exceptions. **Fail loud** — never swallow exceptions. Catch the **specific** exception and
- Catch the **specific** exception, and **log** it. **log** it; don't paper over failures with a bare `except`.
=== "Do"
```python
import logging
log = logging.getLogger(__name__)
def fetch(url: str) -> bytes:
"""fetch ``url``, logging and re-raising on failure"""
try:
return _client.get(url).content
except TimeoutError:
log.warning("fetch timed out: %s", url)
raise
```
=== "Don't"
```python
def fetch(url):
try:
return _client.get(url).content
except Exception:
pass # swallowed — the caller has no idea anything broke
```
When something does break, a loud failure gives you a real traceback to act on:
```python-traceback
Traceback (most recent call last):
File "run.py", line 42, in <module>
data = fetch("https://example.test/feed")
File "aioweb/session.py", line 88, in fetch
return self._client.get(url).content
TimeoutError: request timed out after 30s
```
…and the log line that precedes it tells you where to look:
```text
2026-06-29 14:03:11,204 WARNING aioweb.session fetch timed out: https://example.test/feed
```
!!! note "Logging belongs to the app, not the library"
Libraries **emit only**`log = logging.getLogger(__name__)` and nothing
else. Handlers, levels, and formatting are configured once at the
application entry point, so a lib never dictates how its host logs.

236
docs/stylesheets/extra.css Normal file
View File

@ -0,0 +1,236 @@
/* a rethink development mirror of the rethink Gitea theme
palette pulled from git.rethinkstudios.io (data-theme="rethink") */
:root {
--rt-body: #0a0f1f; /* deep navy page background */
--rt-nav: #061541; /* darker header/nav background */
--rt-surface: #0e1530; /* cards / code blocks, a touch above body */
--rt-text: #eef1f6; /* near-white body text */
--rt-muted: #aab4c5; /* secondary text */
--rt-primary: #569bcc; /* links / primary blue */
--rt-primary-dark: #4a8cbb;
--rt-primary-light: #6ba9d5;
--rt-accent: #55bbff; /* bright cyan-blue accent */
--rt-border: #294274; /* row dividers / borders */
}
/* Map the rethink palette onto Material's slate scheme. */
[data-md-color-scheme="slate"] {
--md-hue: 222;
--md-default-bg-color: var(--rt-body);
--md-default-fg-color: var(--rt-text);
--md-default-fg-color--light: var(--rt-muted);
--md-default-fg-color--lighter: rgba(238, 241, 246, 0.32);
--md-default-fg-color--lightest: rgba(238, 241, 246, 0.12);
--md-primary-fg-color: var(--rt-nav);
--md-primary-fg-color--light: var(--rt-primary-light);
--md-primary-fg-color--dark: var(--rt-nav);
--md-primary-bg-color: var(--rt-text);
--md-primary-bg-color--light: var(--rt-muted);
--md-accent-fg-color: var(--rt-accent);
--md-accent-fg-color--transparent: rgba(85, 187, 255, 0.1);
--md-typeset-color: var(--rt-text);
--md-typeset-a-color: var(--rt-primary);
--md-code-bg-color: var(--rt-surface);
--md-code-fg-color: #d6deeb;
--md-footer-bg-color: var(--rt-nav);
--md-footer-bg-color--dark: var(--rt-body);
}
/* Header / top nav: match Gitea's dark-blue bar. */
.md-header,
.md-tabs {
background-color: var(--rt-nav);
color: var(--rt-text);
}
/* Links hover to the bright accent, like Gitea. */
.md-typeset a:hover {
color: var(--rt-accent);
}
/* Sidebar nav active item picks up the blue. */
.md-nav__link--active,
.md-nav__item .md-nav__link--active {
color: var(--rt-primary);
}
/* Square the corners — Gitea uses radius 0 throughout. */
.md-typeset .admonition,
.md-typeset details,
.md-typeset pre > code,
.md-typeset .highlight,
.md-typeset table:not([class]),
.md-search__form,
.md-typeset .tabbed-set > input:checked + label,
.md-typeset code {
border-radius: 0;
}
/* Subtle blue row dividers in tables, echoing the repo list. */
.md-typeset table:not([class]) {
border: 1px solid var(--rt-border);
}
.md-typeset table:not([class]) th {
background-color: var(--rt-nav);
color: var(--rt-text);
}
.md-typeset table:not([class]) td {
border-top: 1px solid rgba(41, 66, 116, 0.5);
}
/* Search field on the dark bar. */
.md-search__input {
background-color: rgba(255, 255, 255, 0.06);
}
.md-search__input::placeholder {
color: rgba(238, 241, 246, 0.6);
}
/* Admonitions Gitea-style: brand-orange warning, blue note, etc.
The lambda logo's orange (#ffae42 -> #f57c00) drives the warning/danger look. */
.md-typeset .admonition,
.md-typeset details {
border-width: 0 0 0 .2rem;
background-color: var(--rt-surface);
}
/* warning / caution: the orange Gitea callout */
.md-typeset .admonition.warning,
.md-typeset details.warning,
.md-typeset .admonition.caution,
.md-typeset details.caution {
border-color: #f57c00;
}
.md-typeset .warning > .admonition-title,
.md-typeset .warning > summary,
.md-typeset .caution > .admonition-title,
.md-typeset .caution > summary {
background-color: rgba(245, 124, 0, 0.12);
}
.md-typeset .warning > .admonition-title::before,
.md-typeset .caution > .admonition-title::before {
background-color: #f57c00;
}
/* danger: a hotter orange-red for true footguns */
.md-typeset .admonition.danger,
.md-typeset details.danger {
border-color: #e8590c;
}
.md-typeset .danger > .admonition-title,
.md-typeset .danger > summary {
background-color: rgba(232, 89, 12, 0.14);
}
/* note / info / tip: blue + cyan from the rethink palette */
.md-typeset .admonition.note,
.md-typeset details.note,
.md-typeset .admonition.info,
.md-typeset details.info {
border-color: var(--rt-primary);
}
.md-typeset .note > .admonition-title,
.md-typeset .info > .admonition-title {
background-color: rgba(86, 155, 204, 0.12);
}
.md-typeset .admonition.tip,
.md-typeset details.tip,
.md-typeset .admonition.example,
.md-typeset details.example {
border-color: var(--rt-accent);
}
.md-typeset .tip > .admonition-title,
.md-typeset .example > summary {
background-color: rgba(85, 187, 255, 0.1);
}
/* Live libraries table JS-injected, so it needs explicit Material-style rules
(Material only auto-styles markdown tables, not innerHTML ones). */
.rt-lib-table {
overflow-x: auto;
margin: 1em 0;
}
.rt-lib-table table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--rt-border);
font-size: 0.74rem;
}
.rt-lib-table th,
.rt-lib-table td {
text-align: left;
padding: 0.7em 1em;
vertical-align: top;
}
.rt-lib-table td:first-child {
white-space: nowrap;
}
.rt-lib-table thead th {
background-color: var(--rt-nav);
color: var(--rt-text);
font-weight: 700;
border-bottom: 1px solid var(--rt-border);
}
.rt-lib-table tbody tr {
border-top: 1px solid rgba(41, 66, 116, 0.5);
transition: background-color 0.15s;
}
.rt-lib-table tbody tr:hover {
background-color: rgba(86, 155, 204, 0.08);
}
.rt-lib-table td code {
background-color: rgba(85, 187, 255, 0.12);
color: #cfe6ff;
padding: 0.2em 0.55em;
font-size: 0.9em;
font-weight: 600;
}
.rt-lib-table a {
color: var(--rt-accent);
font-weight: 600;
}
.rt-lib-table a:hover code {
background-color: rgba(85, 187, 255, 0.22);
color: #ffffff;
}
/* Language badge in the libs table. */
.rt-lang {
display: inline-block;
padding: 0.1em 0.5em;
border: 1px solid var(--rt-border);
border-radius: 0;
background-color: rgba(86, 155, 204, 0.1);
color: var(--rt-primary-light);
font-size: 0.85em;
white-space: nowrap;
}
.rt-lang--none {
border-color: transparent;
background-color: transparent;
color: var(--rt-muted);
}
/* Code annotation markers (the (1) callouts) in brand blue. */
.md-typeset .md-annotation__index {
background-color: var(--rt-primary);
color: var(--rt-body);
}
/* Landing-page cards: surface tint + blue border like Gitea panels. */
.md-typeset .grid.cards > :is(ul, ol) > li,
.md-typeset .grid > .card {
background-color: var(--rt-surface);
border: 1px solid var(--rt-border);
border-radius: 0;
}
.md-typeset .grid.cards > :is(ul, ol) > li:hover {
border-color: var(--rt-primary);
}

243
docs/workflow.md Normal file
View File

@ -0,0 +1,243 @@
# Workflow
How we actually work day to day at Rethink Studios — where code lives, how we use
git, the AI-assisted dev flow we recommend, and the shell setup that ties it
together. This is the **our-flavored** version: why *we* do it this way and how
*our* setup is wired. For the truly generic parts (installing WSL, learning git),
we link the official docs rather than reteach them.
!!! info "Public, sanitized"
Examples use placeholders — `<you>`, `<key>`, `dev@<you>`, `/mnt/c/<your>/...`.
Swap in your own real values locally; never commit personal paths, key names,
or emails.
## Why git, why WSL
**Git** is the backbone of everything here. Version history and branching are the
obvious part, but our whole deploy model is built on git too: we `pip install`
libraries straight from git by tag, servers pull via per-repo deploy keys, and
changes flow `develop → main` before they ship. If you know git, you already
understand how our code moves from your machine to production.
**WSL2 (Ubuntu)** is where we develop — a real Linux toolchain on a Windows
desktop. You get native Linux tooling (the same environment our servers run) plus
the Windows apps you actually use day to day. Install WSL2 from Microsoft's docs;
this page documents the *our-setup* layer that goes on top.
- [Install WSL (Microsoft)](https://learn.microsoft.com/windows/wsl/install)
- [VS Code Remote — WSL](https://code.visualstudio.com/docs/remote/wsl)
## Signing up on our Gitea
Our code lives on **Gitea** at
[git.rethinkstudios.io](https://git.rethinkstudios.io). Two orgs you'll use:
- **[rethink-public](https://git.rethinkstudios.io/rethink-public)** — public
libraries, resources, and assets.
- **[rethink-software](https://git.rethinkstudios.io/rethink-software)** — our
applications.
**Get an account:**
1. Register at [git.rethinkstudios.io](https://git.rethinkstudios.io) and verify
your email.
2. Add your **SSH public key** under *Settings → SSH / GPG Keys* so you can clone
and push over SSH.
3. For servers, we use a **per-repo deploy-key** model rather than your personal
key — see the [Deploy guide](deploy.md) for how a box gets read access to just
the repos it needs.
## Our git vs. public git (GitHub / GitLab)
It's the same git — just a different host. The thing most devs trip on is
**identity and keys per host**: you may have a GitHub identity *and* a Gitea
identity on one machine, and commits need to be attributed (and signed/pushed)
with the right one per project.
We solve that with **per-repo local git config** — run a small alias inside a repo
to set its local user and the SSH key it pushes with (see
[per-project git identity](#per-project-git-identity) below). No global identity
juggling.
Our conventions, in short:
- **Signed commits**`git commit -s`.
- **No AI co-author trailer** on your own work. (Intern/dev work you're crediting
gets that dev's `Co-Authored-By` — nothing else.)
- **`develop` is staging**, merge to **`main`** via MR when it's ready to ship.
- **Libraries install from git by tag** — pin a version in your deps, bump the
tag when the lib releases.
!!! warning "Set your per-repo identity *before* the first commit"
One machine often carries more than one Gitea identity/key. If you forget to
run `gitsetup` in a fresh clone, your commits attribute to the wrong
user — or push with the wrong key and bounce. Run it right after cloning;
see [per-project git identity](#handy-shell-setup).
## Git basics (our-flavored)
Not a git tutorial — just how the everyday loop looks against our Gitea. For the
generic command reference, keep the
[Git cheat sheet](https://training.github.com/downloads/github-git-cheat-sheet/)
or the [Pro Git book](https://git-scm.com/book) handy.
**Clone** over SSH (the alias maps to a key — see the shell setup):
```bash
git clone git@<alias>:rethink-public/<repo>.git
```
**The everyday loop:**
```bash
git switch -c <feature> # branch off
# ...make a logical change...
git commit -s -m "..." # commit, signed
git push -u origin <feature> # push
# open an MR: develop -> main
```
**Commit small and often** — one commit per logical change, not a giant
end-of-day dump. Small commits are easier to review, revert, and `git bisect`
when something breaks.
**Read history** as a graph with the `gl` alias below:
```bash
gl # git log --graph, oneline, decorated
```
## The dev workflow
This is the part that's distinctly *ours*. AI is a force multiplier, but only with
discipline around it — the two habits that matter most are **plan before you
build** and **verify by executing, not asserting**.
You can use whatever AI you like — but we **recommend Claude** (via
[Claude Code](https://docs.claude.com/en/docs/claude-code/overview), in the
terminal or VS Code). The whole project structure we recommend below — the
`.claude/` folder, `CLAUDE.md` instructions, numbered specs — is built on that
preference: it's designed around how Claude Code reads project context and takes
handoffs. Other tools can read these files too, but the convention assumes
Claude-first.
**Plan here, build in Claude Code.** Do the thinking in chat — plan, write the
spec, make the decisions. Then hand that spec to a Claude Code agent that does the
build: it implements, **verifies by running**, and pushes. The chat plans; the
agent implements.
**tmux + Claude Code split.** In practice that's a Claude Code agent running in a
tmux pane doing the build work while you plan/review in another. One spec in, a
verified change out.
**claudedo (optional, hands-free).** Voice control for Claude Code over tmux: a
wake-word plus local Whisper speech-to-text drives your tmux session without the
keyboard — handy when you're fullscreen or away from the desk. It's available
here:
[git.rethinkstudios.io/rethink-software/claudedo](https://git.rethinkstudios.io/rethink-software/claudedo).
**The `.claude/` project convention.** Every project has a top-level `.claude/`
folder (always gitignored — nothing under it is committed):
- **`CLAUDE.md`** — project-specific instructions for the agent (stack, layout,
conventions). Layered on top of your global instructions.
- **`compact.md`** — a running state log (done / decided / in-flight), updated at
checkpoints so a fresh session catches up fast.
- **`commands.log`** — an append-only record of shell commands run.
- **`spec/`** — numbered, per-change specs named `NN-<type>-<short>.md`
(e.g. `01-feature-logging.md`).
Tell the agent to **"setup project"** to scaffold all of that (and add `.claude/`
to `.gitignore`) in a new repo.
**AI-assist habits.** When you're stuck, give the AI the *exact* context instead
of describing it — the WSL clipboard bridge makes this trivial:
```bash
git diff | clip.exe # then paste the diff straight into the chat
```
Use AI for the plan and the spec, let the agent build, and always **prove it
works by running it** — don't accept "this should work."
!!! tip "Verify by executing, not asserting"
The single habit that separates good AI-assisted work from plausible-looking
nonsense: **run it**. A passing build, a real screenshot, actual output — that
is proof. "It should work" is not.
## Recommended setup
- **WSL2 (Ubuntu) + VS Code + Claude Code.** Develop in WSL, edit in VS Code over
[Remote — WSL](https://code.visualstudio.com/docs/remote/wsl), and run
[Claude Code](https://docs.claude.com/en/docs/claude-code/overview) as your
agent (terminal or the VS Code extension).
- **[pyenv](https://github.com/pyenv/pyenv)** for per-project Python versions
(we target **3.10+**) and isolated `.venv`s — full setup on the
[Virtual environments](environments.md) page.
- **[flake8](https://flake8.pycqa.org/)** with a shared config — max line length
**120** (see the alias below).
## Handy shell setup
Copy-paste these into your `.zshrc` / `.bashrc`. Replace every placeholder with
your real values. The high-value ones are explained underneath.
```bash
# --- WSL <-> Windows bridges ---
alias explorer="explorer.exe ." # open current dir in Windows Explorer
# pipe to the Windows clipboard (great for pasting context into AI):
# git diff | clip.exe -> paste the diff straight into a chat
# auto-alias Windows .exe tools on PATH (e.g. adb/platform-tools):
export PATH="$PATH:/mnt/c/<your>/platform-tools"
for exe in /mnt/c/<your>/platform-tools/*.exe; do
alias "$(basename "${exe}" .exe)"="${exe}"
done
# --- pyenv (Python version management) ---
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
# --- flake8 with a shared config ---
alias flake8='flake8 --config ~/.config/flake8'
# --- per-project git identity (switch identity per repo) ---
# set the LOCAL (per-repo) user + the key to sign/push with:
alias gitsetup='git config --local user.name "<you>"; \
git config --local user.email "dev@<you>"; \
git config --local core.sshCommand "ssh -i $HOME/.ssh/<key>"'
# a second identity for a different account, same pattern:
alias gitea='git config --local user.name "<alt>"; \
git config --local user.email "<alt>@<host>"; \
git config --local core.sshCommand "ssh -i $HOME/.ssh/<alt-key>"'
# --- git log graph ---
alias gl='git log --graph --abbrev-commit --pretty=oneline --decorate'
# --- local bins on PATH ---
export PATH="$HOME/.local/bin:$PATH"
export PATH="$HOME/.npm-global/bin:$PATH"
```
```bash
# cherry-pick a commit onto master/main quickly
gitcs() {
if [ -z "$1" ]; then echo "Usage: gitcs <commit>"; return 1; fi
git checkout master && git cherry-pick "$1"
}
```
**The ones worth understanding:**
- **`git diff | clip.exe`** — the killer WSL trick for AI-assisted dev. Pipe your
working changes straight to the Windows clipboard and paste them into the chat
so the AI sees *exactly* what changed instead of your paraphrase of it.
- **`gitsetup` / `gitea`** — per-repo identity. One machine, multiple Gitea
identities and keys; run the alias inside a repo to set its **local** user and
signing/push key, so commits attribute correctly without touching your global
config.
- **`gl`** — a readable branch graph for understanding history at a glance.
- **`pyenv`** — per-project Python versions, so each repo builds against the
version it targets.

View File

@ -1,26 +1,20 @@
site_name: Rethink Studios Handbook site_name: rethink development
site_description: Libraries, coding standards, and how to deploy on our network. site_description: Libraries, coding standards, and how to deploy on our network.
site_url: https://docs.rethinkstudios.io/ site_url: https://docs.rethinkstudios.io/
copyright: Rethink Studios copyright: rethink development (handbook)
extra_css:
- stylesheets/extra.css
theme: theme:
name: material name: material
language: en language: en
logo: assets/logo.svg
favicon: assets/logo.svg
palette: palette:
- media: "(prefers-color-scheme: light)" scheme: slate
scheme: default primary: custom
primary: indigo accent: custom
accent: indigo
toggle:
icon: material/weather-night
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: indigo
accent: indigo
toggle:
icon: material/weather-sunny
name: Switch to light mode
features: features:
- navigation.instant - navigation.instant
- navigation.tracking - navigation.tracking
@ -59,4 +53,6 @@ nav:
- Home: index.md - Home: index.md
- Libraries: libraries.md - Libraries: libraries.md
- Standards: standards.md - Standards: standards.md
- Workflow: workflow.md
- Virtual environments: environments.md
- Deploy: deploy.md - Deploy: deploy.md

10
requirements.txt Normal file
View File

@ -0,0 +1,10 @@
# Build dependencies for the handbook docs site.
# Host build: python -m venv .venv && . .venv/bin/activate
# pip install -r requirements.txt && mkdocs build
# Output: static site/ served by the reverse proxy at docs.rethinkstudios.io.
mkdocs-material==9.7.6 # theme — pulls mkdocs, pymdown-extensions, pygments
mkdocs==1.6.1 # pinned explicitly for a reproducible build
mkdocs-material-extensions==1.3.1
pymdown-extensions==11.0 # admonitions, tabbed, superfences, annotations
Pygments==2.20.0 # syntax highlighting in code blocks