Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions .claude/skills/backend-api-explore/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,15 +225,21 @@ row-count overview.
things in order:
1. **Is the caller a confirmed participant?** Waitlisted users are refused by
design (`charles` in H1).
2. **Is it capability-gated?** `SetPreference`, `RemovePreference`, `Propose`
and submission RPCs need a capability that only
`HackathonService.SetCapabilities` can switch on — and `cmd/seed` creates
no `HackathonState` row, so **in seeded data these always refuse**. This is
configuration, not a bug. Reproducible today:
2. **Is it capability-gated, and is that capability on _in this hackathon_?**
`SetPreference`, `RemovePreference`, `Propose` and the submission RPCs are
gated on a capability, which is really a casbin row written alongside a
`HackathonState` boolean. `cmd/seed` writes both (`seedCapabilities`) with
a **different set per hackathon**, so the same call succeeds in one seeded
hackathon and is refused in the next — configuration, not a bug. Read the
set instead of guessing:
```
just rpc::as alice aliceandbob hackathon.ProjectService/SetPreference '{"projectId":"<uuid>"}'
→ Code: PermissionDenied
just rpc::as alice aliceandbob hackathon.HackathonService/Get \
'{"hackathonId":"<uuid>"}' | jq '.hackathon.state'
```
As seeded: AI Innovation has register/propose/preferences/submissions on,
Climate Tech the same minus register, Internal Product Sprint only
vote/view-results. A hackathon created through `HackathonService.Create`
gets a state row with every capability `false`.
3. **Is the caller an Owner where the rule grants Member?** The casbin model
has no role inheritance, so an owner is refused by a Member-scoped policy.

Expand Down
62 changes: 40 additions & 22 deletions .claude/skills/backend-seeding/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ description:
Adapting the Hackagon dev seed fixture (components/backend/cmd/seed) — adding
or changing hackathons, users, projects, teams, submissions and phases, the
casbin roles that must accompany every DB row, the sentinel-based idempotency
and how to force a re-seed, and the HackathonState gap that makes seeded
hackathons refuse capability-gated mutations. Use when asked to add or change
dev/test data, make a scenario reproducible locally, or fix a seeded hackathon
that nobody can act in.
and how to force a re-seed, and the HackathonState plus casbin rows that
decide what participants may actually do in each seeded hackathon. Use when
asked to add or change dev/test data, make a scenario reproducible locally, or
fix a seeded hackathon that nobody can act in.
---

# Adapting the seed
Expand Down Expand Up @@ -120,24 +120,42 @@ are hardcoded constants at the top of the file; the admin's comes from config.
Adding a genuinely new user means adding them to Keycloak too
(`tools/configs/keycloak/`), not just here.

## Known gap: no HackathonState row

`cmd/seed` never creates a `HackathonState`. That row is what holds the six
capability booleans, and `HackathonService.SetCapabilities` is what flips them
_and_ writes the matching casbin policy. With no row and no call, **every
capability-gated mutation refuses in seeded data** — `SetPreference`,
`RemovePreference`, `Propose`, submissions. Confirmed live: alice, a confirmed
member of the seeded Climate Tech Hackathon, gets `PermissionDenied` from
`SetPreference`.

This is the single biggest reason a seeded hackathon feels broken. If you're
asked to make the seed usable, this is the fix: create a `HackathonState` per
hackathon with capabilities appropriate to its phase, and write the matching
casbin rows — remembering that `SetCapabilities` grants to `Member`, and the
model has no inheritance, so an owner needs their own grant.

Full write-up, including a partial-write bug in `SetCapabilities` when the state
row is missing: `mydocs/docs/backend-tickets/project-preferences-capability.md`.
## Capabilities: every hackathon needs `seedCapabilities`

A capability is **two writes, not one**: the boolean on `HackathonState` and a
casbin policy row. The enforcer only ever reads the policy, so a state row on
its own grants nothing and a hackathon without both is one nobody can act in —
this used to be the single biggest reason a seeded hackathon felt broken.

