# Photo Slots Asset Inventory

This document is the human-readable inventory of user-facing media assets in
Photo Slots. Its machine-readable companion is [`ASSET_INVENTORY.json`](./ASSET_INVENTORY.json).
The JSON file is the canonical list for scripts; this document explains how
the paths, database references, generated variants, and retention rules fit
together.

The governing product rule is **default first, override second**. A brand-new
Photo Slots instance must be a complete, marketable demonstration without any
GodHost setup. Shared application defaults are bundled once and used by every
instance; uploading or recording through an instance creates a per-instance
override without changing the shared default.

Last verified: 2026-07-31.

## Scope

Included here are images, animated GIFs, audio, video, PWA icons, uploaded
media, and server-generated media. JavaScript, CSS, HTML, manifests, and other
application source files are not classified as media assets, even when the
web server serves them through the same front controller.

## Path vocabulary

| Name | Location or pattern |
|---|---|
| Project root | `Events/Weddings` |
| Production project root | `/home/airbridg/public_html/ceriapps/Airbridge/Events/Weddings` |
| Bundled public media | `apps/web/public` |
| Default uploaded-media root | `server/storage/media` |
| Configurable uploaded-media root | Environment variable `MEDIA_STORAGE_ROOT` |
| Uploaded asset directory | `<MEDIA_STORAGE_ROOT>/<wedding-public-id>/<asset-UUID>/` |
| Public uploaded-media URL | `/media/<wedding-public-id>/<asset-UUID>/<filename>` |
| Production uploaded asset directory | `/home/airbridg/public_html/ceriapps/Airbridge/Events/Weddings/server/storage/media/<wedding-public-id>/<asset-UUID>/` unless `MEDIA_STORAGE_ROOT` overrides it |

`wedding-public-id` identifies one Photo Slots instance (for example,
`daniel-and-alexys`). `asset-UUID` identifies one immutable uploaded version
owned by that instance. Neither identifier appears in a shared-default path.

There are two deliberately separate storage classes:

| Storage class | Example | Scope | How current is selected |
|---|---|---|---|
| Shared application default | `apps/web/public/videos/default-guest-win.mp4` | Every instance with no override | Bootstrap supplies a synthetic `isDefault: true` asset record |
| Per-instance uploaded override | `server/storage/media/daniel-and-alexys/<asset-UUID>/video.mp4` | One instance | That instance's database JSON points to the uploaded asset |

“Immutable blob” in this document means an opaque media file that is not
edited in place. It does **not** mean a MySQL `BLOB` column. Production MySQL
stores asset metadata and URLs in the `instances.config` JSON column; the
binary media remains in media storage.

The PHP front controller maps `/images`, `/videos`, `/GIFs`, `/effects`,
`/airbridge`, and `/media` URLs to the corresponding locations. When the app
is mounted below `/events/weddings`, JSON responses prefix these public URLs
with that application base path.

## Bundled static assets

### PWA and interface images

| Human name | Public URL | Repository location | Consumer |
|---|---|---|---|
| Generic 192px PWA icon | `/public/icons/icon-192.png` | `apps/web/public/icons/icon-192.png` | Static manifest fallback |
| Generic 512px PWA icon | `/public/icons/icon-512.png` | `apps/web/public/icons/icon-512.png` | Static manifest fallback |
| Default welcome poster | `/images/default-welcome-poster.png` | `apps/web/public/images/default-welcome-poster.png` | Shared welcome preview/poster |
| Default Guest-win poster | `/images/default-guest-win-poster.png` | `apps/web/public/images/default-guest-win-poster.png` | Shared Guest-win preview/poster |
| Default welcome video | `/videos/default-welcome.mp4` | `apps/web/public/videos/default-welcome.mp4` | Welcome message when no GodHost override exists |
| Default Guest-win video | `/videos/default-guest-win.mp4` | `apps/web/public/videos/default-guest-win.mp4` | Winner message when no GodHost override exists |
| Fireworks animation | `/images/fireworks.gif` | `apps/web/public/images/fireworks.gif` | Built-in event-animation object |
| Legacy background | `/images/background.png` | `apps/web/public/images/background.png` | Legacy slot skin |
| Legacy reel normal | `/images/reel_normal.png` | `apps/web/public/images/reel_normal.png` | Legacy slot skin |
| Legacy reel blur | `/images/reel_blur.png` | `apps/web/public/images/reel_blur.png` | Legacy slot skin |
| Slot background | `/images/slot/background.png` | `apps/web/public/images/slot/background.png` | Current slot skin |
| Slot reel normal | `/images/slot/reel_normal.png` | `apps/web/public/images/slot/reel_normal.png` | Current slot skin |
| Slot reel blur | `/images/slot/reel_blur.png` | `apps/web/public/images/slot/reel_blur.png` | Current slot skin |
| The Voice normal | `/images/slot/thevoice_normal.png` | `apps/web/public/images/slot/thevoice_normal.png` | Alternate slot skin asset |
| The Voice blur | `/images/slot/thevoice_blur.png` | `apps/web/public/images/slot/thevoice_blur.png` | Alternate slot skin asset |
| Airbridge icon | `/airbridge/Transceiver/abicon.png` | `apps/web/public/airbridge/Transceiver/abicon.png` | Guest-app Airbridge control |
| Airbridge compatibility icon | `/airbridge/abicon.png` | `apps/web/public/airbridge/abicon.png` | Older Airbridge references |

