155 lines
8.9 KiB
Markdown
155 lines
8.9 KiB
Markdown
# CLAUDE.md
|
|
|
|
## Session Start
|
|
|
|
Read the latest handoff in docs/summaries/ if one exists. Load only the files that handoff references — not all summaries. If no handoff exists, ask: what is the project, what type of work, what is the target deliverable.
|
|
|
|
Before starting work, state: what you understand the project state to be, what you plan to do this session, and any open questions.
|
|
|
|
## Identity
|
|
|
|
You work with Patrick, a Solutions Architect, on the OYS Borrow a Boat app (oysqn.app) — a mobile-first PWA for managing a Borrow a Boat program for a Yacht Club. Backend is Supabase.
|
|
|
|
## Project Overview
|
|
|
|
- **App**: OYS Borrow a Boat (oysqn.app) — rewrite of bab-app
|
|
- **Stack**: Nuxt 4 (SSR=false), Ionic Vue (@ionic/vue), PrimeVue 4, TypeScript, Supabase (BaaS)
|
|
- **Purpose**: Manage a Borrow a Boat program for a Yacht Club
|
|
- **Personas**: BAB Member, Certified Skipper, Program Administrator, Boatswain, Volunteer, Instructor
|
|
- **Docs**: docs/planning/ contains personas, user/role/permission model
|
|
|
|
## Architecture
|
|
|
|
### App Shell
|
|
- `app/app.vue` — IonApp + IonMenu + IonRouterOutlet (NO NuxtLayout/NuxtPage)
|
|
- Each page is self-contained: IonPage > IonHeader > IonContent
|
|
- No Nuxt layout system — layouts handled at the page level via IonPage
|
|
- `app/plugins/ionic.client.ts` — installs IonicVue with mode: 'md'
|
|
|
|
### Routing
|
|
- Nuxt file-based routing → Vue Router → IonRouterOutlet handles transitions
|
|
- `app/middleware/auth.ts` — global auth guard via useSupabaseUser()
|
|
- Auth pages use `definePageMeta({ layout: false })` (effectively a no-op with IonRouterOutlet, but documents intent)
|
|
|
|
### Ionic + Nuxt Watch-outs
|
|
Source: https://ionic.nuxtjs.org/get-started/watch-outs
|
|
|
|
**Never use Nuxt navigation components** — `<NuxtPage>`, `<NuxtLayout>`, `<NuxtLink>` are not integrated with this module. Use `<IonRouterOutlet>` and `useIonRouter()` / `router-link` prop on Ionic components instead.
|
|
|
|
**Every page must have `<IonPage>` as its root element** — required for Ionic page transitions and stack navigation.
|
|
|
|
**Route params: always import `useRoute` from `vue-router` directly** — Nuxt's auto-imported `useRoute()` always returns `params: {}` when used with IonRouterOutlet. This is a known bug with no upstream fix yet.
|
|
```typescript
|
|
import { useRoute, useRouter } from 'vue-router' // NOT Nuxt auto-import
|
|
```
|
|
|
|
**`onMounted` / `onBeforeMount` are unreliable** — IonRouterOutlet preserves DOM elements rather than unmounting them, so mounted hooks may not fire when expected. Use Ionic lifecycle hooks (`onIonViewWillEnter`) instead. See Data Fetching Pattern below.
|
|
|
|
**`useHead()` requires workarounds** — not compatible out of the box with IonRouterOutlet's component persistence.
|
|
|
|
**`<keep-alive>`, `<transition>`, `<router-view>` do not work as expected** — IonRouterOutlet manages its own lifecycle; don't layer Vue's built-in equivalents on top of it.
|
|
|
|
**No SSR** — this app targets mobile/PWA; SSR is disabled (`ssr: false` in nuxt.config). Do not add SSR-dependent patterns.
|
|
|
|
### Auth
|
|
- Supabase Auth — magic link + OTP only (no password auth)
|
|
- `useSupabaseUser()` composable (from @nuxtjs/supabase)
|
|
- `app/stores/auth.ts` — isAdmin computed from user_metadata.role
|
|
|
|
### Backend
|
|
- Supabase (replaces Appwrite)
|
|
- Types in `types/supabase.ts` — regenerate with: `npx supabase gen types typescript --project-id YOUR_ID > types/supabase.ts`
|
|
- `useSupabaseClient<Database>()` typed against `types/supabase.ts`
|
|
|
|
### Edge Functions
|
|
- Located in `supabase/functions/<name>/` — each function has its own `deno.json`
|
|
- Auth pattern: extract Bearer token → `adminClient.auth.getUser(token)` (pass JWT directly to service-role client). Do NOT create a separate userClient with the anon key.
|
|
- Use `SUPABASE_SERVICE_ROLE_KEY` (adminClient) for all DB operations inside functions; the caller's identity comes from JWT claims (`claims.sub` = user ID).
|
|
- **SELinux (Fedora/RHEL local dev)**: Before running `supabase functions serve`, label the project directory for container access:
|
|
```
|
|
sudo chcon -Rt container_file_t $(pwd)
|
|
```
|
|
This must be applied after any `git clone` or directory move. Failure symptom: function bootstrap error with no useful stderr output.
|
|
|
|
### Icons
|
|
- Ionicons only (`ionicons/icons`) — no PrimeIcons
|
|
- Always import individual icon names from `ionicons/icons` (tree-shakeable)
|
|
|
|
### Data Fetching Pattern
|
|
Every page that reads from Supabase **must** use this pattern. Do NOT use `onMounted` for data fetches.
|
|
|
|
**Why**: `onMounted` (and `watch(user, { immediate: true })`) fire during component setup — before `IonRouterOutlet` has populated `route.params` and before `@nuxtjs/supabase` has restored the session into the Supabase JS client. The fetch runs unauthenticated, RLS returns 0 rows, and the page shows empty/error state.
|
|
|
|
```typescript
|
|
// Route params: always computed, never destructured at setup time
|
|
const id = computed(() => route.params.id as string)
|
|
|
|
const user = useSupabaseUser()
|
|
|
|
async function load() {
|
|
if (!user.value) return // guard: session not ready
|
|
if (!id.value) return // guard: param pages only
|
|
// ... fetch data
|
|
}
|
|
|
|
// Fires when session becomes available (handles direct URL load / page refresh)
|
|
watch([user, id], ([u, rid]) => { if (u && rid) load() }, { immediate: true })
|
|
// Fires on every Ionic page activation (handles IonRouterOutlet page cache)
|
|
onIonViewWillEnter(() => { if (user.value) load() })
|
|
```
|
|
|
|
- For pages **without** a route param: omit `id` from the watch array, keep `watch(user, ...)`.
|
|
- Reactive filter deps (e.g. `selectedDate`, `filterDateFrom`) use a separate `watch(dep, () => { if (user.value) fetch() })` — not `watch(dep, fetch)`, so they don't fire before the session is ready.
|
|
- `onMounted` is still valid for **non-auth DOM setup** (e.g., `window.addEventListener`, breakpoint detection).
|
|
|
|
### Offline Cache
|
|
- **Rule**: Every table/view read from Supabase must be written to `useAppCache` on success and read from it when offline.
|
|
- **Composables**: `useAppCache` (localStorage, 24h TTL), `useOfflineStatus` (reactive `isOnline`)
|
|
- **Pattern** for any data fetch:
|
|
```typescript
|
|
const cache = useAppCache()
|
|
const { isOnline } = useOfflineStatus()
|
|
|
|
if (!isOnline.value) {
|
|
const cached = cache.peek<T>('my-key')
|
|
if (cached) data.value = cached
|
|
return
|
|
}
|
|
const { data: fresh } = await supabase.from('my_table').select('*')
|
|
data.value = fresh ?? []
|
|
if (fresh) cache.set('my-key', fresh)
|
|
```
|
|
- **Schedule data** is keyed by ISO week Monday: `cache.weekKey(utcIso)` → use keys `intervals:{monday}` and `slots:{monday}`.
|
|
- **Realtime**: `app/app.vue` subscribes to `reservations`, `intervals`, and `boats` changes and patches the cache in real time. When adding a new table subscription, add it to the `app-cache-sync` channel in `app.vue`.
|
|
- **Cross-page navigation state** (not persistence): use `useBookingDraft` as a pattern — module-level `ref` set by the source page, consumed (`take()`) by the destination page. Do not use query params for structured objects.
|
|
|
|
## Rules
|
|
|
|
1. Do not mix unrelated project contexts in one session.
|
|
2. Write state to disk, not conversation. After completing meaningful work, write a summary to docs/summaries/ using templates from templates/claude-templates.md. Include: decisions with rationale, exact numbers, file paths, open items.
|
|
3. Before compaction or session end, write to disk: every number, every decision with rationale, every open question, every file path, exact next action.
|
|
4. When switching work types (research → writing → review), write a handoff to docs/summaries/handoff-[date]-[topic].md and suggest a new session.
|
|
5. Do not silently resolve open questions. Mark them OPEN or ASSUMED.
|
|
6. Do not bulk-read documents. Process one at a time: read, summarize to disk, release from context before reading next.
|
|
7. Sub-agent returns must be structured, not free-form prose. Use output contracts from templates/claude-templates.md.
|
|
|
|
## Where Things Live
|
|
|
|
- templates/claude-templates.md — summary, handoff, decision, analysis, task, output contract templates (read on demand)
|
|
- docs/summaries/ — active session state (latest handoff + project brief + decision records)
|
|
- docs/context/ — reusable domain knowledge
|
|
- docs/planning/ — original planning documents (personas, roles/permissions)
|
|
- types/supabase.ts — Supabase generated types (placeholder until Supabase project configured)
|
|
- app/ — Nuxt app source (Nuxt 4 structure)
|
|
- app/plugins/ionic.client.ts — Ionic Vue plugin
|
|
- app/stores/ — Pinia stores (Supabase-backed)
|
|
- app/composables/ — shared composables
|
|
|
|
## Error Recovery
|
|
|
|
If context degrades or auto-compact fires unexpectedly: write current state to docs/summaries/recovery-[date].md, tell the user what may have been lost, suggest a fresh session.
|
|
|
|
## Before Delivering Output
|
|
|
|
Verify: exact numbers preserved, open questions marked OPEN, output matches what was requested (not assumed), claims backed by specific data, output consistent with stored decisions in docs/context/, summary written to disk for this session's work.
|