# Storage review and cleanup — 24 October 2026

The former **Clean storage** button used `storage_orphans()` and offered a single permanent bulk deletion. It had no file review, duplicate verification, reference consolidation or reliable accounting of partial results. **Review storage** now opens an admin-only review panel with filters, search, pagination, individual selections and at most 20 deletions per request.

## Live inspection findings

Read-only inspection using the supplied admin account found 405 visible public-media objects:

| Bucket | Files | Bytes |
| --- | ---: | ---: |
| event-media | 383 | 1,606,415,220 |
| portfolio-two | 22 | 310,053,528 |
| Total | 405 | 1,916,468,748 |

The bucket-list API returned an empty catalog despite those objects being accessible by their known bucket names. The legacy orphan RPC returned zero rows. Neither result proves storage is clean or that every bucket is visible through browser RLS.

Matching metadata produced 32 possible duplicate groups containing 67 files, representing 321,300,219 extra bytes **before usage checks**. Complete streamed SHA-256 checks confirmed 31 groups (43,054,379 extra bytes). One group containing a roughly 278 MB film remains metadata-only in this inspection; the new resumable verifier can check it. These amounts are not an approved deletion list or guaranteed savings. No production file or media row was deleted, merged or retired during this inspection.

## Manual Supabase setup

### Connection and selection repair — 25 October 2026