### Preset animated backgrounds

All preset background GIFs live in `apps/web/public/GIFs` and are served as
`/GIFs/<filename>`. They are selectable for the wedding background, reel
background, idle-gallery background, and event animation object.

`aura.gif`, `beach.gif`, `brainpump.gif`, `couplemoonlight.gif`, `cross.gif`,
`diamond.gif`, `earthsmall.gif`, `ekg.gif`, `fallleaves.gif`, `fireworks.gif`,
`gaydiamond.gif`, `heart.gif`, `jet.gif`, `medical.gif`, `moonsmall.gif`,
`puppypopup.gif`, `stars.gif`, `starwarp.gif`, `sun.gif`, `tvnoise.gif`, and
`warp.gif`.

### Bundled sound effects

The primary copies live in `apps/web/public/effects` and are served as
`/effects/<filename>`. The Airbridge Transceiver has a compatibility copy of
the sound library under `apps/web/public/airbridge/Transceiver/effects`.

| Runtime purpose | Primary file |
|---|---|
| Slot lever/pull | `slotPull.mp3` |
| No match | `aww1.mp3` |
| Two-symbol match | `clapping.mp3` |
| Guest replacement win | `winning.mp3` |
| Ordinary/default win | `easy.mp3` |
| Protected Couple Win | `WeddingMarchGoth.mp3` |
| Lightbox opening | `rampup.mp3` |
| Lightbox closing | `rampdown.mp3` |

Other bundled effect choices are `WeddingMarch.mp3`, `WeddingMarch2.mp3`,
`abjingle.mp3`, `celticWedding.mp3`, `click.mp3`, `golfClapping.mp3`,
`golftorpedo.mp3`, `pulseeffect.mp3`, `pulserifle.mp3`, `slot3.mp3`,
`slotHandle.mp3`, and `stumble.wav`. The Transceiver compatibility directory
also contains `slot1.mp3`.

## Shared generated defaults

The following URLs are generated by `server/public/index.php`; there are no
individual SVG files on disk. Despite the historical `/assets/demo/` route
name, these are production shared defaults, not test fixtures:

| Public URL | Meaning |
|---|---|
| `/assets/demo/background.svg` | Default wedding background placeholder |
| `/assets/demo/icon.svg` | Default launch/animation heart placeholder |
| `/assets/demo/splash.svg` | Default splash placeholder |
| `/assets/demo/symbol-<1..18>.svg` | Default reel-symbol placeholders |

All 18 reel symbols are present on a fresh instance, appear in its starter
gallery and generated reel sprite, and are independently replaceable by a
GodHost. Until replaced, their occupant URLs point to the shared generated
SVGs rather than to `{publicId}/{assetUUID}` media. This preserves a complete
working reel before administration.

## Default and override selection

