diff --git a/app/components/WeatherWidget.vue b/app/components/WeatherWidget.vue new file mode 100644 index 0000000..7b5def1 --- /dev/null +++ b/app/components/WeatherWidget.vue @@ -0,0 +1,123 @@ + + + + Weather + Marina conditions + + + + + + {{ error }} + + + + {{ weather.temp }}°C + Temperature + + + + {{ weather.description }} + Conditions + + + + {{ weather.windSpeed }} kn + Wind + + + + + + + + + diff --git a/app/pages/index.vue b/app/pages/index.vue index c24964b..588ea1d 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -21,7 +21,46 @@ - Welcome to OYS Borrow a Boat + Welcome, {{ auth.displayName }} + + + + Upcoming Reservations + + + + + + + No upcoming reservations. + + + + + {{ boatName(r) }} + {{ formatDateRange(r.start_time, r.end_time) }} + + {{ r.status }} + + + + + + + + Create Reservation + + + + + Calendar + + + + + + + @@ -30,19 +69,69 @@ diff --git a/docs/summaries/handoff-2026-04-12-splash-and-login.md b/docs/archive/handoffs/handoff-2026-04-12-splash-and-login.md similarity index 100% rename from docs/summaries/handoff-2026-04-12-splash-and-login.md rename to docs/archive/handoffs/handoff-2026-04-12-splash-and-login.md diff --git a/docs/summaries/handoff-2026-04-12-auth-tests-and-backend-plan.md b/docs/summaries/handoff-2026-04-12-auth-tests-and-backend-plan.md new file mode 100644 index 0000000..b24882d --- /dev/null +++ b/docs/summaries/handoff-2026-04-12-auth-tests-and-backend-plan.md @@ -0,0 +1,70 @@ +# Session Handoff: Auth Tests + Backend Ansible Plan +**Date:** 2026-04-12 +**Session Focus:** Fix broken auth unit tests; plan bab-backend-ansible rewrite; update EE dependencies + +## What Was Accomplished + +1. **Deleted `tests/unit/auth-callback.test.ts`** — low-value component test per agreed test strategy; E2E covers this +2. **Extracted pure auth logic** → `app/utils/auth.ts` (`checkAuthRedirect(userValue, path): string | null`) +3. **Simplified `app/middleware/auth.ts`** — delegates to `checkAuthRedirect`; Nuxt-specific code is now minimal +4. **Rewrote `tests/unit/auth-middleware.test.ts`** — tests `checkAuthRedirect` directly, no mocking, node env; **7/7 passing** +5. **Documented Nuxt testing lessons** → memory `feedback_nuxt_testing.md` +6. **Wrote backend rewrite plan** → `docs/summaries/plan-bab-backend-ansible-rewrite.md` +7. **Updated `ee-demo` EE** with: `amazon.aws` collection, `boto3`/`botocore`, `postgresql` RPM, `supabase` CLI install via `SUPABASE_VERSION` build arg; updated `build.sh` (user also added `--redhat` flag) +8. **Resolved all plan open questions** except one (see below) + +## Exact State of Work in Progress + +- `tests/integration/auth-session.test.ts` — written last session, not yet run (requires local Supabase + `SUPABASE_SERVICE_ROLE_KEY`) +- Playwright E2E — not yet set up; `tests/e2e/` directory does not exist +- `.gitea/workflows/build.yaml` — not yet created + +## Key Technical Decisions This Session + +- **Extract-don't-mock pattern**: Nuxt auto-imports compile to concrete dist paths; `vi.mock('#imports')` doesn't intercept them. Pattern: extract logic to `app/utils/` with no Nuxt deps, test directly. CONFIRMED. +- **Skip unit tests for simple page components**: Ionic component registration + Supabase init failures make `mountSuspended` too brittle. Cover with Playwright E2E instead. CONFIRMED. + +## Files Created or Modified + +| File Path | Action | Description | +|-----------|--------|-------------| +| `app/utils/auth.ts` | Created | `checkAuthRedirect` pure function; `PUBLIC_ROUTES` constant | +| `app/middleware/auth.ts` | Modified | Now delegates to `checkAuthRedirect` | +| `tests/unit/auth-middleware.test.ts` | Rewritten | Tests pure function; 7/7 passing | +| `tests/unit/auth-callback.test.ts` | Deleted | Low-value component test | +| `docs/summaries/plan-bab-backend-ansible-rewrite.md` | Created | Full rewrite plan with implementation sequence | +| `docs/context/sdlc-architecture.md` | Updated | Dev URL, nginx webroot, Gitea URL, artifact token path | +| `/home/ptoal/Dev/ExecutionEnvironments/ee-demo/execution-environment.yml` | Modified | Added `postgresql` RPM, `supabase` CLI build step | +| `/home/ptoal/Dev/ExecutionEnvironments/ee-demo/requirements.yml` | Modified | Added `amazon.aws >= 9.0.0` | +| `/home/ptoal/Dev/ExecutionEnvironments/ee-demo/requirements.txt` | Modified | Added `boto3`, `botocore` | +| `/home/ptoal/Dev/ExecutionEnvironments/ee-demo/build.sh` | Modified | Added `SUPABASE_VERSION` guard; user added `--redhat` flag | + +## What the NEXT Session Should Do + +**If continuing oysqn.app frontend:** +1. Set up Playwright — `yarn add -D @playwright/test`, create `tests/e2e/`, write login flow E2E test +2. Create `.gitea/workflows/build.yaml` following bab-app pattern (semantic-release + artifact) + +**If starting bab-backend-ansible rewrite:** +1. Read `docs/summaries/plan-bab-backend-ansible-rewrite.md` — full scope and implementation sequence +2. Start with step 3: `sync_gitea_secrets.yml` (lowest risk, standalone) +3. Note: work in `/home/ptoal/Dev/Projects/bab-backend-ansible`, not oysqn.app + +## Open Questions Requiring User Input + +- [ ] **`kv/oys/dev/supabase/postgres_url`** — not in Vault; needed before `migrate_supabase.yml` can run rollback SQL via `psql` against dev. Add to Vault before first dev migration run. + +## Confirmed Infrastructure Values + +| Item | Value | +|------|-------| +| Dev URL | `https://bab.toal.ca` | +| nginx webroot (bab1) | `/usr/share/nginx/html/` | +| Gitea URL | `https://gitea.toal.ca/` | +| Gitea artifact token | `kv/oys/bab_gitea` | +| Backup path (bab1) | `/var/backups/oysqn/` (assumed — confirm before first prod backup) | + +## Files to Load Next Session + +- **Frontend session:** `docs/summaries/handoff-2026-04-12-splash-and-login.md` (prior UI work context) +- **Backend session:** `docs/summaries/plan-bab-backend-ansible-rewrite.md`; `docs/context/sdlc-architecture.md` diff --git a/docs/summaries/handoff-2026-04-19-playwright-e2e-setup.md b/docs/summaries/handoff-2026-04-19-playwright-e2e-setup.md new file mode 100644 index 0000000..d500b24 --- /dev/null +++ b/docs/summaries/handoff-2026-04-19-playwright-e2e-setup.md @@ -0,0 +1,66 @@ +# Session Handoff: Playwright E2E Setup +**Date:** 2026-04-19 +**Session Focus:** Set up Playwright E2E testing; write and pass auth flow tests + +## What Was Accomplished + +1. **Installed `@playwright/test` 1.59.1** + Chromium headless shell binary +2. **Created `playwright.config.ts`** — Pixel 5 viewport, `reuseExistingServer` for local dev, `github` reporter in CI, 30s timeout, 1 worker (serial) +3. **Created `tests/e2e/helpers/mailpit.ts`** — `deleteAllMail()` and `getMagicLink(email)` against Mailpit at `http://127.0.0.1:54324` +4. **Created `tests/e2e/auth.spec.ts`** — 2 tests: full magic link flow + protected route redirect +5. **Fixed `supabase/config.toml`** — `site_url` changed from `http://127.0.0.1:3000` to `http://localhost:3000`; `additional_redirect_urls` updated to include `http://localhost:3000/auth/callback` and `http://127.0.0.1:3000/auth/callback` +6. **Added scripts to `package.json`**: `test:e2e`, `test:e2e:ui`, `test:e2e:headed` +7. **Both tests pass: 2/2** + +## Key Debugging Discoveries + +| Problem | Root Cause | Fix | +|---------|-----------|-----| +| `getByRole('button', { name: 'Log In' })` not found | `IonButton` with `router-link` renders as `` (role=`link`) | Changed to `getByRole('link', { name: 'Log In' })` | +| `getByLabel('Email address')` not found | `IonLabel` is not associated via `for`/`id` with `IonInput` | Changed to `getByPlaceholder('you@example.com')` | +| `page.goto(magicLink)` → `ERR_CONNECTION_REFUSED` to `127.0.0.1:54321` | Playwright's Chromium headless shell cannot reach Supabase local auth server directly | Use Node.js `fetch(magicLink, { redirect: 'manual' })` to follow redirect server-side; navigate browser to the resulting app callback URL | +| Callback URL was `/?code=` instead of `/auth/callback?code=` | `emailRedirectTo` not whitelisted in `supabase/config.toml`; Supabase fell back to `site_url` | Fixed `config.toml` redirect URLs; required `supabase stop && supabase start` | + +## Ionic-Specific Playwright Patterns (confirmed working) + +- `IonButton` with `router-link` → use `getByRole('link', { name: '...' })` +- `IonButton` without `router-link` → use `getByRole('button', { name: '...' })` +- `IonInput` → use `getByPlaceholder(...)` (label association not standard HTML) +- Never navigate browser directly to Supabase local auth URL — follow redirect server-side first + +## Files Created or Modified + +| File | Action | +|------|--------| +| `playwright.config.ts` | Created | +| `tests/e2e/helpers/mailpit.ts` | Created | +| `tests/e2e/auth.spec.ts` | Created | +| `supabase/config.toml` | Modified — site_url and redirect URLs | +| `package.json` | Modified — added test:e2e scripts | + +## What the NEXT Session Should Do + +**Option A — CI pipeline:** +1. Create `.gitea/workflows/build.yaml` — unit tests + build + E2E (read bab-app pipeline as reference) +2. Note: E2E in CI requires local Supabase running in the pipeline — confirm if feasible or skip E2E in CI for now + +**Option B — Home page content:** +1. Implement authenticated home content in `app/pages/index.vue` (currently "Welcome to OYS Borrow a Boat" placeholder) +2. Likely a boat list or dashboard — check `docs/planning/` for persona requirements + +## Open Questions + +- [ ] Should E2E tests run in CI (requires Supabase in pipeline) or local-only? — OPEN +- [ ] What is the authenticated home content? Boat list, dashboard, or something else? — check planning docs + +## Dev Environment Reference + +```bash +# Required before running E2E: +DOCKER_HOST=unix:///run/user/1000/podman/podman.sock npx supabase start + +# Run E2E: +yarn test:e2e # headless +yarn test:e2e:headed # visible browser +yarn test:e2e:ui # Playwright UI mode +``` diff --git a/package.json b/package.json index 3246e64..296b0d4 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,10 @@ "typecheck": "nuxt typecheck", "test": "vitest run", "test:watch": "vitest", - "test:integration": "vitest run --config vitest.integration.config.ts" + "test:integration": "vitest run --config vitest.integration.config.ts", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:headed": "playwright test --headed" }, "engines": { "node": ">=22" @@ -33,6 +36,7 @@ }, "devDependencies": { "@nuxt/test-utils": "^4.0.2", + "@playwright/test": "^1.59.1", "@vite-pwa/nuxt": "^1.1.1", "@vue/test-utils": "^2.4.6", "happy-dom": "^20.8.9", diff --git a/playwright-report/index.html b/playwright-report/index.html new file mode 100644 index 0000000..0cbbe73 --- /dev/null +++ b/playwright-report/index.html @@ -0,0 +1,90 @@ + + + + + + + + + Playwright Test Report + + + + + + + +data:application/zip;base64,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 \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..376aae6 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: false, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: process.env.CI ? 'github' : 'html', + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'mobile-chrome', + use: { ...devices['Pixel 5'] }, + }, + ], + webServer: { + command: 'yarn dev', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + timeout: 60_000, + }, +}) diff --git a/supabase/config.toml b/supabase/config.toml index 7822441..15cd9ac 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -151,9 +151,9 @@ max_indexes = 5 enabled = true # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used # in emails. -site_url = "http://127.0.0.1:3000" +site_url = "http://localhost:3000" # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://127.0.0.1:3000"] +additional_redirect_urls = ["http://localhost:3000/auth/callback", "http://127.0.0.1:3000/auth/callback"] # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). jwt_expiry = 3600 # JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:/auth/v1). diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..cbcc1fb --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file diff --git a/tests/e2e/auth.spec.ts b/tests/e2e/auth.spec.ts new file mode 100644 index 0000000..1146fcb --- /dev/null +++ b/tests/e2e/auth.spec.ts @@ -0,0 +1,46 @@ +import { test, expect } from '@playwright/test' +import { deleteAllMail, getMagicLink } from './helpers/mailpit' + +const TEST_EMAIL = 'e2e-test@example.com' + +test.beforeEach(async () => { + await deleteAllMail() +}) + +test.describe('Auth flow', () => { + test('splash → login → magic link → home', async ({ page }) => { + // Splash page shows logo and login button + await page.goto('/') + await expect(page.getByRole('img', { name: 'OYS Borrow a Boat' })).toBeVisible() + await expect(page.getByRole('link', { name: 'Log In' })).toBeVisible() + + // Navigate to login page + await page.getByRole('link', { name: 'Log In' }).click() + await expect(page).toHaveURL('/login') + await expect(page.getByText('Sign In')).toBeVisible() + + // Submit email for magic link + await page.getByPlaceholder('you@example.com').fill(TEST_EMAIL) + await page.getByRole('button', { name: 'Send Sign-In Link' }).click() + await expect(page.getByText('Check your email')).toBeVisible() + + // Fetch magic link from Mailpit, then follow the Supabase verify redirect + // server-side (Node.js fetch) to get our app's callback URL. + // Playwright's headless shell can't reach 127.0.0.1:54321 directly. + const magicLink = await getMagicLink(TEST_EMAIL) + const redirectRes = await fetch(magicLink, { redirect: 'manual' }) + const callbackUrl = redirectRes.headers.get('location') + if (!callbackUrl) throw new Error('Supabase did not redirect to app callback') + await page.goto(callbackUrl) + + // Auth callback redirects to home (authenticated state) + await expect(page).toHaveURL('/') + await expect(page.getByText('Welcome to OYS Borrow a Boat')).toBeVisible() + }) + + test('unauthenticated access to protected route redirects to splash', async ({ page }) => { + await page.goto('/dashboard') + await expect(page).toHaveURL('/') + await expect(page.getByRole('link', { name: 'Log In' })).toBeVisible() + }) +}) diff --git a/tests/e2e/helpers/mailpit.ts b/tests/e2e/helpers/mailpit.ts new file mode 100644 index 0000000..90150c9 --- /dev/null +++ b/tests/e2e/helpers/mailpit.ts @@ -0,0 +1,54 @@ +const MAILPIT_URL = 'http://127.0.0.1:54324' + +interface MailpitAddress { + Address: string + Name: string +} + +interface MailpitMessage { + ID: string + To: MailpitAddress[] + Subject: string + Date: string +} + +interface MailpitListResponse { + messages: MailpitMessage[] | null + total: number +} + +interface MailpitMessageDetail { + Text: string + HTML: string +} + +export async function deleteAllMail(): Promise { + await fetch(`${MAILPIT_URL}/api/v1/messages`, { method: 'DELETE' }) +} + +export async function getMagicLink(toEmail: string, timeoutMs = 15_000): Promise { + const deadline = Date.now() + timeoutMs + + while (Date.now() < deadline) { + const res = await fetch(`${MAILPIT_URL}/api/v1/messages`) + if (res.ok) { + const data: MailpitListResponse = await res.json() + const msg = (data.messages ?? []).find(m => + m.To.some(t => t.Address === toEmail), + ) + if (msg) { + const detail: MailpitMessageDetail = await fetch( + `${MAILPIT_URL}/api/v1/message/${msg.ID}`, + ).then(r => r.json()) + + const body = detail.Text || detail.HTML + // Supabase local auth confirmation URL + const match = body.match(/https?:\/\/127\.0\.0\.1:54321\/auth\/v1\/verify\?[^\s"<]+/) + if (match) return match[0] + } + } + await new Promise(r => setTimeout(r, 500)) + } + + throw new Error(`No magic link email for ${toEmail} within ${timeoutMs}ms`) +} diff --git a/yarn.lock b/yarn.lock index 0db6611..6623bb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2134,6 +2134,13 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@playwright/test@^1.59.1": + version "1.59.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.59.1.tgz#5c4d38eac84a61527af466602ae20277685a02d6" + integrity sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg== + dependencies: + playwright "1.59.1" + "@polka/url@^1.0.0-next.24": version "1.0.0-next.29" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.29.tgz#5a40109a1ab5f84d6fd8fc928b19f367cbe7e7b1" @@ -3330,10 +3337,10 @@ acorn@^8.15.0, acorn@^8.16.0, acorn@^8.6.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== -agent-base@8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-8.0.0.tgz#3eb7c2c198ccb93368c77969530594e725b804b5" - integrity sha512-QT8i0hCz6C/KQ+KTAbSNwCHDGdmUJl2tp2ZpNlGSWCfhUNVbYG2WLE3MdZGBAgXPV4GAvjGMxo+C1hroyxmZEg== +agent-base@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-9.0.0.tgz#ec9efb08314e1e75b0852d74aabf9a387f99834e" + integrity sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA== agent-base@^7.1.0, agent-base@^7.1.2: version "7.1.4" @@ -5033,6 +5040,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -5412,12 +5424,12 @@ https-proxy-agent@^7.0.5, https-proxy-agent@^7.0.6: agent-base "^7.1.2" debug "4" -https-proxy-agent@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-8.0.0.tgz#2ab138d2706d473ede6d1f5e7158d7586d5ca738" - integrity sha512-YYeW+iCnAS3xhvj2dvVoWgsbca3RfQy/IlaNHHOtDmU0jMqPI9euIq3Y9BJETdxk16h9NHHCKqp/KB9nIMStCQ== +https-proxy-agent@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz#89256adf9dc20926fe43ea1d3ca0feb9e23cc4bd" + integrity sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g== dependencies: - agent-base "8.0.0" + agent-base "9.0.0" debug "^4.3.4" httpxy@^0.3.1: @@ -7167,6 +7179,20 @@ pkg-types@^2.3.0: exsolve "^1.0.7" pathe "^2.0.3" +playwright-core@1.59.1: + version "1.59.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.59.1.tgz#d8a2b28bcb8f2bd08ef3df93b02ae83c813244b2" + integrity sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg== + +playwright@1.59.1: + version "1.59.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.59.1.tgz#f7b0ca61637ae25264cec370df671bbe1f368a4a" + integrity sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw== + dependencies: + playwright-core "1.59.1" + optionalDependencies: + fsevents "2.3.2" + plist@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9" @@ -8572,12 +8598,12 @@ stylehacks@^7.0.5: postcss-selector-parser "^7.1.1" supabase@^2.84.4: - version "2.84.4" - resolved "https://registry.yarnpkg.com/supabase/-/supabase-2.84.4.tgz#939a14c492fa90b9695352cf47e1cccb9b057cb1" - integrity sha512-+WSe/7FFMuEOa1LJr1tZh12WDwW6lpKSmBjiEmf7m9j/ialf2oxeUMlsJCdYpST5kQ7PN0XDyvqnjE0tv/AB2w== + version "2.92.1" + resolved "https://registry.yarnpkg.com/supabase/-/supabase-2.92.1.tgz#cd874c10cc9070215a075a060c7d66fa0a8f499a" + integrity sha512-BB3olR2glhrE0YGDhq0vknJdrwjROaIHgiC/OZc94eLbBHnsJ3szKeRZkcF9dxRgxuq6QWdxCrn5m14lfu9tug== dependencies: bin-links "^6.0.0" - https-proxy-agent "^8.0.0" + https-proxy-agent "^9.0.0" node-fetch "^3.3.2" tar "7.5.13"
{{ error }}
+ No upcoming reservations. +
{{ formatDateRange(r.start_time, r.end_time) }}