Commit Graph

177 Commits

Author SHA1 Message Date
98eae40755 fix(discovery): defer rate-limited buckets + polish queue table
Rate limits (Mistral web_search 429) used to get counted as hard errors,
marking the bucket as queried and bumping the Errors(24h) strip — even
though the right behavior is to wait and try again later.

Backend:
- isRateLimit() matches "rate limit" / "status 429" in the error string.
- On persistent rate-limit after one 10s retry: leave last_queried_at
  unchanged (bucket stays eligible for next tick) and abort the
  remainder of this tick — Mistral's web_search budget is shared, no
  point hammering more buckets in the same batch.
- TickSummary gains rate_limited counter; Errors stays for real failures.

Frontend:
- Dates: RFC3339 → 'DD.MM.YYYY' German format, range rendered as
  'DD.MM.YYYY – DD.MM.YYYY'.
- Queue table: cell horizontal padding, uppercase compact headers,
  scrollable on narrow viewports, dark-mode variants on every color
  (emerald/amber badges, link color, reject button), Region folds
  bundesland||land into a single column (Land was always 'Deutschland'
  for DACH anyway).
2026-04-18 09:21:05 +02:00
e4ef4adad6 Merge branch 'fix/discovery-json-tags' into 'main'
fix(discovery): add json tags to domain types

