Commit Graph

14 Commits

Author SHA1 Message Date
xarmian 756d91acad fix(ci): gofmt + bump race-detector timeout to 30m (#299)
CI on main has been failing since the PLAN-866 attachment work
landed. Two independent issues:

1. gofmt failures (golangci-lint) — seven files in the attachments
   path had trailing-comment alignment that gofmt wanted nudged a
   column. Pure whitespace; ran `gofmt -w` across the affected
   files. golangci-lint's gofmt linter caught it on every PR /
   push since TASK-870 but we hadn't been watching those signals.

   Files cleaned: internal/attachments/{fs_store_test,mime,
   mime_test,processor_test}.go, internal/server/{
   handlers_attachments_download_test,handlers_attachments_transform,
   render/attachments_test}.go.

   Local guard: `gofmt -l ./...` now exits clean.

2. Race-detector tests timed out at 20m on the GitHub-hosted runner.
   Two contributors:
     - PostgreSQL adds latency on every CREATE/DROP plus on the
       bcrypt hash inside auth/bootstrap (~3s per call under -race
       on the runner). Tests that bootstrap a fresh user (e.g.
       TestSessionIPChange_*) pay the full cost each time.
     - The PLAN-866 image-processing tests (thumbnail derivation,
       rotate / crop transform) added ~2-3 minutes of decode/encode
       work on top of the existing suite.

   The previous "20m gives margin without papering over a hang"
   comment was right at the time it was written; we now genuinely
   need more headroom. Bumped to 30m on both the SQLite and
   PostgreSQL race steps. Genuine deadlocks would still trip this
   and produce the goroutine-dump panic — we just stop confusing
   "slow but progressing" with "permanently hung".

   Reference points before / after:
     - TASK-875 main run #294: Go (PostgreSQL) finished in 17m48s ✓
     - TASK-880 main run #298: Go (PostgreSQL) hit 20m timeout ✗
     - Local: my new tests under -race add ~63s on a developer laptop
       (TestThumbnails + TestTransform + TestProcessor combined).

Verification:
  go test ./...             — pass
  go vet ./...              — clean
  gofmt -l (recursively)    — clean
2026-04-29 15:41:12 -04:00
xarmian 715ec70e94 fix(server): drain ipRateLimiter cleanup goroutines on Stop() (BUG-851) (#276)
NewRateLimiters spawned 9 ipRateLimiter cleanup goroutines per Server,
each in an unbounded `for { time.Sleep(5*time.Minute); ... }` loop with
no exit signal (middleware_ratelimit.go:78-89). Every testServer(t)
call leaked all 9, accumulating across the 210-test internal/server
suite. Under -race the goroutine count + sync overhead pushed the run
past the default 10m timeout, which is why the `Run tests with race
detector` step (gated to main pushes) has been failing on every main
run since the step was added on 2026-04-13.

This is the same flavor as BUG-842 part 2 (request-handler
fire-and-forget goroutines drained via Server.bg WaitGroup). The
rate-limiter case wasn't in BUG-842's scope: those goroutines are
spawned at construction time, not at request time, so they need a
different drain primitive.

Changes:

  - ipRateLimiter gains stopCh + stopOnce + stopWg. cleanup() rewrites
    its loop as a select over stopCh and a 5-minute ticker, deferring
    stopWg.Done(). New Stop() closes stopCh once and waits for the
    cleanup goroutine to return.
  - RateLimiters gains a Stop() that walks all 9 limiters (nil-safe
    via the (*ipRateLimiter).Stop receiver guard).
  - Server.Stop() now also calls s.rateLimiters.Stop() after
    s.bg.Wait(). Test cleanups already call Server.Stop() (added in
    BUG-842), so no test-helper changes needed.
  - New TestServer_Stop_DrainsRateLimiterCleanup pins the contract:
    construct + Stop N servers, assert runtime.NumGoroutine() returns
    to baseline ±3.
  - .github/workflows/ci.yml: bump the -race timeout from the default
    10m to 20m. The full server suite under -race takes ~13m on a dev
    laptop after the leak fix; 20m gives margin without papering over
    an actual hang. Both `Run tests with race detector` (SQLite) and
    `Run tests with race detector against PostgreSQL` are bumped.

Verified locally: go test -race -timeout=1500s ./internal/server/
finishes ok in 776s (12m57s). Without the leak fix, the same command
times out at 600s (10m) with a goroutine dump showing hundreds of
ipRateLimiter.cleanup frames.
2026-04-28 17:20:03 -04:00
xarmian e5e2bd7b86 chore: flip CI only-new-issues=false + scope lint policy (TASK-771) (#253)
* chore: gate CI on full lint, scoped to checks we enforce (TASK-771)

Flip golangci-lint-action's only-new-issues from true to false so CI
fails on ANY linter finding, not just findings on PR-changed lines.
This catches lint regressions on the next push instead of letting them
drift into main.

The gate flip is paired with a deliberate scope-down of .golangci.yml:

1. errcheck is disabled. The codebase has 325 pre-existing unchecked-
   error sites where the error is intentionally discarded (best-effort
   logging writes, defensive parses with zero-valued fallbacks, etc.).
   Auditing every site is its own project — bigger than IDEA-732 by
   an order of magnitude. Tracked as a follow-up if/when we want the
   safety net back.

2. staticcheck is restricted to the SA* check family (real-bug
   detectors). The ST*/QF*/S* families are stylistic/quick-fix
   suggestions we don't gate CI on yet — they would have re-flooded
   the lint output with capitalized error strings, De Morgan's law
   simplification suggestions, etc., that aren't bug-finding signals.
   Re-enable selectively if the team wants them.

After scoping, the live linters are: govet, ineffassign, staticcheck
(SA*), unused, gofmt — exactly the set that IDEA-732 cleaned up.

Other changes in this PR:

- Drop pull-requests:read permission. It was only required by the
  golangci-lint-action when only-new-issues=true (the action used it
  to fetch PR diff metadata). Not needed any more.
- Update the Run-golangci-lint comment block to explain the new
  policy and reference the IDEA-732 cleanup PRs (#247/#249/#251/#252).
- Replace the SA4017 //lint:ignore directive in cmd/pad/main.go:4631
  with an inline //nolint:staticcheck — the multi-line //lint:ignore
  block was too far from the if statement for staticcheck's
  proximity rule, so the directive wasn't taking effect.
- Apply gofmt -w on three files where post-deletion blank-line
  artifacts had drifted (cmd/pad/main.go imports, two trailing newline
  fix-ups in handlers_items.go and middleware_ratelimit.go).

Verified:
- `golangci-lint run ./...` reports 0 issues.
- `go build ./...` clean.
- `go vet ./...` clean.
- `go test ./...` all pass.

Parent: PLAN-644.

* chore: address Codex round 1 on PR #253 (TASK-771)

Two LOWs from Codex on the gate-flip PR:

1. //nolint:staticcheck was broader than necessary (suppressed any
   future staticcheck diagnostic on the line) and didn't self-report
   when the underlying false positive gets fixed upstream. Codex
   suggested swapping back to a tightly-placed //lint:ignore SA4017.

   I tried that, but golangci-lint v2's staticcheck integration does
   not honour //lint:ignore the way direct staticcheck does — the
   directive was silently no-op'd via golangci-lint while the same
   directive worked when staticcheck was invoked directly.

   So instead of fighting the linter wrapper, sidestep the false
   positive entirely: rewrite the keepalive check from
   `strings.HasPrefix(line, ":")` to `len(line) > 0 && line[0] == ':'`.
   Same observable behaviour for a single-byte ASCII prefix, no
   suppression directive needed at all, no exposure when staticcheck
   eventually fixes the false positive.

2. The new lint-step comment in ci.yml said main is "clean of
   staticcheck SA*/U1000" — but U1000 is reported by the standalone
   `unused` linter in .golangci.yml, not by staticcheck.checks.
   Tighten the comment to attribute each enforced check correctly.

Verified:
- `golangci-lint run ./...` reports 0 issues
- `go test ./cmd/pad/...` passes (the SSE watch loop is exercised by
  reconcile_test.go and the broader integration tests).
2026-04-25 12:36:05 -04:00
xarmian 157ca4e88f chore: bump Go toolchain to 1.26 (TASK-763) (#247)
* chore: bump Go toolchain to 1.26 (TASK-763)

Bump Go from 1.25 to 1.26 across all toolchain pins:

- go.mod — go 1.25.0 → go 1.26.0
- Dockerfile — golang:1.25-alpine → golang:1.26-alpine
- .github/workflows/ci.yml — three setup-go steps (Go, Go-Postgres, E2E jobs)
- .github/workflows/release.yml — release pipeline

No `toolchain` directive: the repo is pre-launch with no external
contributors yet, so we set the floor where we want it (hard requirement).

Verified locally before commit:
- golangci-lint v2.11.4 builds and runs under Go 1.26.2 (pinned in CI)
- golang:1.26-alpine and 1.26.2-alpine images present on Docker Hub
- go build ./... clean
- go vet ./... clean
- go test ./... all pass

Parent: PLAN-644 (OSS Repo Hygiene and Launch Polish).

* chore: gofmt -w under Go 1.26 (TASK-763)

Apply Go 1.26's gofmt to the codebase. ~41 files reformatted, all
struct-tag whitespace realignment — no semantic changes. Verified:

- gofmt -l ./cmd ./internal returns empty after
- go build ./... still clean
- go test ./... still passes (run before commit)

Bundling the gofmt diff with the toolchain bump in the same PR because
the formatting drift is a direct consequence of moving from 1.25 to
1.26; splitting them creates a mandatory two-PR ordering for no value.

Parent: PLAN-644.

* docs: bump documented Go floor to 1.26 (TASK-763)

Match go.mod's hard 1.26.0 requirement in the source-build instructions.
Caught by Codex review round 1 on PR #247.

- README.md:158 — "Go 1.25+" → "Go 1.26+"
- CONTRIBUTING.md:9 — "Go 1.25+" → "Go 1.26+"
2026-04-25 11:35:19 -04:00
xarmian c8601a2031 test(e2e): Playwright smoke test infrastructure + 2 dashboard tests (TASK-689) (#225)
* test(e2e): Playwright smoke test infrastructure + 2 dashboard tests (TASK-689)

Option A of TASK-689: land the test infrastructure and a minimal smoke
test on both mobile and desktop viewports. Broader flow coverage (board
view drag, item detail, comments, mobile hamburger, BottomSheet
regression guard) is tracked as TASK-733.

Infrastructure
--------------
- web/playwright.config.ts: two projects (desktop-chromium, mobile-
  chromium via Pixel 7), reporter list+html, trace/video/screenshot
  retained on failure, webServer that wipes + recreates the data dir
  then runs the pad binary. Paths anchored to the config file's
  directory so runs are cwd-invariant.
- web/e2e/global-setup.ts: bootstraps admin via POST /auth/bootstrap,
  logs in, creates the e2e workspace, mints a user-scoped API token,
  and persists the token + resolved admin username to fixture.json.
- web/e2e/fixtures.ts: extends base test so every BrowserContext
  automatically gets Authorization: Bearer <token>. Uses a token
  rather than a session cookie because sessions are User-Agent bound
  in middleware_auth.go and a node-minted session would be rejected
  by a Chromium UA.

Tests
-----
- web/e2e/dashboard.spec.ts: a logged-in user lands on the seeded
  workspace, no login form is rendered, and the workspace name
  appears on the page. Runs in both project viewports.

CI
--
- New `e2e` job in .github/workflows/ci.yml: builds web UI + binary,
  installs Playwright chromium with OS deps, runs the suite, and
  uploads the HTML report as an artifact on failure. Timeout capped
  at 10 minutes (suite itself runs in ~4s today).

Local run (in mcr.microsoft.com/playwright:v1.59.1-noble): 2 passed
in 4.1s.

Parent: PLAN-644.
Follow-up: TASK-733 for broader flow coverage (Option B in the
original ship plan).

* fix(e2e): persist server-returned workspace slug instead of the constant (TASK-689)

Addresses Codex P2 on PR #225. When Playwright's `reuseExistingServer:
true` (local dev), a re-run of globalSetup hits `POST /api/v1/workspaces`
against a DB that already has `e2e`. The server uniquifies the slug
(`e2e` → `e2e-2` → …) and returns the uniquified value, but the old
code wrote `WORKSPACE_SLUG` (the constant) to fixture.json. Tests
then navigated to /e2e-admin/e2e — which might still exist from a
previous run with stale state — instead of /e2e-admin/e2e-2, missing
regressions in freshly-seeded content.

Fix: read `slug` back from the workspace-create response and use that
when writing fixture.json. Local re-runs now always point at the
workspace this run actually created.

Parent: PLAN-644.

* fix(e2e): cross-platform webServer bootstrap via Node wrapper (TASK-689)

Addresses Codex P2 on PR #225: `rm -rf && mkdir -p && pad server start`
in webServer.command is POSIX-only. Windows contributors on cmd.exe or
PowerShell can't run `npm run test:e2e` at all — the e2e suite becomes
Linux/macOS-only, defeating the "CI parity" goal.

Fix: extract the wipe-and-exec logic into web/e2e/run-pad.mjs. Node's
fs.rmSync / mkdirSync / child_process.spawn are uniform across
platforms, and the wrapper forwards SIGTERM/SIGINT so Playwright's
teardown still cleanly kills the child on suite exit.

Local re-run in mcr.microsoft.com/playwright:v1.59.1-noble: 2 passed.

Parent: PLAN-644.
2026-04-22 22:35:08 -04:00
xarmian 04db0c1a67 chore: add .editorconfig, golangci-lint, pre-commit (TASK-682) (#220)
* chore: add .editorconfig, golangci-lint, pre-commit (TASK-682)

Prevents style churn from first-time external contributors by
codifying the project's formatting and lint rules into shared config.

Changes:
- .editorconfig: tabs for code, 2-space for YAML/JSON/Markdown, LF
  everywhere, UTF-8; Makefile overrides enforce tab (syntactic).
- .golangci.yml: enables gofmt, govet, errcheck, ineffassign,
  staticcheck, unused. Scoped to Go sources; excludes web/, docs/,
  deploy/, skills/.
- .pre-commit-config.yaml: repo-local hygiene (trailing whitespace,
  EOF, YAML/JSON checks, merge-conflict markers, 500KB file cap,
  LF line endings), Go formatting (go-fmt, go-imports), and
  prettier for YAML/JSON/Markdown only (Svelte intentionally
  excluded — no Svelte prettier config yet).
- CI: wires golangci-lint into .github/workflows/ci.yml via the
  official pinned action (v6.5.2 → SHA 55c2c144...). Uses
  only-new-issues: true so this PR is not blocked by the 17
  pre-existing findings on main, which are tracked as IDEA-732
  and will flip to strict enforcement after they're resolved.

Verified:
- golangci-lint run with this config locally; config parses and
  --new-from-rev=HEAD is clean
- All YAML parses via yq
- go build/vet/test + web build all green

Parent: PLAN-644.
Follow-up: IDEA-732 (fix legacy lint findings, flip to strict mode).

* fix(ci): upgrade golangci-lint to v2 for Go 1.25 support (TASK-682)

Per Codex review on PR #220: golangci-lint v1.x (including v1.64.8
which I originally pinned) is capped at Go 1.24 support. Running v1
binaries against Go 1.25 source can silently drop/misreport findings,
defeating the purpose of a lint gate.

Switch to:
- Action: golangci/golangci-lint-action@v9.2.0 (pinned SHA 1e7e51e7...)
- Lint version: v2.11.4 (latest stable v2)
- Config rewritten to v2 YAML format (version: "2", linters.default,
  formatters section for gofmt, exclusions.paths)

Verified:
- `golangci-lint config verify` clean
- `golangci-lint run --new-from-rev=HEAD` reports 0 issues on the
  current diff (still well under the safety cap since we're using
  only-new-issues: true)
- Full run against main surfaces the expected legacy findings, which
  remain tracked in IDEA-732

Parent: PLAN-644.

* fix(ci): anchor golangci-lint exclusion paths + add PR read perm (TASK-682)

Addresses two follow-up comments from Codex on PR #220:

P1 — golangci-lint v2 uses regex path matching for exclusions, so the
bare patterns "web" and "skills" would also match unrelated Go files
whose path contains those substrings (e.g. "internal/websocket").
Anchor with a leading "^" and trailing "/" so only the intended
directory trees are skipped.

P2 — The workflow already grants "contents: read" at top level, but
golangci-lint-action with only-new-issues: true also fetches PR diff
metadata from the GitHub API; the action docs list "pull-requests:
read" as required for that path. Add it explicitly so the action
doesn't fall back to scanning all code and defeating the purpose of
scoped issue reporting.

Parent: PLAN-644.
2026-04-22 20:44:52 -04:00
xarmian 1052be7282 security(ci): pin all GitHub Actions to commit SHAs (TASK-677) (#201)
Every third-party Action in .github/workflows/ was using a floating
tag (@v4, @v5, @v6). A compromised maintainer — or a tag that gets
re-pointed at a malicious commit — could execute attacker code in CI
with contents:write, packages:write, and the GHCR token in scope.
Release.yml is especially exposed: a compromised step there could
publish tampered binaries to GitHub Releases and GHCR.

All 12 'uses:' references now pin to a 40-char commit SHA with a
trailing '# vX.Y.Z' comment (the comment is what humans read during
review; the SHA is what GitHub actually resolves):

  actions/checkout@34e114876b  # v4.3.1
  actions/setup-go@40f1582b24  # v5.6.0
  actions/setup-node@49933ea528 # v4.4.0
  docker/setup-buildx-action@8d2750c68a # v3.12.0
  docker/login-action@c94ce9fb46       # v3.7.0
  goreleaser/goreleaser-action@e435ccd777 # v6.4.0

Version bumps: pin to the newest release within the same major that
was previously in use (so behavior stays the same — no major version
jumps hidden inside a security PR). Dependabot (incoming in TASK-678)
will track the commit-pinned refs and open PRs that update both the
SHA and the version comment together.

Parent: PLAN-644.
2026-04-22 15:27:37 -04:00
xarmian 4f298db4d0 docs(readme): add 'Hardening for public deployments' section + govulncheck CI gate (PLAN-643 exit) (#195)
* docs(readme): add 'Hardening for public deployments' + govulncheck CI gate (PLAN-643 exit criteria)

Closes the last two exit criteria of PLAN-643 (OSS Security Hardening):

- README.md gains a full "Hardening for public deployments" section
  walking operators through the network boundary (bind addr, TLS,
  trusted proxies), secrets (PAD_ENCRYPTION_KEY, token scopes,
  bootstrap window), auth hardening (PAD_IP_CHANGE_ENFORCE, password
  strength UI messaging, PAD_CORS_ORIGINS), observability (PAD_METRICS_
  TOKEN, audit-log shipping), and a deploy-day checklist. Cross-
  references every relevant env var documented elsewhere.
- CI workflow gains a govulncheck step on the Go job, mirroring the
  existing `npm audit --audit-level=high --omit=dev` gate on the web
  job. Locally `govulncheck ./...` reports "No vulnerabilities found",
  so the first run on main should pass.

Exit criteria for PLAN-643:
  [x] All CRITICAL + HIGH + MEDIUM findings closed and verified
  [x] `npm audit --audit-level=high --production` clean in web/
  [x] CSP denies inline event handlers (script-src-attr 'none')
  [x] Docker default compose publishes to 127.0.0.1 only
  [x] README has a "Hardening for public deployments" section
  [x] `govulncheck ./...` clean

* fix(ci): pin govulncheck to v1.2.0 instead of @latest (PLAN-643)

Addresses Codex P2 on PR #195: tracking @latest on every CI run makes
the gate non-deterministic — a future upstream release could change
behavior or require a newer Go toolchain than the workflow's pinned
`go-version: 1.25` and break unrelated PRs. Pin to the currently-
released v1.2.0 (Go 1.26.2 toolchain) and update intentionally.

* docs(readme): recommend pinned govulncheck install in hardening section

Follows Codex P2 on PR #195: the earlier commit pinned the CI workflow
to v1.2.0 but the README's hardening-checklist bullet still told
operators to install @latest. Teams copying that into their own CI
would re-introduce the non-determinism the pin was meant to fix.
Update the docs to recommend a pinned tag (matching the workflow's
v1.2.0) and note that the pin should be bumped intentionally.
2026-04-22 13:46:32 -04:00
xarmian 3544e42de1 chore(web): npm audit fix + CI audit gate (TASK-654) (#181)
web/package-lock.json had 11 advisories (1 low, 1 moderate, 9 high)
before this PR: @sveltejs/kit (redirect/body-size), cookie<0.7.0,
dompurify<=3.3.3, vite 7.0.0-7.3.1, lodash-es, picomatch, chevrotain,
etc. All required a mix of `npm audit fix` and targeted upgrades.

Changes:
- web/package.json: upgrade @sveltejs/kit to ^2.57.1. Add `overrides`
  map pinning cookie to ^0.7.2 (upstream @sveltejs/kit@2.57.1 still
  ships cookie@0.6.0 which is LOW severity but trivially fixable).
- web/package-lock.json: regenerated via `npm install` + `npm audit fix`.
- .github/workflows/ci.yml: add `npm audit --audit-level=high --omit=dev`
  step after `npm ci`. Fails the build on any HIGH+ advisory in
  production deps; dev-only issues stay informational so CI isn't held
  hostage by unfixable upstream chevrotain/vite dev-server advisories.

`npm audit --audit-level=high --omit=dev` now reports 0 vulnerabilities
locally. `npm run build` and `go test ./...` remain green.

Parent: PLAN-643 (OSS Security Hardening).
2026-04-21 21:37:16 -04:00
xarmian b2b4feecb9 feat: console navigation, PostgreSQL CI, and operational improvements
- Route root (/) to /console for centralized workspace management
- Update TopBar user dropdown with console nav links (workspaces, settings, billing, admin)
- Move account settings (profile, password, tokens) from workspace settings to /console/settings
- Enhance admin page with email configuration UI and CSRF-protected writes
- Add PostgreSQL CI job to GitHub Actions with race detector on main
- Add `make test-pg` for local PostgreSQL testing via docker-compose
- Expand health/ready endpoint with DB connection pool stats
- Increase item number retry limit for high-concurrency environments
- Add concurrent store benchmarks and FTS search quality tests
- Add AGENTS.md for multi-agent development guidance
2026-04-13 01:29:15 +00:00
xarmian 239ce19d1f ci: reduce CI usage by ~60-70% with optimized workflow
- Add concurrency groups to cancel superseded in-progress runs
- Move race detector to main-only (saves ~9.5 min per PR run)
- Merge go-vet into go job (eliminates separate VM)
- Remove redundant full-build job (release.yml handles real builds)
- Add binary build+verify as steps in go job for smoke testing
2026-04-09 00:27:55 +00:00
xarmian 14fe7fd631 Fix CI: create web build placeholder for Go embed in test and vet jobs
The go:embed directive requires web/build/* to exist. Go test and vet jobs
run without building the web UI, so create a minimal placeholder directory
to satisfy the embed pattern.
2026-03-27 03:35:55 +00:00
xarmian 7ba69abb88 Misc improvements: CLI field summaries, editor enhancements, CI and UI polish
Show field summary after create/update CLI commands. Make svelte-check
blocking in CI. Improve editor block handling, field editor layout,
conventions page, and minor UI consistency fixes across pages.
2026-03-27 01:13:07 +00:00
xarmian 81579847c6 Initial release
Pad — project management for developers and AI agents.
Single Go binary with embedded SvelteKit web UI, SQLite storage,
CLI, and Claude Code /pad skill integration.

https://getpad.dev
2026-03-26 01:52:36 +00:00