vikingowl a89bed4a3e
All checks were successful
ci/someci/push/backend Pipeline was successful
fix(research): expand multi-day opening hours; reject aggregator websites
Two follow-ups to the apply-shape fix:

opening_hours: previous converter only kept datum_von's weekday, dropping
the rest of multi-day ranges. A Sat-Sun event ended up with only Samstag
saved. Now iterates [datum_von, datum_bis] inclusive, projects each date
to its German weekday, and dedupes by (weekday, open, close). Different
hours on the same weekday still produce separate rows so the admin sees
the conflict. Capped at 7 distinct weekdays — the form's expressive limit.

website validator: the LLM was emitting aggregator/listing URLs (e.g.
suendenfrei.tv detail pages) as the official event website because that's
where it found grounding info. The prompt already forbade this, but the
model ignores soft rules. Add a hard validator-level rejection for known
aggregator domains: suendenfrei.tv, mittelalterkalender.info,
marktkalendarium.de, festival-alarm.com, mittelaltermarkt.online. These
suggestions now land in the "rejected" bucket with a clear reason.
image_url / logo_url are unaffected (those legitimately come from any host).

Prompt: enumerate the same aggregator domains explicitly under both the
"primary source" fallback list and the "website forbidden" rule, so the
model has a concrete blacklist instead of a category description.

Existing markets with already-saved aggregator websites need manual
clearing — the validator only kicks in on new applies.

Tests: 4 new opening-hours subtests (range expansion, weekday dedupe,
cross-entry dedupe, hours-conflict-kept-separate) and 4 validator subtests
(three aggregator domains rejected as website; aggregator host as image_url
still ok).
2026-04-28 21:22:43 +02:00
2026-04-28 17:40:01 +02:00

Marktvogt

The central portal for medieval markets ("Mittelaltermaerkte") in the DACH region.

Marktvogt connects visitors, merchants, artists, camp groups, and organizers on one platform — replacing the patchwork of Facebook groups, phone calls, mailing lists, and scattered websites that the scene currently runs on.

The name comes from the historical Marktvogt — the keeper of order at medieval markets. Same job, different century.


The Problem

The medieval-market scene in the DACH region is large (hundreds of markets per year) but digitally fragmented:

  • Finding markets: Facebook groups, mittelalterkalender.info, private sites
  • Bookings (merchants, artists, camps): phone, e-mail, Facebook Messenger
  • Camp groups: organize almost exclusively on Facebook
  • Tickets: usually box-office-only or one-off organizer sites

There is no single portal that brings all participants together. Marktvogt is that portal.

The Solution

A two-sided marketplace:

  • Supply side — organizers list markets, open plots, programs, tickets
  • Demand side — visitors discover markets and buy tickets; merchants, artists, and camp groups apply for spots

Network effects: organizers attract applicants, applicants make the platform indispensable for organizers, and visitors follow the content.

Roles

Six roles, with the Veranstalter (organizer) as the central actor:

Role Description
Gast Anonymous visitor — browse markets, no account
User Registered visitor — tickets, favorites, profile
Haendler Merchant group applying for plots
Kuenstler Artist group applying for performance slots
Lager Reenactment camp group applying for camp space
Veranstalter Organizer — creates markets, reviews applications, sells tickets

All applicants (Haendler / Kuenstler / Lager) operate as groups. A solo merchant is just a one-person group. Mitarbeiter (staff) is a sub-role under Veranstalter with granular permissions.


Architecture

Monorepo. Components are plain directories, not git submodules.

Layer Stack
Backend Go REST API + WebSocket; PostgreSQL (PostGIS), Redis, S3
Web SvelteKit + Tailwind 4, SSR for SEO, runs on Bun
Mobile Flutter (Android + iOS)
Auth Custom (Go), e-mail+password / magic link / OAuth / 2FA
Payments Stripe Connect
LLM Google Gemini (gemini-2.5-flash-lite) for enrichment
CI/CD Woodpecker (ci.somegit.dev) — .gitlab-ci.yml retained as fallback
Hosting Kubernetes (itsh.dev), Helm chart at helm/marktvogt/
Monitoring Prometheus, Loki, Grafana, Sentry

Repo Layout

backend/    Go REST API + WebSocket chat
web/        SvelteKit frontend (SSR, Bun runtime)
app/        Flutter mobile app
helm/       Monolithic Helm chart (backend + web + Postgres + Dragonfly)
planning/   Vision, roadmap, MVP scope, feature specs (German, ASCII-only)
scripts/    Out-of-band tooling (e.g. K8s secret sync)
.woodpecker/ CI pipelines for somegit.dev

The admin dashboard lives in its own repo and will be added later.


Getting Started

The repo is driven by a Justfile. Run just for the full list.

# One-time setup: pre-commit hooks, web deps, golangci-lint
just hooks-install

# Backend
just backend-dev        # Postgres + Valkey via docker compose
just backend-run        # go run ./cmd/api
just backend-test
just backend-lint

# Web
just web-dev            # SvelteKit dev server
just web-check          # svelte-check (types)
just web-lint

# Everything
just lint
just test
just fmt

For component-specific details, see backend/README.md, web/README.md, and app/README.md.


Deployment

Single Helm release marktvogt in namespace tenant-2, deployed from helm/marktvogt/ (monolithic chart covering backend, web, Postgres, and Dragonfly). CI runs:

helm upgrade marktvogt --reuse-values \
  --set-string backend.image.tag=<sha> \
  --set-string web.image.tag=<sha>

--set-string is mandatory — all-digit short SHAs get float-coerced into scientific notation otherwise, which then fails K8s label validation.

K8s Secrets are pre-created out-of-band by scripts/k8s-secrets-sync.sh reading from .env.helm (gitignored). CI never touches secret values.

The container registry (registry.itsh.dev/vikingowl/marktvogt.de/{backend,web}) is Zot-backed and requires attestations on every pushed image. The Woodpecker pipelines use woodpeckerci/plugin-docker-buildx, which handles this by default.


Status

Active development as of 2026-04-28. backend/, web/, and app/ all contain working code (Go API scaffolding + auth, SvelteKit pages, Flutter skeleton).

  • Current phase scope: planning/15-mvp.md
  • Phased feature plan: planning/17-roadmap.md
  • Vision: planning/00-vision.md

Planning docs are in German and use ASCII only (no umlauts) for cross-platform compatibility.


Conventions

  • API design: REST for CRUD, WebSocket for real-time chat
  • Auth: custom-built using Go libraries — no external auth provider
  • Offline capability: required for QR-ticket validation and venue maps
  • Commits: conventional commit messages
  • Source of truth for product: planning/ — read it before adding features
Description
No description provided
Readme 1.7 MiB
Languages
Go 60.3%
Svelte 20.3%
Dart 11.1%
TypeScript 5%
PLpgSQL 1.1%
Other 2.1%