# Project export: Muse

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Your smart digital wardrobe. Organize, visualize, and create outfits from your own clothes on a dedicated Raspberry Pi powered device.
- Devpost: https://devpost.com/software/muse-fuihs0
- GitHub: https://github.com/LazyWheelDev/Muse
- Video: https://www.youtube.com/embed/ZXjGipjJ7RQ?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — LazyWheelDev (1 commits)

## Devpost submission (written by the team)

### Inspiration

I spend a large part of my time learning, coding, experimenting, and building projects. My goal is to become a software engineer and eventually specialize in intelligent physical systems and robotics. I also live with a significant physical disability, which creates practical challenges in my daily life. That has strongly influenced the way I see technology. Code and algorithms exist to solve problems, so whenever I face a difficulty, my instinct is often to imagine a system that could remove it, simplify it, or make it more accessible. I enjoy solving my own problems, but I also love building things that make other people’s lives easier. Seeing someone smile because something I created helped them is one of the main reasons I enjoy engineering. Muse started with my sister. While preparing to move into a new apartment, she saw an Instagram video of a woman using a wardrobe-related application that her husband had built for her. The video only showed the concept briefly, but my sister sent it to me and said, “You could build something like this.” At first, it remained only an idea. Later that same evening, I discovered OpenAI Build Week and learned that participants would use Codex and GPT-5.6 to build a complete project. I immediately remembered my sister’s suggestion and decided to combine something useful for someone I love with something I genuinely enjoy doing. I did not want to build only a clothing gallery or a simple list of outfits. I imagined a dedicated physical device mounted near a wardrobe, acting as its digital counterpart. The idea also connected directly to my own life. A nurse helps me get ready every morning, and depending on the situation, selecting and retrieving clothes can require assistance. With Muse, I can prepare an outfit in advance and simply show the exact garments that should be taken from the wardrobe. It also helps with another frustrating problem: clothes can get misplaced. A digital wardrobe creates a persistent visual record of what I own, even when the physical item is temporarily difficult to find. What started as an idea for my sister became a product that could help both of us, while also being useful to many other people.

### What it does

Muse is a local-first smart wardrobe running on a dedicated Raspberry Pi 5 with a touchscreen. It allows users to create a visual digital version of their physical wardrobe. Garments can be added, organized, viewed, and used to build complete outfits. The main features include: cataloguing garments; adding clothing from a phone through a temporary QR code; browsing a visual wardrobe; creating outfits with a drag-and-drop builder; positioning, resizing, and layering garments; saving completed outfits; reopening saved outfits later; managing local data, backups, storage, and device information. The phone upload workflow happens over the local network. Muse displays a temporary QR code, the user scans it with a phone, selects a garment photo, enters its information, and uploads it directly to the Raspberry Pi. The new garment then appears inside Muse. Muse is designed as a dedicated appliance rather than an ordinary website opened in a browser. When the Raspberry Pi starts, Muse launches automatically into a full-screen touchscreen interface. The core wardrobe data is stored locally in SQLite on the device. The main experience does not require a permanent cloud account or remote application server. For someone receiving help while getting dressed, Muse can also act as a clear visual reference. A saved outfit shows exactly which garments should be retrieved, reducing uncertainty and making communication easier.

### How we built it

I began by sketching the product, defining the user experience, and deciding what a dedicated wardrobe device should feel like. I then selected the hardware, assembled the Raspberry Pi 5, installed its cooling system, connected the touchscreen, prepared the operating system, and tested the physical components. Muse uses a React and TypeScript frontend, a FastAPI backend, and a SQLite database. The Raspberry Pi runs the application through systemd services. Chromium is configured as a full-screen touchscreen kiosk. The main API listens only on the local loopback interface, while a separate restricted listener handles temporary phone uploads over the local network. The project also includes: database migrations with Alembic; automated backups and backup verification; atomic release directories; deployment and activation scripts; service hardening; health checks; frontend and backend tests; production browser tests; Linux validation through GitHub Actions. I used Codex throughout the project for architecture, implementation, code review, testing, debugging, deployment, and documentation. GPT-5.6 helped reason across the different layers of the system, including the interface, backend, networking, Chromium, Wayland, systemd, and the physical Raspberry Pi environment. This was not a one-prompt generation process. I defined the product vision, created the sketches, selected and assembled the hardware, made the design decisions, tested the application on the real device, reproduced failures, inspected the results, and decided what should be changed. The development process was highly iterative. I tested Muse physically, reported exactly what happened, collected terminal output and logs, and then used Codex to investigate and implement tested corrections. The final repository contains hundreds of automated tests covering the backend, frontend, outfit builder, phone uploads, backups, database migrations, deployment tooling, kiosk startup, and production workflows.

### Challenges we ran into

