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: specdocs/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:
- Upload the file to Supabase Storage → receive a
public_url(reuses the existing object-image upload path; hookuseClassImageUploadatsrc/hooks/useClassImageUpload.ts). - Call
setObjectClassImage(classPointer, imageUrl)— store-only mutation, demotes any existing main image row then inserts the new one. The mutation returns the class’s newimageUrl(coalescedprocessedUrl ?? 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).
- List: read — caller sees only classes owned by their orgs.
- Create: caller must be a member of the target org (
organization_memberscheck in Postgres before any Neo4j write). - Edit / set image: the class is loaded from Neo4j, its
owner_org_idis retrieved, then checked against the caller’s memberships in Postgres. Unknown class → error. Non-member → error (generic message, no information leak).
This mirrors the ownership pattern used by changeObjectOwner and updateOrganization (ADR-0060).
What is out of scope for this flow (Backlog)
- Hard-delete a class — requires cascade rules; deferred.
- Dependency editing —
SUBCLASS_OF, properties, functional relations (the futureclass-management-consoletree view). - Class image pipeline — cutout/crop/cover for class images;
setObjectClassImagestays store-only. - Browse the public catalog — other organisations’ classes; search/filter across the full community catalog.
- Class-aware search — D2 of the class display/search/admin design.
- iOS “create from object” — a distinct flow with local-first write-behind/reconcile complexity.
- Pagination — v1 assumes small class counts; keyset pagination is Backlog.
- Match-before-mint — dedup via trigram/embeddings belongs to the classification pipeline.
- Rendering instances — the admin shows instance count, not instance names or a browse surface.