Tasky
159 taskůKanban board se všemi tasky napříč projekty.
Nový src/middleware.ts s defineMiddleware — parse session cookie, lookup přes Session service, populate Astro.locals.user + locals.session. SessionExpired catchTag → silent Option.none (uživatel se chová jako anonymní). DbError i ConfigMissing v middleware se logují a fallthrough na null locals (nezhroutí stránku).
Nová stránka src/pages/account.astro — prerender=false, čte Astro.locals.user, redirect na /login pokud null. Per-page guard místo middleware-driven redirect (žádný hidden seznam protected rout).
pnpm tsc --noEmit a pnpm astro check oba běží clean. Jeden typecheck error po prvním write (passwordHash je nullable v schema, login musí na null branch vrátit no_password) opraven přidáním passwordHash null guardu v login.ts před verify.
Nový endpoint src/pages/api/auth/login.ts — POST form/JSON, findByEmail → verify PBKDF2 → vytvoří session a vrátí cookie. UserNotFound i wrong_password se klientovi vrací jako jeden tag invalid_credentials, aby chyba neodhalila existenci účtu. Login schema (src/auth/schemas/login.ts) vyžaduje jen přítomnost hesla, ne min 8, kvůli legacy slabým heslům.
Nová stránka src/pages/login.astro mirroring signup form — prerender=false, error banner z URL query (invalid_credentials/validation/internal), link na signup. Czech labels.
Nový endpoint src/pages/api/auth/logout.ts — idempotent (vždy 303 → /), revoke session když cookie existuje, clear-cookie header. Žádný 404 při chybějící cookie aby logout fungoval i po expiraci.
Cookie attributes (HttpOnly/Secure/SameSite) byly hardcoded a env vars (SESSION_COOKIE_NAME, SESSION_TTL_DAYS) viseli nezapojené. Vznikl nový auth/config.ts který přes Effect.gen čte env a vrátí CookieConfig (name, maxAge, secure z import.meta.env.PROD); pokud chybí cookie name, fail s ConfigMissing. Session cookie serialize/clear přijímá secure: boolean — dev běh bez HTTPS funguje.
Typecheck pnpm tsc --noEmit a pnpm astro check oba 0 errors / 0 warnings. Hlavní typing landmine: TS 5.9 Uint8Array default ArrayBufferLike nesedí na BufferSource — explicit cast.
Layer.mergeAll(Session+User+Password) přes DbLive. runAuthToResponse přidán optional onFailure handler — umožní per-endpoint content negotiation (form redirect vs JSON).
POST /api/auth/signup — handluje JSON i form. Browser: cookie + 303 redirect na /account, error 303 na /signup?error=&email=. JSON klient: 201 nebo errored JSON.
Pure Astro form bez Svelte/JS hydration. prerender=false (čte query). Error banner s czech texty mapuje tag z URL.
PBKDF2 přes WebCrypto SubtleCrypto — 600k iter SHA-256, 16B salt, format pbkdf2$<iter>$<salt-b64>$<hash-b64>. Constant-time compare ve verify, žádný WASM payload.
Effect Schema validace pro signup body (email pattern, password 8-200, name optional). decodeUnknownEffect → ValidationError s message z SchemaError.
User service nad Db: create/findByEmail/findById. UNIQUE constraint violation detekuji přes substring v error.message a remapuji DbError → UserAlreadyExists (409 místo 500). Email lowercase normalize.
Login flow potrebuje session management. Session service (zavislost na Db) expose create/findByCookie/revoke/revokeAllForUser. Session ID = 32B random bytes base64url, default TTL 30 dni, findByCookie kontroluje expiry + maze expired session + touchuje lastUsedAt. K tomu cookie helpers (serialize/clear/parse) pro Run 01-03.
Astro endpointy potrebovaly entrypoint do Effect. Runtime.ts ma module-level ManagedRuntime (cached per Worker isolate) co builds Layer z DbLive + SessionLive + provideD1(env.DB) z cloudflare:workers. Export runAuth() a runAuthToResponse() (druha mapuje AuthError tagy na 400/401/409/500 Response).
Astro middleware potrebuje typed locals. env.d.ts deklaruje App.Locals.user a App.Locals.session jako nullable. Smoke endpoint /api/smoke overuje runtime+D1 connect (vraci pocet useru), v dev serveru vratil {ok:true,users:0}, pred commitem smazan.
Projekt nemel auth deps ani D1. Pridany effect@4.0.0-beta.67, drizzle-orm, drizzle-kit; vytvorena D1 databaze oskar-db s bindingem DB v wrangler.jsonc; pridany scripty db:generate, db:migrate:local/remote.
Auth potreboval persistent storage. Drizzle schema definuje 4 tabulky (users, sessions, oauth_accounts s composite PK, password_resets) vcetne FK cascade a indexu. Tabulky pro Sprint 002 jsou pridane preventivne aby nebyla druha migrace. Migrace aplikovana lokalne.
Auth flow potreboval typove vsechny failure mody. AuthError ADT v errors.ts pres Data.TaggedError pokryva 9 tagu (UserAlreadyExists, UserNotFound, InvalidCredentials, SessionNotFound/Expired, DbError, ValidationError, ConfigMissing, TokenInvalid). Runtime adapter pres ne exhaustive switchuje na HTTP statusy.
Effect services potrebovaly D1 abstrakci. Db service (Context.Service v Effect 4) drzi drizzle handle + raw D1Database a expose run(operation, fn) helper co maps Promise na Effect s DbError. D1Binding je separatni service co se providuje z env.DB v runtime adapteru.
Nastavení zablokováno: CF API 10053 binding conflict (živý worker měl BUILD_ACCESS_TOKEN jako var). Fix: nejdřív deploy (odstraní var binding), pak wrangler secret put BUILD_ACCESS_TOKEN. Zdokumentováno v access-setup.md.
access-setup.md: přidána tabulka vars vs secrets (BUILD_ACCESS_CLIENT_ID public var, BUILD_ACCESS_TOKEN/CF_API_TOKEN/WEBHOOK_URL secrets), pořadí setup (deploy first → secret put), per-owner CF token flow přes dočasný AUTH_ADMIN_TOKEN.
BUILD_ACCESS_CLIENT_ID (public client_id service tokenu) přidán do vars — hodnota je v kódu, není secret. WEBHOOK_URL přidán s default prázdný string. BUILD_ACCESS_TOKEN přesunut z vars do secrets-only (wrangler vars a secret nemůžou mít stejné jméno).
Nový helper src/common/notifyWebhook.ts: POST JSON payload na URL, swallows all errors, canceluje response body (aby nezůstala otevřená CF Workers connection). Exportován z common/index.ts.
buildRunner.ts po recordAndAck zkontroluje env.WEBHOOK_URL — pokud nastaven, zavolá notifyWebhook s payload { repoId, buildId, status, commitSha }. Empty string je falsy, takže nenastaven = skip.
cf-typegen přegeneroval worker-configuration.d.ts s novými vars (BUILD_ACCESS_CLIENT_ID, WEBHOOK_URL). BUILD_ACCESS_TOKEN přidán do env-augment.d.ts jako ambient type. Typecheck prošel čistě.
Git clone v containeru posílal HTTP basic auth, ale git.vyvoj.dev je za CF Access (vyžaduje Client-Id + Client-Secret headers). Přepnuto na git -c http.extraheader syntax — oba headers předány jako env var přes setEnvVars.
R2 nemá nativní cross-account replikaci. Zdokumentovány tři varianty: rclone (cross-account), Scheduled Worker (same-account), Super Slurper (jednorázový export).
run-013-r2-backup.done.md s design doporučením a poznámkou o bucket.list() paginaci (max 1000 objektů/call) přidáno do sprint dokumentace.
Sprint 005 adresář chyběl — vytvořen, sprint.md a run-012 soubory zapsány ve stejném formátu jako Sprint 004.
Workers Observability auto-zachycuje HTTP metadata (IP, Ray, path, status) — 7 dní retence. Pro delší retenci doporučen Logpush → R2, zdokumentováno v sprint.md.
git.ts nemělo žádný logging. Přidány tři audit eventy (git:service-advertise, git:fetch-start, git:push-start/auth-failed) s owner/repoId/IP kontextem.
Logpush doporučení přidáno do sprint.md s porovnáním variant (Workers Observability vs Logpush → R2 vs externí) a konkrétními wrangler příkazy.
BuildsCard potřeboval překlady pro 8 stavů buildu a 4 sloupce tabulky. Přidáno 18 klíčů admin.builds.* + help.step6_title do dict.cs.ts.
Uživatel nevěděl, že server umí auto-deploy. Přidán Step 6 'Automatický deploy workerů' v /napoveda — vysvětluje detekci, frontu, kontejner a kde najít výsledek. e2e-notes.md dokumentuje postup pro manuální ověření.
Sprint 004 neměl zdokumentovanou CI/CD architekturu. Přidána sekce do access-setup.md (architektura push→deploy, potřebné secrets, diagnostika, limity). Sprint audit: 5/6 DoD splněno, E2E deferred.
Admin UI nemělo viditelnost buildů. Přidán BuildsCard.tsx s tabulkou buildů, expandovatelným log tailem a status badges (zelená/červená). Data přicházejí ze server-side fetchnutého getBuildHistory() RPC v adminPage.ts.
Secrets v build logu by prosakly do DO storage. Přidán redactSecrets.ts s regex chainem (Bearer/JWT/hex/base64/KEY=) a truncateLog pro UTF-8 safe ořez. AVA test pokrývá 10 vzorů.
RepoDO neměl kde ukládat výsledky buildů. Přidán BuildRecord typ (stav, log tail, version_id) do repoState.ts a recordBuild/getBuildHistory RPC do repoDO.ts — ring buffer 50 záznamů per repo.
handleBuildMessage byl no-op (log + ack). Nahrazen skutečným buildRunner.ts: getSandbox → setEnvVars → git clone přes credential helper → checkout → npm ci → build → wrangler deploy → recordBuild do DO. destroy() vždy v finally.
Build runner potřeboval worker-context test bez module-level mockování. Řešení: sandboxFactory parametr s default getSandbox, testy injektují mock. Pokrývá happy path (success + version_id parsing) + clone fail + deploy fail.
Dockerfile byl jen v docs/sprints/004-ci-cd/Dockerfile.spike (proof-of-concept), produkce ho neměla. Promotnul jsem ho do rootu jako Dockerfile, wrangler pinnutý na 4.37.1 (= devDep verze v package.json) aby kontejner a dev tooling mluvily stejným CLI. Base image cloudflare/sandbox:0.7.0 + git + ca-certificates + smoke check.
AUTH_DO neměl kde držet CF API tokeny per owner — build runner potřebuje předat reálný token wrangler deploy v containeru. Přidal jsem typed key cfTokens do AuthStateSchema + RPC getCfToken/setCfToken (plaintext, nehashujem — token jde do CLI, ne pro verifikaci). HTTP endpoint POST /auth/api/cf-token v auth.ts (Bearer admin gate, ne admin.ts — ten má per-owner Basic auth = nesprávný gate pro system secret). UI pro ownery je až Sprint 005.
Wrangler.jsonc neměl Sandbox container ani DO binding — runtime tedy neuměl spustit sandbox.exec(). Přidal jsem containers[] (lite, max_instances 1 per architecture §2 R3 serial build), Sandbox DO binding, migrations v3 new_sqlite_classes [Sandbox] (Sandbox SDK vyžaduje SQLite-backed DOs). BUILD_ACCESS_TOKEN secret zůstává mimo JSON — manuálně přes wrangler secret put.
Sandbox SDK nebyl v projektu — bez něj nešlo importovat Sandbox class ani postavit container binding. Nainstaloval jsem @cloudflare/sandbox@0.7.0 (verze sladěná se Sandbox Docker image) a re-exportuju Sandbox class z src/index.ts; Workers runtime ji potřebuje vidět na worker entry, jinak DO binding nezaregistruje.
Producer logika potřebovala test coverage před plug-in build runneru v Run 010. Vitest worker test seedne pack-first repo s definovanou root tree a verifikuje 3 scénáře: worker repo enqueue / no-config skip / re-push HEAD-unchanged. Vše pass.
Run zadání předpokládalo že AUTH_DO je Drizzle SQLite, ve skutečnosti je to typed KV storage (asTypedStorage). Architecture.md D4 + D5 + Decision summary opraveny, cf_api_token storage přesunut do Run 010 kde ho build-runner skutečně konzumuje. Také D5 implementace upravena z DO RPC na Worker-side helper, aby se neporušilo single-hop boundary.
Bez transakčního trackingu by enqueue na feature-branch push nebo re-push téhož commitu vždy zařadil duplicitní build job. Přidán RPC markBuildHead na RepoDO + lastBuildHead key ve schema, single-threaded DO storage dělá read-then-write transakci natural — žádný blockConcurrencyWhile potřeba.
Detekce musela běžet Worker-side (ne v DO), jinak by Worker → DO → R2 porušila single-hop pravidlo z AGENTS.md. Nová detectWorkerProject v src/maintenance/buildDetect.ts čte HEAD commit a root tree přes existing readCommit/readTree, vrací tagged result (shouldBuild + configType jsonc/toml).
Producer-side build pipeline chyběla celá: nový buildEnqueue.ts s 3-step guard chain (HEAD → markBuildHead → detect → BUILD_QUEUE.send), dispatcher rozšířen v queue.ts o build kind, wire-up v routes/git.ts po addRepoToOwner s vlastním try/catch, BUILD_QUEUE binding v wrangler.jsonc.
Nový test v package.spec ověřuje že non-JSON upstream odpověď produkuje 502 s error.contains('malformed').
Pokud npmjs.org vrátí 200 ale non-JSON tělo (HTML chybová stránka při výpadku), worker padal na náhodné 500. Teď try/catch okolo JSON.parse vrací strukturované 502 Bad Gateway.
Zápis tarballu do R2 cache (ctx.waitUntil) mohl selhat tiše s unhandled rejection. Přidán .catch s logováním package+key, klient dostane stream bez ohledu na cache.
Fallback tarbally se v R2 hromadily bez TTL — pro popular balíčky to mohlo růst do GB ročně. Daily scheduled volá funkci, která projde R2 list s customMetadata, filtruje source=fallback + uploaded > 90 dní a batch maže.
wrangler.jsonc triggers.crons '0 3 * * *' — denně ve 3:00 UTC. Cron registrace ověřena přes CF API workers/scripts/.../schedules.
Vitest 3 testy: recent fallback survive, own packages never deleted, no side effects. Backdate uploaded netestováno (R2 binding read-only), jen invariants pro in-TTL chování.
Astro 6 adapter generoval jen fetch handler, scheduled cron nešel jednoduše přidat. Vytvořen custom worker entry kombinující Astro handle (přes @astrojs/cloudflare/handler) a scheduled handler.
Produkční build container chybí specifikaci. Vytvořen Dockerfile.spike extending cloudflare/sandbox:0.7.0 s git + wrangler@3 + smoke step; komentář o cílové image size ~600 MB jako vstup pro Run 010 produkční Dockerfile.
Feasibility Sandbox SDK ve workeru nebyla nikdy ověřena. Vytvořen standalone worker v docs/sprints/004-ci-cd/spike/ (4 soubory, mimo hlavní package.json) s POST /spike-build endpointem volajícím sandbox.exec. Spuštění blokované absencí Dockeru v dev env — acceptance pro Run 010.
Sandbox SDK je v cloudflare:sandbox-sdk skillu i context7 docs. Tři queries (exec+filesystem, lifecycle, wrangler containers binding) → potvrzeno: sandbox.exec vrací {stdout,stderr,exitCode,success}, getSandbox podporuje sleepAfter/keepAlive, containers binding v wrangler.jsonc je zaužívaný shape.
Bez explicitních rozhodnutí by Run 009-011 psaly hypotézy. Vytvořen kanonický doc s Mermaid push→deploy flow, pěti rozhodnutími (delivery, lifecycle, timeout, secrets, detekce), 9-řádkovou failure modes tabulkou a decision summary jako vstupem pro Run 009.
Run 012 nepokrýval success path proxy stahování. Test ověří round-trip: metadata fetch z fallbacku má dist.tarball přepsaný na vlastní host, druhý request tarball stream vrátí mock body byte-by-byte.
Scoped balíčky byly bug-prone (regex transformace + scope strip). Test ověří úplný řetězec: URL-encoded /@types%2Fnode metadata → rewrite na double-scope, navazující tarball download projde 5-seg scoped route a strip-scope upstream fetch.
Po prvním cache miss by druhý fetch měl jít z R2 bez upstream callu. Test pre-populuje BUCKET (eliminuje waitUntil timing flakiness) a ověří, že read s cache-hit metadatami vrátí body bez jakéhokoli mocku k upstream.
Service změnila kontrakt z R2ObjectBody na ReadableStream — router teď wrapuje do Response s Content-Type: application/gzip a předává c.executionCtx pro async cache write.
Fallback metadata vracela URL na npmjs.org — v plném proxy režimu klient pak chodil mimo Repoflare. Teď JSON.parse + per-version dist.tarball rewrite na vlastní host; scoped balíčky se transformují na Repoflare double-scope formát, aby matchly existující 5-segment scoped route.
Návod byl psaný jen pro plný proxy režim, scope-first nebyl vysvětlen. Sekce 2 přepsána na scope-first jako doporučené, sekce 5 přejmenována na 'Plný proxy režim' pro lockdown setupy s anchor odkazem mezi nimi.
Tarbally veřejných balíčků se stahovaly přímo z npmjs.org → v lockdown sítích nešly a R2 cache se neplnila. Service teď na R2 miss fetchne upstream, tee() split stream do cache i klientovi, ctx.waitUntil schová R2 put na pozadí.
Texty u prázdných seznamů byly nekonzistentní ('Žádné tokeny.' vs jiné varianty). Sjednoceno na vzor 'Zatím tu nejsou žádné X.' u tokenů i balíčků.
Astro default 404 vrací plain text. Přidána vlastní stránka 404.astro s BaseLayout, headerem a CTA tlačítky na úvod a dashboard.
Při zamítnutí oprávnění na clipboard padalo unhandled promise. Tlačítka pro kopírování teď mají tristate (idle/copied/failed) s 2s flashí 'Kopírování selhalo'.
Chybová hláška u tokenu byla jen tichý červený text. Nyní má výraznější border, dismiss tlačítko a aria-describedby, takže screen reader chybu spojí s polem názvu.
Klávesnicová a screen-reader přístupnost. Šipky '←' v back-link odkazech obaleny aria-hidden, fokus-visible ring doplněn na plain <a> odkazy, search input dostal aria-label a počet je aria-live.
Middleware isAstro allowlist neměl /docs. Rozšířen o pathname === '/docs' — request projde Access verify a renderuje docs.astro.
Návštěvníci neměli kde najít jak registry používat. Vytvořen src/pages/docs.astro: 6 sekcí v CS (vygenerovat token z dashboardu, nastavit .npmrc per-host i per-scope, publish, install s npm.org fallback, scoped balíčky, troubleshoot 401/403/409).
Homepage měla 3 tlačítka (Dashboard, API dokumentace, GitHub) — bez návodu. Přidán Návod button mezi Dashboard a API dokumentace, variant=outline (secondary CTA).
Access app chránila jen / a /dashboard*. /docs by bez Access destination obešel ochranu (Astro middleware sice gate-uje, ale CF Access by header neposlal). Přidán třetí destination 'npm.vyvoj.dev/docs' (exact match) do existing Access app via API.
Catch-all middleware z Run 002 delegoval všechny non-/dashboard requesty na Hono. Pro novou homepage / je potřeba povolit Astro render. Přidána explicit kontrola isAstroRoot — / projde na next() (Astro), zbytek non-/dashboard dál na Hono (NPM protokol).
https://npm.vyvoj.dev/ vracelo Hono 404 — návštěvník netušil co je to za službu. Vytvořen src/pages/index.astro s minimal landing: heading, 2 odstavce popisu (D1+R2+CF Access+bearer tokeny, jak nastavit .npmrc), 3 Buttons (Otevřít dashboard primary, API dokumentace outline, GitHub ghost external s rel=noopener).
Run 003 nainstaloval jen Button. Run 005-007 napsaly komponenty s plain HTML inputy/selecty/error bloky — proti shadcn rule 'always use components'. Doinstalován Input, Select, Card, Alert (+ Separator transitively) přes shadcn CLI. Z select-content.svelte odstraněn defaultní shadow-md (skill no-shadow rule).
CreateTokenForm měl plain input pro name, plain select pro scope, custom red text pro error, custom green border pro success token reveal — nekonzistentní s design systemem. Refactorováno: Input pro name, Select.Root/Trigger/Content/Item pro scope (s triggerLabel $derived), Alert variant=destructive pro error, Alert default pro success s code+copy uvnitř Description.
PackageSearch měl plain <input type=search> s ručně napsanými border classes. Nahrazeno za <Input type=search class='max-w-md'> — méně klutterového kódu, konzistentní s ostatními formuláři.
Delete button v tokens listu byl plain <button> s ručně napsaným destructive border + hover bg. Nahrazeno za <Button variant=destructive size=sm>. Astro renderuje SSR-only (žádný client:* directive), zero JS shipped pro tento widget — submit je native form behavior.
List balíčků se nedal prohledávat — při růstu počtu nepoužitelný. Vytvořen PackageSearch.svelte: Svelte 5 island s runes ($state pro query, $derived pro filtered list), case-insensitive substring match. Empty state rozlišuje 'no packages' vs 'no match for query'.
Detail page nezobrazila install command — user musel hádat package name pro npm install. Vytvořen InstallSnippet.svelte: code block s 'npm install <name>' + Copy button (navigator.clipboard) s 2s 'Zkopírováno' flash, stejný pattern jako CreateTokenForm.
Inline list rendering v index.astro znemožnoval interaktivní filter. Refactor: list přesunut do PackageSearch islandu (mount client:load, packages prop), SSR fetch zůstává v frontmatter. Index.astro je teď tenký shell.
Detail page chyběl install snippet. Mount InstallSnippet islandu mezi description a README sekci s pkg.name jako prop, client:load pro interaktivitu copy buttonu.
Pod /dashboard/tokens neexistovala žádná stránka. Vytvořen tokens/index.astro: SSR fetch tokenů, masked display (jen xxxx…yyyy, plaintext nikdy v listu), scope summary, formátované createdAt, delete form per řádek + mount Svelte create islandu nahoře.
Form pro create token potřebuje JS pro one-shot reveal plaintext tokenu (no-JS fallback by chtěl session storage flash). CreateTokenForm.svelte: Svelte 5 island s runes ( pro inputs/loading/error/copied, pro canSubmit), fetch POST → success render token + Copy button (navigator.clipboard) s 2s flash 'Zkopírováno'.
Tokens stránky neměly query helper, pattern stejný jako u packages. Přidán listTokens v dashboard-queries (findMany ordered desc by createdAt) + samostatný mask-token util pro UI display ve formátu xxxx…yyyy bez plaintextu.
Bez Astro endpointu by Svelte island neměl kam fetchnout. Hono /-/npm/v1/tokens potřebuje bearer token (assertTokenAccess) — CF Access user ho nemá. Vytvořen create.ts: POST endpoint pod /dashboard/tokens/create (uvnitř middleware gate), zod validace + delegace na tokenService.createToken, vrací 201 s plaintext tokenem.
Form delete potřeboval endpoint pod /dashboard/* (NE /api/*) jinak middleware skipne a Hono catch-all vrátí 404. Vytvořen [token]/delete.ts: POST handler, deleteToken + 303 redirect zpět na list. CF Access JWT funguje jako de facto CSRF token (cross-origin requesty bez něj dostanou 403).
Detail page chyběl celý. Vytvořen [...name].astro (rest param kvůli scoped balíčkům @scope/pkg), fetch package + latest release, render README přes renderMarkdown. 404 pokud balíček neexistuje, fallback hláška pokud manifest nemá readme field.
Dev escape hatch v middleware nechá Astro.locals.user = null bez Access headeru. Bez gate by neautorizovaný request prošel na dashboard. Přidán shared util require-user, který vrací Response 403 nebo AccessUser — explicit return je idiomatic v Astro page frontmatter (oproti throw).
Dashboard stránky neměly query helpery, raw SQL na page by zaplevelilo frontmatter. Přidán dashboard-queries.ts: listPackages (findMany ordered desc) + getPackageWithLatestRelease (2 queries — package, pak release dle distTags.latest). Relational with-filter nešel kvůli závislosti na parent column.
marked v18 neescapuje raw HTML ani nesanitizuje URL schémata — README publisher může injectnout <script> nebo [link](javascript:...). Přidán render-markdown.ts: Marked instance s renderer.html escape + allowlist URL schemes (http/https/mailto/relative) v link/image renderer.
Pod /dashboard byl prázdný placeholder z Run 003. Index page nyní SSR fetchuje balíčky přes listPackages, zobrazí name + latest version + cs-CZ formátované updated_at, link na detail. Empty state pro 0 balíčků.
Astro middleware měl jen Hono delegaci a /dashboard/* puštěné dál veřejně. Přidána větev: pro /dashboard* ověří Access JWT, jinak delegate na Hono beze změny. NPM protokol nezměněn.
JWT verify extracted do samostatného lib/access-jwt.ts. Module-level JWKS singleton (createRemoteJWKSet) sdílený přes requesty, jwtVerify s issuer + audience, payload.email uložen do Astro.locals.user.
TEAM_DOMAIN a POLICY_AUD přidány do vars v wrangler.jsonc s empty defaults — produkce doplní podle access-setup.md. Wrangler types regenerated, env.* je type-safe.
App.Locals neměl typ pro přihlášeného uživatele. Nový src/env.d.ts augmentuje App.Locals o user: AccessUser | null, takže pages pod /dashboard/* můžou číst locals.user.email type-safe.
Žádný návod, jak Access nakonfigurovat. Nový docs/access-setup.md popisuje Self-Hosted Application + Policy + zkopírování AUD tagu, dev escape hatch a troubleshooting.
shadcn-svelte CLI v1.2.7 vyžaduje interaktivní preset selection, neslo skriptovat. Manuálně vytvořeny components.json + src/lib/utils.ts s cn helperem + src/styles/global.css s neutral oklch tokens. Outcome ekvivalentní.
Dashboard byl bare HTML bez stylingu. Vytvořen src/layouts/BaseLayout.astro (slot + global.css import + viewport meta), dashboard/index.astro ho používá s Tailwind container utilities.
Plán chtěl ověřit Svelte hydration na CF Workers. pnpm dlx shadcn-svelte add button vygeneroval Button komponentu, dashboard ji ukazuje 2× — client:load (Svelte island, hydratovaný) a outline (SSR-only). Bonus: check-types přepnut z tsc na astro check kvůli Svelte type re-exports.
astro.config.mjs měl jen adapter, web UI nemělo žádný styling. Přidán svelte() integration + @tailwindcss/vite plugin + $lib alias přes vite.resolve. Tailwind classes a Svelte 5 hydration teď fungují v .astro souborech.
Původně catch-all endpoint v src/pages/[...npm].ts vyhodil 404 pro /_ui/, protože Astro routing prioritizuje endpoint nad page. Přepsáno na src/middleware.ts který volá next() pro /dashboard/* a app.fetch() pro vše ostatní — žádný preempt.
wrangler.jsonc: main na @astrojs/cloudflare/entrypoints/server (Astro 6 v13 unified entry), přidán account_id, custom_domain route na npm.vyvoj.dev, kv/r2/d1 bindings s reálnými ID. Worker name přejmenován na npm-vyvoj-dev.
vitest.config.ts měl stale wrangler.toml path a src/index.ts main z Run 001. Fix na wrangler.jsonc + nový hono-app.ts main. package.json: dev na astro dev, check-types doplněn o astro sync (Locals types). Bonus: turbo/biome/knip/sherif odstraněny.
Lokální astro build + vitest run prošly (55/55). Wrangler deploy --dry-run čistý. Po deploy do produkce smoke test přes curl: /dashboard/ → Astro hello-world 200, /_/docs → Scalar 200, /<package> → npm proxy 200, /-/npm/v1/tokens admin auth 200.
Hono app byl přímo v src/index.ts a sloužil jako Worker entry. Přesun do src/lib/hono-app.ts s dual exportem (named app + default app) — named pro Astro middleware delegaci, default kvůli vitest pool worker main.
git mv apps/api → apps/worker, package name @npflared/api → @npflared/worker. biome.json path update. Workspace glob apps/* nepotřeboval změnu.
wrangler.toml → wrangler.jsonc se referencí, observability:enabled, compatibility_date 2026-05-10. Bindings (D1, R2, vars) zachovány s původními placeholder hodnotami.
pnpm add astro 6.3, @astrojs/cloudflare 13.5, @astrojs/svelte 8.1, svelte 5.55, jose 6.2, marked 18.0. Wrangler bumped 4.58 → 4.90 protože adapter peer dep požaduje ^4.83.
astro.config.mjs minimální (jen adapter cloudflare()), svelte() zatím vypnutá komentem - aktivuje Run 003. Hello-world stránka pod /_ui/ namespace aby budoucí catch-all na rootu nekolidovala s npm protokolem.
pnpm astro build prošel s 0 errors. Build vyprodukoval dist/client + dist/server (Astro 6 nová struktura, ne stará dist/_worker.js). Acceptance kritérium runu splněno.
Hero search bar v patičce ukazoval mock 10 fake položek. Teď se po 250ms debounce zeptá API a klik na výsledek vede přímo na detail spotu. Generation-token guard zahodí pomalý response, kdyby user mezitím doťukal další písmeno.
Vyhledávací input nahoře na /mapa byl decorative — neposílal nic. Obalený do <form action=/mapa>, Enter teď refreshuje stránku s ?q=… a backend filtruje. Stávající filtry (amenities, terrain, rating) přežijí přes hidden inputy.
Backend listSpots neuměl text search — UI parametr ?q=... se ignoroval. Teď filtruje přes LIKE na názvu, regionu a popisu. SQLite default ASCII case-insensitive, diakritika přesný match (pro v1 dataset stovek spotů OK).
Záložka Oblíbené v profilu místo placeholderu načítá první stránku skutečně uložených spotů (přes /api/favorites) a zobrazí je stejnými kartami. Prázdný stav má vlastní hlášku a tlačítko na mapu.
Číslo u záložky Oblíbené bylo hardcoded 0. /api/users/me/stats teď vrací reálný počet z DB a profil ho zobrazí v záložce.
Smoke test produkce zbývá ručně po deploy: kliknout ♡ na kartě, ověřit že se ve /ucet objeví, refresh drží stav, druhý klik odebere.
♡ tlačítko na kartě a detailu spotu chybělo — bylo jen statický placeholder. Přidaný islet s animací prázdné→plné srdce, který reaguje hned (optimistic), a v případě chyby se vrátí do původního stavu. Pro nepřihlášené přesměruje na /sign-in.
Karty spotů (homepage Top spoty + /ucet) i stránka detailu teď mají funkční ♡. Backend vrací stav přes ?include=isFavorited, takže po refreshi srdce drží uloženou hodnotu.
Vytvořena migration 0007 + Drizzle schema pro user_favorite tabulku — relation many-to-many (user, spot) s unique(userId, spotId), cascade delete + 2 indexy pro listFavorites a include batch lookup. Před prvním deployem fronend musí proběhnout 'wrangler d1 migrations apply hammocknook-prod --remote'.
Přidán Effect Schema FavoriteResponse (id, userId, spotId, createdAt) v libs/api-models/src/favorite.ts + isFavorited optional Boolean field na SpotResponse. Reuse SpotListResponse pro GET /api/favorites — žádný extra DTO.
FavoritesApi HttpApiGroup s 3 endpointy v libs/api-models/src/api/favorites.ts: POST/DELETE /api/spots/:spotId/favorite + GET /api/favorites (auth, vrací plný SpotListResponse). Wired do HammocknookApi.
Handlery v api/src/api-groups/favorites.ts: idempotent POST přes onConflictDoNothing+SELECT fallback, idempotent DELETE bez existence check, listFavorites s INNER JOIN spot + cursor pagination + privacy filter (ADMIN vidí vše, jinak isPrivate=false OR createdBy=me).
?include=isFavorited query flag: listSpots batch lookup přes inArray() na page IDs (1 query pro celou stránku), getSpot single SELECT row check. Pro neauth user se flag tiše ignoruje, isFavorited se neobjeví v response.
Profil potřeboval real čísla místo hardcodovaných. Přibyl GET /api/users/me/stats — vrací počet spotů, recenzí, hlasů a datum registrace. 4 paralelní COUNT queries přes Effect.all, jeden D1 roundtrip.
Endpoint potřeboval session ověření a živou dokumentaci. Mount pod /api/users/* za sessionMiddleware, schema UserStatsResponse v api-models a OpenAPI 3.1 metadata pro Scalar UI.
Profil ukazoval jen 'X sdíleno' a hardcoded '14 lesů'. Header teď má 3 živá počítadla (sdíleno · recenzí · hlasů) + sekce 'S Hammocknookem od {měsíc rok}' z reálného joinedAt.
Taby na profilu byly statické. Teď fungují přes ?tab= URL parametr — 'Moje spoty' default ukazuje cards, 'Oblíbené' prázdný state (přijde Sprint 012). 'Výlety'/'Nastavení' zůstávají vizuálně disabled.
Doposud chybělo 9 auth endpointů v dokumentaci. Doplněn celý better-auth-parity router (sign-up/sign-in email i sociální, OAuth callback, refresh, sign-out, forget+reset password, verify-email). Body shapes jako generic object placeholder s odkazem na better-auth, protože ne Effect Schema.
Hlasování (upvote/downvote) bylo neviditelné v Scalar UI. Přidán PUT endpoint s VoteInput/VoteResponse schemas — body popsán jako idempotent toggle (1 upvote, -1 downvote, 0 remove) a response counters jako celkový stav po batch update.
Admin endpointy (role flip + anonymizace) chyběly. Doplněny s tagy ['Admin'], security pro session cookie a description vysvětlující last-admin guard (403 při pokusu sundat posledního ADMINa, 409 při pokusu ho anonymizovat).
Build celého OpenAPI dokumentu (Effect Schema → JSON Schema walk + ref rewrite) běžel při každém GET /openapi.json — zbytečný CPU cost. Přidán module-level cachedDocument; první request build-uje, další serve cached. Cold-start race je idempotent, takže safe.
Curl /openapi.json: 200 application/json, 31KB validní OpenAPI 3.1, 18 paths / 24 operations / 13 schemas. Sprint 016 hotovo, sprint.md status změněn na 'hotovo (čeká audit)'.
JSON Schema výstup byl ošklivý — Schema.Number povoluje NaN/Infinity přes string encoding (anyOf branche). Swap na Schema.Finite (dedikované schema, ne jen check) tyhle větve odstraní → lat/lng/rating mají v Scalar UI čisté {type:'number'}.
Spots měly hand-tagged jen GET list (R059 PoC). Doplněno POST /api/spots, GET /api/spots/{id}, PATCH /api/spots/{id}, DELETE /api/spots/{id} — security pro auth-protected, response refs na Spot.* schemas, error responses 400/401/403/404 dokumentovány.
Tři Photos endpointy chyběly v dokumentaci — POST link userFile ke spotu, GET list (s visitorOnly filtrem co odděluje hero galerii od galerie návštěvníků), DELETE unlink (multi-owner check: file owner / spot owner / ADMIN). Photo.* schemas přidány do components.
Čtyři Reviews endpointy doplněny: POST + GET pod /api/spots/{spotId}/reviews, PATCH + DELETE top-level /api/reviews/{reviewId}. Doc původně zmiňoval že ADMIN nesmí PATCH cizí recenze — code skutečně dovoluje, opraveno v review fází. Review.* schemas v components.
Curl /openapi.json vrátil 200 application/json (23KB validní OpenAPI 3.1, 6 paths / 12 operations / 11 named schemas). SpotResponse.lat má teď shape {type:'number'} bez anyOf NaN/Infinity větví. Typecheck/oxlint čistý, 35/35 vitest pass.
Endpoint pro list spotů (12 query params, response refs na SpotListResponse) je první a vzor pro R060/R061. Tagy, summary, parameters jsou ručně — Effect Schema neumí URL routing, takže path/query metadata se píší per-route.
Routes /docs (Scalar UI) a /openapi.json (runtime spec) namountovány v app.ts mimo blok session middleware — jsou public, žádný auth pro doc browsing. Pozice mimo chained routes const je záměr (nepatří do Hono RPC AppType, web wrapper je nepotřebuje).
curl /openapi.json vrátil 200 application/json (16KB validní OpenAPI 3.1: openapi/info/servers/components/paths). curl /docs vrátil 200 text/html (Scalar HTML shell). Typecheck/oxlint čistý, 35/35 vitest pass.
Kořenový openapi.json byl mrtvý NestJS dump z dob před přepisem na Hono. generate-openapi script ukazoval na neexistující soubor a táhl s sebou jiti devDep. Vše smazáno (api + root), místo file dumpu nasazujeme runtime endpoint. @scalar/hono-api-reference instalován jako základ pro UI.
Effect Schema (single source of truth pro DTO) se převádí na OpenAPI components přes builtin converter. Vlastní openapi.ts builder volá Schema.toJsonSchemaDocument na pojmenované schemas a toMultiDocumentOpenApi3_1 přepíše refy do OpenAPI tvaru. Bez ručního duplikování shape v Zod nebo jiném schema lib.