The live function endpoint returned HTTP 404 `NOT_FOUND` (including the browser's preflight). This explains the generic “Failed to send a request to the Edge Function” message: the SQL was installed, but `storage-cleanup` was not deployed under that name. The latest saved scan reported **405 files: 398 referenced, 7 outside scope and 0 unused candidates**; guards were ready. Of the 67 possible duplicate files, 64 were referenced and 3 excluded. There were no originals preserved solely by unused library entries. These are snapshot counts, not an approved deletion list.

The admin now checks the authenticated function connection separately from the SQL scan, shows deployment/reconnection instructions, explains empty selection, and disables unavailable actions. A failed request is never retried as a mutation. A credential-free GET can identify a missing function when its failed preflight conceals the response. Used files remain unselectable; verify identical portfolio copies and consolidate their references before reviewing the redundant file for deletion. Excluded files no longer offer byte verification.

**If your SQL scan already works, no new SQL is required for this repair.** Deploy the complete updated `index.ts` below; it adds a read-only, admin-authenticated readiness action. Then reload the admin and choose **Review storage → Check connection → Run fresh scan**. The setup notice disappears after the authenticated check succeeds. A new browser connection failure can also indicate the allowed origin, gateway JWT setting or network; the UI does not assume every failure means a missing function.

The frontend can be deployed independently, but deletion/verification remain unavailable until the owner deploys the function in Supabase. No live media was deleted, consolidated or retired during this diagnosis. Existing scan records were read without creating another scan.

This repair passed 155 Node checks, Deno type checking, and desktop/iPhone/iPad review flows in Chromium and Linux WebKit. Tests cover hidden preflight failures, missing deployment, authenticated readiness without writes, reconnection, zero selectable files, preserved uncertain-operation warnings and no automatic mutation retries. No physical iOS device was attached. Evidence is retained with the master audit under `storage-cleanup-access/`.

Use project `gpyyxvnymmwhpdmijyvb`, following the owner's preference to paste backend changes manually.

1. If you have not activated the shared portfolio library, run its existing [setup SQL](../supabase/PORTFOLIO_SHARED_SETUP.sql) first. The cleanup scanner also works without that library, but its library-specific actions need it.
2. Paste the complete [storage cleanup migration](../supabase/migrations/202610240007_storage_cleanup.sql) into **SQL Editor → New query**, and run it. It is additive/rerunnable and does not delete stored files. It adds scan/audit records, reference guards and admin/service RPCs. It revokes browser execution of the old `storage_orphans()` function so cached admin tabs cannot use that workflow.
3. In **Edge Functions**, create a function named exactly **`storage-cleanup`**. Paste the complete [index.ts](../supabase/functions/storage-cleanup/index.ts) and deploy. Turn **Verify JWT with legacy secret** off for this new function: the handler validates the caller's JWT with `auth.getUser()` and checks their current `staff_roles` admin role before any storage action. No anonymous mutation is permitted. Keep existing functions unchanged.
4. Supabase supplies `SUPABASE_URL`, `SUPABASE_ANON_KEY` and `SUPABASE_SERVICE_ROLE_KEY` inside the function. Never add the service key to the frontend. `ALLOWED_ORIGIN` defaults to `https://album.glomium.co`; keep that production origin or configure the actual admin origin.
5. Reload the admin, choose **Review storage → Check connection → Run fresh scan**. Until SQL is installed, the panel offers setup links. If the function is not deployed or reachable, the panel explains the connection problem and disables its actions while keeping scan results available for review.

These backend files were tested locally, not applied/deployed to production by Codex. The deployed frontend and sample Netlify preview are separate from backend activation. Preview records are fictional, and all real writes are disabled there.

## Review workflow

- **Unused candidates:** no saved reference was found, the path is within public album/portfolio scope, and the object has not changed for 72 hours. Select specific files or up to 20 visible candidates. The final confirmation lists their paths and size. A fresh server-side usage check runs before removal.
- **In use / preserved:** includes hidden projects, original files, both portfolio designs and all their variants. Original, grid, poster and crop files are not duplicates just because their filenames resemble each other.
- **Possible & verified duplicates:** metadata is only a shortlist. **Verify identical bytes** checks both objects in bounded 8 MiB ranges, compares a SHA-256 digest for every chunk, records progress and supports pause/resume. A complete proof covers every byte; it is not a claim that the metadata eTag is a content checksum.
- **Share identical portfolio source:** available after byte verification. It rewrites only known portfolio URL fields to use the retained identical source, preserving each page's crop, dimensions, captions, order and publication state. Both files' MIME/encoding metadata must agree. Alternate library previews and the removed library record are retained in the catalog/audit. Unknown, signed, embedded or historical references cause a complete rollback. This action does not remove file bytes: review the spare copy as an unused candidate afterward.
- **Remove unused library entry:** only offered for a library original preserved solely by its catalog entry. The server checks its source and every variant again. Any reference elsewhere retains the entry. Removing a genuinely unwanted entry saves its record in the audit, leaves file bytes intact and requires a fresh scan before you can select files for deletion.
- **Recent uploads / Outside cleanup scope:** retain files being uploaded, unknown paths, private staff documents and crew storage. Automatic deletion is restricted to `event-media/portfolio/`, existing UUID album/project folders in `event-media`, and `portfolio-two/media/`. Unusual traversal-like paths are excluded.
- **Uncertain results:** recheck the original operation. Do not blindly repeat deletion. Absence in Storage metadata is required before the UI counts a removal; freed-byte totals describe confirmed removed Storage objects, not a live billing measurement.

## Integrity, concurrency and recovery

The scanner reads `storage.objects` and extracts references from every current public application table, including nested JSON and URLs inside saved HTML. It normalizes public, signed, authenticated and rendered image URLs, decodes percent escapes once, preserves literal plus signs and conservatively matches raw paths across buckets. Rows are inspected with definer privileges; the browser's partial RLS visibility is not treated as complete coverage. Only metadata and reference-owner table labels enter the review response, not customer row contents.

Before deletion, the server compares the current object identity/version/metadata to the scanned fingerprint and rechecks saved references. Reference-writing transactions acquire shared advisory locks; a deletion reservation holds the exclusive lock while checking and fencing paths. New references and overwrites of reserved/deleted paths are refused. This closes the race between the final check and the Storage API call. New filenames continue to work normally.

The migration installs guards on current public tables and Storage object writes. If a later migration creates an unguarded table, deletion is disabled until this cleanup SQL is rerun. Shared portfolio setup installed afterward is one example. Initial scans remain informational and can become stale; mutation requires a scan less than 24 hours old.

Only the service-side function uses the Storage API for deletion. SQL never deletes `storage.objects` rows. Scan/audit tables are not writable by browser roles. An interrupted worker may leave a path reserved; **Recheck result** reconciles absent objects without issuing another removal. If Storage still reports the object, the reservation stays in place. An operator must inspect function/Storage logs and resolve that specific reservation after confirming no removal remains in flight. Do not bulk-clear these audit rows or fences.

The scanner checks saved application references, not links that were distributed outside the application without being recorded. This is why the file review and explicit selection remain part of deletion. This release does not automatically purge private records, backups, historical mail links or unknown buckets.

## Validation

142 Node checks passed, including authorization, selected-only deletion, stale responses, error persistence, bounded chunk reads, false metadata matches, pause/progress handling and uncertain outcomes. Ten disposable PostgreSQL scenarios cover migration reruns, URL normalization, shared originals, recent/private files, stale snapshots, concurrent reference insertion, deletion fences, real catalog triggers, atomic consolidation rollback and unused-entry retirement. Deno type checking passed.

Chromium and Linux WebKit exercise desktop, iPhone and iPad layouts, light/dark appearance, pagination/search, selection limits, error/reconciliation paths, setup guidance, modal focus and restored scrolling. No physical iOS device was attached. Browser and database test mutations use fictional/local data only; the live inspection was read-only.