| Feature | Shared default | Per-instance override field | Reset behavior |
|---|---|---|---|
| Welcome video | `/videos/default-welcome.mp4` plus `/images/default-welcome-poster.png` | `weddings[publicId].welcomeVideo` | “Use Built-In Default Video” removes the field; Bootstrap supplies the shared default |
| Guest-win video | `/videos/default-guest-win.mp4` plus `/images/default-guest-win-poster.png` | `weddings[publicId].guestWinMessageVideos[0]` | Reset empties the array; Bootstrap supplies the shared default |
| 18 reel symbols | `/assets/demo/symbol-1.svg` through `symbol-18.svg`; raster equivalents seed the reel sprite | `weddings[publicId].symbols[*].occupant` and `starterOccupants[*]` | Reset restores that symbol's generated shared default |
| Background, splash and launch icon | `/assets/demo/background.svg`, `splash.svg`, and `icon.svg` | Corresponding `weddings[publicId].theme` fields | Default remains until a GodHost upload changes the field |
| Runtime sounds | Bundled files in `/effects` | Reserved sound roles are storage-recognized but not fully wired to admin overrides | Bundled sound remains active |

Bundled defaults are application code and are replaced during deployment.
Per-instance overrides are production content and must never be overwritten
by deploying the application.

## Uploaded and dynamic asset roles

The backend accepts the following role identifiers. Uploaded content is
stored below the wedding/UUID directory; the active URL and metadata are
stored in the wedding database record.

| Backend role | Human name | Database reference | Normal stored output | Primary consumer |
|---|---|---|---|---|
| `launch_icon` | Wedding launch icon | `wedding.theme.launchIconUrl` | `reel.webp` or `reel.gif` | Dynamic PWA manifest and Apple touch icon |
| `background` | Wedding background | `wedding.theme.backgroundImageUrl` | `reel.webp` or `reel.gif` | Guest app background |
| `reel_background` | Reel background | `wedding.theme.reelBackgroundUrl` | `reel.webp` or `reel.gif` | Slot machine panel |
| `idle_gallery_background` | Gallery background | `wedding.theme.idleGalleryBackgroundUrl` | `reel.webp`, `reel.gif`, or `video.mp4` | Idle gallery |
| `splash` | Reserved splash upload role | No active `applySetupAsset` mapping; new weddings contain a placeholder `wedding.theme.splashImageUrl` | `reel.webp` or `reel.gif` | Storage-recognized but not currently exposed by admin |
| `protected_image` | Protected couple image, left/center/right | `wedding.theme.protectedImages`, `protectedImageOriginals`, captions/status maps | `reel.webp` or `reel.gif` | Protected overlay and gallery |
| `default_symbol` | GodHost starter image, slots 0–17 | `wedding.symbols[*].occupant` and `wedding.starterOccupants[*]` | `reel.webp` or `reel.gif` | Reel sprite and starter gallery |
| `couple_welcome_video` | Wedding-party welcome video | `wedding.welcomeVideo` | `video.mp4` | Welcome-message player |
| `guest_win_message_video` | GodHost Guest-win override | `wedding.guestWinMessageVideos[0]` | `video.mp4`; optional poster stored as an image asset | Winner-message player |
| `animation_object` | Uploaded event-animation object | `wedding.animationObjects[]`; selected URL is also stored in animation trigger configuration | `reel.webp` or `reel.gif` | Event animation runtime |
| `music` | Reserved music role | No active admin-field or `applySetupAsset` mapping | `audio.<ext>` | Storage-recognized only |
| `spin_sound` | Reserved custom spin-sound role | No active admin-field or `applySetupAsset` mapping | `audio.<ext>` | Storage-recognized only |
| `win_sound` | Reserved custom win-sound role | No active admin-field or `applySetupAsset` mapping | `audio.<ext>` | Storage-recognized only |
| `protected_sound` | Reserved Protected Couple Win sound role | No active admin-field or `applySetupAsset` mapping | `audio.<ext>` | Storage-recognized only |
| `arrival_sound` | Reserved arrival-sound role | No active admin-field or `applySetupAsset` mapping | `audio.<ext>` | Storage-recognized only |
| `departure_sound` | Reserved departure-sound role | No active admin-field or `applySetupAsset` mapping | `audio.<ext>` | Storage-recognized only |
| `sound` | Reserved generic-sound role | No active admin-field or `applySetupAsset` mapping | `audio.<ext>` | Storage-recognized only |

## Guest submissions and derived assets

Guest slot wins and wishing-well submissions use the internal role
`guest_replacement`. The database records live in `replacementUploads` while
pending and are copied into symbol occupants or `galleryMedia` when approved.