`seedCapabilities` (`main.go:67`) does both, and every hackathon must call it.
It copies the role each capability grants to from `SetCapabilities`
(`hackathon_service.go:616-653`) so seeded hackathons behave like ones created
through the API — registration granting to `*` rather than a role, since the
point is that a non-member can join, and `vote` writing **two** rows
(`Vote:Create` plus `VoteCategory:Read`, without which a member cannot see what
there is to vote on). It also takes the phase an organizer has declared current,
or nil.

One row goes **beyond** the handler on purpose: team preferences are granted to
`Owner` as well as `Member`. The handler grants `Member` only and the model has
no inheritance, so an owner cannot express a preference — judged wrong and
tracked in `mydocs/docs/backend-tickets/project-preferences-capability.md`
(which also documents a partial-write bug in `SetCapabilities` when the state
row is missing). Drop that row if you would rather the fixture mirror the
handler exactly.

The sets are chosen per hackathon so the fixture spans the interesting states:

| Hackathon | On | Current phase |
| ----------------------- | ------------------------------------------- | ------------- |
| AI Innovation 2026 | register, propose, preferences, submissions | none |
| Climate Tech 2026 | propose, preferences, submissions | Hacking |
| Internal Product Sprint | vote, view-results | Demo |

So "capability-gated mutation refuses" is now a question about _which_
hackathon, not about the seed as a whole. Phase tags stay decorative: a phase
tagged `vote` in a hackathon whose `voting_enabled` is false is a legitimate
fixture — it says when voting is meant to happen, not that it is open.

## After changing the seed

Expand Down
14 changes: 10 additions & 4 deletions .claude/skills/backend-service-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,16 @@ auto-enable or disable any capability."_ `SetCurrentPhase` sets
and never touches state or casbin. Advancing a phase changes what the UI
displays, not what anyone may do.

**`cmd/seed` creates no `HackathonState` row at all.** Seeded hackathons
therefore report no capabilities and every capability-gated handler refuses. If
a mutation returns `PermissionDenied` in seeded data, check this before
suspecting your code.
**`cmd/seed` writes both halves itself.** `seedCapabilities`
(`cmd/seed/main.go:67`) creates each hackathon's `HackathonState` row _and_ the
casbin rows that go with it, copying the role each capability grants to from
`SetCapabilities` — plus one row `SetCapabilities` does not write: team
preferences to `Owner` as well as `Member`, so the fixture shows the intended
behaviour ahead of the handler (see
`mydocs/docs/backend-tickets/project-preferences-capability.md`). The set
differs per hackathon, so a `PermissionDenied` in seeded data means that
capability is off in _that_ hackathon — read `state` from `HackathonService.Get`
before suspecting your code.

Known gaps are written up in `mydocs/docs/backend-tickets/` — start with
`project-preferences-capability.md`, which traces the whole chain for
Expand Down
8 changes: 5 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,11 @@ These hold across the whole codebase; the skills explain the mechanisms.
of surprising `PermissionDenied`.
- **A phase does not control what participants may do.** `Phase.capabilities` is
informational; `HackathonState` plus its casbin rows is what actually gates
anything, and `SetCurrentPhase` does not touch either. `cmd/seed` creates no
`HackathonState`, so capability-gated mutations refuse in seeded data. Details
in **backend-service-dev**; diagnosis in **backend-api-explore**.
anything, and `SetCurrentPhase` does not touch either. `cmd/seed` writes both
halves (`seedCapabilities`) with a **different capability set per hackathon**,
so a refused mutation in seeded data means that capability is off _there_ —
not that the fixture is unusable. Details in **backend-service-dev**;
diagnosis in **backend-api-explore**.
- **Don't trust written status.** Determine what's implemented by asking the
running server — see **backend-api-explore**. Any inventory committed to a
markdown file is a snapshot that starts rotting immediately, this one
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,17 +172,22 @@ not the DB). Server-only helpers of this kind live in
`src/lib/server/hackathon/` (`capabilities.ts`, `projectEdit.ts`) — they read
generated types, so a `.svelte` file must never import them.

## Capability-gated mutations fail in seeded dataexpect it
## Capability-gated mutations depend on the hackathonread the set first

