Commit Graph

33 Commits

Author SHA1 Message Date
vikingowl cd836564f1 feat(discovery): Pass 0 halbmonat buckets + konfidenz/status + link verification
Pass 0 splits every month into two halves (H1 = days 1-15, H2 = 16-EOM)
so each agent call fits within Mistral's 4096 max_tokens budget. The
response schema picks up richer per-market signals and dead agent URLs
get filtered before they land in the admin queue.

DB:
- 000015: add halbmonat char(2) to discovery_buckets, widen unique key,
  backfill existing rows as H1 + insert H2 siblings (624 → 1248 rows).
- 000016: rename discovered_markets.extraktion → konfidenz with
  best-effort value mapping (verbatim→hoch, abgeleitet→mittel); add
  agent_status column.

Backend:
- model: Bucket gains Halbmonat; Pass0Bucket same. Pass0Market renames
  Extraktion → Konfidenz and adds AgentStatus (JSON tag "status").
  DiscoveredMarket mirrors both fields; queue-lifecycle Status column
  stays distinct from agent-reported AgentStatus.
- repository: all SELECT/INSERT touched to use the new columns; picker
  orders by year_month, halbmonat so H1 runs before H2 in the same
  month.
- agent client: prompt now injects halbmonat and recherche_datum (today)
  so the agent has explicit date context.
- link verification: new LinkChecker does concurrent HEAD (GET fallback
  on 405) with a 5s timeout. FilterURLs runs before InsertDiscovered —
  markets whose quellen all fail are dropped and counted as
  link_check_failed in TickSummary. Failing website URLs are cleared
  but don't block insert.
- Service.linkChecker is a narrow interface so tests inject a noop
  stub instead of hitting the network.

Web:
- DiscoveredMarket type gains konfidenz + agent_status, drops extraktion.
- Queue column renames "Extraktion" → "Konfidenz" with three-level
  coloring (hoch=emerald, mittel=amber, niedrig=red, else neutral).
- A small pill next to markt_name surfaces agent_status when it's not
  "bestaetigt" — red for "abgesagt", amber for "unklar" and
  "vorjahr_unbestaetigt" — so risky entries are obvious before accept.
2026-04-18 09:51:57 +02:00
vikingowl bf72095348 feat(discovery): edit pending entries + surface quellen links
Expanding any row in the discovery queue now reveals:
- Quellen as clickable URLs (was just a count)
- Hinweis if the agent emitted one
- Inline edit form for markt_name, stadt, bundesland, start/end date,
  and website — the fields the Pass 0 agent gets wrong most often

Backend:
- PATCH /admin/discovery/queue/:id applies a partial update to pending
  entries via a COALESCE-based SQL update. Only fields that were set
  are written.
- Service recomputes name_normalized when markt_name or stadt change so
  dedup stays consistent after edits.
- Status check ensures only 'pending' entries are mutable.

Web:
- Row state $expandedId holds at most one open drawer at a time.
- Dates round-trip through <input type="date"> using the shared
  dateInputValue helper; form action converts back to RFC3339 for Go.
- Existing Accept/Reject buttons untouched — workflow is edit-then-accept.
2026-04-18 09:33:14 +02:00
vikingowl 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
vikingowl 173e7c5013 fix(web): dark-mode variants for discovery empty-queue card 2026-04-18 08:53:25 +02:00
vikingowl 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
vikingowl 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
vikingowl 2a1fb1eece feat(web): admin discovery review queue with accept/reject 2026-04-18 07:54:18 +02:00
vikingowl 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
vikingowl 5ea6afca3e feat: group market editions by series in search and admin list
Public search now deduplicates to one card per series with an edition
count badge. Admin list uses a grouped endpoint with expandable rows.
Market detail page shows an edition year switcher when multiple editions
exist. Admin detail page includes series edition management.
2026-03-05 18:14:47 +01:00
vikingowl 2d32a098cc feat: add search, sorting, and filtering to admin market list
- Search box for name/city filtering
- Clickable column headers for sorting (name, city, status, date, created)
- Sort direction toggle (asc/desc) with arrow indicators
- All filters preserved through pagination and sort changes
2026-03-05 15:22:13 +01:00
vikingowl 6f8df87f80 feat: add AI research, geocoding, enriched market forms and public submit upgrade
- AI research panel with structured display (opening hours, admission)
- Shared MarketForm component with admin/public mode
- Geocode button to derive coordinates from address
- Opening hours, admission info fields in all forms
- Currency-aware pricing, full European country list
- Public submit now includes all market fields
- Weekday normalization from date for AI research results
- Duplicate detection warning on admin detail view
2026-03-05 15:17:59 +01:00
vikingowl 0a59225e81 fix: match 2FA error code and escape TOTP pattern in login form
Backend returns code '2fa_required' but frontend checked for
'totp_required', so the TOTP input field never appeared. Also fix
the same Svelte pattern brace escaping issue as in TOTPSetup.
2026-02-27 14:55:50 +01:00
vikingowl 25c0a265a4 chore: remove OAuth buttons from login page 2026-02-27 14:54:58 +01:00
vikingowl 18ac35c477 fix: escape regex braces in TOTP input pattern attribute
Svelte was interpreting {6} in pattern="[0-9]{6}" as an expression,
rendering the pattern as "[0-9]6" instead of the intended 6-digit regex.
2026-02-27 14:46:50 +01:00
vikingowl dd4e6184ac feat: add user dropdown menu, password management, fix Turnstile keys
- Replace inline nav items with UserMenu dropdown (display name trigger,
  Profil/Sicherheit/Admin/Abmelden, click-outside/Escape to close)
