Adam French a116ec2614
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 24s
Add Programming CV and revise General CV layout and content
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:58:56 +01:00
2026-01-25 20:14:17 +00:00
2026-02-18 21:45:17 +00:00
2026-02-18 21:45:17 +00:00

My Web

screenshot

Welcome to the source code for my website! Please contact me if you would like to collaborate and thank you for visiting.

This website is self-hosted on my Raspberry Pi. Any interference or hax and the killswitch will activate and stop the UK national grid power system so please don't tamper with my domain :).

The use of AI

This has been created with a heavy amount of AI. Initially, I began with articles from medium informing me of how to use Nginx to host static sites and reverse proxy to other services. They were incredible helpful and I wish I were able to give credit them, but sadly they were read too long ago.

After hearing all the hype on LinkedIn and feeling the pressure to keep up a high output without AI, I eventually caved in. There is an immense advantage not having to scan though documentation to find relevant functions, asking how other developers implement their infrastructure and finding what command will achieve your outcome quickly. Sure, it would be good to have that already cached in your own human memory. But my reasoning is that you will have cached it after being reminded by AI enough times, similar to how flashcards work. I may not be an expert in the specific tool, though I've been able to get good enough at it for my own purposes.

Architecture

All services run in Docker containers orchestrated by Docker Compose behind Nginx as a reverse proxy on a single bridge network.

vue              ── Frontend build (outputs dist to shared volume)
nginx (80, 443)  ── Frontend SPA + Reverse Proxy
backend (8080)   ── Go API (GraphQL + REST)
db (5432)        ── PostgreSQL 16
icecast2 (8000)  ── Audio Streaming (Icecast2 + Liquidsoap)
gitea (3000)     ── Self-Hosted Git
quartz (8080)    ── Obsidian Notes Publisher (Quartz v4.4.0)
searxng (8080)   ── Meta Search Engine
hasura (8080)    ── Hasura GraphQL Engine (Docker profile: hasura)
autoheal         ── Auto-restart unhealthy containers
certbot          ── SSL Certificate Management (disabled in dev)

Tech Stack

Frontend - Vue 3, Vite, Tailwind CSS v4, Pinia, Vue Router, markdown-it (wikilinks + KaTeX), Rust/WASM

Backend - Go (Gin), gqlgen (GraphQL), GORM, PostgreSQL, JWT auth, WebSockets

Integrations - Spotify API, Steam API, Anthropic Claude API, Icecast2

Infrastructure - Docker Compose, Nginx, Let's Encrypt (Certbot), Gitea + Act Runner

Features

  • Spotify integration (currently playing, recently played)
  • Steam integration (online status, recent games)
  • Obsidian note viewer via Quartz
  • Live radio streaming via Icecast2 + Liquidsoap
  • Real-time chat over WebSockets with image/video uploads
  • Blog with admin panel (CRUD)
  • Activity and rowing session tracking
  • AI image processing integration to extract rowing data from images (will be superseeded by openCV scanning)
  • Fan shrines (GTO, Evangelion, Demoman, Skip Skip Benben)
  • Self-hosted Git (Gitea) with CI/CD and commit feed on homepage
  • Printable CV with role-specific sections
  • Job application tracker with status workflow and CSV export (admin-only, /cv/jobs)
  • Database-backed bookmarks grouped by category, managed via GraphQL (admin-only)
  • SearXNG meta search engine (admin-only)
  • Hasura GraphQL console (admin-only)
  • Admin-gated routes: /searxng, /notes, /hasura require admin JWT via Nginx auth_request
  • Landing page with animated stamps section
  • Route transitions (slide/fade) and performance optimizations (gzip, WOFF2 fonts, lazy loading)
  • Backend healthcheck with autoheal container for automatic recovery

Pages