Several backend mutations are gated on a **capability**, not just a role:
`SetPreference`/`RemovePreference` need `SET_TEAM_PREFERENCES`, `Propose` needs
`PROPOSE_PROJECTS`, submissions need `CREATE_PROJECT_SUBMISSIONS`. A capability
is only "on" if `HackathonService.SetCapabilities` wrote both a `HackathonState`
boolean and a casbin policy row — and `cmd/seed` creates no `HackathonState` row
at all. Nothing in the frontend calls `setCapabilities`.

So in a seeded hackathon these calls return `PERMISSION_DENIED` **by
configuration, not by a bug in your wiring**. Two consequences:
is "on" only when both a `HackathonState` boolean and a casbin policy row exist.
Two things write the pair: `HackathonService.SetCapabilities`, which the manage
hub's capability panel calls from the frontend
(`src/lib/server/hackathon/stateActions.ts`), and `cmd/seed` directly
(`seedCapabilities`), with a **different set per hackathon**.

So the same call succeeds in one hackathon and returns `PERMISSION_DENIED` in
the next, **by configuration, not by a bug in your wiring** — and an organizer
can change the set under you. The set is already loaded: the `[id]` layout
passes `hackathon.state` through `enabledCapabilities` into
`hackathonState.enabled`. Read it before debugging. Two consequences:

- Don't "fix" it frontend-side. Translate the error into something a user can
read and move on.
Expand Down
20 changes: 14 additions & 6 deletions components/frontend/.claude/skills/frontend-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Route groups carry the auth boundary:
- `(app)/` — everything behind Keycloak login: `/dashboard`, `/manage/...`,
`/hackathons/create`, and the member hackathon subtree
`/my/hackathon/[id]/{overview,projects,teams,participants,submissions,timeline,pages/[pageId]}`.
- `(participant)/`, plus top-level `signin`, `signout`, `+error.svelte`.
- Top-level `signin`, `signout`, `+error.svelte`. There is no other route group.

The public `/hackathon/[id]` and member `/my/hackathon/[id]/...` are **disjoint
path spaces** on purpose; a signed-in visitor to the public page is redirected
Expand Down Expand Up @@ -54,8 +54,8 @@ Use runes, not the Svelte 4 `export let` / reactive `$:` style:

## Components (`src/lib/components/<area>/`)

Areas: `hackathon/`, `dashboard/`, `layout/`, `forms/`, `profile/`. Prototype
for a list row is `hackathon/HackathonRow.svelte`. Conventions:
Areas: `hackathon/`, `dashboard/`, `layout/`, `forms/`. Prototype for a list row
is `hackathon/HackathonRow.svelte`. Conventions:

- Type props inline in the `$props()` destructure; give optionals defaults.
- Keep display props **generic strings** so a component is reusable — e.g.
Expand Down Expand Up @@ -158,9 +158,17 @@ wanted anyway. Same for inspecting your work before a commit: one
Bring the stack up with `just start` from the repo root, then open
http://localhost:8081.

Auth-gated `(app)` pages can't be screenshotted headlessly — Keycloak redirects
— so either log in interactively as `alice`/`aliceandbob`, or verify the
underlying data directly with `just rpc::as alice aliceandbob <svc>/<method>`.
Auth-gated `(app)` pages need a real session before they render anything —
Keycloak redirects an anonymous request away — and nothing in this repo captures
one today. That is a missing setup, not an impossibility: Playwright can drive
the Keycloak form (username → Sign In → password) and reuse the session, which
is how the `sketch/experimental-frontend-deployment` branch screenshots every
member tab at desktop and phone widths — see
`.claude/skills/hackathon-e2e/helpers/login.ts` **on that branch** if you decide
to port it (its realm config differs from main's, so the flow needs re-verifying
here). Until then, either log in interactively as `alice`/`aliceandbob`, or
verify the underlying data with `just rpc::as alice aliceandbob <svc>/<method>`.

Public routes (`/`, `/hackathon/[id]`) render without a session and can be
captured with whatever headless tool you have; if you take a screenshot, **look
at the PNG** rather than assuming it rendered.
Expand Down
Loading