Artifact 50bc9cd57e73921242e90c5e833e448c2dfde368bc511e15843d82bf17998927:
- File
docs/SETUP.md
— part of check-in
[5e434ca14e]
at
2026-04-07 19:19:41
on branch trunk
— Dark theme overhaul: buttons, forms, chat, diffs, and page-specific fixes
Comprehensive dark-mode styling pass covering:
- Submenu link-buttons styled as pills with hover/active/focus states - Custom dark-themed checkboxes replacing browser defaults - Submit/action buttons with hover, active, and disabled states - Wiki editor: tab bar, toolbar layout, fieldsets - Chat: centered layout, blue-tinted bubbles, styled input/buttons, mobile - Timeline: fileage/tree-view hover overrides (was bright white) - Diff: subtle green/red ins/del, diffskip rows, SBS marker columns - Annotate: dark overlay on inline pastel backgrounds - Forum: search form layout, post containers, debug panel - Tickets: tktDspValue dark surface instead of light grey - Security audit: capsumRead/Write/Off with dark-appropriate colors - Search form: proper flex layout with text input priority - Sitemap dropdown: multi-column with mobile single-column fallback - Header: logo image from /logo instead of F text mark - span.error/mark: dark-mode yellow highlight treatment - Various Fossil built-in light-theme color overrides (user: dmestas, size: 16196) [annotate] [blame] [check-ins using]
# fossil.craftdesign.group — Full Setup Guide Complete instructions for reproducing this server from scratch on a fresh VPS. **Tested on:** Ubuntu 24.04.3 LTS, aarch64 (ARM64) **Result:** `https://fossil.craftdesign.group` — multi-repo Fossil SCM behind Cloudflare tunnel --- ## Architecture ``` Internet └── Cloudflare (fossil.craftdesign.group) └── cloudflared tunnel (named: fossil) └── Caddy :8090 (reverse proxy + static landing page) ├── GET / → /var/fossil/www/index.html ├── GET /a11y.js → /var/fossil/www/a11y.js └── everything else → Fossil :8081 └── serves /var/fossil/repos/*.fossil ``` **Why this stack:** - **Fossil** handles version control, wikis, tickets, forum, chat — one binary, no dependencies - **Caddy** provides the landing page and absorbs static file serving, keeping Fossil's URL space clean - **Cloudflare tunnel** gives HTTPS without opening any inbound ports on the VPS — no firewall changes, no cert management, works behind NAT - **Port layout:** Coolify/Traefik already owns port 8080, so Fossil uses 8081, Caddy uses 8090 --- ## 1. Prerequisites ```bash sudo apt update && sudo apt upgrade -y sudo apt install -y curl wget unzip build-essential libssl-dev zlib1g-dev ``` --- ## 2. Build Fossil from Source Fossil ships as a single C source amalgamation. The resulting binary has no runtime dependencies. ```bash # Download the source tarball (check https://fossil-scm.org/home/uv/download.html for latest) # The URL contains a hash — get the current link from the download page wget https://fossil-scm.org/home/tarball/<hash>/fossil-src-<version>.tar.gz -P /tmp cd /tmp tar xzf fossil-src-<version>.tar.gz cd fossil-src-<version> # Configure with OpenSSL so HTTPS works for clone/push/pull ./configure --prefix=/usr/local --with-openssl=auto # Build (single-threaded is fine, it's fast) make # Install to /usr/local/bin/fossil sudo make install # Verify fossil version ``` **Build dependencies explained:** - `build-essential` — gcc, make, libc headers - `libssl-dev` — enables TLS for `fossil clone`/`push`/`pull` over HTTPS - `zlib1g-dev` — compression support (bundled in Fossil but system lib is preferred) --- ## 3. Install Caddy Caddy is installed from the official Cloudsmith apt repository. This gives automatic updates via `apt upgrade`. ```bash # Add Caddy's apt repo sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \ | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \ | sudo tee /etc/apt/sources.list.d/caddy-stable.list sudo apt update sudo apt install caddy # The system caddy service will start automatically — stop it # We run our own service with a custom Caddyfile sudo systemctl stop caddy sudo systemctl disable caddy # Install the binary to /usr/local/bin so it runs as our user sudo cp /usr/bin/caddy /usr/local/bin/caddy # Verify caddy version # → v2.9.1 ``` --- ## 4. Install cloudflared cloudflared is installed from Cloudflare's official apt repository. ```bash # Add Cloudflare's apt repo curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \ | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' \ | sudo tee /etc/apt/sources.list.d/cloudflared.list sudo apt update sudo apt install cloudflared # Verify cloudflared version # → cloudflared version 2026.3.0 # Create a symlink so it's on the expected path sudo ln -sf /usr/bin/cloudflared /usr/local/bin/cloudflared ``` --- ## 5. Set Up the Cloudflare Tunnel This creates a named tunnel that Cloudflare routes traffic through. You need a Cloudflare account with `fossil.craftdesign.group` in your DNS. ### 5a. Authenticate ```bash cloudflared tunnel login ``` This opens a browser URL — visit it and authorise cloudflared for your Cloudflare account. It writes a certificate to `~/.cloudflared/cert.pem`. ### 5b. Create the tunnel ```bash cloudflared tunnel create fossil ``` This creates a tunnel named `fossil` and writes a credentials JSON file: ``` ~/.cloudflared/<tunnel-id>.json ``` Note the tunnel ID printed — you'll need it in the config. ### 5c. Route DNS ```bash cloudflared tunnel route dns fossil fossil.craftdesign.group ``` This creates a CNAME in Cloudflare DNS: `fossil.craftdesign.group → <tunnel-id>.cfargotunnel.com`. The record is managed by cloudflared automatically. ### 5d. Write the config file ```bash mkdir -p ~/.cloudflared cat > ~/.cloudflared/config.yml << 'EOF' tunnel: 45ea54d2-1b49-496b-bc35-c01c45d95297 # replace with your tunnel ID credentials-file: /home/dmestas/.cloudflared/45ea54d2-1b49-496b-bc35-c01c45d95297.json ingress: - hostname: fossil.craftdesign.group service: http://localhost:8090 - service: http_status:404 EOF ``` **Ingress rules explained:** - All traffic for `fossil.craftdesign.group` is forwarded to Caddy on port 8090 - The final `http_status:404` catch-all is required by cloudflared — any unmatched hostname returns 404 --- ## 6. Directory Structure ```bash sudo mkdir -p /var/fossil/repos /var/fossil/skin /var/fossil/www sudo chown -R dmestas:dmestas /var/fossil ``` | Path | Purpose | |------|---------| | `/var/fossil/repos/` | All `.fossil` repository files | | `/var/fossil/skin/` | Shared skin source files — pushed to repos via `apply.py` | | `/var/fossil/www/` | Static assets served by Caddy: `index.html`, `a11y.js` | | `/var/fossil/Caddyfile` | Caddy config | --- ## 7. Caddyfile Caddy serves two roles: static files at `/` and `/a11y.js`, and reverse proxy to Fossil for everything else. ``` # /var/fossil/Caddyfile :8090 { @static path / /a11y.js handle @static { file_server { root /var/fossil/www } } reverse_proxy localhost:8081 } ``` **Key points:** - Listens on port 8090 (not 80/443 — TLS is handled by Cloudflare at the edge) - `@static` matcher catches only `/` (the landing page) and `/a11y.js` (accessibility script) - Everything else passes through to Fossil at 8081 - No TLS config needed here — Cloudflare terminates TLS, sends plain HTTP to the tunnel --- ## 8. Systemd Services Three services run as the `dmestas` user. They start in order on boot. ### fossil.service ```bash sudo tee /etc/systemd/system/fossil.service << 'EOF' [Unit] Description=Fossil SCM Server After=network.target [Service] Type=simple User=dmestas ExecStart=/usr/local/bin/fossil server /var/fossil/repos \ --port 8081 \ --localhost \ --repolist \ --https \ --baseurl https://fossil.craftdesign.group \ --notfound / Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOF ``` **Fossil flags explained:** | Flag | Why | |------|-----| | `/var/fossil/repos` | Directory mode — serves all `*.fossil` files in this directory automatically | | `--port 8081` | Local port (8080 is taken by Coolify/Traefik) | | `--localhost` | Binds to 127.0.0.1 only — Fossil is never exposed directly to the internet | | `--repolist` | Enables repo listing at `/` (Caddy overrides this with the custom landing page) | | `--https` | Tells Fossil it's behind an HTTPS proxy — without this, Fossil generates `http://` redirect loops | | `--baseurl` | Forces all generated URLs to use the public domain | | `--notfound /` | Redirects unknown root-level paths back to the repo listing instead of 404 | ### caddy-fossil.service ```bash sudo tee /etc/systemd/system/caddy-fossil.service << 'EOF' [Unit] Description=Caddy reverse proxy for Fossil SCM After=network.target fossil.service Requires=fossil.service [Service] Type=simple User=dmestas ExecStart=/usr/local/bin/caddy run --config /var/fossil/Caddyfile Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOF ``` ### cloudflared-fossil.service ```bash sudo tee /etc/systemd/system/cloudflared-fossil.service << 'EOF' [Unit] Description=Cloudflare Tunnel for Fossil SCM After=network-online.target fossil.service Wants=network-online.target Requires=fossil.service [Service] Type=simple User=dmestas ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate --config /home/dmestas/.cloudflared/config.yml run Restart=always RestartSec=10 [Install] WantedBy=multi-user.target EOF ``` ### Enable and start ```bash sudo systemctl daemon-reload sudo systemctl enable fossil caddy-fossil cloudflared-fossil sudo systemctl start fossil caddy-fossil cloudflared-fossil # Check all three are running sudo systemctl status fossil caddy-fossil cloudflared-fossil ``` --- ## 9. Create the First Repository ```bash # Initialise on the server fossil init /var/fossil/repos/myrepo.fossil # Fossil picks up new .fossil files automatically — no restart needed # Verify it's visible curl -s http://localhost:8081/ | grep myrepo ``` ### Set admin credentials Visit `https://fossil.craftdesign.group/myrepo/setup_ulist` to set the admin password for user `dmestas`. Or do it via the CLI: ```bash fossil user password dmestas <newpassword> -R /var/fossil/repos/myrepo.fossil ``` --- ## 10. Apply the Skin The skin lives in `/var/fossil/skin/` and must be written into each repo's SQLite config table. The `apply.py` script does this for all repos at once. ```bash python3 /var/fossil/skin/apply.py ``` Run this after: - Creating a new repository - Editing any skin file (`css.txt`, `header.txt`, `footer.txt`) ### How it works Fossil stores skin parts (`css`, `header`, `footer`) as rows in the `config` table of each `.fossil` SQLite database. The script writes them directly: ```python import sqlite3, time, os, glob REPOS = glob.glob("/var/fossil/repos/*.fossil") SKIN_DIR = "/var/fossil/skin" parts = { "css": open(f"{SKIN_DIR}/css.txt").read(), "header": open(f"{SKIN_DIR}/header.txt").read(), "footer": open(f"{SKIN_DIR}/footer.txt").read(), } mtime = int(time.time()) for repo in REPOS: con = sqlite3.connect(repo) for name, value in parts.items(): con.execute("INSERT OR REPLACE INTO config(name,value,mtime) VALUES(?,?,?)", (name, value, mtime)) con.commit() con.close() ``` **Note:** Fossil's skin config keys are `css`, `header`, `footer` — no `skin:` prefix. This is different from what Fossil's web UI calls them internally. --- ## 11. The a11y.js Accessibility Layer Fossil's built-in HTML has several accessibility gaps (missing ARIA landmarks, invisible graph lines on dark backgrounds, no keyboard nav on the hamburger menu). These are fixed via a JavaScript file served as a static asset by Caddy. **Why a static asset and not inline script:** Fossil's Content Security Policy is `script-src 'self' 'nonce-<per-request>'`. Inline scripts without the correct nonce are blocked. A file served from the same origin (`/a11y.js`) satisfies `'self'` and loads cleanly. The header template loads it with `defer` so it runs after Fossil's own inline scripts but before `window.load`: ```html <script defer src="/a11y.js"></script> ``` **What it fixes:** - `isDark` flag — Fossil hardcodes `false` for all custom skins; patched post-load so the timeline graph uses correct dark-mode colors - Hamburger menu ARIA — `aria-expanded`, `aria-controls`, focus management, Escape-to-close - Timeline table — `aria-label`, `aria-hidden` on graph/filler cells, accessible time labels - Table headers — `scope="col"` on all sortable tables - Submenu selects — `aria-label` derived from adjacent label text --- ## 12. Shared Login Across Repositories (Login Group) Fossil supports a **login group** — a set of repos that share authentication state. Log in to any one repo and you're automatically authenticated on all others. Each repo still has its own user records, but credentials are synchronised via the group. ### How it works Each `.fossil` file stores two config keys: - `login-group-name` — human-readable group name (e.g. `craftdesign`) - `login-group-code` — shared secret hex code that ties the repos together Plus `peer-repo-<name>` entries pointing to the absolute paths of sibling repos on disk. When you log in, Fossil updates the session cookie for all peers in the group. ### Current setup All three repos are in the `craftdesign` login group: - `/var/fossil/repos/edgesync.fossil` - `/var/fossil/repos/go-libfossil.fossil` - `/var/fossil/repos/fossil-skin.fossil` ### Adding a new repo to the group ```bash fossil login-group join /var/fossil/repos/edgesync.fossil \ -R /var/fossil/repos/<newrepo>.fossil ``` Verify it joined: ```bash fossil login-group -R /var/fossil/repos/<newrepo>.fossil ``` ### Setting up the group from scratch If rebuilding, configure the group via the Python script (direct SQLite — the CLI `join` requires an open checkout): ```python import sqlite3, os, glob, hashlib, time repos = glob.glob("/var/fossil/repos/*.fossil") mtime = int(time.time()) group_code = hashlib.md5(("craftdesign" + str(mtime)).encode()).hexdigest()[:16] for repo in repos: con = sqlite3.connect(repo) con.execute("INSERT OR REPLACE INTO config(name,value,mtime) VALUES('login-group-name','craftdesign',?)", (mtime,)) con.execute("INSERT OR REPLACE INTO config(name,value,mtime) VALUES('login-group-code',?,?)", (group_code, mtime)) for other in repos: if other != repo: key = "peer-repo-" + os.path.basename(other).replace(".fossil", "") con.execute("INSERT OR REPLACE INTO config(name,value,mtime) VALUES(?,?,?)", (key, other, mtime)) con.commit() con.close() ``` --- ## 13. Adding a New Repository (Ongoing) ```bash # 1. Create the repo file fossil init /var/fossil/repos/<name>.fossil # 2. Set admin password fossil user password dmestas <password> -R /var/fossil/repos/<name>.fossil # 3. Apply skin python3 /var/fossil/skin/apply.py # 4. Add a card to the landing page # Edit /var/fossil/www/index.html — copy an existing .repo-card block # 5. Commit the landing page change to the fossil-skin repo cp /var/fossil/www/index.html /path/to/fossil-skin-checkout/www/ cd /path/to/fossil-skin-checkout fossil commit -m "Add <name> repo card" --user dmestas fossil push ``` From a local machine: ```bash fossil clone https://[email protected]/<name>/ <name>.fossil # or push an existing repo: fossil remote add hub https://[email protected]/<name>/ fossil push hub ``` --- ## Service Management Reference ```bash # Status of all three services sudo systemctl status fossil caddy-fossil cloudflared-fossil # Restart all sudo systemctl restart fossil caddy-fossil cloudflared-fossil # Tail logs sudo journalctl -u fossil -f sudo journalctl -u caddy-fossil -f sudo journalctl -u cloudflared-fossil -f # Reload Caddy config without restart (note: reload not supported — use restart) sudo systemctl restart caddy-fossil ``` --- ## Troubleshooting ### Fossil generates `http://` redirect loops Ensure `--https` flag is present on `fossil server`. Without it, Fossil sees a plain HTTP request from Caddy and tries to redirect to HTTPS, creating a loop. ### Skin not showing up after edit `apply.py` must be run after every skin edit — Fossil caches skin in the repo's SQLite config and doesn't read from the filesystem. ### New repo not appearing Fossil in directory mode picks up new `.fossil` files without restart, but it may take a few seconds. If it still doesn't appear, `sudo systemctl restart fossil`. ### Cloudflare tunnel disconnects If credentials expire: `cloudflared tunnel login` and re-run. The credentials JSON in `~/.cloudflared/` is valid for years under normal circumstances. ### Port 8080 conflict This VPS runs Coolify, whose Traefik proxy owns port 8080. Fossil uses 8081, Caddy uses 8090. If you're on a clean VPS without Coolify, you could use 8080/8081 instead — just update the Caddyfile and fossil.service accordingly.