See merge request vikingowl/marktvogt.de!10
2026-04-18 07:11:55 +00:00
b6ace52ada fix(discovery): add json tags to domain types
Without snake_case json tags, Go serializes fields as PascalCase (ID,
MarktName, etc.) — but the Svelte frontend reads snake_case. Every
row.id on the client was undefined, which made Svelte 5 see identical
'undefined' keys across the {#each queue as row (row.id)} loop and
throw each_key_duplicate.

Adds explicit snake_case tags to Bucket, DiscoveredMarket, and
RejectedDiscovery to match what the TypeScript types already expect.
2026-04-18 09:11:44 +02:00
8f1efe73f2 Merge branch 'fix/discovery-cron-service-port' into 'main'
fix(helm): CronJob curls the Service port, not the container port

See merge request vikingowl/marktvogt.de!9
2026-04-18 07:00:26 +00:00
5a561b3092 fix(helm): CronJob curls the Service port, not the container port
Service listens on port 80 (target: container 8080). The CronJob was
curling :8080 directly, which isn't exposed by the Service — every tick
timed out after ~135s with "Could not connect to server".

Switch to {{ .Values.service.port }} so the template always tracks the
actual Service port.
2026-04-18 09:00:16 +02:00
1252044d60 Merge branch 'fix/discovery-empty-card-dark' into 'main'
fix(web): dark-mode variants for discovery empty-queue card

See merge request vikingowl/marktvogt.de!8
2026-04-18 06:53:43 +00:00
173e7c5013 fix(web): dark-mode variants for discovery empty-queue card 2026-04-18 08:53:25 +02:00
ce75448f1e Merge branch 'fix/discovery-empty-slice-null' into 'main'
fix(discovery): render empty queue as [] not null (500 on empty prod)

See merge request vikingowl/marktvogt.de!7
2026-04-18 06:44:52 +00:00
14e1a36622 fix(discovery): render empty queue as [] not null (500 on empty prod)
Go's nil slice marshals as JSON null, not [], which crashed the Svelte
page's .length access on fresh installs where no discovery tick has
happened yet. Reproduced in production: /admin/discovery → 500 because
data.queue was null and {queue.length} dereferenced it.

Backend: initialize every returning slice in repository.go via
make([]T, 0) so zero rows serialize as [] consistently. Also applies to
PickStaleBuckets, ListSeriesByCity, and Stats.RecentErrors.

Web: coalesce data.queue / data.stats.recent_errors at the top of the
Svelte script with `?? []` so future nil-slice regressions don't take
the whole page down.
2026-04-18 08:44:17 +02:00
134cc9726b Merge branch 'feature/discovery-admin-stats' into 'main'
feat(discovery): admin stats strip + sidebar nav link

See merge request vikingowl/marktvogt.de!6
2026-04-18 06:35:10 +00:00
b7670b6152 feat(discovery): admin stats strip + sidebar nav link
Surfaces CronJob health signals without needing kubectl: last tick time
(stale-amber if > 6h), buckets due now, errors in the last 24h (with an
expandable list of the most recent failing buckets), and queue size.

Also wires the previously-orphaned /admin/discovery route into the admin
sidebar next to Märkte.

- backend: new GET /admin/discovery/stats endpoint; Stats + BucketError
  types; repository Stats() aggregates four counters + top 5 failing
  buckets.
- web: +page.server.ts fetches stats in parallel with queue;
  +page.svelte renders a 4-card strip above the queue table.
2026-04-18 08:34:34 +02:00
2debc15bc7 Merge branch 'fix/discovery-cron-podsecurity' into 'main'
fix(helm): add restricted PodSecurity settings to discovery CronJob

See merge request vikingowl/marktvogt.de!5
2026-04-18 06:27:52 +00:00
1ba8f856b4 fix(helm): add restricted PodSecurity settings to discovery CronJob
Previous deploys emitted 4 warnings on the discovery-tick Pod template
against the restricted:latest policy. Today they are warnings; if the
namespace enforcement tightens, admission will silently drop the Pod.

Pod-level: runAsNonRoot, runAsUser/runAsGroup 100 (curlimages/curl's
built-in non-root UID), seccompProfile RuntimeDefault.
Container-level: allowPrivilegeEscalation false, capabilities drop ALL.
2026-04-18 08:26:40 +02:00
0a408a40ba Merge branch 'ci/discovery-helm-vars' into 'main'
ci(deploy): wire AI_AGENT_DISCOVERY and DISCOVERY_TOKEN into helm upgrade

See merge request vikingowl/marktvogt.de!4
2026-04-18 06:17:23 +00:00
502d1d2109 ci(deploy): wire AI_AGENT_DISCOVERY and DISCOVERY_TOKEN into helm upgrade 2026-04-18 08:17:06 +02:00
f36558b784 Merge branch 'feature/dach-market-discovery' into 'main'
feat(discovery): DACH market discovery pipeline

See merge request vikingowl/marktvogt.de!3
2026-04-18 06:12:03 +00:00
b222d5fbc8 fix(discovery): use interval multiplication for forward-window query
pgx cannot implicitly encode int arg into text for the `$1 || ' month'`
concatenation pattern (error: "unable to encode 12 into text format for
text (OID 25): cannot find encode plan"). Multiplication with a known
interval works directly with the int parameter and is semantically
equivalent.

Discovered during the T19 smoke test — the tick endpoint returned 500
on every call before this fix.
2026-04-18 08:08:27 +02:00
31ce937f55 feat(helm): add discovery CronJob + token secret + env wiring
Adds a batch/v1 CronJob that POSTs to /api/v1/admin/discovery/tick on a
configurable schedule (default every 4h). Wires DISCOVERY_TOKEN into the
ci-secrets Secret and projects discovery/AI env vars into the backend
Deployment.
2026-04-18 07:57:18 +02:00
2a1fb1eece feat(web): admin discovery review queue with accept/reject 2026-04-18 07:54:18 +02:00
3d785d711b feat(discovery): wire domain into API server
Construct discoveryRepo, discoveryAgent, discoveryService, and
discoveryHandler in registerRoutes(); register all 4 discovery routes
on /api/v1 with bearer-token guard on /tick and admin-session guard on
queue management endpoints.
2026-04-18 07:50:14 +02:00
f0ba134514 feat(discovery): add HTTP handlers and route registration 2026-04-18 07:43:38 +02:00
4a9d1ff908 feat(middleware): add constant-time bearer token auth for machine routes 2026-04-18 07:39:31 +02:00
540298fb88 feat(discovery): implement Accept + Reject with transactional state changes 2026-04-18 07:37:39 +02:00
4e7120e958 feat(discovery): implement Tick with dedup + queue write 2026-04-18 07:34:14 +02:00
aa14724947 feat(discovery): add service skeleton with bucket picker + series matcher 2026-04-18 07:30:47 +02:00
92a5b05875 feat(discovery): add Pass 0 agent client + parser (Mistral) 2026-04-18 07:28:25 +02:00
e245f3ec22 feat(discovery): add repository with bucket/queue/rejection queries 2026-04-18 07:25:54 +02:00
f7e4bab2c3 feat(discovery): add domain types (Bucket, DiscoveredMarket, Pass0*) 2026-04-18 07:23:25 +02:00
a9462d1695 feat(discovery): add DiscoveryConfig (token, batch, forward months) 2026-04-18 07:22:13 +02:00
174635e241 feat(discovery): seed 24-month DACH bucket window 2026-04-18 07:20:32 +02:00
fa15810dbb feat(discovery): add rejected_discoveries sticky-reject table migration 2026-04-18 07:17:21 +02:00
cddd0196c0 feat(discovery): add discovered_markets queue table migration 2026-04-18 07:14:19 +02:00
d0b2ad6362 feat(discovery): add discovery_buckets table migration 2026-04-18 07:11:53 +02:00
c78e24e4f9 fix(ai): address review feedback on rate limiter (sort/tolerance, ctx-cancel TODO) 2026-04-18 07:06:22 +02:00
c95261d747 feat(ai): add process-wide 1 req/s rate limiter to Mistral client 2026-04-18 07:00:35 +02:00
aa965d292a fix(discovery): address review feedback on name/city normalization 2026-04-18 06:55:38 +02:00
c95ce55318 feat(discovery): add market name normalization with stripword guard
Normalizes market names for dedup matching: lowercase, umlaut expansion,
punctuation stripping, whitespace collapse, and leading/trailing filler
word removal. Guards stripping so edge fillers are preserved when the
remaining content is purely numeric (e.g. 'Markt 2026' stays 'markt 2026').
2026-04-18 06:48:33 +02:00
abe5f53a54 Merge branch 'fix/market-search-plz-filter' into 'main'
fix(search): support PLZ filter on home page

See merge request vikingowl/marktvogt.de!1
2026-04-18 03:32:59 +00:00
79001011df fix(search): support PLZ filter on home page
The home page dropped `plz` server-side, so /markets was called with
radius but no center (unfiltered) and the PLZ input rendered empty
after reload. +page.server.ts now reads plz, geocodes via /geocode,
and echoes plz back in searchParams for form rehydration.

Relaxes /geocode DTO + guard to accept PLZ without city — Nominatim
already supports postal-only lookups. URL lat/lon (GPS flow) take
priority over plz on tie-break; geocode failures fall through to no
geo-filter so the page always renders.
2026-04-18 05:32:28 +02:00
df5b0563c9 refactor(web): bundle server with esbuild, slim runtime to alpine+node
Post-process adapter-node output into a single self-contained
build/bundle.mjs (614 KB) via esbuild, then ship it on a fresh
alpine:3.21 base with just the node binary copied in. Drops the
node_modules + package manager baggage that comes with node:25-alpine.

- Add esbuild devDep + `bundle` script (scripts/bundle.mjs)
- Dockerfile: drop `deps` stage; final stage is alpine + node binary +
  bundle + static client assets
- Uncompressed image: 177 MB -> 149 MB (-16%)
- Verified: /, /healthz, static assets all respond identically;
  outbound TLS to api.marktvogt.de works via node's built-in CA bundle
2026-04-18 05:05:07 +02:00
f9b77f362f chore(helm): right-size resource requests/limits per cluster telemetry
Drop requests to match observed peak usage and widen CPU limits for
burst headroom (Burstable QoS). Backend, web, Postgres, and Dragonfly
all had requests == limits pinned at defaults well above measured
7-day peaks.

- backend: req 100m/128Mi -> 50m/64Mi, lim 100m/128Mi -> 200m/128Mi
- web:     req 100m/128Mi -> 50m/96Mi, lim 100m/128Mi -> 200m/128Mi
- postgres (CNPG): req 50m/256Mi -> 15m/128Mi, lim 200m/512Mi -> 100m/256Mi
- dragonfly: req 100m/128Mi -> 100m/72Mi, lim 100m/128Mi -> 150m/128Mi

RAM limits unchanged where reasonable to preserve OOM protection;
Dragonfly CPU request kept at 100m (peak 74m) but limit raised to
avoid throttling under brief bursts.
2026-04-18 04:36:12 +02:00
0b797aec66 chore: sync CLAUDE.md with GitLab migration; add SOURCE_DATE_EPOCH to builds
- CLAUDE.md: correct stale Woodpecker/submodules claims; note
  registry.itsh.dev's attestation requirement and the docker-container
  driver prerequisite so future contributors don't rediscover the issue.
- .gitlab-ci.yml: export SOURCE_DATE_EPOCH from commit time so the
  rewrite-timestamp=true buildx flag produces genuinely reproducible
  images (layer digests stable across rebuilds of the same commit).
2026-04-18 04:16:13 +02:00
cd7ea0b47b fix(ci): override alpine/helm entrypoint so shell wrapper runs
The alpine/helm:4.1 image has 'helm' as ENTRYPOINT. GitLab CI wraps
step_script in sh, which the container resolves to 'helm sh ...',
failing with 'unknown command sh for helm'. Setting entrypoint: [""]
lets GitLab's sh wrapper execute normally before invoking helm.
2026-04-18 03:57:50 +02:00
ad846be2c7 fix(ci): create docker context before buildx to handle dind TLS
buildx create --driver docker-container cannot inherit TLS env vars
from docker:dind directly; it needs a named context. Create 'tls-env'
from the ambient DOCKER_HOST/DOCKER_CERT_PATH, then point buildx at it.
2026-04-18 03:51:33 +02:00
faa63de3c7 fix(ci): use docker-container driver so buildx emits attestations
The default buildx driver inside docker:29-dind is 'docker' (host daemon),
which cannot produce attestations. Even with default provenance enabled,
the docker driver silently drops attestation-related flags and emits a
bare single-image manifest. registry.itsh.dev (Zot with strict attestation
policy) rejects these with 'manifest invalid'.

Creating a 'docker-container' driver builder before each build gives
buildkit full export capabilities, matching both the Woodpecker plugin's
behavior and what works from local development machines.
2026-04-18 03:48:37 +02:00
83b369e339 fix(ci): re-enable buildx attestations (registry requires them)
Empirical finding: registry.itsh.dev rejects pushes that contain zero
attestations ("manifest invalid"). Attestations cause buildx to emit
an OCI image index; without any, it emits a bare single-image manifest
which this Zot instance refuses in this namespace.

Remove --provenance=false --sbom=false, switch to the explicit output
form matching woodpeckerci/plugin-docker-buildx (proven working against
the same registry for the infinity-tales project).

Root cause of the original 610ca91 regression: that commit added
--provenance=false, which stripped the last required attestation once
SBOM generation was disabled in a later buildkit default change.
2026-04-18 03:43:38 +02:00
29ec565206 test(ci): bump docker:27 -> docker:29 to match Woodpecker plugin env
The woodpeckerci/plugin-docker-buildx image that successfully pushes to
this same registry uses docker:29.4.0-dind with buildx 0.33.0. Version
skew between docker:27's bundled buildkit and the registry is the next
hypothesis after ground-truth manifest inspection ruled out OCI format
and attestations as the cause.
2026-04-18 03:29:52 +02:00
bb72b4ce94 chore(ci): trigger rebuilds when .gitlab-ci.yml changes
Previously only backend/** or web/** file changes would trigger the
respective docker+deploy jobs. CI config changes now also trigger
rebuilds of both services, ensuring any pipeline-definition changes
are verified end-to-end on the same commit that introduces them.
2026-04-18 03:19:23 +02:00
dd7d52e249 fix(ci): disable SBOM attestations on buildx to unblock registry push
Matches woodpeckerci/plugin-docker-buildx defaults. Without --sbom=false
buildkit emits an OCI image index with SBOM attestation that the itsh.dev
registry rejects with 'manifest invalid'. Provenance was already disabled.
2026-04-18 03:17:20 +02:00
808f4ddda6 chore(deps): bump Kit 2.57.1, Vite 7.3.2, quic-go 0.57.0; override cookie 0.7.2
Resolves 11 Semgrep Supply Chain findings (4 reachable HIGH, 3 unreachable HIGH,
4 moderate/low). Build verified on web (pnpm build) and backend (go build ./...).
2026-04-18 02:53:15 +02:00