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
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Project overview

RubricMaker is an **offline-first** rubric and grading application for teachers. All data lives in browser `localStorage`; an optional Supabase backend provides cloud sync and multi-device access. The app targets static hosting (no server required in offline mode) and can also be self-hosted with Docker.
RubricMaker is a rubric and grading application for teachers. It is **self-hostable with full functionality** (Supabase backend for persistence, sync, multi-device and multi-teacher features) and **offline-capable with reduced capabilities**: without a Supabase connection the app still runs from browser `localStorage`, but cloud-dependent features (collaboration, student portal, multi-device access) are unavailable. The app targets static hosting and can also be self-hosted with Docker.

Key domains:
- **Rubric Builder** — create/edit rubrics with criteria, levels, scoring modes
Expand Down Expand Up @@ -38,7 +38,7 @@ npm run db:reset # Reset and re-apply all migrations
| Build | Vite 8 |
| Routing | React Router v7 (lazy-loaded pages) |
| State | React Context (`AppContext`) + `useReducer` |
| Persistence | `localStorage` primary; Supabase optional sync |
| Persistence | Supabase primary when configured; `localStorage` offline-capable fallback |
| Rich text | TipTap 3 (ProseMirror) |
| Charts | Recharts |
| Export | `docx`, `pdfjs-dist`, `file-saver` |
Expand All @@ -59,9 +59,9 @@ User action → AppContext dispatch → useReducer → new state (always)

`src/store/storage.ts` is the single write point for persistence. Never write to `localStorage` directly from components or hooks.

### Offline-first rule
### Storage rule (Supabase-primary, offline-capable)

The app must work completely without Supabase: the `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` env vars are optional, and if absent, all database sync is silently skipped and `localStorage` is the sole, permanent store (unchanged legacy behavior).
Supabase is the primary store whenever a connection is configured. The app must still start and run without Supabasethe `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` env vars are optional, and if absent, all database sync is silently skipped and `localStorage` is the sole, permanent store — but this local mode is a reduced-capability fallback, not the primary design target.

When a Supabase connection is live, `localStorage` is no longer a permanent duplicate copy — it's only a temporary buffer:

Expand Down
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Rubric Maker

A comprehensive, offline-first rubric creation and grading tool built with React and TypeScript. Designed for educators who need to design complex rubrics, grade students efficiently, and analyse performance — including language proficiency tracking aligned to the Common European Framework of Reference (CEFR).
A comprehensive rubric creation and grading tool built with React and TypeScript — self-hostable with full functionality, and offline-capable (with reduced capabilities) when no backend is configured. Designed for educators who need to design complex rubrics, grade students efficiently, and analyse performance — including language proficiency tracking aligned to the Common European Framework of Reference (CEFR).

## Features

Expand Down Expand Up @@ -92,12 +92,12 @@ A comprehensive, offline-first rubric creation and grading tool built with React

### 8. Installation

- **Installable PWA**: RubricMaker can be installed to a device's home screen or desktop (look for the install icon in the browser address bar) for an app-like, browser-chrome-free launch — useful for shared classroom devices. This only affects installability of the static app shell; it does not change the offline-first localStorage data model, and the service worker never caches Supabase API requests (`/rest/`, `/auth/`, `/realtime/`, `/storage/`, `/functions/` paths are always network-only).
- **Installable PWA**: RubricMaker can be installed to a device's home screen or desktop (look for the install icon in the browser address bar) for an app-like, browser-chrome-free launch — useful for shared classroom devices. This only affects installability of the static app shell; it does not change the storage model, and the service worker never caches Supabase API requests (`/rest/`, `/auth/`, `/realtime/`, `/storage/`, `/functions/` paths are always network-only).

### 9. Data Management

- **Offline-first**: All data lives in the browser's `localStorage`. No account required.
- **Cloud sync** (optional): Supabase backend for multi-device access and multi-teacher collaboration. Sync hydrates `localStorage` from Supabase on load and after reconnect; per-record conflicts resolve last-write-wins (newest `updatedAt` wins), and offline edits queued in the pending-sync queue are protected from being clobbered by stale cloud data until they are pushed (see `src/utils/syncMerge.ts`).
- **Offline-capable**: Without a configured backend, all data lives in the browser's `localStorage` and no account is required — with reduced capabilities (no collaboration, student portal, or multi-device access).
- **Cloud sync** (recommended): Supabase backend as the primary store for multi-device access and multi-teacher collaboration. Sync hydrates `localStorage` from Supabase on load, after reconnect, and in near-real-time whenever another device changes data (Postgres change events on every synced table, debounced into a single refresh — see `StorageSync.startRealtimeSync`); per-record conflicts resolve last-write-wins (newest `updatedAt` wins), and offline edits queued in the pending-sync queue are protected from being clobbered by stale cloud data until they are pushed (see `src/utils/syncMerge.ts`).
- **Backup & restore**: Export the entire dataset to JSON; restore from any prior backup.
- **Admin panel**: School-level management — user roles, onboarding, student anonymisation, data-retention policies.

Expand Down Expand Up @@ -300,6 +300,16 @@ The script uses the Storage HTTP API — it does **not** delete rows directly fr

On Supabase Cloud, schedule the `delete-old-attachments` edge function instead (see [Supabase Dashboard → Edge Functions](https://supabase.com/dashboard/project/_/functions)).

**Nightly cloud backup (recommended for Supabase Cloud and the official self-hosted stack):**

`scripts/backup.sh` (see "Backup and restore" above) only works against **this repo's own `docker-compose.yml`** — it calls `docker-compose exec db pg_dump` and archives a hardcoded volume name (`rubricmaker_storage-data`), both specific to that bundled stack. It does nothing for Supabase Cloud or for a Supabase instance you're self-hosting separately (e.g. the [official self-hosted Supabase Docker stack](https://supabase.com/docs/guides/self-hosting/docker)) — different container/volume names, or no server access at all on Cloud.

The `nightly-backup` edge function covers both of those cases instead: for every teacher/admin, it dumps their rows via `public.export_owner_backup()` and uploads a JSON snapshot to the private `backups` Storage bucket at `{userId}/{timestamp}.json`, keeping the 7 most recent per user. Like `set-student-password`, it needs a functions runtime — this repo's bundled `docker-compose.yml` doesn't have one, but the official self-hosted stack does (deploy by copying `index.ts` into that stack's `volumes/functions/nightly-backup/index.ts`, no separate deploy step), and so does Cloud.

Schedule it nightly: on Cloud via [Supabase Dashboard → Edge Functions](https://supabase.com/dashboard/project/_/functions) or Cron Jobs; on the official self-hosted stack via a `pg_cron` + `pg_net` job or an external cron hitting the function URL. It authenticates the same way as `delete-old-attachments` — the scheduler must pass the project's service role key as a bearer token.

This is a disaster-recovery snapshot of raw table rows, not a file you can feed back into the app's own JSON import (Settings → Backup & Restore) — restoring it means re-inserting the rows directly (e.g. via `psql` or the Supabase SQL editor), not through `importFullBackup()`. It's metadata only: rows that reference uploaded files (essay submissions, speaking-session recordings) store a Storage-bucket path, not the file itself, so back up the `essays`/`recordings`/`attachments` buckets separately if you need those files recoverable too. If you're self-hosting Supabase separately from this repo's stack, you may also want your own `pg_dump`-based backup of the whole instance — `export_owner_backup()` only covers the app's own tables, scoped per teacher/admin.

**Stress-test logging (optional):**

Before a school-wide rollout, you can enable a diagnostic event stream to a
Expand Down
Loading
Loading