initial commit
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
# RadiantWave — Agent Instructions
|
||||
|
||||
## What This Is
|
||||
|
||||
A kiosk app for RadiantWave wellness technology. Displays scrolling proclamation
|
||||
text (Fibonacci line-length pattern, per-frequency color palettes), plays
|
||||
solfeggio-frequency audio, and runs a first-run provisioning walkthrough on a
|
||||
headless Linux kiosk. Talks to `https://portal.radiantwavetech.com/api/v2`.
|
||||
|
||||
**Language:** C++17 (no extensions). **Build:** CMake + Ninja.
|
||||
**Graphics:** SDL3 + SDL3_ttf/SDL3_image. **Audio:** SDL3_mixer.
|
||||
**Also links:** SDL3_net, SQLite3, libcurl, OpenSSL, GTest.
|
||||
**Linux-only:** libsystemd (sd-bus) via pkg-config, compiled only when
|
||||
`UNIX AND NOT APPLE` — guarded by `#ifdef __linux__` in the network manager.
|
||||
|
||||
---
|
||||
|
||||
## Build & Run
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
|
||||
cmake --build build # builds the app AND every test binary
|
||||
./build/radiantwave
|
||||
```
|
||||
|
||||
The Home footer's top "Version" row is baked in at build time. Default it to
|
||||
the project version, or pass your own at configure time:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DAPP_VERSION="1.2.3"
|
||||
```
|
||||
|
||||
The Nix package passes its flake version automatically via `cmakeFlags`.
|
||||
|
||||
The portal API key is also a build-time argument: `-DAPI_KEY="..."` (CMake) or
|
||||
`apiKey` on `callPackage` (Nix). It is compiled into the binary as the
|
||||
`Authorization: Bearer` token and defaults to empty (no auth header). Keep the
|
||||
real value in CI secrets, never in source — rotate by rebuilding with a new
|
||||
key.
|
||||
|
||||
- Development happens on macOS; WiFi/D-Bus code is Linux-only, but
|
||||
`NetworkManager::hasInternet()` (a raw TCP probe) works everywhere, so the
|
||||
provisioning walkthrough does not trap a Mac on the WiFi page.
|
||||
- The app creates a **1920x1080 fullscreen** window with VSync and a hidden
|
||||
cursor. `~/.local/radiantwave/config.yaml` (YAML) and
|
||||
`~/.local/radiantwave/radiantwave.db` (SQLite, WAL) are created at first run.
|
||||
- No root `flake.nix` exists; the Nix flake in `archive/` is reference only.
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
There is **no ctest integration** and no single "run all" target. Each test
|
||||
file is its own `add_executable` in `CMakeLists.txt`; build them all with
|
||||
`cmake --build build`, then run each `./build/test_*` binary.
|
||||
|
||||
Two frameworks coexist:
|
||||
- `tests`, `test_assets`, `test_provisioning` — the minimal built-in harness
|
||||
(`tests/test_harness.h`, `TEST(name)` + `RUN_ALL_TESTS()`).
|
||||
- Everything else — GoogleTest (`GTest::gtest_main`). GTest is a hard
|
||||
`find_package` requirement; the build fails without it.
|
||||
|
||||
Test targets: `tests`, `test_assets`, `test_provisioning`, `test_network_unit`,
|
||||
`test_config`, `test_db`, `test_proclamation_sync`, `test_api_client`,
|
||||
`test_license_service`, `test_license_status`, `test_validation`,
|
||||
`test_date_format`, `test_timezone`, `test_logger`, `test_audio`,
|
||||
`test_audio_downloader`, `test_fibonacci_pattern`, `test_scroller_streams`,
|
||||
`test_fibonacci_scrambler`, `test_color_palettes`.
|
||||
|
||||
New test files must be registered as a new `add_executable` target in
|
||||
`CMakeLists.txt` (there is no glob). `test_assets` must pass before committing —
|
||||
it checks the font and image files exist.
|
||||
|
||||
---
|
||||
|
||||
## Runtime data & keys
|
||||
|
||||
- `AppState` (in `app_state.h`) holds renderer, `PageManager`, `KeybindManager`,
|
||||
`config`, `db`, `network`, `audio`, `audioDownloader`, `licenseTracker`, and
|
||||
provisioning flags. No globals — pass `AppState&` around.
|
||||
- Device identity/state lives in the SQLite `system_kv` table. Canonical keys
|
||||
are in `src/db/db_keys.h`: `email_address`, `license_key`,
|
||||
`subscription_level_id/name/description`, `license_last_verified` (7-day
|
||||
offline grace window for a known-good license).
|
||||
- `ConfigManager` hot-reloads `config.yaml` from a watcher thread and fires
|
||||
`onChanged` callbacks; `AudioManager` volume/device/`dynamic_audio` are read
|
||||
from it.
|
||||
|
||||
---
|
||||
|
||||
## First-run provisioning walkthrough
|
||||
|
||||
Fixed prerequisite order (`src/provisioning.h`), evaluated on every boot; the
|
||||
first missing step's page is pushed:
|
||||
|
||||
1. Internet reachable? → `WiFiSetupPage`
|
||||
2. Email address set? → `EmailAddressPage`
|
||||
3. License key set? → `LicenseKeyPage`
|
||||
4. Everything satisfied → `Home`
|
||||
|
||||
`advance()` pops the current step and re-evaluates; internet is only re-probed
|
||||
when it was still missing (cached in `state.walkthroughInternetUp`). A
|
||||
brand-new device (nothing configured) only sets the `firstRunWalkthrough`
|
||||
placeholder flag — the welcome walkthrough is not implemented yet.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Entry point (`src/main.cpp`)
|
||||
|
||||
SDL3 callback API (`SDL_MAIN_USE_CALLBACKS`): `SDL_AppInit` inits subsystems
|
||||
and calls `provisioning::begin`, `SDL_AppEvent` routes through keybinds then
|
||||
pages, `SDL_AppIterate` updates/renders, `SDL_AppQuit` tears down.
|
||||
|
||||
### Pages & navigation
|
||||
|
||||
`Page` lifecycle (`src/page.h`): `onPush`/`onResume` create textures,
|
||||
`onPause` destroys them, `onPop` final-cleans. `PageManager` is a stack
|
||||
(`push`/`pop`/`popToRoot`/`replace`). Home and the provisioning steps use
|
||||
`replace()`-style flows; only settings sub-pages stack deeply. TAB pops the
|
||||
stack and pushes a fresh Home.
|
||||
|
||||
Existing pages: Home, Settings, WiFiSetupPage, EmailAddressPage, LicenseKeyPage,
|
||||
LicenseBlockedPage, AudioSettingsPage, ProclamationOptionsPage, ScrollerPage,
|
||||
LogViewerPage, TimezonePage, DateFormatPage.
|
||||
|
||||
### Keybinds (`src/keybind_manager.cpp`)
|
||||
|
||||
Binds are checked **before** the active page sees events. Left/right modifier
|
||||
pairs are normalized to combined masks, so a bind's modifiers must be a subset
|
||||
of the held modifiers (plain binds fire regardless of modifiers).
|
||||
|
||||
Global binds in `main.cpp`: **TAB** returns Home, **UP/DOWN** move the Home
|
||||
menu selection. There is no quit keybind — the app is a kiosk. App quit (SDL
|
||||
window/system close) fades audio out over ~5s before teardown. Note: Home's
|
||||
UP/DOWN handlers both increment `currentMenuItem` `(x+1)%2` (see
|
||||
`src/home.cpp`) — there is no separate "down" path.
|
||||
|
||||
### ScrollerPage
|
||||
|
||||
Full-screen vertical scroller. Proclamation streams come from the DB
|
||||
(`scroller_streams.cpp`), colors from per-frequency palettes with a 528 Hz
|
||||
fallback, lines built from a Fibonacci length pattern
|
||||
(`fibonacci_line_pattern.cpp`). Audio is crossfaded per stream via
|
||||
`AudioManager` fade APIs. Word textures are cached and page text rendering is
|
||||
cached.
|
||||
|
||||
---
|
||||
|
||||
## Portal API & color palettes
|
||||
|
||||
- Request/response changes against the portal are tracked in `ApiUpdates.md`
|
||||
(sections #4 and #5).
|
||||
- **Color palettes are meant to come from the portal, not code releases.** The
|
||||
client-side pieces are implemented but deliberately **not wired into any sync
|
||||
or startup flow** until the portal routes exist:
|
||||
- `api::fetchColorPalettes()` / `api::parseColorPalettesJson()` — the
|
||||
`GET /color-palettes` route.
|
||||
- `sync::syncColorPalettesFromApi()` / `sync::syncProclamationPalettesFromApi()`
|
||||
/ `sync::parseProclamationPalettesJson()` — replace the local
|
||||
`color_palettes` table via `db::replaceColorPalettes()`.
|
||||
- `db::getPaletteForFrequencyId()` falls back to the 528 Hz palette for
|
||||
frequency ids with no rows; the startup seed only backfills missing ids, so
|
||||
portal values win after a sync.
|
||||
- Do **not** call the above sync functions from the app until the portal routes
|
||||
ship. Tests live in `tests/test_color_palettes.cpp` (`test_color_palettes`
|
||||
target).
|
||||
- **Intended wiring points** (marked with `TODO(portal #4/#5)` comments):
|
||||
`syncProclamationsFromApi()` in `src/network/proclamation_sync.cpp`, plus the
|
||||
call sites `Home::syncProclamationsIfEmpty()` (`src/home.cpp`) and
|
||||
`ProclamationOptionsPage::onPush()` (`src/proclamation_options_page.cpp`).
|
||||
|
||||
---
|
||||
|
||||
## Style & UI Layout Rules
|
||||
|
||||
- `PascalCase` for types, `camelCase` for functions/vars, `snake_case` for
|
||||
files, `UPPER_SNAKE` for constants/macros.
|
||||
- Use `std::unique_ptr`; no raw `new`/`delete` outside the SDL callbacks.
|
||||
- Use `override` on all virtual implementations. `#pragma once` headers.
|
||||
- One class per header/source pair. SDL3 headers first, then stdlib, then
|
||||
project headers.
|
||||
|
||||
### UI Menu & List Layout Conventions
|
||||
- **Left Alignment & Centered Bounding Box**: In vertical menus/lists, all text
|
||||
options are left-aligned with each other (`sharedX`). The bounding box
|
||||
enclosing all options (width = widest item, height = sum of item heights +
|
||||
margins) MUST BE centered on the screen.
|
||||
- **Vertical Spacing**: All margins between consecutive list items are
|
||||
**12 pixels**.
|
||||
- **Selection Underline**: The 6-pixel underline occupies half the 12px margin
|
||||
(2-3px gap below the item text, 3-4px gap above the next item).
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Do not commit audio assets (`.wav` files) — downloaded at runtime.
|
||||
- Do not modify files in `archive/` — reference only.
|
||||
- Build needs SDL3 + SDL3_ttf/image/mixer/net + GTest installed; CI uses
|
||||
self-hosted Forgejo runners.
|
||||
- `test_assets` must pass before committing — it catches missing fonts/images
|
||||
that break the app at runtime.
|
||||
- `cmake --build build` builds everything; pass `--target <name>` to build one
|
||||
test binary.
|
||||
Reference in New Issue
Block a user