Javis OS
Javis OS는 기업가를 돕는 에이전시 AI로, 업무를 지원하고 함께 성장합니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Bilingual (CZ/EN) event registration for the Czech Diamond Way Buddhism network — group sign-up, server-side pricing with two independent price tiers, bilingual email confirmations, centre-scoped admin with a full registration editor, built-in help and XLSX export. Live at registrace.online.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Bilingual (CZ / EN) event-registration platform for Diamond Way Buddhism (BDC) centres. Public visitors register themselves and fellow participants for meditation and community events; the app prices the stay server-side and emails a bilingual confirmation. Centre admins manage events, registrations and exports — all scoped by role and centre. Registrace is a production web application built for Buddhismus Diamantové cesty (BDC / Diamond Way Buddhism) — a network of Czech meditation centres. It replaces ad-hoc spreadsheets and email threads with a single bilingual flow:README 전문 보기 ↗
Bilingual (CZ / EN) event-registration platform for Diamond Way Buddhism (BDC) centres.
Public visitors register themselves and fellow participants for meditation and community events; the app prices the stay server-side and emails a bilingual confirmation. Centre admins manage events, registrations and exports — all scoped by role and centre.
Registrace is a production web application built for Buddhismus Diamantové cesty (BDC / Diamond Way Buddhism) — a network of Czech meditation centres. It replaces ad-hoc spreadsheets and email threads with a single bilingual flow:
260020108).It is live in production at registrace.online on Vercel + Supabase, and has been through a full internal build (B1–B8), a production-hardening pass (P1–P8) and a multi-agent security audit, and has run real events since. This README is the single source of orientation for anyone joining the project.
The registrations in these screenshots are a demo dataset — fictional families on the RFC-2606 reserved
example.*domains, never real registrants; the maintainer's throwaway test events are left out of the events list. The organiser contact on the event page is the maintainer's own. The admin email addresses and the audit-log IP column are blurred here on purpose for privacy: they are shown normally to admins in the running app — only these public screenshots hide them.
YYEEENNNN) and a polished, BDC-branded
confirmation email whose meal summary is grouped by day and collapsed twice — meals of one
day with the same eaters share a line, and a meal nobody missed reads "everyone (N)" rather
than ten repeated names, so a ten-person booking of 132 meals fits in seven lines.SUPER_ADMIN sees everything; ADMIN is scoped to their assigned
centre(s); an owner tier guards super-admin management.GET /api/cron/event-lifecycle) writes the stored status to match — closed
after that moment, archived three days later — so the admin list tells the truth too. What an
admin sees: from that same moment the event itself is read-only — its Edit link becomes
View (the whole event on one page: price list, meal days, settings; nothing to save) and the
server refuses the write — while its registrations stay fully editable; once archived
its registrations move behind the registrations list's "show archived" switch (the event's own
view still lists them all). An event closed by hand before its end can be published again
until the end, and once anyone has registered a live event cannot go back to draft.| Layer | Choice | Notes |
|---|---|---|
| Framework | Next.js 16 (App Router, Turbopack) · React 19 | Server Components + route handlers; no middleware.ts — edge logic lives in proxy.ts |
| Language | TypeScript (strict, noUncheckedIndexedAccess) | — |
| ORM | Prisma 7 + @prisma/adapter-pg (pg) | Driver-adapter pattern; client generated to generated/prisma (gitignored) |
| Database & Auth | Supabase (PostgreSQL + Auth) | RLS deny-all; all data access goes through Prisma |
| Validation | Zod 4 | Client-safe schemas (no Prisma imports) |
| Forms | React Hook Form + @hookform/resolvers | — |
| i18n | next-intl 4 | Locales cs (default) / en |
| Resend | Bilingual, inline-CSS, non-blocking | |
| Export | exceljs | XLSX (chosen over the vulnerable xlsx package) |
| Styling | Tailwind CSS v4 | Design tokens via @theme in globals.css, no JS config |
| Tests | Vitest (+ v8 coverage) | 512 unit / integration tests |
| Analytics | Vercel Web Analytics | Cookieless page analytics; the only third party in the page |
| Hosting | Vercel + own domain (Wedos DNS) | Auto-deploy on push to main |
Exact versions live in package.json. No Docker.
These invariants are enforced across the codebase (full list in
CLAUDE.md):
modules/pricing) is pure, server-only, no DB access.*_cs / *_en).deletedAt) on events, registrations and participants; centres are
deactivated, never deleted. An admin account removed from the panel is deleted outright:
its removal is audited with the account's email, but its earlier audit entries lose their
actor.flowchart LR
subgraph Client["Browser (CZ / EN)"]
Pub["Public pages<br/>+ registration form"]
Adm["Admin panel"]
end
Edge["proxy.ts — edge middleware<br/>matcher: pages + /api/admin/** only<br/>i18n routing · CSP nonce · session refresh<br/>admin API: rate-limit · CSRF · 401 gate"]
subgraph Next["Next.js server"]
Pages["Server Components<br/>app/[locale]/**"]
PubAPI["Public handlers<br/>/api/events · /api/registration/** · /api/cron/**<br/>rate-limit in handler"]
AdmAPI["Admin handlers<br/>/api/admin/**<br/>role + ownership guard"]
SVC["Services<br/>modules/**"]
Price["Pricing engine<br/>modules/pricing (pure)"]
end
DB[("Supabase<br/>PostgreSQL")]
AuthSvc["Supabase Auth"]
Mail["Resend<br/>(email)"]
Pub -->|"page requests"| Edge
Adm -->|"page requests"| Edge
Edge --> Pages
Adm -->|"manage"| Edge
Edge --> AdmAPI
Pub -->|"submit / calculate-price<br/>(edge NOT in path)"| PubAPI
Pages --> SVC
PubAPI --> SVC
AdmAPI --> SVC
SVC --> Price
SVC -->|Prisma| DB
Adm -.->|"signInWithPassword"| AuthSvc
Edge -.->|"session"| AuthSvc
SVC -.->|"non-blocking"| Mail
proxy.ts) is deliberately not a global gate: its config.matcher covers
pages and /api/admin/** only. On pages it does locale routing, the CSP nonce and
Supabase session refresh; on the admin API it adds rate-limiting (120/min/IP), a CSRF
same-origin check on mutations, and a 401 for anonymous callers. It checks session
presence only — and it is not the admin pages' only gate: the matcher skips every
path containing a dot, and [locale] would accept any string, so /a.b/admin/help used to
reach the app without meeting the proxy at all. An unknown locale is therefore a 404 in the
[locale] layout, the panel layout redirects to the login without an admin, and every panel
page resolves the admin itself./api/events, /api/registration/**,
/api/auth/me and the cron endpoint /api/cron/** are excluded by the matcher and reach
their handlers directly, so each one enforces its own rate limit (submit 10/h, price 60/min,
public reads 60/min per IP; the cron endpoint demands the bearer Vercel sends and throttles
only failed attempts, see Routes).modules/*, which Server
Components call directly rather than fetching their own API.modules/pricing/index.ts is pure and defensive (a missing rule or degenerate stay yields
0, never throws — the price endpoint calls it mid-edit on incomplete input). Per participant:
participation = dailyRate × days
− arrival discount (by arrival time: morning / afternoon / evening)
− early-departure discount
+ nightRate × (days − 1) (only when accommodation is chosen)
floored at 0
meals = Σ price of each unique, open, known selected meal
subtotal = participation + meals
Pricing is data-driven: every age is charged by its matching PricingRule.dailyRate — no
age is hard-coded to 0. Young children carry a 0-rate rule, but an event may charge, say,
ages 8–14 (the real BDC "MLK" course does, at 100 CZK/day). Discounts apply to 15+ only
because child rules carry 0 discounts — not via any age branch. The discounts are per
tier, not per event: most events give a supported arrival a smaller discount than a standard
one, so the price overview lists them by tier whenever they differ.
Each person carries two independent price tiers, and both apply at every age:
pricingType prices the stay, mealPricingType prices the meals. Surplus accommodation with
supported meals is a normal, intended combination — neither tier is ever derived from the
other. A meal costs MealPricingRule(mealType, ageCategory, pricingType), a 36-cell price list
per event keyed by the eater's meal tier; EventMeal.price is a legacy mirror of the
15+/standard cell and is used only by an event that has no price list at all. An event declares
which tiers it offers for each half separately, and every surface honours that: the price
overview filters each of its two tables by its own set, and the registration form renders a
selector only for a half that offers more than one tier — so the common single-tier event shows
none at all. Because the tier is real at every age, the admin registration detail, the
confirmation email and the export all print both tiers for every participant, children
included.
12 Prisma models, 9 enums, 9 applied migrations. The source of truth is
prisma/schema.prisma.
| Model | Purpose |
|---|---|
| User | id @db.Uuid (= Supabase Auth id), email, role. |
| UserCenter | Explicit User ↔ Center join (which centres an admin manages). |
| Center | 25 seeded rows — 23 BDC centres plus the Jiné / Mimo ČR (Other / outside CZ) catch-alls a visitor can pick as their home centre. Bilingual names, sortOrder, soft-active. |
| Event | Bilingual title/subtitle/description, contact fields, status, centerId (host centre), dates, createdBy, mealRegistrationDeadline, numberPrefix + registrationSeq (reg-number support), and the two independent sets of offered tiers — participationPricingTypes / mealPricingTypes. |
| EventDate | A day of the event; used as arrival/departure reference. |
| PricingRule | Per event × ageCategory × pricingType: dailyRate, nightRate and the four *Discount fields (subtracted). |
| MealPricingRule | The event's meal price list: per mealType × ageCategory × pricingType (36 cells), what one meal costs one person. Keyed by the eater's meal tier. |
| EventMeal | A meal slot on a given day: mealType, price, isClosed. price is a legacy mirror of the 15+/standard cell, used only by an event with no price list. |
| Registration | The submission: home centerId, arrival/departure, hasAccommodation, email, gdprConsent, totalPrice, status, idempotencyKey, registrationNumber, locale, ipAddress. |
| Participant | One person: ageCategory, two independent tiers — pricingType (stay/accommodation) and (meals) — (diet), computed prices. |
Enums: AgeCategory · PricingType (standard / supported / surplus) · ArrivalTime ·
EarlyDeparture · EventStatus (draft / published / closed / archived) · MealType
(breakfast / lunch / dinner) · MealCategory (meat / vegetarian) · RegistrationStatus
(registered / cancelled / paid) · UserRole (super_admin / admin).
Public (not matched by proxy.ts — each handler rate-limits itself)
GET /api/events · GET /api/events/[id] — 60/min per IPPOST /api/registration/calculate-price — 60/min per IPPOST /api/registration/submit — 10/hour per IPAdmin (edge: session + rate-limit + CSRF; handler: role/ownership)
GET/POST /api/admin/events, GET/PUT /api/admin/events/[id],
PATCH /api/admin/events/[id]/statusGET /api/admin/registrations, GET /api/admin/registrations/[id],
PUT /api/admin/registrations/[id]/full (full edit), POST /api/admin/registrations/[id]/calculate-price
(its live price), POST /api/admin/registrations/export, POST /api/admin/registrations/[id]/resend-confirmationGET/POST /api/admin/centers, PUT/DELETE/PATCH /api/admin/centers/[id]
(DELETE soft-deletes, PATCH restores)GET/POST /api/admin/users, PUT/DELETE /api/admin/users/[id],
POST /api/admin/users/[id]/reset-passwordGET /api/admin/audit-logAuth — GET /api/auth/me (60/min per IP; login/logout go through the Supabase browser
client, not a route handler).
Scheduled (Vercel Cron, not matched by proxy.ts) — GET /api/cron/event-lifecycle, daily
(vercel.json). Requires Authorization: Bearer $CRON_SECRET — the header Vercel adds itself —
and fails closed with 503 when the secret is unset. The bearer is checked first and only
failed attempts are rate-limited (10/min per IP), so the real run can never be throttled;
?dryRun=1 reports what would change without writing.
Validation errors return a canonical 400 { error, details } (Zod issues) via the shared
validationError() helper.
strict-dynamic (set in
proxy.ts), dropping unsafe-inline from script-src in production.next.config.ts, applied to every response —
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload,
X-Frame-Options: DENY, X-Content-Type-Options: nosniff,
Referrer-Policy: strict-origin-when-cross-origin,
Permissions-Policy: camera=(), microphone=(), geolocation=() and
Cross-Origin-Resource-Policy: same-origin — except the link-preview images
(opengraph-image), which are cross-origin so chat apps can fetch them. The X-Powered-By
header is off. (The preload directive is a one-way commitment and only takes effect once
the domain is submitted at hstspreload.org.)NEXT_PUBLIC_APP_URL. Fail-closed: a missing Origin and Referer, or an unset
NEXT_PUBLIC_APP_URL, is rejected; the any-localhost relaxation is gated to non-production._prisma_migrations):
row security is on and zero policies are defined, so nothing is reachable through the
anon key. This is a backstop, not the access control — the real authorization is the
role/ownership gate in the handlers and services. It lives in the migrations
(20260721104500_enable_rls_on_all_tables) and prisma/rls.test.ts fails the build if a
model is added without it. That is a correction, not a preference: RLS was originally set by
hand in the Supabase dashboard, which covers the tables that exist at that moment — so
, created by a migration, went live with RLS off and granted
SELECT/INSERT/UPDATE/DELETE on it through PostgREST. Verify the live state with
(expect all
) and (expect no rows).
Supabase's Security Advisor reports this as informational "RLS Enabled No Policy", which is
the intended state here.cs (default) and en, prefixed in
the URL (/cs/..., /en/...) and handled in proxy.ts.locales/cs.json / locales/en.json;
keys are namespaced (form, home, event, badge, admin, …).title_cs / title_en, etc.) — not in the
locale files — so admins author both languages per event.fetch/WebSocket)git clone https://github.com/Martin8O/Registrace.git registrace
cd registrace
npm install # runs `prisma generate` via postinstall
cp .env.example .env.local
# then fill in the values — see “Environment variables” below
# Apply all migrations to your database (uses DIRECT_URL)
npx prisma migrate deploy
# Seed the centre rows (23 BDC centres + 2 catch-alls) — and nothing else.
# Idempotent, deletes nothing, safe to re-run.
npx prisma db seed
Create events and registrations from the admin panel. There is deliberately no demo data
anywhere in the setup path: the seeder creates only the centre rows every instance needs.
Two demo-data paths existed during the build and both are gone — one truncated Event and
Registration before reseeding, the other put a fictional published event on the public
homepage of whatever instance ran the documented setup command. Both are in the git history
if a throwaway environment ever wants them.
npm run dev # http://localhost:3000
Admins sign in with Supabase Auth at /<locale>/admin/login. The first SUPER_ADMIN is
provisioned manually: create the user in Supabase Auth, then set their role in the database
(e.g. npx tsx --env-file .env.local prisma/promote-super-admin.ts <email> once their User
row exists). From then on, further admins are invited from the panel's Admins (Správci)
screen, which assigns roles and centres.
Note: opening an invite/reset link signs that browser in as the link's user, replacing any session already present in every window (cookie auth is one session per browser). This is deliberate — the identity must come from the token, never from whoever happens to be logged in. If you're testing an invite while signed in as a super-admin, open it in a private window to keep your own session; the set-password page also states whose account it is.
Copy .env.example to .env.local. All are required in production unless
noted.
| Variable | Purpose |
|---|---|
DATABASE_URL | Pooled Supabase connection (port 6543) — used by the app at runtime. |
DIRECT_URL | Direct Supabase connection (port 5432) — used by Prisma migrate/seed. |
NEXT_PUBLIC_SUPABASE_URL | Supabase project URL (also feeds the CSP connect-src; must be set at build). |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Supabase anon key (browser auth client). |
SUPABASE_SERVICE_ROLE_KEY | Service-role key for admin user management (server only). |
RESEND_API_KEY | Resend API key for confirmation emails. |
NEXT_PUBLIC_APP_URL | The app's own origin — used for invite/reset links, the admin CSRF check and metadataBase (every absolute URL in a link preview). A wrong value silently 403s every admin write. |
EMAIL_FROM | Verified sender, e.g. BDC Registrace <[email protected]>. |
CRON_SECRET | Bearer token Vercel sends with the daily /api/cron/event-lifecycle call (Vercel adds the header itself once the variable exists on the project). Any random string of 32+ chars. Unset → the endpoint refuses to run (503) and event statuses stay manual. |
OWNER_USER_IDS | Comma-separated Supabase Auth user UUIDs allowed to manage super-admins (preferred, immutable). Find them in Supabase → Authentication → Users. |
OWNER_EMAILS | Legacy fallback — verified emails allowed to manage super-admins. Both owner lists empty → nobody can manage super-admins. |
SUPER_ADMIN_EMAIL | Optional, tooling only. Fallback address for prisma/promote-super-admin.ts when no argument is passed. Not read by the app. |
NEXT_PUBLIC_*values are inlined at build time; on Vercel they must be present when the build runs.EMAIL_FROMis a runtime value, so changing it needs a redeploy.
| Script | What it does |
|---|---|
npm run dev | Start the dev server (Turbopack) on :3000. |
npm run build | Production build. |
npm start | Serve the production build. |
npm run lint | ESLint. |
npm run typecheck | tsc --noEmit over the whole project, test files included. |
npm run gate | The one-command quality gate: typecheck → lint → tests → build, stopping at the first failure. |
npm test | Run the Vitest suite once (CI-friendly). |
npm run test:watch | Vitest in watch mode. |
npm run test:coverage | Vitest with v8 coverage. |
postinstall | prisma generate (regenerates the gitignored client). |
Database utilities: npx prisma migrate deploy (apply migrations), npx prisma db seed
(centre rows), prisma/promote-super-admin.ts (bootstrap a super-admin).
app/
[locale]/(public)/ public pages (home, event detail + registration form)
[locale]/admin/(panel)/ admin panel — dashboard, events, registrations,
centres (/admin/centers), admins (/admin/users), logs, help, profile
[locale]/admin/login|set-password|auth/confirm auth entry points
api/ route handlers (public + admin) + _lib (guard, http, full-edit refusals)
components/{public,admin,shared} UI components
modules/{events,registrations,pricing,auth,centers,users} business services (no fat handlers)
lib/ infrastructure
{db,security,email,export,metadata,og,supabase,utils,mock,admin}/ modules
validation/ client-safe Zod schemas + the admin password policy
audit.ts · types.ts audit-log writer · shared types
auth-errors.ts Supabase auth-error code → next-intl key
locales/{cs,en}.json UI translations (+ the Czech-only admin help, in cs.json)
prisma/
schema.prisma · migrations/ data layer (9 applied migrations)
seed.ts the 25 centre rows (no demo data — see Getting started)
promote-super-admin.ts one-off super-admin bootstrap
public/images/ static assets (BDC logo)
proxy.ts edge middleware (i18n + session + admin hardening + CSP)
i18n/request.ts next-intl request config
next.config.ts static security headers
vercel.json function region (dub1) + the daily lifecycle cron
assets/ font for the link-preview cards (Crimson Pro, OFL)
prisma.config.ts Prisma CLI config (reads DIRECT_URL)
vitest.config.ts test runner config
generated/prisma/ generated Prisma client (gitignored)
docs/screenshots/ README images
Note the naming: the “centres” screen lives at /admin/centers and the “admins” screen at
/admin/users (the route names use the model names).
npm test runs 512 Vitest tests across 38 files, with no database required:
0 rule, ages 8–14 on a configured rate, 15+ per tier, discounts
subtracted, accommodation nights, meal pricing per age × tier (a child's lunch priced
differently from an adult's, and the tier moving both), the two independent tiers pricing the
stay and the meals without either leaking into the other, the flat-price fallback that keeps
pre-matrix events billing exactly what they always did, defensive behaviour (missing rule,
degenerate stay, over-large discount → 0, never a throw) and the full aggregated result.0, never the flat price. Plus the rule that a payload arriving without a meal tier
falls back to that person's own tier rather than to the standard one.updatedAt the save is guarded by, and a person or a meal listed twice refused with its path.YYEEE prefix a new event's registration numbers carry:
one past the highest issued for the event's year, so a draft moved to another year cannot make
the next create re-issue a taken prefix (it used to fail on the unique index on every retry);
a new year starts at 001; a collision with a concurrent create is retried once with a fresh
prefix, and only a unique collision is.vi.mock('@/lib/db')) while
keeping the real engine, so totalPrice is asserted end-to-end; plus the two tiers pricing
the two halves independently, both being persisted, each meal snapshotted at the meal tier's
price, a tier the event does not offer being refused before anything is written, and both
tiers reaching the confirmation email — . Three more pin
that each ordered meal reaches the mail with its day and that day's sort order, in the
caller's language: the confirmation groups meals by day, which it cannot do from the
pre-composed slot label it used to be handed. Four more pin the three stay-order rules on the
submit path (and that a same-day morning visit leaving after breakfast is accepted).main (~1–2 min).eu-west-1). Migrations are applied with
prisma migrate deploy (a no-op when already in sync).dub1 (Dublin) in vercel.json, next to the database —
every request makes several database round-trips, and the move cut the admin price preview
from about 1–2 s to about 0.3 s.registrace.online — apex canonical, www → 308 → apex, DNS kept at Wedos.send.registrace.online
(DKIM/SPF/DMARC), isolating sending reputation.vercel.json registers a daily cron (15 2 * * *, UTC) that calls
GET /api/cron/event-lifecycle. It needs CRON_SECRET set on the Vercel project — Vercel
then sends it as the bearer itself; without it the endpoint answers 503 every night and event
statuses stay manual, with the only symptom in the cron log. On the Hobby plan the run lands
anywhere inside the scheduled hour and delivery is best-effort; the job reconciles rather than
steps, so a missed or doubled run is harmless. Check a fresh deploy with
curl -H "Authorization: Bearer $CRON_SECRET" "https://registrace.online/api/cron/event-lifecycle?dryRun=1".postinstall hook; all NEXT_PUBLIC_* vars must be set at build time.| Document | What's in it |
|---|---|
CLAUDE.md | Project constitution — the numbered architectural invariants, roles, folder map and translation-key conventions the code is held to. Written for Claude Code, which built the app; readable as plain architecture notes. It also references a local/ workspace that is gitignored and not published — see the note at the top of the file. |
AGENTS.md | Briefing for AI coding agents (the agents.md convention, read by Claude Code, Cursor, Copilot and others) — commands, the non-negotiable rules, and the things about this codebase that surprise people. Its top block is Next.js-managed and points agents at the version-matched docs bundled in node_modules/next/dist/docs/. |
.env.example | Annotated environment-variable template — every variable with its purpose, where to find its value, and the build-time vs runtime distinction. |
LICENSE | MIT. |
This README is the only document a reader needs; the rest are supporting detail.
The full build (B1–B8) and production-hardening (P1–P8) phases are complete, and the app is deployed and verified in production. A multi-agent security audit has been run and its findings fixed. Since then: two independent price tiers with a per-age meal price list, a daily lifecycle job, link previews, a full admin registration editor, and the built-in admin help.
Known parking-lot items (non-blocking):
bdc.cz, the link previews need one pass.
NEXT_PUBLIC_APP_URL is the single origin the whole card is built from — change it on Vercel
and every og:url / og:image follows — but the card also prints registrace.online as its
own watermark, and that string is hard-coded in the two opengraph-image routes rather than
derived from the origin. Deliberate: the watermark is a wordmark, not a link, and deriving it
would have printed localhost:3000 on every card in development. It is written down here so
the move does not leave a beautifully rendered card advertising an address the site no longer
answers on. Chat clients cache scraped cards for days, so plan on re-sharing links after the
cutover rather than expecting the old previews to refresh themselves.Licensed under the MIT License — see LICENSE. The code is open to read, learn
from and reuse.
The Buddhismus Diamantové cesty (BDC) name, logo and visual identity belong to BDC and are
not covered by the MIT grant, which applies to the source code only. The link-preview cards
use Crimson Pro (SIL Open Font License, assets/CrimsonPro-OFL.txt).
Built with Next.js, Prisma, Supabase, next-intl, Zod, React Hook Form, Resend, exceljs, Tailwind CSS and Vitest.
Designed and built by Martin Svoboda — svobodamartin.dev.
mealPricingTypemealType| ParticipantMeal | Participant ↔ EventMeal join with the charged price. |
| AuditLog | Append-only trail: userId, action, entityType, entityId, oldData/newData, ip. |
MealPricingRuleanonselect tablename, rowsecurity from pg_tables where schemaname = 'public'trueselect * from pg_policies where schemaname = 'public'OWNER_USER_IDS, immutable Supabase ids; verified-email
OWNER_EMAILS fallback) may create/modify super-admins. Both lists empty → nobody can
(fail-closed).lib/validation/password). Note the split, which mirrors the pricing rule: admin passwords
are set by the browser calling Supabase Auth directly, with no route of ours in between,
so the checklist is informational and the authoritative gate is the policy configured in
the Supabase project (Authentication → Providers → Email), currently minimum length 12 +
lowercase, uppercase, digits and symbols. The two must be kept in sync, and the client must
never be the laxer of the pair — a checklist that goes all-ticks on a password Supabase
then refuses is worse than no checklist.
The subtle part: GoTrue validates with strings.ContainsAny against literal ASCII
sets, not Unicode categories. Ž is not an uppercase letter to it and § is not a symbol,
so the rules here mirror those exact sets (and the labels say "a–z" / "A–Z" out loud, because
otherwise a Czech admin types Ž, reads "uppercase ○" and assumes the form is broken).
Length is the one deliberate asymmetry: we count characters where GoTrue counts bytes, which
makes us stricter on accented input — the safe direction.z.literal(true) consent; the stored ipAddress is retained solely for
abuse prevention and never appears in the UI or exports.<Analytics /> in app/layout.tsx) runs on every page. It is cookieless and does not
fingerprint or track visitors across sites; its beacon posts to /_vercel/insights on this
origin, which is why connect-src 'self' covers it. It is the only third party in the page,
and it never sees registration data — that all moves over our own API.=, +, -, @, tab and CR are prefixed) across title, headers and every data row.updatedAt the editor loaded (someone saved in between → 409, no audit — also when the
stale save un-cancels), the new updatedAt written and handed back so a second save needs no
reload, un-cancelling onto a full event refused under an Event row lock that never touches the
registration numbers, the whole before/after image audited, and the live preview pricing the
same state without writing. One exemption, the meal twin of the stranded tier: a meal a
person already holds outside a stay that has not moved is kept and still priced — such
rows exist, and refusing them made every save fail, marking the registration paid or cancelled
included — while a meal added outside the stay, or any move of the stay, is refused as before.
Plus every reader of people — the two counts (list, beds per night), the edit's own load, the
detail, the re-sent e-mail, the kitchen totals and the export — pinned to live participants
only: a removed person stays in the table, and each of those filters is one dropped line away
from putting them back into a price, a mail or the kitchen's count.code; a body that is not JSON is a 400, not a 500), then the save
and the preview each: guard first, validation, the service called with the id and context, a
refusal passed through, an unexpected error not swallowed; the save refuses a body without its
updatedAt.event_ended, unpublish_refused)
rather than as a 500 it could only call a failed save.isPubliclyVisible at every
instant. The same instant ends admin editing: a draft is always editable, anything that went
public (a hand-closed or hand-archived event included) only until then. Plus the scheduled job's write: guarded by the status it read (a concurrent admin
edit wins and is skipped, audit entry included), audited as a system write, and a dry run
that reports the same plan and writes nothing.CRON_SECRET fails closed (503) rather than open, that an authorised call is never
throttled while failed attempts are, and that ?dryRun=1 reaches the service as a dry run. Two
more pin vercel.json to the route: it schedules exactly this path, with a once-a-day expression
(the Hobby plan rejects anything more frequent at deploy) — move the route and this fails instead
of the job silently stopping.vercel.app
origin while running in production.eventId this admin may not
see is a 404 rather than a 200 carrying an empty file — refused before the row query runs.updatedAt, ids for stored people and none for a new one, no
e-mail, no amounts — and every edit to a stored person and to the stay is in it; a refused meal
names the person, a refused tier the person and the half, a concurrent save offers a reload.
Nothing can be edited while a save is under way (it would be dropped under a "saved" toast), and
after it the screen starts again from the refreshed data, so a second save cannot add the new
person twice. Re-ticking a meal is not a change. An in-app link asks before leaving unsaved
changes (a click that opens another tab does not, nor one made after the save went through), and
the two exits that are buttons rather than links — the language switch and the logout — ask
through a shared guard, pinned from both ends (the last two numbers are the language switch's
and the logout's own suites; the logout asks before the session is ended). A resend that fails says the resend failed. The tier guarantees of the editor it replaced are kept:
the tier-select variants, each half listing only its own set, an empty set reading as all three,
a stranded tier kept visible and selected — and still on offer after moving off it — and the resend button disabled for a cancelled
registration (following the SELECTED status) — and now also while there are unsaved changes,
because it always sends the stored version.
The event wizard: the description survives the review step — the last screen before publishing —
with every line break and the blank line intact, under the rule that makes those breaks visible
rather than collapsed, and its box is tall enough to look like somewhere paragraphs belong.
That suite exists because opening the wizard means signing in, which means typing a password
into a form, so it is the one screen no click-through covers.
The same suite pins that publishing is a transition, not a state: an event that is already
public is saved without being asked for permission to publish it, is reported as saved rather
than as newly published, and offers one button instead of two — while a draft, a closed event
going public again, and a brand-new event all still confirm before they become visible. It
also pins that a live event with registrations is not offered Draft, that the server's two
refusals (the event is over; back to draft) are worded rather than reported as a failed save,
that a locked event's save sends the status only when the admin changed it (echoing the one
the page was opened with would undo a colleague's hand-close), that a payload the server's
schema refuses says so instead of "try again", and that an event
that is over opens as its whole review with no step, no field and no save — printing the
stored price list, where a cell the event never priced is 0 (what the engine charges) and
not the catalogue default the wizard proposes for a new event.
The help hint: closed until clicked, every paragraph formatted rather than raw tags, a link to
its recipe that opens in a new tab (a same-tab link would throw away a half-filled wizard), and
closing on Escape or a click elsewhere.dailyRate === 0 as “free” while the nights
were priced by the other half of the same rule. Nine of its cases re-price the same scenario
through the real engine and compare totals, so the informational panel and the
authoritative arithmetic cannot drift apart in silence. Two more pin that the label for that
number says the same thing in all three places it appears — the overview's first table, the
form row and the breakdown's total — because the stay half is a daily rate AND a rate per
night, and naming only one of them is how 400 CZK ended up under a heading that never
mentioned nights.proxy.ts alone. Its matcher
skips every path containing a dot and [locale] accepts any string, so /a.b/admin/help
reached a page that had no check of its own and rendered for anyone. Pinned: an unknown locale
is a 404 (and builds no metadata), the panel layout redirects to the login without an admin,
and — read from disk, so a new page cannot forget — every panel page is a server component
that resolves the admin itself. Six more hold the one route handler under [locale], the
invite / password-reset landing: a layout never wraps a route handler, its params arrive
decoded, and it puts the locale straight into a redirect — so /%5Cevil.com/admin/auth/confirm
redirected to /\evil.com/…, which a browser resolves to another site. An unknown locale now
falls back to the default one before the one-time token is spent.ENABLE ROW LEVEL SECURITY in a migration,
and that no migration defines a policy or forces RLS on the owner. It reads the schema and
the migration SQL, not the database. It exists because RLS used to be a dashboard setting:
it covered the tables that existed when it was clicked, and MealPricingRule arrived later
through a migration with RLS off and anon holding read/write on it.it.each expands and
regex literals aren't mistaken for code), as are the badge, the tech-stack row and
AGENTS.md. It also checks that every counted file is one Vitest is configured to run, so a
test outside the include globs can't inflate the total with cases nobody executes. It
exists because "a 22-scenario matrix" outlived the matrix by two audits: both checked the
total, which was right, and trusted the prose beside it.