Administrera klasser — class admin (web-primary)

Created: Sun 28 Jun 2026 17:20:00 CEST Updated: Sun 28 Jun 2026 17:20:00 CEST - Initial flow doc, ships with the web Class Admin implementation Document Version: 1.0 Target Audience: Maintainers, product, Claude Code agents


What this flow is

“Administrera klasser” is the web-primary admin surface for an organisation’s community :ObjectClass nodes. A class is a shared blueprint — it supplies the name, description, and cover image that thin instances inherit (ADR-0058). This flow lets an org member create new classes, browse the ones their org owns, edit their metadata, and set or replace their cover image.

The flow lives at /admin/classes (session-protected admin route). It is web-primary by design: class administration benefits from a larger screen; the iOS app’s only class role — “create from object” — is a separate, deferred flow.

Why: see the use-case (⚠️ intent to be confirmed with Paul). Why it works this way: ADR-0060 (org-ownership + member gate), ADR-0058 (point-to-class display inheritance), ADR-0057 (Neo4j canonical for class identity), ADR-0055 (community class_ref). History: spec docs/specs/2026-06-28-class-admin-web-design.md.


Master–detail layout

The screen is a two-column master–detail:

Column Content
Left — class list Search box + scrollable list of myObjectClasses (thumbnail, name, instance count, classRef). A “Ny klass” button sits at the top.
Right — edit panel Shown when a class is selected. Cover image with “byt bild” upload; editable name and description; read-only classRef and visibility pill; instance count badge (“N exemplar pekar hit”).

Selecting a class in the list populates the edit panel. The list reflects live Apollo-cache state so creates/edits appear immediately without a full refetch.


Operations

List (myObjectClasses)

GraphQL query myObjectClasses — returns all :ObjectClass nodes whose owner_org_id matches any org the authenticated caller belongs to. Empty list (never an error) when the caller owns none.

Component: ClassList (src/components/admin/classes/ClassList.tsx) Hook: useObjectClasses (src/hooks/useObjectClasses.ts) GraphQL: MY_OBJECT_CLASSES in src/graphql/classes.ts

Create (createObjectClass)

The “Ny klass” button opens NewClassDialog. The user enters a name and optional description; the org is the caller’s active/only org. On submit, the mutation createObjectClass(organizationId, name, description) mints a new :ObjectClass node (non-crypto class_ref, class_kind: community, visibility: public, owner_org_id: organizationId). The new class is selected in the detail panel on success; an optional image-upload step follows via setObjectClassImage.

Authz gate: caller must be a member of organizationId — checked before any write. No match-before-mint: duplicate names are allowed in v1 (ADR-0015 — names are human labels, not unique keys). Deduplication belongs to the future classification pipeline.

Component: NewClassDialog (src/components/admin/classes/NewClassDialog.tsx) GraphQL: CREATE_OBJECT_CLASS in src/graphql/classes.ts

Edit (updateObjectClass)

In the edit panel the user edits name and/or description inline and presses Save. The mutation updateObjectClass(classPointer, name?, description?) performs a partial update — only provided fields are written. Renames propagate to all thin instances automatically (point-to-class display inheritance — ADR-0058; no per-instance copy is updated).

Authz gate: owner-org membership (loaded from Neo4j → checked against Postgres organization_members).

Component: ClassEditor (src/components/admin/classes/ClassEditor.tsx) GraphQL: UPDATE_OBJECT_CLASS in src/graphql/classes.ts

Set image (setObjectClassImage)

The “byt bild” control in the edit panel triggers a two-step upload:

  1. Upload the file to Supabase Storage → receive a public_url (reuses the existing object-image upload path; hook useClassImageUpload at src/hooks/useClassImageUpload.ts).
  2. Call setObjectClassImage(classPointer, imageUrl) — store-only mutation, demotes any existing main image row then inserts the new one. The mutation returns the class’s new imageUrl (coalesced processedUrl ?? croppedUrl ?? url).

Authz gate: owner-org membership (tightened in this slice — previously authenticated-only; gap closed per ADR-0060 / spec D-D).

Image pipeline note: setObjectClassImage is store-only in v1. The full cutout/crop/cover pipeline for class images is Backlog.

Known gap (Backlog): if the upload in step 1 succeeds but setObjectClassImage fails, the uploaded blob is orphaned in Storage with no class record pointing to it. A cleanup sweep is deferred.

Hook: useClassImageUpload (src/hooks/useClassImageUpload.ts) GraphQL: SET_OBJECT_CLASS_IMAGE in src/graphql/classes.ts


Authorization model

All operations require an authenticated session (Supabase Auth JWT).

This mirrors the ownership pattern used by changeObjectOwner and updateOrganization (ADR-0060).


What is out of scope for this flow (Backlog)