Almost every part of Muse was new to me. Before this project, I had mainly worked with software. I had never built a complete physical product using a Raspberry Pi and a bare touchscreen. I had to learn how to install the operating system, assemble the Raspberry Pi, position thermal pads, install the cooling system, connect power, connect video, connect the touchscreen controller, and verify that every component behaved correctly. The touchscreen arrived with its electronics exposed at the back, without a protective enclosure. That made even simple actions feel important because every connector, board, and cable had to be handled carefully. I had assembled a desktop computer before, but this was very different. Desktop hardware usually has standardized mounting points and clearer layouts. With the Raspberry Pi, I was looking at exposed chips and thermal pads of different sizes while trying to understand exactly where everything belonged. The software deployment introduced an entirely different set of challenges. I had used Linux before, but I had never managed a Linux device remotely from a Mac through SSH at this level. Muse had to coordinate: Linux users and permissions; systemd dependencies; service sandboxing and hardening; Chromium kiosk startup; Wayland access; private runtime directories; local network discovery; QR upload sessions; database migrations; backups; release activation; automatic startup after reboot. Several issues appeared only on the real Raspberry Pi. Chromium initially failed because of Crashpad, keyring behavior, private runtime directories, and restricted access to the Wayland session. The QR upload system later failed because the backend used socket.if_nameindex() to discover local network interfaces, but the service sandbox did not allow AF_NETLINK. The most important failure appeared during a real cold boot. Muse depended on /run/muse, but /run is volatile and is cleared during reboot. The directory had previously been created only during installation. On the next boot, systemd attempted to build a protected mount namespace around a path that no longer existed, causing 226/NAMESPACE. The preparation service failed, and every dependent Muse service failed with it. The earlier automated validation had not reproduced that exact situation because some tests created the directory in advance, while previous physical checks had been warm activations directly after installation. We corrected the architecture by adding a proper systemd tmpfiles configuration that recreates /run/muse during boot with the required root:muse ownership and 0750 permissions. We also added regression coverage that begins with the directory absent and verifies its creation in a clean Linux environment. After deploying the corrected release, removing all temporary overrides, and performing a genuine cold boot, the Raspberry Pi started normally and Muse launched automatically with no Muse errors in the current boot journal.

### Accomplishments we're proud of

The accomplishment I am most proud of is that Muse became a real physical product, not only a prototype running on my development computer. This was my first time building with a Raspberry Pi, my first time using a bare touchscreen, and my first complete physical computing project. Muse now: boots automatically into its touchscreen interface; survives a complete cold reboot; preserves garments and saved outfits across deployments; accepts garment uploads from a phone through a QR code; keeps the main API restricted to the device; stores its core data locally; creates and verifies backups; runs through hardened Linux services; passes hundreds of automated tests; has a reproducible release and deployment process. I am also proud that we did not stop at temporary fixes. When physical testing revealed problems, we investigated the real cause and integrated permanent corrections into the repository. For example, the final cold-boot issue could have been hidden by manually recreating /run/muse each time. Instead, we corrected the system architecture, added a proper boot-time producer for the directory, wrote regression tests, rebuilt the release, deployed it, removed the temporary overrides, and verified the result through a real reboot. I am equally proud that my original sketches and product decisions remained central throughout the process. Codex helped me move much faster, but Muse still reflects the experience I wanted to create and the real-life problems I wanted to solve. Building something useful for my sister, while also creating something that can help me in my own daily routine, made the project personally meaningful.

### What we learned

Muse taught me that building a real product is very different from making an interface work once on a development computer. A product must survive installation, permissions, service failures, networking conditions, database changes, deployments, and complete reboots. It must protect user data and recover predictably. I learned how hardware and software affect each other. A configuration that looks correct in source code can still fail because of the physical display session, the Linux boot sequence, a missing volatile directory, or a restricted system capability. I gained practical experience with: Raspberry Pi hardware; touchscreen integration; Linux administration; SSH; systemd; Chromium kiosk applications; Wayland; local networking; FastAPI; React and TypeScript; SQLite and Alembic; deployment automation; release packaging; backups; service hardening; browser and integration testing; production debugging. I also learned how valuable AI-assisted engineering can be when it is used as a collaboration rather than as a replacement for human decisions. Codex could analyze logs, inspect code, write tests, and propose fixes, but the real device still required observation and judgment from me. The AI could not physically see whether the touchscreen displayed the correct interface, whether a cable was connected properly, whether Muse appeared after reboot, or whether the experience felt right to use. That collaboration allowed me to attempt something far beyond my previous hardware experience without approaching it completely alone. One of my secondary-school teachers used LaTeX to create our lessons, gave me some of the first programming books I ever read, and told me that I could go far with code. Completing my first working physical product made me think about those words again.

### What's next

The Build Week version of Muse focuses on creating a reliable local-first digital wardrobe, importing garments from a phone, and visually building and saving outfits. The next major feature I want to add is weather-aware outfit assistance. Muse could display the current local temperature and suggest saved outfits that are appropriate for the weather. While creating an outfit, it could also provide small practical notes, such as warning that a top may be too light for the expected temperature or suggesting an additional layer. The home screen could adapt visually to the season, with different atmospheres for winter, spring, summer, and autumn. Other future improvements include: improved garment background removal; better body and outfit visualization; automatic software updates with rollback; multiple Muse devices and user profiles; improved Wi-Fi and display controls; custom enclosures; a dedicated printed circuit board; synchronization options that preserve the local-first architecture; support for households and families; smarter outfit recommendations based on context, season, weather, and personal preferences. In the longer term, Muse could move from answering “What is in my wardrobe?” to answering “What should I wear today?” Muse is my first physical computing product, but it will not be my last.

## README (from the GitHub repository)

# Muse

Muse is a local-first smart wardrobe for a dedicated Raspberry Pi 5 touchscreen.
Clothing, outfits, image processing, preferences, and storage remain on the
device. No cloud account, subscription, hosted service, or paid API is mandatory;
phone upload uses only the trusted local network. Optional Internet connectivity
may later be used for explicitly initiated software updates.