- Add password set/change form to profile security section
- Fix Turnstile site key (extra A, swapped l/1)
2026-02-27 14:39:01 +01:00
vikingowl 83264b4b41 fix: enable auth nav, turnstile deployment, country dropdown, profile routes
- Add PUBLIC_TURNSTILE_SITE_KEY as Docker build arg and Woodpecker CI arg
- Uncomment auth nav in Header and MobileNav (login/logout/profile links)
- Move ThemeToggle from header to footer
- Expand country dropdown from DACH-only to all European countries
- Replace profile route redirect with requireAuth guard
- Set cookie secure flag based on environment (secure in prod)
- Add error handling to admin markets page (403 instead of 500)
2026-02-27 14:12:23 +01:00
vikingowl e4e5fbf9f5 fix: correct magic link API endpoint path
Frontend was calling /auth/magic-link/request but the backend
route is POST /auth/magic-link, causing a 404.
2026-02-27 13:40:38 +01:00
vikingowl 325a1121a0 fix: replace ASCII-encoded umlauts with proper German characters in UI text
User-facing strings used ae/oe/ue/ss instead of ä/ö/ü/ß, carried over
from ASCII-only planning docs convention.
2026-02-27 11:12:21 +01:00
vikingowl 2e6acceb33 feat: add admin panel, market submission form, and legal updates
- Admin layout with auth guard at /admin
- Admin market list with status filter tabs (pending/approved/rejected)
- Admin market detail/review with approve/reject actions
- Admin market create and edit forms with shared MarketForm component
- Anonymous market submission form at /markt/einreichen with Turnstile
- Optional latitude/longitude fields on submission form
- Admin and submission types added to API types
- requireAdmin guard for role-based frontend access
- Header/mobile nav updated with admin and submission links
- Auth layout redirect removed to re-enable login flow
- Login form action renamed to fix named actions conflict
- Impressum updated with user-submitted content section
- Datenschutz updated with submission form and Turnstile sections
2026-02-27 11:04:31 +01:00
vikingowl 3447402059 fix: add missing recommended fields to Event structured data
Resolve Google Search Console warnings for Event structured data by
adding image fallback, organizer url, offers url, and offers for free
markets.
2026-02-24 12:56:54 +01:00
vikingowl c582c7c4d6 feat: parse admission notes into structured price table
Parse semi-structured notes text (e.g. "Samstag: Erw. 15 €, Kinder 8 €")
into grouped table rows instead of showing raw text below the table.

- Split notes on sentence boundaries (". " + uppercase), handling
  abbreviations like "Erw." correctly
- Extract "Label: Category Price €" patterns into table groups
- Expand common abbreviations (Erw. → Erwachsene, Erm. → Ermäßigt)
- Guard against "Cat: Price, Cat: Price" format (colons in pricesStr)
- Guard against bare prices after colon (letter-start requirement)
- Graceful fallback to flat table + raw notes when parsing fails
2026-02-23 06:12:39 +01:00
vikingowl 6b23cc330e feat: add Prettier, ESLint, and pre-commit hooks
- Add Prettier with svelte and tailwindcss plugins
- Add ESLint with typescript-eslint and eslint-plugin-svelte
- Add pre-commit hooks (trailing-whitespace, end-of-file-fixer,
  check-yaml, check-json, no-commit-to-branch, prettier, eslint,
  svelte-check)
- Add format, format:check, lint, lint:fix scripts to package.json
- Rename CI typecheck step to lint, add format:check and lint commands
- Fix ESLint issues: unused imports, Leaflet type import, extract
  JSON-LD from {@html} template literals to script-block variables