Route Description
/ Landing page
/stp Home dashboard with grid layout
/admin Admin login + panel (authenticated)
/cv Curriculum Vitae (printable)
/cv/jobs Job application tracker (admin-only, hidden print)
/bookmarks Bookmarks (database-backed, grouped by category)
/notes/:path Obsidian note viewer (via Quartz, admin-only)
/shrines Fan shrine index + individual shrines

API

GraphQL

Endpoint: POST /api/graphql Playground: GET /api/graphql (when GQL_PLAYGROUND=true)

Operation Type Description
users Query Get all users
user(id) Query Get user by ID
me Query Get authenticated user
posts Query Get all posts
post(id) Query Get post by ID
activities Query Get all activities
favorites Query Get all favorites
rowingSessions Query Get all rowing sessions
post(id) Query Get post by ID
activities Query Get all activities
favorites Query Get all favorites
rowingSessions Query Get all rowing sessions
messages Query Get all messages
spotifyListening Query Currently playing track
spotifyRecent Query Recently played tracks
giteaFeed Query Latest Gitea activity
steamStatus Query Steam online status
login Mutation Authenticate user
logout Mutation Logout
refreshToken Mutation Refresh auth token
createPost / updatePost / deletePost Mutation Post CRUD (admin)
createUser / deleteUser Mutation User management (admin)
setUserAdmin Mutation Toggle admin status
createFavorite Mutation Add favorite (admin)
createActivity Mutation Add activity (admin)
bookmarks Query Get all bookmarks
createBookmark / deleteBookmark Mutation Bookmark CRUD (admin)
jobApplications Query Get all job applications (admin)
createJobApplication / updateJobApplication / deleteJobApplication Mutation Job application CRUD (admin)

REST Endpoints

Auth

Method Path Description
POST /api/auth/login Login (rate limited: 5/min)
POST /api/auth/refresh Refresh token
GET /api/auth/check Check token validity
POST /api/auth/logout Logout
GET /api/auth/validate-admin Validate admin JWT (used by Nginx auth_request)

Access tokens are valid for 7 days; refresh tokens for 365 days. ValidateAdmin also refreshes the access token if it is within 24 hours of expiry.

Spotify

Method Path Description
GET /api/spotify/callback OAuth callback
GET /api/spotify/listening Currently playing
GET /api/spotify/recent Recently played

Public

Method Path Description
GET /api/favorites Get all favorites
GET /api/rowing Get all rowing sessions
GET /api/activity Get all activities
GET /api/posts Get all posts
GET /api/posts/:id Get post by ID
GET /api/user Get all users
GET /api/user/:id Get user by ID

Protected (auth required)

Method Path Description
POST /api/messages/upload Upload message file (rate limited: 5/min)

Admin (auth + admin required)

Method Path Description
POST /api/favorites Create favorite
POST /api/rowing Create rowing session
POST /api/activity Create activity
POST /api/posts Create post
PUT /api/posts/:id Update post
DELETE /api/posts/:id Delete post
POST /api/user Create user
PUT /api/user/:id Update user
DELETE /api/user/:id Delete user
PATCH /api/user/:id/admin Set/unset admin
POST /api/radio/upload Upload radio song
GET /api/radio/songs List radio songs
DELETE /api/radio/songs/:filename Delete radio song
PATCH /api/radio/songs/:filename/disable Disable radio song
PATCH /api/radio/songs/:filename/enable Enable radio song

WebSocket

Method Path Description
GET /api/ws WebSocket chat (10s ping keepalive)

Nginx Proxy Routes

Route Target Notes
/api backend:8080 API (rate limited: 30r/s)
/radio icecast:8000 Audio streaming
/gitea gitea:3000 Git service
/hasura hasura:8080 GraphQL console + WebSocket (admin-only)
/notes quartz:8080 Obsidian notes (admin-only)
/searxng searxng:8080 Search engine (admin-only)
/uploads local alias User-uploaded files