The current implementation includes the complete garment, phone-import, outfit,
and product-experience slices: secure local streaming import, short-lived QR
handoff, exact-original preservation, safe local derivatives, SQLite
persistence, Wardrobe and Clothing Details, the manual Outfit Builder,
deterministic local preview generation, the approved Saved Outfits grid,
readiness-aware Splash, typed Settings, local backups, capability-aware device
information, and the complete Raspberry Pi release/deployment architecture. On
July 17, 2026, the application was physically exercised on a Raspberry Pi 5 with
8 GB RAM, Raspberry Pi OS, labwc/Wayland, and the intended `1280 × 800`
touchscreen. The touchscreen UI, Wardrobe, Details, QR phone upload, Outfit
Builder, Saved Outfits, Settings, local-network status, persistence, and backups
all operated on that device. Each immutable release still requires its own clean
deployment and cold-boot acceptance record.

## MVP principles

- Local-first data and processing
- Touch-first at `1280 × 800`
- User-controlled outfit selection
- No mandatory paid API or hosted service
- Reliable Raspberry Pi kiosk operation
- Approved mockups and design-system documentation as the visual source of truth

See [docs/mvp-scope.md](docs/mvp-scope.md) for product scope and
[docs/architecture.md](docs/architecture.md) for the runtime and persistence
design. Production deployment is documented in
[docs/raspberry-pi-deployment.md](docs/raspberry-pi-deployment.md), with the
physical sequence in
[docs/raspberry-pi-operator-runbook.md](docs/raspberry-pi-operator-runbook.md).

## OpenAI Build Week

Muse was designed and implemented during OpenAI Build Week with Codex as the
primary engineering collaborator. Codex helped audit product documents and
mockups, translate the approved scope into phased architecture, implement and
test the React/FastAPI/SQLite system, review security boundaries, build release
tooling, and diagnose failures observed on the physical Raspberry Pi. GPT-5.6
Sol powered the primary Codex implementation sessions. The repository records
the material assisted work and verification evidence in
[docs/codex-build-log.md](docs/codex-build-log.md).

The human retained the product and release decisions: MVP scope, approved visual
direction, the choice to keep garment selection manual and storage local, the
two-listener phone-upload boundary, the Raspberry Pi hardware and network setup,
which trade-offs were acceptable, and whether physical behavior passed the demo
gate. Codex did not replace hardware observation, operator authentication, or
the final release decision. Muse has no runtime dependency on Codex, GPT-5.6
Sol, another OpenAI model, or an OpenAI API.

The reproducible entry points are:

