diff --git a/.gitignore b/.gitignore index 3900c97..30c4de8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +# Editor files +.vscode/ +.idea/ +.zed/ + .DS_Store .env *.tfstate @@ -7,9 +12,12 @@ infra/terraform.tfvars infra/**/*.tfvars ansible/inventory/hosts.yml -ansible/group_vars/all/main.yml -ansible/group_vars/all/vault.yml -ansible/group_vars/prod/vault.yml -ansible/group_vars/staging/vault.yml +ansible/inventory/group_vars/all/main.yml +ansible/inventory/group_vars/all/vault.yml +ansible/inventory/group_vars/prod/vault.yml +ansible/inventory/group_vars/staging/vault.yml app/bin/ app/tailwindcss + +# MCP +.playwright-mcp/ diff --git a/AGENTS.md b/AGENTS.md index 65739f6..dd9539d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,114 +1,34 @@ -# AGENTS.md / OpenCode Execution Plan +# Repository Guide -## 1. Executive Summary & Objective -Build a complete, production-ready, highly maintainable infrastructure and web application stack: -1. **Infrastructure:** OpenTofu configs deploying Rocky Linux 10 on Hetzner Cloud, isolated via private networking, configured with S3-compatible Hetzner Object Storage. -2. **App:** Lightweight Go To-Do web app using native `html/template`, xorm,and TailwindCSS, running via rootless Podman / systemd containers. -3. **Database & Backups:** PostgreSQL running on host/container, using `WAL-G` for continuous archiving and hourly base backups to Hetzner Object Storage. -4. **Staging Environment:** Automated daily database sync (6:00 AM and 12:00 PM `America/Chicago`) restoring production S3 state onto a separate Staging instance. -5. **Scale Path:** Pre-wired for future multi-instance app scaling behind a Hetzner Cloud Load Balancer. +## Boundaries ---- +- `app/` is the Go 1.22 module. The server entrypoint is `app/cmd/server/main.go`; handlers own HTTP behavior, `internal/db` owns xorm persistence, and `app/web` embeds templates plus compiled CSS into the binary. +- `infra/` is one OpenTofu root module and one shared state for both production and staging. `infra/environments/*` contain input files, not independent stacks. +- `ansible/playbooks/setup-node.yml` configures both hosts; `deploy-app.yml` pulls and restarts the rootless Podman/Quadlet app. Environment-specific behavior is selected by generated inventory groups and `deployment_environment`. +- Staging restores unchanged production data. Treat staging, backups, logs, real tfvars, Vault files, and state as production-sensitive; inspect examples instead of secret-bearing files. -## 2. Project Directory Structure -Generate the codebase using this strict layout: +## Commands -```text -. -├── infra/ -│ ├── modules/ -│ │ ├── compute/ # OpenTofu module for Rocky Linux instances -│ │ └── storage/ # OpenTofu module for Hetzner S3 bucket -│ ├── environments/ -│ │ ├── prod/ # Production HCL configs -│ │ └── staging/ # Staging HCL configs -│ └── main.tf -├── ansible/ -│ ├── playbooks/ -│ │ ├── setup-node.yml # System setup, Podman, PostgreSQL, WAL-G installation -│ │ └── deploy-app.yml # Pull container & reload systemd unit -│ ├── roles/ -│ └── templates/ -│ ├── wal-g.env.j2 # Hetzner S3 credentials for WAL-G -│ ├── db-sync.service.j2 -│ └── db-sync.timer.j2 # Systemd timer set for America/Chicago -├── app/ -│ ├── cmd/ -│ │ └── server/ -│ │ └── main.go -│ ├── internal/ -│ │ ├── db/ # Postgres connections & queries -│ │ ├── handler/ # HTTP handlers using html/template -│ │ └── model/ # Todo struct definitions -│ ├── web/ -│ │ ├── templates/ # Go HTML templates -│ │ │ ├── layouts/ -│ │ │ └── pages/ -│ │ └── static/ # Compiled Tailwind CSS output -│ ├── Dockerfile # Multi-stage build for Go app + Tailwind CLI -│ ├── tailwind.config.js -│ ├── go.mod -│ └── Makefile -└── README.md +- Run repository checks with `make validate`: Go tests, `go vet`, OpenTofu validation, then both Ansible syntax checks. +- Run one Go test from `app/`: `go test ./internal/handler -run '^TestName$'` or `go test ./internal/db -run '^TestStoreLifecycle$'`. +- `internal/db` integration tests silently skip unless `TEST_DATABASE_URL` points to PostgreSQL. Normal `make test` needs no database. +- Format Go and OpenTofu files with `make format`; it runs `gofmt` and recursive `tofu fmt` (including real ignored tfvars), but does not format YAML or templates. +- Build the embedded app with `make build-app`. This first runs Tailwind and writes `app/web/static/app.css`; `make css` performs only that asset step. +- Build the deployment image with `docker build -t todo:local app`. Its CSS stage is self-contained and does not use the local `app/tailwindcss` binary. -3. Component & Technical Specifications -A. Infrastructure (OpenTofu) -Use the official hetznercloud/hcloud provider. +## Generated And Runtime Behavior -Provision an hcloud_network (private subnet 10.0.1.0/24) and attach both Production and Staging instances. +- Edit Tailwind source in `app/web/input.css` and templates under `app/web/templates/`; regenerate the tracked `app/web/static/app.css` with `make css` after class or style changes. +- The standalone Tailwind binary is downloaded at pinned version `v3.4.17` to ignored `app/tailwindcss`. +- The server requires `DATABASE_URL`, defaults `APP_ADDR` to `:8080`, and runs xorm `Sync2` for the `todos` table during startup. Keep `xorm.io/xorm` paired with `github.com/lib/pq` in this stack. +- `make generate-inventory` reads applied OpenTofu output and overwrites ignored `ansible/inventory/hosts.yml`; rerun it after server replacement or address changes. -Configure hcloud_primary_ip for static public IPv4/IPv6 assignment. +## Infrastructure And Deployment -Include structural outputs for an hcloud_load_balancer to easily attach future app instances. - -B. Application (Golang + TailwindCSS) -Standard library net/http router (or lightweight chi). - -UI: Native html/template rendering Todo List operations (Create, Read, Toggle Complete, Delete). - -Styling: Standalone TailwindCSS CLI integrated into a multi-stage Dockerfile (build CSS step -> Go build step -> minimal Scratch/Distroless image). - -Database Driver: jackc/pgx/v5 targeting local PostgreSQL. - -Service Management: App runs containerized via Podman and configured as a systemd user service (app-todo.service). - -C. Database & Continuous Backups (Production) -Install PostgreSQL on the Rocky Linux instance. - -Configure WAL streaming and continuous archiving using WAL-G. - -Systemd service executes wal-g backup-push hourly, pushing binary base backups and WAL logs to Hetzner Object Storage (S3 API). - -D. Automated Staging Restore Pipeline -Create a systemd timer on the Staging server configured explicitly for America/Chicago timezone: - -db-sync.timer: - -Ini, TOML -[Unit] -Description=Run DB Restore from S3 Hourly/Daily Snapshot - -[Timer] -OnCalendar=*-*-* 06,12:00:00 America/Chicago -Persistent=true - -[Install] -WantedBy=timers.target -db-sync.service Execution Steps: - -systemctl stop app-todo - -Execute wal-g backup-fetch LATEST onto PostgreSQL staging instance - -Apply any necessary staging sanitization scripts - -systemctl start app-todo - -4. Execution Rules for OpenCode -Idiomatic Code: Write standard, strictly formatted Go code (gofmt). - -Full Implementation: Do not use placeholder code (// TODO: implement later) inside Go handlers or Ansible templates. Generate complete, runnable files. - -No External Framework Bloat: Keep dependencies minimal (xorm for database, standard library for templates/routing). - -Shell Script Verification: Provide a Makefile in the root directory automation tasks: make build-app, make infra-plan, make deploy-ansible. \ No newline at end of file +- OpenTofu commands must use both var files against the root stack, for example `tofu -chdir=infra plan -var-file=environments/prod/prod.tfvars -var-file=environments/staging/staging.tfvars`. The staging file only supplies staging-specific overrides. +- Prefer `TF_VAR_hcloud_token`, `TF_VAR_s3_access_key`, and `TF_VAR_s3_secret_key`; values in `*.tfvars` override those environment variables. +- Keep the AWS provider on `~> 5.69.0`: newer releases wait for an AWS-only response header absent from Hetzner Object Storage. +- Run Ansible from `ansible/`, as the Make targets do, or set `ANSIBLE_CONFIG=ansible/ansible.cfg`; otherwise the repository role path is not loaded. +- `make deploy-ansible` is an operational deployment, not a verification command. It regenerates inventory, runs host setup before app deployment, requires decryptable Vault files, and acts on both production and staging. +- The Quadlet uses `Pull=never`; `deploy-app.yml` intentionally pulls `app_image` before restarting `app-todo.service`. +- Production WAL-G archives under a prefix ending in `/prod`. Staging's scheduled `db-sync` destructively replaces `/var/lib/pgsql/data` from `LATEST` and leaves the app stopped on failure; do not weaken its path/prefix guards or use it as a routine test. diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg index 5762f63..2c04a37 100644 --- a/ansible/ansible.cfg +++ b/ansible/ansible.cfg @@ -1,4 +1,5 @@ [defaults] +nocows = 1 roles_path = roles interpreter_python = auto_silent host_key_checking = True diff --git a/ansible/group_vars/all/main.yml.example b/ansible/inventory/group_vars/all/main.yml.example similarity index 100% rename from ansible/group_vars/all/main.yml.example rename to ansible/inventory/group_vars/all/main.yml.example diff --git a/ansible/group_vars/prod/vault.yml.example b/ansible/inventory/group_vars/prod/vault.yml.example similarity index 100% rename from ansible/group_vars/prod/vault.yml.example rename to ansible/inventory/group_vars/prod/vault.yml.example diff --git a/ansible/group_vars/staging/vault.yml.example b/ansible/inventory/group_vars/staging/vault.yml.example similarity index 100% rename from ansible/group_vars/staging/vault.yml.example rename to ansible/inventory/group_vars/staging/vault.yml.example