/hasura, /notes, and /searxng are protected by auth_request to GET /api/auth/validate-admin. Requests without a valid admin JWT are rejected with 401.

Deprecated Endpoints

Backend note API (GET /api/notes/*path) - The backend has a REST endpoint that serves note files directly from the mounted Obsidian vault. This is superseded by the Quartz service which now handles note rendering at /notes/. The backend endpoint still exists in code but is no longer the primary note serving path.

Setup

.env

Create a .env file in the project root. All services read from this file.

Variable Description
POSTGRES_USER PostgreSQL username
POSTGRES_PASSWORD PostgreSQL password
POSTGRES_DB Main app database name
POSTGRES_PORT PostgreSQL port (typically 5432)
POSTGRES_HOST PostgreSQL hostname (use db for Docker)
GITEA_HOST Gitea hostname (use gitea for Docker)
GITEA_PORT Gitea HTTP port (typically 3000)
GITEA_INTERNAL_TOKEN Gitea internal API token (generate with gitea generate secret INTERNAL_TOKEN)
GITEA_LFS_JWT_SECRET Gitea LFS JWT secret (generate with gitea generate secret LFS_JWT_SECRET)
GITEA_OAUTH2_JWT_SECRET Gitea OAuth2 JWT secret (generate with gitea generate secret JWT_SECRET)
POSTGRES_GITEA_DB Gitea database name
UPTIMEKUMA_HOST Uptime Kuma hostname (planned)
UPTIMEKUMA_PORT Uptime Kuma port (planned)
SEARXNG_HOST SearXNG hostname (use searxng for Docker)
SEARXNG_PORT SearXNG port (typically 8080)
SEARXNG_SECRET_KEY SearXNG secret key (random hex string)
WALLABAG_HOST Wallabag hostname (planned)
WALLABAG_PORT Wallabag port (planned)
QUARTZ_HOST Quartz hostname (use quartz for Docker)
QUARTZ_PORT Quartz port (typically 8080)
GITEA_RUNNER_HOST Gitea runner hostname
GITEA_RUNNER_NAME Gitea runner display name
GITEA_RUNNER_REGISTRATION_TOKEN Token to register Gitea Actions runner
BACKEND_PORT Backend port (typically 8080)
BACKEND_HOST Backend hostname (use backend for Docker)
BACKEND_SECRET JWT signing secret
BACKEND_ENDPOINT API path prefix (typically /api)
OBSIDIAN_DIR Absolute path to Obsidian vault on host machine
SPOTIFY_CLIENT_ID Spotify app client ID
SPOTIFY_CLIENT_SECRET Spotify app client secret
SPOTIFY_REDIRECT_URI Spotify OAuth redirect (e.g. https://www.<DOMAIN>/api/spotify/callback)
SPOTIFY_AUTH_STATE Arbitrary state string for Spotify OAuth
ICECAST_SOURCE_PASSWORD Icecast source connection password
ICECAST_RELAY_PASSWORD Icecast relay password
ICECAST_ADMIN_USER Icecast admin username
ICECAST_ADMIN_PASSWORD Icecast admin password
ICECAST_HOST Icecast hostname (use icecast for Docker)
ICECAST_PORT Icecast port (typically 8000)
ICECAST_MOUNT Icecast mount point (e.g. /stream)
LIQUIDSOAP_HARBOR_MOUNT Liquidsoap live input mount (e.g. /live)
LIQUIDSOAP_HARBOR_PORT Liquidsoap harbor port (e.g. 8005)
DOMAIN Production domain name
EMAIL Email for Let's Encrypt registration
CLAUDE_API_KEY Anthropic Claude API key
STEAM_API_KEY Steam Web API key
STEAM_ID Steam user ID
HASURA_GRAPHQL_ADMIN_SECRET Hasura admin secret
HASURA_HOST Hasura hostname (use hasura for Docker)
HASURA_PORT Hasura port (typically 8080)
SEED_DB Set to true to seed test data on startup

Gitea Config

Copy from the template and fill in secrets:

cp gitea/config/app.ini.template gitea/config/app.ini

Populate LFS_JWT_SECRET, SECRET_KEY, INTERNAL_TOKEN, JWT_SECRET, and the database PASSWD. Alternatively, the Gitea entrypoint generates app.ini from the template using environment variables.

SearXNG Config

Copy from the template:

cp searxng/settings.yml.template searxng/settings.yml

The Docker entrypoint handles environment variable substitution (${BASE_URL}, ${SEARXNG_SECRET_KEY}) automatically, so manual setup is only needed when running outside Docker.

Spotify Token Setup

  1. After authorization, Spotify redirects to the callback endpoint which stores tokens at /backend/token/spotify_token.json
  2. Tokens are refreshed automatically; the file persists across container restarts via volume mount

Obsidian Notes Setup

  1. Set OBSIDIAN_DIR in .env to the absolute path of your Obsidian vault on the host machine
  2. The vault is mounted read-only into the Quartz container at /quartz/content
  3. Quartz builds a static site from the vault on startup and serves it at /notes/
  4. The backend also mounts the vault at /backend/notes (legacy, see deprecated endpoints above)

SSL Certificates (Certbot)

Initial setup (production):

  1. Set DOMAIN and EMAIL in .env
  2. On first run, Nginx starts with nginx_setup.conf.template which only serves the ACME challenge route at /.well-known/acme-challenge/
  3. Certbot requests a certificate via certbot certonly --webroot
  4. Once the certificate is issued to certbot/conf/live/<DOMAIN>/, restart Nginx — it will detect the certs and switch to the full nginx.conf.template
  5. Certbot checks for renewal every 12 hours automatically

Dev mode: Nginx generates a self-signed certificate for localhost automatically.

Icecast Radio

Place at least one .mp3 file in icecast2/fallback_music/. Liquidsoap plays these as fallback when no live source is connected. Connect a live source to port 8001 on the LIQUIDSOAP_HARBOR_MOUNT.

Gitea Runner

  1. Download the act_runner binary from Gitea releases and place in gitea-runner/
  2. Set GITEA_RUNNER_REGISTRATION_TOKEN in .env
  3. The runner registers automatically on first startup

Dev Mode

Run the full stack over plain HTTP without SSL certificates:

docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build

This:

  • Uses an HTTP-only Nginx config with all routing (SPA, backend proxy, radio, gitea, etc.)
  • Generates a self-signed certificate for localhost
  • Disables certbot
  • Seeds the database with test data (SEED_DB=true)
  • Enables GraphQL playground and introspection
  • Enables Hasura console and dev mode

Visit http://localhost to test.

Frontend only (hot reload)

cd vue && npm run dev

Vite dev server proxies /api to localhost:8080, /gitea to localhost:3000, /radio to localhost:8000.

Untracked Files

These files are git-ignored and must be created manually:

File Notes
.env See setup section above
gitea/config/app.ini Copy from app.ini.template or let entrypoint generate it
searxng/settings.yml Copy from settings.yml.template or let entrypoint generate it
certbot/conf/, certbot/www/ Created automatically by certbot; use dev mode to skip
backend/token/ Created automatically by Docker volume mount
icecast2/fallback_music/*.mp3 Place at least one MP3 file
gitea-runner/act_runner Download from Gitea releases
gitea-runner/.runner Generated on first runner startup

Future Ideas

  • More Rust to WASM
  • ML for chatboards
  • Cache requests
  • Design more webpages
  • Calendar to show radio times
  • Nice smooth function background and transitions
  • Design shrines
  • Redis (not really but practical experience)
Description
A web server hosted locally on my rasberry pi
Readme 228 MiB
Languages
Vue 57.3%
Go 24.9%
JavaScript 8.5%
Rust 3.4%
CSS 3%
Other 2.9%