| Goal                                | Command or section                                                                                                                       |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Install development dependencies    | Follow [First-time setup](#first-time-setup) with `uv sync --locked --all-groups` and `npm ci`                                           |
| Run locally                         | Start `muse-backend migrate`, `muse-backend serve --reload`, then `npm run dev` as documented in [Local development](#local-development) |
| Run the complete verification suite | Use the backend, frontend, browser, kiosk, and release commands in [Verification](#verification)                                         |
| Build an immutable Pi release       | Run `./kiosk/build-release.sh` from a clean synchronized commit                                                                          |
| Deploy and validate the Pi          | Follow [the operator runbook](docs/raspberry-pi-operator-runbook.md), beginning with a verified production backup                        |

## Repository structure

```text
assets/      Approved design references, brand assets, icons, and media
backend/     FastAPI API, SQLite migrations, persistence, and local storage
docs/        Product, interface, architecture, and roadmap documentation
frontend/    React, TypeScript, and Vite touchscreen application
kiosk/       Raspberry Pi startup, deployment, and kiosk configuration
```

## Supported toolchains

- Python `3.13.x` (constrained to `>=3.13,<3.14`)
- uv `0.11.28`
- Node.js `24.18.0`
- npm `11.16.0`

Python 3.13 is available for current 64-bit Raspberry Pi OS and gives Muse a
single reproducible development and ARM64 production target. The exact Python
minor and uv version are recorded in `backend/.python-version` and
`backend/pyproject.toml`; Python dependencies are frozen in `backend/uv.lock`.
Node and npm requirements are recorded in `.nvmrc`, `frontend/package.json`, and
`frontend/package-lock.json`.

Install [uv](https://docs.astral.sh/uv/) `0.11.28` and `nvm`, or provide the same
toolchain versions another way. No globally installed Python packages are used.

On macOS, if Python reports that hidden `.pth` files inside `.venv` were skipped,
set `export UV_PROJECT_ENVIRONMENT=venv` before `uv sync` and in each new shell
that runs backend commands. The alternate `backend/venv/` directory is already
gitignored; Linux and Raspberry Pi environments can use uv's normal `.venv`.

## First-time setup

From the repository root:

```bash
cd backend
uv sync --locked --all-groups

cd ../frontend
nvm install
nvm use
npm ci
npx playwright install chromium
```

On Debian, Ubuntu, or Raspberry Pi OS development images, Playwright's browser
and operating-system dependencies can be installed with:

```bash
cd frontend
npx playwright install --with-deps chromium
```

Playwright is a development/CI dependency only. The deployed kiosk uses the
system Chromium package.

## Local development

Copy the documented backend settings only when you need local overrides; the
checked-in defaults already work for development:

```bash
cp backend/.env.example backend/.env
```

Start the backend before the frontend. In terminal 1:

```bash
cd backend
uv run muse-backend migrate
uv run muse-backend serve --reload
```

In terminal 2:

```bash
cd frontend
npm run dev
```

`npm run dev:mobile` and `npm run preview:mobile` are available for isolated
responsive styling work, but they do not replace the token-authorized LAN
listener. Use the two-process procedure below for a real phone upload.

The ordinary Vite workflow exercises the loopback device application. To test
the real restricted phone surface, build both frontends and give the main and
restricted processes the same disposable configuration. Set these values in
the untracked `backend/.env` before starting either process:

```dotenv
MUSE_DATA_ROOT=/tmp/muse-phone-upload-dev
MUSE_PHONE_UPLOAD_ENABLED=true
MUSE_PHONE_UPLOAD_BIND_HOST=127.0.0.1
MUSE_PHONE_UPLOAD_TRUSTED_HOSTS=["127.0.0.1","localhost"]
MUSE_PHONE_UPLOAD_FRONTEND_BUILD_PATH=../frontend/dist-phone
```

Then build, migrate the disposable database, restart the main process in
terminal 1, and start the restricted process in terminal 3:

```bash
cd frontend
npm run build

cd ../backend
uv run muse-backend migrate
uv run muse-backend serve --reload

# Run from backend/ in another terminal with the same backend/.env.
uv run muse-backend serve-phone-upload
```

When testing from a separate phone, set both bind and advertised IPv4 to the Pi
or development machine's same exact private LAN address, add it to the phone
trusted-host 

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 294 recognized source files, 1914 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (120 of 329)

```
.editorconfig
.github/workflows/ci.yml
.gitignore
.nvmrc
AGENTS.md
assets/ui/mockups/README.md
backend/.env.example
backend/.python-version
backend/alembic.ini
backend/migrations/env.py
backend/migrations/README
backend/migrations/script.py.mako
backend/migrations/versions/20260715_0001_initial_schema.py
backend/migrations/versions/20260715_0002_garment_import_processing.py
backend/migrations/versions/20260715_0003_phone_upload_sessions.py
backend/pyproject.toml
backend/README.md
backend/src/muse_backend/__init__.py
backend/src/muse_backend/api/__init__.py
backend/src/muse_backend/api/dependencies.py
backend/src/muse_backend/api/errors.py
backend/src/muse_backend/api/v1/__init__.py
backend/src/muse_backend/api/v1/clothing.py
backend/src/muse_backend/api/v1/health.py
backend/src/muse_backend/api/v1/media.py
backend/src/muse_backend/api/v1/outfits.py
backend/src/muse_backend/api/v1/phone_upload_sessions.py
backend/src/muse_backend/api/v1/router.py
backend/src/muse_backend/api/v1/settings.py
backend/src/muse_backend/application.py
backend/src/muse_backend/cli.py
backend/src/muse_backend/config.py
backend/src/muse_backend/database/__init__.py
backend/src/muse_backend/database/base.py
backend/src/muse_backend/database/engine.py
backend/src/muse_backend/database/migrations.py
backend/src/muse_backend/database/models/__init__.py
backend/src/muse_backend/database/models/clothing.py
backend/src/muse_backend/database/models/outfit.py
backend/src/muse_backend/database/models/phone_upload.py
backend/src/muse_backend/database/models/setting.py
backend/src/muse_backend/domain/__init__.py
backend/src/muse_backend/domain/enums.py
backend/src/muse_backend/domain/exceptions.py
backend/src/muse_backend/domain/validation.py
backend/src/muse_backend/frontend.py
backend/src/muse_backend/main.py
backend/src/muse_backend/middleware/__init__.py
backend/src/muse_backend/middleware/device_action.py
backend/src/muse_backend/middleware/request_id.py
backend/src/muse_backend/middleware/security.py
backend/src/muse_backend/middleware/settings_security.py
backend/src/muse_backend/phone_upload/__init__.py
backend/src/muse_backend/phone_upload/application.py
backend/src/muse_backend/phone_upload/security.py
backend/src/muse_backend/phone_upload/static.py
backend/src/muse_backend/platform/__init__.py
backend/src/muse_backend/platform/contracts.py
backend/src/muse_backend/platform/local.py
backend/src/muse_backend/repositories/__init__.py
backend/src/muse_backend/repositories/clothing.py
backend/src/muse_backend/repositories/outfits.py
backend/src/muse_backend/repositories/settings.py
backend/src/muse_backend/schemas/__init__.py
backend/src/muse_backend/schemas/clothing.py
backend/src/muse_backend/schemas/common.py
backend/src/muse_backend/schemas/outfit.py
backend/src/muse_backend/schemas/phone_upload.py
backend/src/muse_backend/schemas/settings.py
backend/src/muse_backend/services/__init__.py
backend/src/muse_backend/services/application_settings.py
backend/src/muse_backend/services/background_processing.py
backend/src/muse_backend/services/backups.py
backend/src/muse_backend/services/clothing.py
backend/src/muse_backend/services/device_control.py
backend/src/muse_backend/services/garment_import_workflow.py
backend/src/muse_backend/services/image_processing.py
backend/src/muse_backend/services/images.py
backend/src/muse_backend/services/import_admission.py
backend/src/muse_backend/services/imports.py
backend/src/muse_backend/services/lan_address.py
backend/src/muse_backend/services/maintenance.py
backend/src/muse_backend/services/multipart_import.py
backend/src/muse_backend/services/outfit_preview_renderer.py
backend/src/muse_backend/services/outfit_previews.py
backend/src/muse_backend/services/outfits.py
backend/src/muse_backend/services/phone_upload_listener.py
backend/src/muse_backend/services/phone_upload_sessions.py
backend/src/muse_backend/services/presenters.py
backend/src/muse_backend/services/production.py
backend/src/muse_backend/services/runtime_lock.py
backend/src/muse_backend/services/storage_summary.py
backend/src/muse_backend/storage/__init__.py
backend/src/muse_backend/storage/local.py
backend/tests/__init__.py
backend/tests/conftest.py
backend/tests/support.py
backend/tests/test_backups_and_maintenance.py
backend/tests/test_clothing_api.py
backend/tests/test_config_database.py
backend/tests/test_garment_import.py
backend/tests/test_images_and_media.py
backend/tests/test_import_migration.py
backend/tests/test_kiosk_deployment.py
backend/tests/test_outfit_preview_renderer.py
backend/tests/test_outfit_previews_api.py
backend/tests/test_outfits_api.py
backend/tests/test_phone_upload_cleanup.py
backend/tests/test_phone_upload_listener_probe.py
backend/tests/test_phone_upload.py
backend/tests/test_production_frontend.py
backend/tests/test_settings_api.py
backend/tests/test_storage.py
backend/tests/test_system_api.py
backend/uv.lock
CONTRIBUTING.md
docs/architecture.md
docs/backlog.md
docs/codex-build-log.md
docs/design-principles.md
[209 more files omitted for size]
```

### Dependencies

- backend/pyproject.toml: alembic@==1.18.5, fastapi@==0.139.0, pillow@==12.3.0, pydantic@==2.13.4, pydantic-settings@==2.14.2, python-multipart@==0.0.32, sqlalchemy@==2.0.51, uvicorn@==0.51.0
- frontend/package.json: @eslint/js@9.39.5, @fontsource-variable/inter@5.2.8, @fontsource-variable/playfair-display@5.2.8, @playwright/test@1.61.1, @tanstack/react-query@5.101.2, @testing-library/dom@10.4.1, @testing-library/jest-dom@6.9.1, @testing-library/react@16.3.2, @testing-library/user-event@14.6.1, @types/node@24.13.3, @types/pngjs@6.0.5, @types/react@19.2.17, @types/react-dom@19.2.3, @vitejs/plugin-react@6.0.3, eslint@9.39.5, eslint-plugin-jsx-a11y@6.10.2, eslint-plugin-react-hooks@7.1.1, eslint-plugin-react-refresh@0.5.3, globals@17.7.0, jsdom@29.1.1, jsqr@1.4.0, lucide-react@1.24.0, pngjs@7.0.0, prettier@3.9.5, qrcode.react@4.2.0, react@19.2.7, react-dom@19.2.7, react-router-dom@7.18.1, typescript@6.0.3, typescript-eslint@8.64.0, vite@8.1.4, vitest@4.1.10

### Recent commits (newest first)

- docs(deployment): record cold-boot namespace failure
- fix(deployment): recreate Muse runtime on cold boot
- docs: record Raspberry Pi demo baseline
- fix(kiosk): integrate Raspberry Pi runtime hardening
- Satisfy kiosk launcher shell validation
- Document validated Raspberry Pi kiosk setup
- Fix Chromium kiosk service startup
- fix(e2e): wait for garment processing before outfit creation
- fix(deployment): make sandbox installation idempotent on Linux
- fix(ci): provide complete systemd verification sandbox
- fix(deployment): close migration and diagnostic database handles
- docs: add Raspberry Pi deployment and acceptance runbook
- test(ci): verify deployment assets and release safety
- feat(platform): add constrained Raspberry Pi device capabilities
- feat(kiosk): add reproducible Raspberry Pi appliance deployment
- fix(frontend): use garment cutouts immediately in outfit builder
- docs: record the final product experience milestone
- test(ci): verify the production settings lifecycle
- feat(frontend): complete settings and startup experience
- feat(backend): complete local settings and maintenance core

## Key source files (fetched from GitHub, selected and truncated for size)

### AGENTS.md

```markdown
# AGENTS.md

## Project

Muse is an offline-first smart wardrobe designed for a dedicated Raspberry Pi touchscreen device.

## Product Goal

Build a polished MVP that allows users to:

1. Import clothing items
2. Organize them in a visual wardrobe
3. Place them on a silhouette
4. Control garment layers manually
5. Save complete outfits
6. Run the product in kiosk mode on Raspberry Pi

## Product Rules

- The MVP must remain focused.
- Do not add features outside the approved scope.
- New ideas must be documented under Version 2.
- Prefer reliability over feature count.
- Prefer polish over complexity.
- Essential functionality must work without Internet.
- No mandatory paid APIs.
- No runtime dependency on OpenAI models.
- The user must remain in control of outfit selection.
- Avoid hardcoded demo-only behavior.

## Engineering Rules

- Use clear, maintainable code.
- Keep modules small and focused.
- Add types wherever supported.
- Add validation at system boundaries.
- Add error handling for user-facing actions.
- Avoid unnecessary dependencies.
- Document important architectural decisions.
- Never commit secrets or credentials.
- Keep setup reproducible.
- Update documentation when behavior changes.

## Proposed Stack

### Frontend

- React
- TypeScript
- Vite
- CSS modules or a lightweight styling system

### Backend

- Python
- FastAPI
- SQLite
- Pillow or equivalent local image processing tools

### Device

- Raspberry Pi OS
- Chromium kiosk mode
- systemd service for automatic startup

## Development Priorities

1. Product foundation
2. Wardrobe data model
3. Wardrobe library
4. Outfit builder
5. Import workflow
6. Persistence
7. Kiosk deployment
8. Polish
9. Demo reliability

## Forbidden During MVP

Do not implement:

- AI-generated outfit recommendations
- Cloud synchronization
- Social networking
- Marketplace features
- Complex scraping
- Native mobile apps
- Photorealistic virtual try-on
- Multi-user account systems

## Definition of Done

A task is done only when:

- The feature works
- Errors are handled
- The UI is understandable
- Tests pass when applicable
- Documentation is updated
- The feature does not break the MVP scope
```

### CONTRIBUTING.md

```markdown
# Contributing to Muse

Muse is a local-first smart wardrobe for a dedicated Raspberry Pi
touchscreen. Keep changes within the approved MVP and follow `AGENTS.md`, the
design-system documentation, and the approved mockups.

## Toolchain and setup

Use the checked-in toolchain versions:

- Python `3.13.x`
- uv `0.11.28`
- Node.js `24.18.0`
- npm `11.16.0`

From the repository root:

```bash
cd backend
uv sync --locked --all-groups

cd ../frontend
nvm install
nvm use
npm ci
npx playwright install chromium
```

Use `npx playwright install --with-deps chromium` on a Linux environment that
also needs Playwright's operating-system packages. Do not install project Python
packages globally or replace `npm ci` with an unlocked CI install.

If macOS Python reports skipped hidden `.pth` files from `.venv`, export
`UV_PROJECT_ENVIRONMENT=venv` before sync and backend commands; that alternate
environment path is gitignored.

If dependency declarations change, regenerate and commit the appropriate lockfile
with the tool's pinned version. Do not hand-edit generated lock data.

## Development startup

Start and migrate FastAPI first:

```bash
cd backend
uv run muse-backend migrate
uv run muse-backend serve --reload
```

Then start Vite in a second terminal:

```bash
cd frontend
npm run dev
```

Use `npm run dev:mobile` or `npm run preview:mobile` only for isolated responsive
styling. They do not emulate session authorization or the restricted LAN
surface; functional phone work must use the listener below.

Phone-upload changes must also exercise the real restricted listener rather
than binding the main application to the LAN. Build both frontends and put the
following common settings in the untracked `backend/.env` so the loopback main
process and restricted process use the same disposable database:

```dotenv
MUSE_DATA_ROOT=/tmp/muse-phone-upload-dev
MUSE_PHONE_UPLOAD_ENABLED=true
MUSE_PHONE_UPLOAD_BIND_HOST=127.0.0.1
MUSE_PHONE_UPLOAD_TRUSTED_HOSTS=["127.0.0.1","localhost"]
MUSE_PHONE_UPLOAD_FRONTEND_BUILD_PATH=../frontend/dist-phone
```

Then migrate once and start the two processes separately:

```bash
cd frontend
npm run build

cd ../backend
uv run muse-backend migrate
uv run muse-backend serve --reload

# Run from backend/ in another terminal with the same backend/.env.
uv run muse-backend serve-phone-upload
```

The main server must remain on `127.0.0.1`. Use a configured private interface
only for an intentional physical-phone test.

The browser calls relative `/api/v1` paths. Vite proxies `/api` to FastAPI; do
not add a hard-coded production host. Local API documentation is available at
`http://127.0.0.1:8000/api/docs`.

Copy `backend/.env.example` or `frontend/.env.example` only when overrides are
needed. Never commit `.env`, secrets, machine paths, local databases, uploaded
media, test output, or coverage data.

## Required checks

Before handing off backend changes, run:

```bash
cd backend
uv run ruff format --check .
uv run ruff check .
uv run mypy
[truncated — 14533 more characters]
```

### backend/pyproject.toml

```
[build-system]
requires = ["uv_build==0.11.28"]
build-backend = "uv_build"

[project]
name = "muse-backend"
version = "0.2.0"
description = "Local FastAPI and SQLite backend for the Muse smart wardrobe."
readme = "README.md"
requires-python = ">=3.13,<3.14"
license = "MIT"
authors = [{ name = "LazyWheelDev" }]
dependencies = [
  "alembic==1.18.5",
  "fastapi==0.139.0",
  "pillow==12.3.0",
  "pydantic==2.13.4",
  "pydantic-settings==2.14.2",
  "python-multipart==0.0.32",
  "sqlalchemy==2.0.51",
  "uvicorn==0.51.0",
]

[project.scripts]
muse-backend = "muse_backend.cli:main"

[dependency-groups]
dev = [
  "asgi-lifespan==2.1.0",
  "httpx==0.28.1",
  "mypy==2.3.0",
  "pytest==9.1.1",
  "pytest-asyncio==1.4.0",
  "pytest-cov==7.1.0",
  "ruff==0.15.21",
]

[tool.uv]
required-version = "==0.11.28"
required-environments = [
  "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.13'",
]

[tool.ruff]
target-version = "py313"
line-length = 100

[tool.ruff.lint]
select = ["ASYNC", "B", "E4", "E7", "E9", "F", "I", "RUF", "SIM", "UP"]

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["B017"]

[tool.mypy]
python_version = "3.13"
strict = true
pretty = true
show_error_codes = true
show_error_context = true
warn_unreachable = true
plugins = ["pydantic.mypy"]
mypy_path = "src"

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = [
  "--strict-config",
  "--strict-markers",
  "-ra",
  "--cov=muse_backend",
  "--cov-report=term-missing",
  "--cov-fail-under=85",
]
asyncio_mode = "auto"
markers = [
  "unit: isolated unit test",
  "integration: API, migration, or persistence integration test",
]

[tool.coverage.run]
branch = true
source = ["muse_backend"]
omit = ["src/muse_backend/cli.py"]

[tool.coverage.report]
exclude_also = [
  "if TYPE_CHECKING:",
  "if __name__ == .__main__.:",
  "raise NotImplementedError",
]
show_missing = true

[tool.uv.build-backend]
module-name = "muse_backend"
module-root = "src"

```

### frontend/package.json

```
{
  "name": "muse-frontend",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "description": "Offline-first touchscreen interface for the Muse smart wardrobe.",
  "engines": {
    "node": ">=24.18.0 <25",
    "npm": ">=11.16.0 <12"
  },
  "packageManager": "npm@11.16.0",
  "allowScripts": {
    "fsevents": false
  },
  "scripts": {
    "dev": "vite",
    "dev:mobile": "vite --config vite.mobile.config.ts",
    "build": "tsc -b && npm run build:device && npm run build:mobile",
    "build:device": "vite build",
    "build:mobile": "vite build --config vite.mobile.config.ts",
    "preview": "vite preview",
    "preview:mobile": "vite preview --config vite.mobile.config.ts",
    "typecheck": "tsc -b --pretty false",
    "lint": "eslint . --max-warnings 0",
    "format": "prettier --write .",
    "format:check": "prettier --check .",
    "test": "vitest run",
    "test:watch": "vitest",
    "pretest:e2e": "npm run build",
    "test:e2e": "playwright test",
    "test:e2e:reuse-build": "playwright test",
    "test:e2e:production": "playwright test e2e/production-integration.spec.ts e2e/production-outfit-integration.spec.ts --workers=1",
    "test:e2e:production:p4": "playwright test e2e/production-phone-upload-integration.spec.ts --workers=1",
    "test:e2e:production:p5": "playwright test e2e/production-outfit-integration.spec.ts --workers=1",
    "test:e2e:production:p6": "playwright test e2e/production-settings-integration.spec.ts --workers=1"
  },
  "dependencies": {
    "@fontsource-variable/inter": "5.2.8",
    "@fontsource-variable/playfair-display": "5.2.8",
    "@tanstack/react-query": "5.101.2",
    "lucide-react": "1.24.0",
    "qrcode.react": "4.2.0",
    "react": "19.2.7",
    "react-dom": "19.2.7",
    "react-router-dom": "7.18.1"
  },
  "devDependencies": {
    "@eslint/js": "9.39.5",
    "@playwright/test": "1.61.1",
    "@testing-library/dom": "10.4.1",
    "@testing-library/jest-dom": "6.9.1",
    "@testing-library/react": "16.3.2",
    "@testing-library/user-event": "14.6.1",
    "@types/node": "24.13.3",
    "@types/pngjs": "6.0.5",
    "@types/react": "19.2.17",
    "@types/react-dom": "19.2.3",
    "@vitejs/plugin-react": "6.0.3",
    "eslint": "9.39.5",
    "eslint-plugin-jsx-a11y": "6.10.2",
    "eslint-plugin-react-hooks": "7.1.1",
    "eslint-plugin-react-refresh": "0.5.3",
    "globals": "17.7.0",
    "jsdom": "29.1.1",
    "jsqr": "1.4.0",
    "pngjs": "7.0.0",
    "prettier": "3.9.5",
    "typescript": "6.0.3",
    "typescript-eslint": "8.64.0",
    "vite": "8.1.4",
    "vitest": "4.1.10"
  }
}

```

### frontend/src/main.tsx

```typescript
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

import { App } from './app/App';
import './styles/fonts.css';
import './styles/tokens.css';
import './styles/global.css';

const rootElement = document.getElementById('root');

if (rootElement === null) {
  throw new Error('Muse could not find its application root.');
}

createRoot(rootElement).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

```

### backend/src/muse_backend/main.py

```python
from muse_backend.application import create_app

app = create_app()

```

### frontend/mobile/src/main.tsx

```typescript
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

import { MobileUploadApp } from './MobileUploadApp';
import '../../src/styles/fonts.css';
import '../../src/styles/tokens.css';
import './mobile.css';

const rootElement = document.getElementById('root');
if (rootElement === null) {
  throw new Error('Muse could not find the phone upload root.');
}

createRoot(rootElement).render(
  <StrictMode>
    <MobileUploadApp />
  </StrictMode>,
);

```

### frontend/src/app/App.tsx

```typescript
import { QueryClientProvider } from '@tanstack/react-query';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';

import { OutfitBuilderProvider } from '../features/outfit-builder/OutfitBuilderProvider';
import { DisplayPreferencesProvider } from '../features/settings/DisplayPreferencesProvider';
import { StartupExperience } from '../components/startup/StartupExperience';
import { museRoutes } from './AppRoutes';
import { createMuseQueryClient } from './queryClient';

const queryClient = createMuseQueryClient();
const router = createBrowserRouter(museRoutes);

export function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <DisplayPreferencesProvider>
        <StartupExperience>
          <OutfitBuilderProvider>
            <RouterProvider router={router} />
          </OutfitBuilderProvider>
        </StartupExperience>
      </DisplayPreferencesProvider>
    </QueryClientProvider>
  );
}

```

### backend/src/muse_backend/cli.py

```python
import argparse
import ipaddress
import logging
import sys
from pathlib import Path

import uvicorn

from muse_backend.application import create_app
from muse_backend.config import Environment, Settings
from muse_backend.database.engine import Database
from muse_backend.database.migrations import (
    check_migration_consistency,
    migration_status,
    upgrade_database,
)
from muse_backend.domain.exceptions import MuseError
from muse_backend.phone_upload.application import create_phone_upload_app
from muse_backend.services.background_processing import reconcile_temporary_imports
from muse_backend.services.import_admission import InterprocessImportLock
from muse_backend.services.maintenance import apply_staged_maintenance
from muse_backend.services.phone_upload_sessions import PhoneUploadSessionService
from muse_backend.services.production import (
    create_verified_backup,
    prepare_production,
    verify_backup,
)
from muse_backend.storage.local import LocalStorageService

logger = logging.getLogger(__name__)


def _parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(prog="muse-backend", description="Manage the local Muse API")
    commands = parser.add_subparsers(dest="command", required=True)

    serve = commands.add_parser("serve", help="run the FastAPI server")
    serve.add_argument("--host", default="127.0.0.1")
    serve.add_argument("--port", type=int, default=8000)
    serve.add_argument("--reload", action="store_true")

    commands.add_parser(
        "serve-phone-upload",
        help="run the restricted phone-upload listener on its configured LAN interface",
    )
    apply_maintenance = commands.add_parser(
        "apply-staged-maintenance",
        help="apply a staged restore or delete-all operation while both listeners are stopped",
    )
    apply_maintenance.add_argument(
        "--confirm",
        required=True,
        help="must be exactly 'APPLY STAGED MUSE MAINTENANCE'",
    )

    migrate = commands.add_parser("migrate", help="upgrade the configured database")
    migrate.add_argument("revision", nargs="?", default="head")

    commands.add_parser("migration-status", help="show current and expected revisions")
    commands.add_parser("migration-check", help="check models against the migration head")
    commands.add_parser(
        "prepare-production",
        help="apply confirmed staged maintenance, migrate, and reconcile before startup",
    )
    commands.add_parser("create-backup", help="create and verify a local safety backup")
    verify = commands.add_parser("verify-backup", help="verify one backup or the latest backup")
    verify.add_argument("backup_id", nargs="?", default=None)
    commands.add_parser(
        "cleanup-phone-upload-sessions",
        help="reconcile and remove one bounded batch of retained phone-upload sessions",
    )

    reset = commands.add_parser("reset-dev", help="reset only the configured development database")
    reset.add_argument(
        "--confirm",
        action="store_true",
        help="confirm destructive removal of the configured development database",
    )
    return parser


def _remove_database_files(database_path: Path) -> None:
    for candidate in (
        database_path,
        Path(f"{database_path}-wal"),
        Path(f"{database_path}-shm"),
    ):
        if candidate.is_symlink():
            raise RuntimeError("refusing to remove a symbolic-link database file")
        if candidate.exists():
            if not candidate.is_file():
                raise RuntimeError("refusing to remove a non-file database path")
            candidate.unlink()


def _reset_development_database(settings: Settings, *, confirmed: bool) -> None:
    if settings.environment is not Environment.DEVELOPMENT:
        raise RuntimeError("reset-dev is available only in the development environment")
    if not confirmed:
        raise RuntimeError("reset-dev requires --confirm")
    storage = LocalStorageService(settings)
    storage.create_required_directories()
    _remove_database_files(settings.database_path)
    upgrade_database(settings)


def _is_loopback_host(host: str) -> bool:
    if host.lower().removesuffix(".") == "localhost":
        return True
    try:
        return ipaddress.ip_address(host).is_loopback
    except ValueError:
        return False


def _cleanup_phone_upload_sessions(settings: Settings) -> int:
    storage = LocalStorageService(settings)
    storage.create_required_directories()
    storage.secure_database_file()
    database = Database(settings.database_path)
    try:
        if not migration_status(settings, database).is_current:
            raise RuntimeError("phone-upload cleanup requires a current database migration")
        service = PhoneUploadSessionService(database=database, settings=settings)
        with InterprocessImportLock(settings).acquire(blocking=True):
            processed = service.cleanup()
            return processed + reconcile_temporary_imports(
                settings=settings,
                storage=storage,
                database=database,
                limit=max(0, settings.phone_upload_cleanup_batch_size - processed),
            )
    finally:
        database.dispose()


def main() -> None:
    arguments = _parser().parse_args()
    settings = Settings()
    logging.basicConfig(level=settings.log_level)

    try:
        if arguments.command == "serve":
            if settings.environment is Environment.PRODUCTION and not _is_loopback_host(
                arguments.host
            ):
                raise RuntimeError("the production Muse application must bind to loopback")
            if arguments.reload:
                if settings.environment is not Environment.DEVELOPMENT:
                    raise RuntimeError("--reload is available only in development")
                uvicorn.run(
                    "muse_backend.main:app",
                    host=arguments.host,
                    port=arguments.port,
        
[truncated — 2974 more characters]
```

### frontend/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="color-scheme" content="light" />
    <meta name="theme-color" content="#f6efe5" />
    <meta
      name="description"
      content="Muse is an offline-first smart wardrobe for a dedicated touchscreen device."
    />
    <title>Muse</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

[281 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]