| Submission material | Stored output | Presentation behavior |
|---|---|---|
| One photo | `archive.webp`, `reel.webp`, `thumbnail.webp` in one asset UUID directory | Reel, moderation, popup, or gallery as appropriate |
| Slot/well-wish video | Picked-frame variants above plus `video.mp4` in a separate asset UUID directory | Picked frame is the reel/gallery poster; MP4 plays in the popup/gallery |
| Four-photo submission | Four sets of image variants plus a fifth composite set | Four originals appear in moderation; the composite is one public popup/gallery item; photo 1 is the reel face for a slot win |
| Sticker derivative | `reel-sticker.png` | Transparent reel presentation when generated |

Every accepted input video (MP4, WebM, or QuickTime/MOV) is normalized by
FFmpeg to H.264/AAC MP4 with `yuv420p`, H.264 High Profile Level 4.0, stripped
source metadata, and fast-start metadata before it is published. Output keeps
its aspect ratio, is never upscaled, and is constrained to at most 1280 pixels
in either orientation for mobile PWA delivery. Only the first standard audio
track is selected so iPhone Spatial Audio companion tracks cannot break
server conversion. The `FFMPEG_BINARY` environment variable may override the
binary name/path.

## Server-generated assets

| Asset | Storage path | Database reference or consumer |
|---|---|---|
| Current reel sprite | `<MEDIA_STORAGE_ROOT>/<wedding-public-id>/reel-sprite/normal.png` | `wedding.theme.reelSpriteUrl`; reel renderer |
| Wedding-specific Apple touch icon | Generated per request from the configured launch icon; not retained as a separate file | `/api/v1/weddings/<public-id>/apple-touch-icon.png` |
| Wedding-specific web manifest | Generated per request; not retained as a separate file | `/api/v1/weddings/<public-id>/manifest.webmanifest` |
| Offline replay bundle assets | Copied into the replay export below the wedding media root | Replay export metadata and generated bundle |

## Database authority

Uploaded files in media storage are immutable media objects; the database
decides which uploaded object is current. The video/image bytes are not stored
in `welcomeVideo` or `guestWinMessageVideos`, nor in a MySQL `BLOB`. Those
containers hold JSON metadata such as asset ID, role, URL, MIME type, byte
size, and SHA-256. Production normally uses the MySQL `instances.config` JSON
column. Local/fallback operation uses `server/storage/dev-database.json`.
Important logical containers include:

- `weddings[publicId].theme`
- `weddings[publicId].symbols`
- `weddings[publicId].starterOccupants`
- `weddings[publicId].welcomeVideo`
- `weddings[publicId].guestWinMessageVideos`
- `weddings[publicId].animationObjects`
- `weddings[publicId].galleryMedia`
- `replacementUploads`
- `galleryMedia[publicId]`
- `occupantHistory[publicId]`

Never determine whether an asset is active merely by finding its file on
disk. Resolve uploaded media through the active database record. Resolve a
shared default through Bootstrap's documented fallback when the override field
is absent or empty.

## Replacing a bundled video default

1. Produce a generic source that does not require couple-specific names.
2. Encode it through the same FFmpeg H.264/AAC mobile pipeline described
   above and publish it as the existing `.mp4` filename.
3. Replace its poster with a reasonably sized PNG matching the video's aspect
   ratio.
4. Compute SHA-256 for video and poster, then replace the first 12 characters
   used in the `?v=` cache keys in `BootstrapController.php`.
5. Verify codec, dimensions, file size, fast-start playback, and both a fresh
   instance and an instance with an override.

Changing a bundled default affects every instance that has no corresponding
GodHost override. It does not modify or copy anything under
`server/storage/media/{publicId}/{assetUUID}`.

## Retention and deletion

- The 18 GodHost starter occupants and three protected couple images are
  protected from the “Delete all submissions” operation.
- That operation removes guest replacement records and their unprotected
  media references, restores starter occupants, regenerates the reel sprite,
  and rebuilds the gallery from the 18 starter plus three protected images.
- Replacing a setup asset changes the active database reference; old immutable
  asset directories may remain until an explicit cleanup policy removes them.
- `server/storage/media` is production content and must be backed up. It must
  not be overwritten or deleted during an application-code deployment.
- Bundled static assets are deployed with application code and are recoverable
  from the repository.

## Maintenance rule

Whenever a media file, public route, backend asset role, generated derivative,
database reference, or deletion rule changes, update both this Markdown file
and `ASSET_INVENTORY.json` in the same change. Validate the JSON with:

```bash
python3 -m json.tool Events/Weddings/docs/ASSET_INVENTORY.json >/dev/null
```
