Commit Graph
26 Commits
Author SHA1 Message Date
clawbot 075044f67d Empty the add target form on Cancel; encoding failures stay a 500
check / check (push) Waiting to run
The form's reason and values now come from the targetForm component,
loaded from the section's data attributes after a refusal and emptied
by Cancel, which also resets the form. Each type's fields used to be
recreated with the refused values written into the markup, so they
came back after Cancel. The browser test checks this after a refusal.

A target configuration that cannot be encoded is again a logged 500
with the generic error page, on the add and the edit path; only
refusals of submitted values come back on the form.

The README paragraph on the browser test is re-wrapped at 80 columns
and names Cancel at the type step.

Model: opus-5-5
2026-10-02 18:36:08 +00:00
clawbot f9aba94ef9 Targets section: one Add, then a type and Next, then its fields (closes #370)
The webhook page's targets section lists only its targets until Add is
clicked. Add shows a choice of target type with Next; Next shows only
that type's fields, with Save and Cancel. The `database` and `log`
types have no URL field, and the `slack` form gains max retries.

A refused target now shows the webhook page again with the form open on
its type, the values entered and the reason, instead of a bare text
page. Target validation returns that message rather than writing the
response; newTarget validates a whole new target for reuse by the
new-webhook page. The edit page still answers a refusal in plain text.

Model: opus-5-5
2026-10-02 18:36:08 +00:00
clawbot 8cf5acaf1d Justify the handlers tests' start limit with a measurement (closes #225)
check / check (push) Successful in 3m57s
The internal/handlers tests were load-fragile because every test app hashed the admin password at 64 MB; that went with the cheaper test hashing already on next, and measuring under the host's real load found nothing left to fix in how the tests run. The comment on newTestApp now says its start limit, fx's default, is there to catch a start that hangs, and that the slowest measured start is far inside it. The header of script/test gives current figures in place of ones from before that change. Neither limit changes, and no test changes.

Model: opus-5-5
2026-10-02 14:08:54 +02:00
clawbot fd036774f9 Name each database target's archive for its webhook and target (closes #376)
check / check (push) Successful in 3m29s
Each database target now writes its own archive file, archive-WEBHOOKNAME-TARGETNAME-TARGETID.db, named by delivery.ArchiveFileName, in place of one archive per webhook keyed on its UUID. Renaming a webhook or a target renames its archive files (with any -wal and -shm) before the new name is saved, never over an existing file, and moves every one back if a rename or the save fails. The webhook edit, the target edit and target creation share one lock so no two interleave. Deleting a webhook or target leaves its files on disk. Nothing looks for the old archive-WEBHOOKID.db files. The README gives the naming and the recovery steps.

Model: opus-5-5
2026-10-02 13:28:49 +02:00
clawbot 287e47df7f Add a read-only Settings page for the loaded configuration (closes #402)
check / check (push) Successful in 3m19s
A new page at /settings, linked from the navigation bar and behind the login, lists every configuration field the server loaded at startup: its environment variable, the README table's description, and the value in effect. METRICS_PASSWORD and SENTRY_DSN show only as set or not set; their values are replaced before rendering and never reach the template. The route is GET only and its group is built like the other admin page groups. Tests set the credentials one at a time so each value shown is checked against its own field and a set secret never appears in the page.

Model: opus-5-5
2026-10-02 12:30:21 +02:00
clawbot 2bb4683512 Discard fx's own log in tests that build an fx app (closes #230)
check / check (push) Successful in 3m19s
Every test that builds an fx app with fxtest.New (handlers, server, resetpw, gormlog, config) now passes fx.NopLogger, so fx's own log no longer goes to t.Logf. A hook still running after a start or stop timeout can then no longer write to a test that has already returned, which the race detector reported as a data race. What the tests assert is unchanged, and nothing about the race detector is suppressed.

Model: opus-5-5
2026-10-02 11:08:38 +02:00
clawbot eb4c4cc849 Serve /metrics from a registry of its own (closes #227)
check / check (push) Successful in 3m25s
A second metrics-enabled router in one process panicked on a duplicate collector registration, because every collector registered on Prometheus's global default registry. metrics.NewRegistry now builds one registry with the Go runtime and process collectors; fx provides it and the delivery metric set built on it. The middleware builds its HTTP recorder once on that registry (NewForTest on a fresh one), the engine and handlers take the metric set from fx, and nothing registers on the global default any more.

/metrics is served from the new registry with the same series names, labels and auth. A test builds two metrics-enabled routers in one process.

Model: opus-5-5
2026-10-02 09:06:20 +02:00
clawbot 2416528b77 Render admin page errors in the normal layout (closes #382)
check / check (push) Successful in 3m12s
Every 400, 403, 404 and 500 on an admin page now answers with an error page in the normal layout: the status, one fixed line explaining it, and a link back to the webhook list, or to sign-in when nobody is signed in. Unknown paths reach it through the router's not-found handler, a refused form token through the CSRF middleware, and a panic through a recoverer each admin page route group installs first. Status codes are unchanged, and the page always sends Cache-Control: no-store.

If the error page fails to render, the answer is the same status in plain text; if it panics, the answer is a 500. The receiver, the healthcheck and /metrics keep their plain answers.

Model: opus-5-5
2026-10-02 08:15:12 +02:00
clawbot 43ed8d4834 Move webhook pages to /hook/ID and inbound URLs to /h/UUID (closes #367)
check / check (push) Successful in 3m10s
The webhook page and everything under it move from /source/ID to /hook/ID; the list and new-webhook form from /sources to /hooks and /hooks/new; the event log from .../logs to /hook/ID/events; entrypoint URLs, the ones senders post to, from /webhook/UUID to /h/UUID. The old paths are simply gone, per the owner's ruling. Every link, redirect, form action, test, comment and README mention follows; every link to the event log, and its title and heading, read "Full Event Log".

Keeping the UUID out of logs, metrics labels and error reports, and the receiver rate limits, key on the route itself, so they cover /h/ unchanged. Routed tests follow every link and form the pages render to a moved page.

Model: opus-5-5
2026-10-02 06:32:14 +02:00
clawbot 03cd1859d7 Add an egress CIDR allowlist to the SSRF guard (closes #204) (#217)
check / check (push) Successful in 3m1s
2026-08-20 10:34:42 +02:00
clawbot 3b0ed826bc Add per-delivery replay to the event log (closes #203) (#240)
check / check (push) Successful in 3m21s
There was no redelivery path anywhere: once a delivery exhausted
max_retries it was failed permanently, even though the event body is
durably stored. Storing an event and being unable to re-send it defeats
the reason it is stored, and the ordinary case is a destination that was
down longer than the backoff ladder.

Adds POST /source/{sourceID}/deliveries/{deliveryID}/replay, inside the
authenticated group so it inherits MaxBodySize, CSRF, NoCache and
RequireAuth. Replay creates a NEW pending delivery against the target's
CURRENT config and hands it to the engine through the same notifier the
receiver uses, so it runs the normal path with the retry ladder, the
SSRF-guarded transport and the circuit breaker. The original delivery's
rows are never touched, and the stored event body is re-sent, never the
recorded response.

Replay is refused, with a distinct message, for a non-terminal delivery, a
deleted target, a deactivated target, and when an earlier replay of the
same event and target is still in flight. Bounded by a per-client rate
limit and by that in-flight check.

The new delivery row is written with Omit(clause.Associations) and with
neither Event nor Target populated, so it cannot upsert a targets row into
the per-webhook event database (#206).

Counted by webhooker_delivery_replays_total on the existing target_type
label. A replay also moves the ordinary attempt, outcome and duration
series, because it is a real delivery.
2026-08-20 08:11:35 +02:00
clawbot 977fe87588 Verify login credentials before spending rate-limit budget (closes #150)
check / check (push) Successful in 2m46s
In the shipped default, any stranger denied the operator the only
administrative path at 5 requests per minute: TRUSTED_PROXIES is empty,
the README requires a reverse proxy, so every login POST shared one
bucket keyed on the proxy.

Credentials are now verified first and only a FAILED attempt spends
budget, so a correct password is never throttled. Failures are counted
per (client bucket, submitted username), bounded. Concurrent Argon2id
verifications are capped at two, and the queue for them at 16 — because
verifying first lets an attacker force a 64 MB hash per request, and
bounding the wait alone bounds nothing.

The issue's own recommendation was insufficient and is rejected here:
keying by username stops an attacker locking out a DIFFERENT account,
but this is a single-admin product with a predictable bootstrap
username, so flooding the operator's own name still locks them out.

This is speculative — it implements a corrected recommendation ahead of
the owner's ruling so the decision can be made by merging or reverting.
Three things are disclosed rather than glossed: online guessing rises
from 5/min to roughly 27/s, because the 429 is a label on the response
and not a gate in front of the hash; the residual exposure is a loss of
login AVAILABILITY, not latency, and a determined flood still denies
login while it runs, at ~400x the cost and clearing the moment it
stops; and the endpoint should be provisioned for ~400 MB resident, not
the 203 MB of live commitment it itemises.

Independently reviewed four times. Reviewers disproved the suspected
FIFO starvation by measurement, then caught two successive memory
bounds the code did not have — the second by parking waiters and
reading the heap rather than checking the arithmetic.
2026-08-18 01:55:41 +02:00
clawbot 0b457ea713 Render templates via a buffer, not the ResponseWriter (closes #123)
check / check (push) Superseded by a newer commit; never tested
2026-08-14 06:18:22 +02:00
clawbot 3e261d2f01 Evict archive writers on deletion and sweep idle archives (closes #89) (#95)
check / check (push) Superseded by a newer commit; never tested
Per-webhook archive writers are now evicted when the webhook or its last
database target is deleted, and a background sweeper prunes expired rows from
idle archives that no longer receive writes. Archive files themselves are never
deleted.
2026-08-10 15:52:20 +02:00
clawbotandsneak ee7c626071 Implement the database archiving target (closes #43) (#84)
check / check (push) Successful in 4s
Implements the `databaseTarget` as a real archiving target, replacing the always-successful stub. Delivering to a `database` target now writes the full event into a per-webhook archive SQLite file for long-term storage.

## Archive-writer semantics

- **Separate file:** each webhook's full events are written as rows into `archive-{webhookID}.db` under the data dir, distinct from the per-webhook event DB (`events-{webhookID}.db`). The file and its schema are created on first write if missing. Each row carries the full event: body, headers, method, content type, webhook id, entrypoint id, event id, and an archived-at timestamp.
- **Close/reopen with debounce:** after each write the archive handle is closed and reopened, unless the last (re)open was less than one second ago. This lets an operator move the archive file away for offline archiving while bounding file churn under load. A per-webhook `archiveWriter` owns this debounce state and serialises writes.
- **Auto-recreate:** the file is opened create-if-missing (`mode=rwc`) and its schema re-migrated on every open, so if the archive was moved or removed since the last open, the next write recreates it. The writer also detects a missing file before writing and reopens first, so a moved-away file is recreated rather than lost.
- **Optional expiry, validated at creation:** an optional `expiry` in the target's config JSON (e.g. `{"expiry":"720h"}`) is validated when the target is created (`ValidateArchiveExpiry`; bad values are rejected with a 400 at the add-target form, the Slack URL precedent). The default (missing, empty, or `"never"`) keeps rows forever with no pruning. When a positive duration is set, rows older than it (measured from each row's archived-at time) are pruned on every (re)open; because the file is reopened after writes, prune-on-open keeps the archive swept without a separate background sweeper. A set-but-invalid expiry in a stored config (unparseable, zero, or negative) is an error at delivery time too — never a silent default.
- **No-retry, fail-loud:** the target performs a single attempt with no retries. On success it records one successful attempt and marks the delivery delivered. If the archive write fails, the attempt is recorded as failed with the error and the delivery is marked failed — archiving errors never report success.

## Scope

- `internal/delivery/target_database.go` — the `databaseTarget` (no-retry) archives via a per-webhook writer registry; an archive error records a failed attempt and marks the delivery failed.
- `internal/delivery/target_database_archive.go` (new) — the `archiveWriter`, the archived-row model, config/expiry parsing (fail-loud on set-but-invalid values), `ValidateArchiveExpiry`, and prune-on-open.
- `internal/handlers/source_management.go` — database targets get a creation-validated `expiry` config (`buildDatabaseTargetConfig`); the expiry form value is read where the request body is bounded and bad values are rejected with a 400 at target creation.
- `templates/source_detail.html` — the add-target form shows an expiry field for database targets.
- `README.md` — the database-target documentation describes the archiving semantics.
- `internal/delivery/export_test.go`, `internal/delivery/target_database_test.go`, `internal/handlers` tests — tests and their exported shims.

No changes to the `Target` interface or other targets.

## Tests

- a row is archived (both at the writer level and end-to-end through `Deliver`)
- a forced archive failure (bad stored expiry config) yields a `Failed` delivery with a non-success `DeliveryResult` carrying the error and no archive file created
- the file is recreated after removal, with only the post-removal row
- the one-second reopen debounce (rapid writes reopen once; a write after the window reopens again)
- expiry pruning removes rows older than the configured expiry
- expiry config parsing (empty / `never` / duration accepted; unparseable, zero, and negative values error)
- expiry validation at target creation (`TestValidateArchiveExpiry`; valid values build the config, bad values get a 400)

## Validation

`docker build .` exits 0 (fmt-check, lint, test, build all pass).

Closes #43

Co-authored-by: sneak <sneak@sneak.berlin>
Reviewed-on: #84
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-08-07 22:50:08 +02:00
clawbotandsneak 752d6beead Validate Slack target URLs at creation time (closes #68) (#73)
check / check (push) Successful in 4s
Slack delivery targets were only checked by the request-time dialer guard, not at creation, giving them a weaker SSRF gate than HTTP targets.

This validates the Slack incoming-webhook URL with `delivery.ValidateTargetURL` in the Slack target creation path (`buildSlackTargetConfig`), before persisting, mirroring the existing HTTP-target path. On failure the create is rejected with the same clear, non-leaking user-facing error the HTTP path uses.

Adds handlers-package tests covering both an accepted public URL and a rejected private/reserved URL. Confined to `internal/handlers/`; `internal/delivery/` is unchanged.

Closes #68

Co-authored-by: sneak <sneak@sneak.berlin>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #73
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-08-07 14:03:56 +02:00
clawbotandclawbot afe88c601a refactor: use pinned golangci-lint Docker image for linting (#55)
check / check (push) Successful in 5s
Closes [issue #50](#50)

## Summary

Refactors the Dockerfile to use a separate lint stage with a pinned golangci-lint Docker image, following the pattern used by [sneak/pixa](https://git.eeqj.de/sneak/pixa). This replaces the previous approach of installing golangci-lint via curl in the builder stage.

## Changes

### Dockerfile
- **New `lint` stage** using `golangci/golangci-lint:v2.11.3` (Debian-based, pinned by sha256 digest) as a separate build stage
- **Builder stage** depends on lint via `COPY --from=lint /src/go.sum /dev/null` — build won't proceed unless linting passes
- **Go bumped** from 1.24 to 1.26.1 (`golang:1.26.1-bookworm`, pinned by sha256)
- **golangci-lint bumped** from v1.64.8 to v2.11.3
- All three Docker images (golangci-lint, golang, alpine) pinned by sha256 digest
- Debian-based golangci-lint image used (not Alpine) because mattn/go-sqlite3 CGO does not compile on musl (off64_t)

### Linter Config (.golangci.yml)
- Migrated from v1 to v2 format (`version: "2"` added)
- Removed linters no longer available in v2: `gofmt` (handled by `make fmt-check`), `gosimple` (merged into `staticcheck`), `typecheck` (always-on in v2)
- Same set of linters enabled — no rules weakened

### Code Fixes (all lint issues from v2 upgrade)
- Added package comments to all packages
- Added doc comments to all exported types, functions, and methods
- Fixed unchecked errors flagged by `errcheck` (sqlDB.Close, os.Setenv in tests, resp.Body.Close, fmt.Fprint)
- Fixed unused parameters flagged by `revive` (renamed to `_`)
- Fixed `gosec` G120 warnings: added `http.MaxBytesReader` before `r.ParseForm()` calls
- Fixed `staticcheck` QF1012: replaced `WriteString(fmt.Sprintf(...))` with `fmt.Fprintf`
- Fixed `staticcheck` QF1003: converted if/else chain to tagged switch
- Renamed `DeliveryTask` → `Task` to avoid package stutter (`delivery.Task` instead of `delivery.DeliveryTask`)
- Renamed shadowed builtin `max` parameter to `upperBound` in `cryptoRandInt`
- Used `t.Setenv` instead of `os.Setenv` in tests (auto-restores)

### README.md
- Updated version requirements: Go 1.26+, golangci-lint v2.11+
- Updated Dockerfile description in project structure

## Verification

`docker build .` passes cleanly — formatting check, linting, all tests, and build all succeed.

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #55
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-25 02:16:38 +01:00
33e2140a5a feat: redirect root path based on auth state (#52)
check / check (push) Successful in 1m54s
Closes #51

The root path `/` now checks for an authenticated session and redirects accordingly:

- **Authenticated users** → `303 See Other` redirect to `/sources` (the webhook dashboard)
- **Unauthenticated users** → `303 See Other` redirect to `/pages/login`

### Changes

- **`internal/handlers/index.go`** — Replaced the template-rendering `HandleIndex()` with a session-checking redirect handler. Removed `formatUptime` helper (dead code after this change).
- **`internal/handlers/handlers.go`** — Removed `index.html` from the template map (no longer rendered).
- **`internal/handlers/handlers_test.go`** — Replaced the old "handler is not nil" test with two proper redirect tests:
  - `unauthenticated redirects to login` — verifies 303 to `/pages/login`
  - `authenticated redirects to sources` — sets up an authenticated session cookie, verifies 303 to `/sources`
  - Removed `TestFormatUptime` (tested dead code).
- **`README.md`** — Updated the API endpoints table to describe the new redirect behavior.

### How it works

The handler calls `session.Get(r)` and `session.IsAuthenticated(sess)` — the same pattern used by the `RequireAuth` middleware and `HandleLoginPage`. No new dependencies or session logic introduced.

The login flow is unaffected: `HandleLoginSubmit` redirects to `/` after successful login, which now forwards to `/sources` (one extra redirect hop, but correct and clean).

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Co-authored-by: clawbot <clawbot@eeqj.de>
Reviewed-on: #52
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-17 16:35:08 +01:00
clawbot 4dd4dfa5eb chore: consolidate DBURL into DATA_DIR, codebase audit for 1.0.0
check / check (push) Successful in 56s
DBURL → DATA_DIR consolidation:
- Remove DBURL env var entirely; main DB now lives at {DATA_DIR}/webhooker.db
- database.go constructs DB path from config.DataDir, ensures dir exists
- Update DATA_DIR prod default from /data/events to /data
- Update all tests to use DataDir instead of DBURL
- Update Dockerfile: /data (not /data/events) for all SQLite databases
- Update README configuration table, Docker examples, architecture docs

Dead code removal:
- Remove unused IndexResponse struct (handlers/index.go)
- Remove unused TemplateData struct (handlers/handlers.go)

Stale comment cleanup:
- Remove TODO in server.go (DB cleanup handled by fx lifecycle)
- Fix nolint:golint → nolint:revive on ServerParams for consistency
- Clean up verbose middleware/routing comments in routes.go
- Fix TODO fan-out description (worker pool, not goroutine-per-target)

.gitignore fixes:
- Add data/ directory to gitignore
- Remove stale config.yaml entry (env-only config since rework)
2026-03-01 23:33:20 -08:00
clawbot 32bd40b313 refactor: self-contained delivery tasks — engine delivers without DB reads in happy path
check / check (push) Successful in 58s
The webhook handler now builds DeliveryTask structs carrying all target
config and event data inline (for bodies ≤16KB) and sends them through
the delivery channel. In the happy path, the engine delivers without
reading from any database — it only writes to record delivery results.

For large bodies (≥16KB), Body is nil and the engine fetches it from the
per-webhook database on demand. Retry timers also carry the full
DeliveryTask, so retries avoid unnecessary DB reads.

The database is used for crash recovery only: on startup the engine scans
for interrupted pending/retrying deliveries and re-queues them.

Implements owner feedback from issue #15:
> the message in the <=16KB case should have everything it needs to do
> its delivery. it shouldn't touch the db until it has a success or
> failure to record.
2026-03-01 22:09:41 -08:00
clawbot 9b9ee1718a refactor: auto-generate session key and store in database
check / check (push) Successful in 57s
Remove SESSION_KEY env var requirement. On first startup, a
cryptographically secure 32-byte key is generated and stored in a new
settings table. Subsequent startups load the key from the database.

- Add Setting model (key-value table) for application config
- Add Database.GetOrCreateSessionKey() method
- Session manager initializes in OnStart after database is connected
- Remove DevSessionKey constant and SESSION_KEY env var handling
- Remove prod validation requiring SESSION_KEY
- Update README: config table, Docker instructions, security notes
- Update config.yaml.example
- Update all tests to remove SessionKey references

Addresses owner feedback on issue #15.
2026-03-01 21:57:19 -08:00
clawbot 5e683af2a4 refactor: event-driven delivery engine with channel notifications and timer-based retries
check / check (push) Successful in 58s
Replace the polling-based delivery engine with a fully event-driven
architecture using Go channels and goroutines:

- Webhook handler notifies engine via buffered channel after creating
  delivery records, with inline event data for payloads < 16KB
- Large payloads (>= 16KB) use pointer semantics (Body *string = nil)
  and are fetched from DB on demand, keeping channel memory bounded
- Failed retry-target deliveries schedule Go timers with exponential
  backoff; timers fire into a separate retry channel when ready
- On startup, engine scans DB once to recover interrupted deliveries
  (pending processed immediately, retrying get timers for remaining
  backoff)
- DB stores delivery status for crash recovery only, not for
  inter-component communication during normal operation
- delivery.Notifier interface decouples handlers from engine; fx wires
  *Engine as Notifier

No more periodic polling. No more wasted cycles when idle.
2026-03-01 21:46:16 -08:00
clawbot 43c22a9e9a feat: implement per-webhook event databases
check / check (push) Successful in 1m50s
Split data storage into main application DB (config only) and
per-webhook event databases (one SQLite file per webhook).

Architecture changes:
- New WebhookDBManager component manages per-webhook DB lifecycle
  (create, open, cache, delete) with lazy connection pooling via sync.Map
- Main DB (DBURL) stores only config: Users, Webhooks, Entrypoints,
  Targets, APIKeys
- Per-webhook DBs (DATA_DIR) store Events, Deliveries, DeliveryResults
  in files named events-{webhook_uuid}.db
- New DATA_DIR env var (default: ./data dev, /data/events prod)

Behavioral changes:
- Webhook creation creates per-webhook DB file
- Webhook deletion hard-deletes per-webhook DB file (config soft-deleted)
- Event ingestion writes to per-webhook DB, not main DB
- Delivery engine polls all per-webhook DBs for pending deliveries
- Database target type marks delivery as immediately successful (events
  are already in the dedicated per-webhook DB)
- Event log UI reads from per-webhook DBs with targets from main DB
- Existing webhooks without DB files get them created lazily

Removed:
- ArchivedEvent model (was a half-measure, replaced by per-webhook DBs)
- Event/Delivery/DeliveryResult removed from main DB migrations

Added:
- Comprehensive tests for WebhookDBManager (create, delete, lazy
  creation, delivery workflow, multiple webhooks, close all)
- Dockerfile creates /data/events directory

README updates:
- Per-webhook event databases documented as implemented (was Phase 2)
- DATA_DIR added to configuration table
- Docker instructions updated with data volume mount
- Data model diagram updated
- TODO updated (database separation moved to completed)

Closes #15
2026-03-01 17:06:43 -08:00
clawbot d4eef6bd6a refactor: use go:embed for templates
Templates are now embedded using //go:embed and parsed once at startup
with template.Must(template.ParseFS(...)). This avoids re-parsing
template files from disk on every request and removes the dependency
on template files being present at runtime.

closes #7
2026-03-01 16:01:44 -08:00
clawbotandclawbot f9a9569015 feat: bring repo up to REPO_POLICIES standards (#6)
check / check (push) Successful in 8s
## Summary

This PR brings the webhooker repo into full REPO_POLICIES compliance, addressing both [issue #1](#1) and [issue #2](#2).

## Changes

### New files
- **`cmd/webhooker/main.go`** — The missing application entry point. Uses Uber fx to wire together all internal packages (config, database, logger, server, handlers, middleware, healthcheck, globals, session). Minimal glue code.
- **`REPO_POLICIES.md`** — Fetched from authoritative source (`sneak/prompts`)
- **`.editorconfig`** — Fetched from authoritative source
- **`.dockerignore`** — Sensible Go project exclusions
- **`.gitea/workflows/check.yml`** — CI workflow that runs `docker build .` on push to any branch (Gitea Actions format, actions/checkout pinned by sha256)
- **`configs/config.yaml.example`** — Moved from root `config.yaml`

### Modified files
- **`Makefile`** — Complete rewrite with all REPO_POLICIES required targets: `test`, `lint`, `fmt`, `fmt-check`, `check`, `build`, `hooks`, `docker`, `clean`, plus `dev`, `run`, `deps`
- **`Dockerfile`** — Complete rewrite:
  - Builder: `golang:1.24` (Debian-based, pinned by `sha256:d2d2bc1c84f7...`). Debian needed because `gorm.io/driver/sqlite` pulls `mattn/go-sqlite3` (CGO) which fails on Alpine musl.
  - golangci-lint v1.64.8 installed from GitHub release archive with sha256 verification (v1.x because `.golangci.yml` uses v1 config format)
  - Runs `make check` (fmt-check + lint + test + build) as build step
  - Final stage: `alpine:3.21` (pinned by `sha256:c3f8e73fdb79...`) with non-root user, healthcheck, port 8080
- **`README.md`** — Rewritten with all required REPO_POLICIES sections: description line with name/purpose/category/license/author, Getting Started, Rationale, Design, TODO (integrated from TODO.md), License, Author
- **`.gitignore`** — Fixed `webhooker` pattern to `/webhooker` (was blocking `cmd/webhooker/`), added `config.yaml` to prevent committing runtime config with secrets
- **`static/static.go`** — Removed `vendor` from embed directive (directory was empty/missing)
- **`internal/database/database_test.go`** — Fixed to use in-memory config via `afero.MemMapFs` instead of depending on `config.yaml` on disk. Test is now properly isolated.
- **`go.mod`/`go.sum`** — `go mod tidy`

### Removed files
- **`TODO.md`** — Content integrated into README.md TODO section
- **`config.yaml`** — Moved to `configs/config.yaml.example`

## Verification
- `docker build .` passes (lint ✅, test ✅, build ✅)
- All existing tests pass with no modifications to assertions or test logic
- `.golangci.yml` untouched

closes #1
closes #2

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #6
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 19:01:44 +01:00
sneak 1244f3e2d5 initial 2026-03-01 22:52:08 +07:00