2026-02-23 05:33:44 +01:00
vikingowl b0d7e6c4aa fix: add lightweight /healthz endpoint, skip SSR for k8s probes
Add /healthz handler in hooks.server.ts that returns early without auth
or SSR processing. Update Helm probes from / to /healthz to avoid
unnecessary log noise and wasted SSR renders.
2026-02-22 20:27:00 +01:00
vikingowl 7282f25986 feat: add SEO landing pages for states and cities
Add /maerkte/ browse-by-state overview, /maerkte/[state]/ state detail
pages, and /maerkte/[state]/[city]/ city detail pages. Each page fetches
all markets and filters client-side (SSR). Includes slug utilities with
umlaut handling, JSON-LD structured data, breadcrumb navigation on all
pages including market detail, and sitemap entries for all new routes.
2026-02-22 20:21:48 +01:00
vikingowl 072c9dc934 feat: add version polling and auto-reload on deploy
- Enable kit.version.pollInterval (60s) so SvelteKit detects new
  deploys via version.json
- Add beforeNavigate guard that forces a full page reload when a
  new version is detected, preventing stale client-side state
- Preload fonts, JS, and CSS for faster initial paint
2026-02-22 19:24:18 +01:00
vikingowl 6f1709a258 fix: use PUBLIC_API_BASE_URL env var instead of hardcoded localhost
SSR was fetching from http://localhost:8080 in production, causing
the markets endpoint to fail silently and return 0 results.
2026-02-22 18:36:26 +01:00
vikingowl f83c712b2b fix: remove PageServerLoad annotations from disabled routes
The auth and profile layout redirects break SvelteKit's type
generation for child page loads, causing false-positive type
errors where PageData requires `user` from the root layout.
2026-02-22 11:50:14 +01:00
vikingowl df93f83fcd feat(seo): add sitemap, canonical URLs, structured data, and OG/Twitter tags
- Add dynamic sitemap.xml with static pages and market entries
- Add canonical URLs and base OG/Twitter meta tags in root layout
- Add JSON-LD WebSite schema with SearchAction on home page
- Add JSON-LD Event schema on market detail pages
- Add twitter:card summary_large_image for markets with images
- Add OG tags to impressum and datenschutz pages
- Add font preloading for critical rendering path fonts
- Add Sitemap directive to robots.txt
2026-02-22 11:45:27 +01:00
vikingowl 2c462144a7 chore: temporarily disable login/signup and profile routes
Comment out auth navigation in Header and MobileNav.
Add redirect-to-home layout guards on /auth/* and /profile/* routes.
2026-02-22 11:34:05 +01:00
vikingowl cd29b49b0e feat: add impressum and datenschutz pages
Add legal pages required by German law (TMG §5, DSGVO).
Impressum includes operator identity, contact, and liability notices.
Datenschutz covers all data processing: registration, OAuth, sessions,
cookies, geolocation, map tiles, and user rights.
2026-02-22 11:32:22 +01:00
vikingowl 76afe71854 feat: add signet ring logo and favicon with fleur-de-lis
Introduces brand identity with a medieval signet ring motif:
gold fleur-de-lis on dark green signet face. Adds SVG favicon,
raster fallbacks (ICO/PNG), apple-touch-icon, web manifest,
and inline signet ring logo in the header next to the wordmark.
2026-02-18 07:44:41 +01:00
vikingowl 2b02168747 feat: medieval aesthetic restyling with dark/light mode
- Add MedievalSharp + Crimson Pro fonts (woff2)
- Implement OKLCH color theme: forest green primary, gold accent,
  warm stone neutrals, parchment/vellum surfaces, brick red danger
- Add dark/light mode with system preference detection and user toggle
  (ThemeToggle component cycling system/light/dark)
- Prevent FOUC with inline script in app.html
- Replace native <select> with custom accessible dropdown (combobox/
  listbox pattern, keyboard nav, type-ahead, app-themed styling)
- Add IP geolocation fallback via geojs.io when native geolocation
  fails, with visual location badge showing resolved city name
- Shared form control styles via @layer base
- WCAG/a11y: skip-to-content link, aria-invalid, aria-describedby,
  aria-live, focus-visible, role="search", color-scheme:dark
- Update all 23 component/page files with new color system and
  dark: variants
2026-02-18 07:19:30 +01:00
vikingowl 1bcab1f1d4 feat: implement SvelteKit web frontend MVP
SvelteKit 2 (Svelte 5 runes) + Tailwind 4 + TypeScript frontend for
the Marktvogt medieval market platform.

- Market search with keyword FTS, location/radius, date range, sorting
- List/map toggle (Leaflet with OSM tiles, dynamic import)
- Market detail pages with SSR, SEO meta/OG tags, opening hours,
  admission prices, embedded map
- Auth: login (password + magic link + OAuth), register, logout
- Profile: view/edit display name + avatar, account deletion
- 2FA: TOTP setup, verify, disable
- httpOnly cookie auth with JWT access + session token refresh
- Responsive layout with mobile hamburger nav
- German UI text for DACH audience
2026-02-18 06:27:51 +01:00