Run prettier on all relevant files

This commit is contained in:
Claudio Ortolina
2026-05-05 10:45:25 +01:00
parent 0b1f839091
commit 5888324cc7
17 changed files with 447 additions and 404 deletions
+3 -1
View File
@@ -154,6 +154,7 @@ mix scrobble.audit --format json
### Understanding the Audit Report ### Understanding the Audit Report
The audit report shows: The audit report shows:
- Total number of scrobbled tracks - Total number of scrobbled tracks
- Artists with missing MusicBrainz IDs (grouped by artist name) - Artists with missing MusicBrainz IDs (grouped by artist name)
- Albums with missing MusicBrainz IDs (grouped by album title and artist) - Albums with missing MusicBrainz IDs (grouped by album title and artist)
@@ -166,6 +167,7 @@ After identifying issues, you can:
1. **Create Scrobble Rules**: Navigate to the Scrobble Rules page in the web interface and add rules to map artist or album names to their correct MusicBrainz IDs. 1. **Create Scrobble Rules**: Navigate to the Scrobble Rules page in the web interface and add rules to map artist or album names to their correct MusicBrainz IDs.
2. **Apply Rules**: Use the "Apply Rules" button in the Scrobble Rules page to update existing tracks, or run in IEx: 2. **Apply Rules**: Use the "Apply Rules" button in the Scrobble Rules page to update existing tracks, or run in IEx:
```elixir ```elixir
MusicLibrary.ScrobbleRules.apply_all_rules() MusicLibrary.ScrobbleRules.apply_all_rules()
``` ```
@@ -173,12 +175,12 @@ After identifying issues, you can:
3. **Re-audit**: Run the audit again to verify the fixes worked. 3. **Re-audit**: Run the audit again to verify the fixes worked.
The application also provides helper functions in the `MusicLibrary.ScrobbleActivity` context: The application also provides helper functions in the `MusicLibrary.ScrobbleActivity` context:
- `count_tracks_missing_artist_musicbrainz_id/0` - `count_tracks_missing_artist_musicbrainz_id/0`
- `count_tracks_missing_album_musicbrainz_id/0` - `count_tracks_missing_album_musicbrainz_id/0`
- `get_artists_missing_musicbrainz_id/1` - `get_artists_missing_musicbrainz_id/1`
- `get_albums_missing_musicbrainz_id/1` - `get_albums_missing_musicbrainz_id/1`
## Deployment ## Deployment
The application is deployed via Coolify, using a Docker Compose strategy. The application is deployed via Coolify, using a Docker Compose strategy.
+5 -2
View File
@@ -16,7 +16,10 @@
@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &); @custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
/* Make LiveView wrapper divs transparent for layout */ /* Make LiveView wrapper divs transparent for layout */
[data-phx-session], [data-phx-teleported-src] { display: contents } [data-phx-session],
[data-phx-teleported-src] {
display: contents;
}
@layer base { @layer base {
a, a,
@@ -37,7 +40,7 @@
@theme { @theme {
--font-sans: "InterVariable", sans-serif; --font-sans: "InterVariable", sans-serif;
--font-sans--font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11'; --font-sans--font-feature-settings: "cv02", "cv03", "cv04", "cv11";
--animate-shake: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; --animate-shake: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
--animate-shine: shine 3s ease infinite; --animate-shine: shine 3s ease infinite;
--animate-equalizer-bar: equalizer-bar 1.2s ease-in-out infinite; --animate-equalizer-bar: equalizer-bar 1.2s ease-in-out infinite;
+27 -20
View File
@@ -33,12 +33,12 @@ import { createLiveToastHook } from "live_toast";
import banner from "./banner"; import banner from "./banner";
// the duration for each toast to stay on screen in ms // the duration for each toast to stay on screen in ms
const duration = 4000 const duration = 4000;
// how many toasts to show on screen at once // how many toasts to show on screen at once
const maxItems = 3 const maxItems = 3;
const liveToastHook = createLiveToastHook(duration, maxItems) const liveToastHook = createLiveToastHook(duration, maxItems);
let Hooks = FluxonHooks; let Hooks = FluxonHooks;
Hooks.FormatNumber = FormatNumberHook; Hooks.FormatNumber = FormatNumberHook;
@@ -56,8 +56,8 @@ const liveSocket = new LiveSocket("/live", Socket, {
params: (view) => { params: (view) => {
return { return {
_csrf_token: csrfToken, _csrf_token: csrfToken,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
} };
}, },
hooks: { ...Hooks, ...colocatedHooks }, hooks: { ...Hooks, ...colocatedHooks },
dom: { dom: {
@@ -121,32 +121,39 @@ window.liveSocket = liveSocket;
// 2. click on elements to jump to their definitions in your code editor // 2. click on elements to jump to their definitions in your code editor
// //
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
window.addEventListener("phx:live_reload:attached", ({ detail: reloader }) => { window.addEventListener(
"phx:live_reload:attached",
({ detail: reloader }) => {
// Enable server log streaming to client. // Enable server log streaming to client.
// Disable with reloader.disableServerLogs() // Disable with reloader.disableServerLogs()
reloader.enableServerLogs() reloader.enableServerLogs();
// Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component // Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
// //
// * click with "c" key pressed to open at caller location // * click with "c" key pressed to open at caller location
// * click with "d" key pressed to open at function component definition location // * click with "d" key pressed to open at function component definition location
let keyDown let keyDown;
window.addEventListener("keydown", e => keyDown = e.key) window.addEventListener("keydown", (e) => (keyDown = e.key));
window.addEventListener("keyup", e => keyDown = null) window.addEventListener("keyup", (e) => (keyDown = null));
window.addEventListener("click", e => { window.addEventListener(
"click",
(e) => {
if (keyDown === "c") { if (keyDown === "c") {
e.preventDefault() e.preventDefault();
e.stopImmediatePropagation() e.stopImmediatePropagation();
reloader.openEditorAtCaller(e.target) reloader.openEditorAtCaller(e.target);
} else if (keyDown === "d") { } else if (keyDown === "d") {
e.preventDefault() e.preventDefault();
e.stopImmediatePropagation() e.stopImmediatePropagation();
reloader.openEditorAtDef(e.target) reloader.openEditorAtDef(e.target);
} }
}, true) },
true,
);
window.liveReloader = reloader window.liveReloader = reloader;
}) },
);
} }
// Credit: https://andrewtimberlake.com/blog/2025/03/see-what-liveview-changes-are-being-made // Credit: https://andrewtimberlake.com/blog/2025/03/see-what-liveview-changes-are-being-made
+5 -2
View File
@@ -1,6 +1,7 @@
export default function banner() { export default function banner() {
const version = document.querySelector('meta[name="version"]').content; const version = document.querySelector('meta[name="version"]').content;
console.log(`%c console.log(
`%c
███╗ ███╗██╗ ██╗███████╗██╗ ██████╗ ██╗ ██╗██████╗ ██████╗ █████╗ ██████╗ ██╗ ██╗ ███╗ ███╗██╗ ██╗███████╗██╗ ██████╗ ██╗ ██╗██████╗ ██████╗ █████╗ ██████╗ ██╗ ██╗
████╗ ████║██║ ██║██╔════╝██║██╔════╝ ██║ ██║██╔══██╗██╔══██╗██╔══██╗██╔══██╗╚██╗ ██╔╝ ████╗ ████║██║ ██║██╔════╝██║██╔════╝ ██║ ██║██╔══██╗██╔══██╗██╔══██╗██╔══██╗╚██╗ ██╔╝
██╔████╔██║██║ ██║███████╗██║██║ ██║ ██║██████╔╝██████╔╝███████║██████╔╝ ╚████╔╝ ██╔████╔██║██║ ██║███████╗██║██║ ██║ ██║██████╔╝██████╔╝███████║██████╔╝ ╚████╔╝
@@ -8,5 +9,7 @@ export default function banner() {
██║ ╚═╝ ██║╚██████╔╝███████║██║╚██████╗ ███████╗██║██████╔╝██║ ██║██║ ██║██║ ██║ ██║ ██║ ╚═╝ ██║╚██████╔╝███████║██║╚██████╗ ███████╗██║██████╔╝██║ ██║██║ ██║██║ ██║ ██║
╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═════╝ ╚══════╝╚═╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═════╝ ╚══════╝╚═╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝
Version: ${version} Version: ${version}
`, "font-family: IBM Plex Mono,monospace; font-size: 12px"); `,
"font-family: IBM Plex Mono,monospace; font-size: 12px",
);
} }
+11 -4
View File
@@ -1,4 +1,8 @@
export default function createNavigationHook({ getContainer, inputId, onSelect }) { export default function createNavigationHook({
getContainer,
inputId,
onSelect,
}) {
return { return {
mounted() { mounted() {
this.selectedIndex = -1; this.selectedIndex = -1;
@@ -89,13 +93,16 @@ export default function createNavigationHook({ getContainer, inputId, onSelect }
if (searchInput) { if (searchInput) {
searchInput.focus(); searchInput.focus();
} }
} else if (this.selectedIndex >= 0 && this.selectedIndex < results.length) { } else if (
this.selectedIndex >= 0 &&
this.selectedIndex < results.length
) {
const selectedResult = results[this.selectedIndex]; const selectedResult = results[this.selectedIndex];
selectedResult.setAttribute("aria-selected", "true"); selectedResult.setAttribute("aria-selected", "true");
selectedResult.scrollIntoView({ selectedResult.scrollIntoView({
block: "nearest", block: "nearest",
behavior: "smooth" behavior: "smooth",
}); });
} }
}, },
@@ -105,6 +112,6 @@ export default function createNavigationHook({ getContainer, inputId, onSelect }
if (!container) return []; if (!container) return [];
return Array.from(container.querySelectorAll('[role="option"]')); return Array.from(container.querySelectorAll('[role="option"]'));
} },
}; };
} }
+2 -2
View File
@@ -4,5 +4,5 @@ export default {
}, },
updated() { updated() {
this.el.innerText = parseInt(this.el.innerText).toLocaleString(); this.el.innerText = parseInt(this.el.innerText).toLocaleString();
} },
} };
+1 -1
View File
@@ -9,5 +9,5 @@ export default createNavigationHook({
} else { } else {
hook.navigateUp(); hook.navigateUp();
} }
} },
}); });
+1 -1
View File
@@ -9,5 +9,5 @@ export default createNavigationHook({
} else { } else {
hook.navigateUp(); hook.navigateUp();
} }
} },
}); });
+1 -1
View File
@@ -28,7 +28,7 @@ export default {
onEnd: () => { onEnd: () => {
const items = this.el.querySelectorAll("[data-sortable-item]"); const items = this.el.querySelectorAll("[data-sortable-item]");
const recordIds = Array.from(items).map( const recordIds = Array.from(items).map(
(item) => item.dataset.recordId (item) => item.dataset.recordId,
); );
const payload = { record_ids: recordIds }; const payload = { record_ids: recordIds };
@@ -2,5 +2,5 @@ import createNavigationHook from "./create-navigation-hook";
export default createNavigationHook({ export default createNavigationHook({
getContainer: () => document.getElementById("universal-search-root"), getContainer: () => document.getElementById("universal-search-root"),
inputId: "universal-search-input" inputId: "universal-search-input",
}); });
@@ -1,9 +1,10 @@
--- ---
id: doc-10 id: doc-10
title: 'Deepseek v4 Pro, xhigh analysis' title: "Deepseek v4 Pro, xhigh analysis"
type: other type: other
created_date: '2026-05-04 15:11' created_date: "2026-05-04 15:11"
--- ---
# Nerves Deployment Feasibility Analysis for Music Library # Nerves Deployment Feasibility Analysis for Music Library
> Research report — do not implement. May 2026. > Research report — do not implement. May 2026.
@@ -33,7 +34,7 @@ Nerves compatibility. Devonly tools (esbuild, tailwind, credo, etc.) are
excluded — they never reach the firmware. excluded — they never reach the firmware.
| Dependency | Purpose | NIF type | Precompiled ARM? | Nerves feasibility | | Dependency | Purpose | NIF type | Precompiled ARM? | Nerves feasibility |
|---|---|---|---|---| | ----------------------------------- | ------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **exqlite**`ecto_sqlite3` | SQLite driver | C NIF (`cc_precompiler`) | ✅ `aarch64-linux-gnu/musl` (no `armv7-*`) | **🟢 Good** — `force_build: true` compiles from source inside the Nerves toolchain. Explicitly mentioned in Exqlites README as an embedded use-case. | | **exqlite**`ecto_sqlite3` | SQLite driver | C NIF (`cc_precompiler`) | ✅ `aarch64-linux-gnu/musl` (no `armv7-*`) | **🟢 Good** — `force_build: true` compiles from source inside the Nerves toolchain. Explicitly mentioned in Exqlites README as an embedded use-case. |
| **vix** → libvips | Image processing (covers, thumbnails) | C NIF (`elixir_make`) | ❌ No ARM binaries advertised | **🔴 High risk** — libvips is ~30 MB with transitive deps (libjpeg, libpng, libwebp, etc.). Each needs Buildroot integration or crosscompilation. Biggest blocker for a lean firmware. | | **vix** → libvips | Image processing (covers, thumbnails) | C NIF (`elixir_make`) | ❌ No ARM binaries advertised | **🔴 High risk** — libvips is ~30 MB with transitive deps (libjpeg, libpng, libwebp, etc.). Each needs Buildroot integration or crosscompilation. Biggest blocker for a lean firmware. |
| **mdex** → comrak + ammonia | Markdown → HTML | Rust NIF (RustlerPrecompiled) | ⚠️ Standard Rustler targets (`aarch64-unknown-linux-gnu`, possibly `arm-*`) | **🟡 Needs crosscompilation** — RustlerPrecompiled targets use different triples than Nerves (`aarch64-nerves-linux-gnu`). Precompiled `.so` likely wont load. Crosscompile from source with a Nervesaware Rust toolchain. | | **mdex** → comrak + ammonia | Markdown → HTML | Rust NIF (RustlerPrecompiled) | ⚠️ Standard Rustler targets (`aarch64-unknown-linux-gnu`, possibly `arm-*`) | **🟡 Needs crosscompilation** — RustlerPrecompiled targets use different triples than Nerves (`aarch64-nerves-linux-gnu`). Precompiled `.so` likely wont load. Crosscompile from source with a Nervesaware Rust toolchain. |
@@ -46,8 +47,8 @@ excluded — they never reach the firmware.
### Targetarchitecture specifics ### Targetarchitecture specifics
| Target | CPU | Nerves toolchain triple | Precompiledbinary match? | | Target | CPU | Nerves toolchain triple | Precompiledbinary match? |
|---|---|---|---| | --------- | ------------------------- | ------------------------------ | --------------------------------------------------------------------------------- |
| RPi 4 / 5 | CortexA72 / A76 (64bit) | `aarch64-nerves-linux-gnu` | ⚠️ `aarch64-linux-gnu` *may* be ABIcompatible (exqlite). Rust NIFs need rebuild. | | RPi 4 / 5 | CortexA72 / A76 (64bit) | `aarch64-nerves-linux-gnu` | ⚠️ `aarch64-linux-gnu` _may_ be ABIcompatible (exqlite). Rust NIFs need rebuild. |
| RPi 3 | CortexA53 (32bit) | `armv7-nerves-linux-gnueabihf` | ❌ No precompiled binaries match. Everything must be compiled from source. | | RPi 3 | CortexA53 (32bit) | `armv7-nerves-linux-gnueabihf` | ❌ No precompiled binaries match. Everything must be compiled from source. |
**Recommendation**: Target RPi 4 or 5 (64bit) first — the precompiled ecosystem is **Recommendation**: Target RPi 4 or 5 (64bit) first — the precompiled ecosystem is
@@ -61,7 +62,7 @@ The application loads two runtime extensions via Exqlites `load_extensions
config: config:
| Extension | Source | ARM status | Notes | | Extension | Source | ARM status | Notes |
|---|---|---|---| | ----------- | ---------------------------------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **vec0** | [sqlite-vec](https://github.com/asg017/sqlite-vec) | ✅ **Good** | “Written in pure C, no dependencies, runs anywhere SQLite runs (… Raspberry Pis, etc.)” — explicitly tested on ARM. Compilable for any Nerves target. | | **vec0** | [sqlite-vec](https://github.com/asg017/sqlite-vec) | ✅ **Good** | “Written in pure C, no dependencies, runs anywhere SQLite runs (… Raspberry Pis, etc.)” — explicitly tested on ARM. Compilable for any Nerves target. |
| **unicode** | likely [sqlean](https://github.com/nalgeon/sqlean) (`text` module or bundle) | ✅ **ARM64 available** | Sqlean ships `sqlean-linux-arm64.zip`. For ARMv7, compile from C source. | | **unicode** | likely [sqlean](https://github.com/nalgeon/sqlean) (`text` module or bundle) | ✅ **ARM64 available** | Sqlean ships `sqlean-linux-arm64.zip`. For ARMv7, compile from C source. |
@@ -91,7 +92,7 @@ This is the **hardest problem** and the most architecturally open of the three.
### What Litestream does (and doesnt) ### What Litestream does (and doesnt)
| Capability | Litestream | Needed for device ↔ cloud sync | | Capability | Litestream | Needed for device ↔ cloud sync |
|---|---|---| | ----------------------------------- | ---------- | ------------------------------ |
| Unidirectional backup (app → S3) | ✅ | ❌ | | Unidirectional backup (app → S3) | ✅ | ❌ |
| Disaster recovery (restore from S3) | ✅ | ❌ | | Disaster recovery (restore from S3) | ✅ | ❌ |
| Bidirectional sync | ❌ | ✅ | | Bidirectional sync | ❌ | ✅ |
@@ -191,7 +192,7 @@ path. This is independent from the production Litestream instance.
## Summary of risks ## Summary of risks
| Risk | Severity | Mitigation | | Risk | Severity | Mitigation |
|---|---|---| | -------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------- |
| **vix / libvips on Nerves** | 🔴 High | Replace image processing with a lighter alternative, or offload to the production server via API. | | **vix / libvips on Nerves** | 🔴 High | Replace image processing with a lighter alternative, or offload to the production server via API. |
| **Datasync strategy** | 🔴 High | No offtheshelf solution. Start with readonly replica (option A), iterate toward separate write domains (option B). | | **Datasync strategy** | 🔴 High | No offtheshelf solution. Start with readonly replica (option A), iterate toward separate write domains (option B). |
| **typst on embedded** | 🟡 Medium | Switch to serverside PDF generation; client downloads the result. | | **typst on embedded** | 🟡 Medium | Switch to serverside PDF generation; client downloads the result. |
@@ -201,7 +202,7 @@ path. This is independent from the production Litestream instance.
| **Firmware size** | 🟡 Medium | libvips + typst + sqlite extensions → image could exceed 200 MB. Typical Nerves firmware is 2080 MB. | | **Firmware size** | 🟡 Medium | libvips + typst + sqlite extensions → image could exceed 200 MB. Typical Nerves firmware is 2080 MB. |
| **sqlitevec on ARM** | 🟢 Low | Explicitly supported. Pure C, no deps. Compiles trivially with the Nerves toolchain. | | **sqlitevec on ARM** | 🟢 Low | Explicitly supported. Pure C, no deps. Compiles trivially with the Nerves toolchain. |
| **exqlite on Nerves** | 🟢 Low | Designed for embedded use. Source compilation wellsupported and documented. | | **exqlite on Nerves** | 🟢 Low | Designed for embedded use. Source compilation wellsupported and documented. |
| **dominant\_colors** | 🟢 Low | Tiny Rust NIF. Straightforward crosscompilation. | | **dominant_colors** | 🟢 Low | Tiny Rust NIF. Straightforward crosscompilation. |
--- ---
@@ -2,8 +2,9 @@
id: doc-11 id: doc-11
title: Nerves Deployment Research — Consolidated Summary title: Nerves Deployment Research — Consolidated Summary
type: other type: other
created_date: '2026-05-04 15:25' created_date: "2026-05-04 15:25"
--- ---
# Nerves Deployment Research — Consolidated Summary # Nerves Deployment Research — Consolidated Summary
> Synthesized from three parallel research reports (Opus 4.7 xhigh, GPT 5.5 high, Deepseek v4 Pro xhigh) on 2026-05-04. > Synthesized from three parallel research reports (Opus 4.7 xhigh, GPT 5.5 high, Deepseek v4 Pro xhigh) on 2026-05-04.
@@ -113,6 +114,7 @@ Litestream is asynchronous backup and disaster recovery. It streams WAL pages to
#### D. Offline-Capable Writes (Most Complex) #### D. Offline-Capable Writes (Most Complex)
Options if offline writes are required: Options if offline writes are required:
- **Application-level outbox**: Local command log + idempotency keys + push to production on reconnect. - **Application-level outbox**: Local command log + idempotency keys + push to production on reconnect.
- **CRDTs** (Automerge, Yjs): True offline-first, no Elixir/SQLite bridge exists. - **CRDTs** (Automerge, Yjs): True offline-first, no Elixir/SQLite bridge exists.
- **Event sourcing**: Append-only mutation log, replay to reconstruct state. Complex. - **Event sourcing**: Append-only mutation log, replay to reconstruct state. Complex.
@@ -150,7 +152,7 @@ Options if offline writes are required:
## 5. Risk Summary ## 5. Risk Summary
| Risk | Severity | Mitigation | | Risk | Severity | Mitigation |
|---|---|---| | ----------------------------- | --------- | ------------------------------------------------------------------------- |
| vix / libvips on Nerves | 🔴 High | Offload image processing to production API, or build custom Nerves system | | vix / libvips on Nerves | 🔴 High | Offload image processing to production API, or build custom Nerves system |
| Data-sync strategy | 🔴 High | Start read-only, iterate toward write-through API | | Data-sync strategy | 🔴 High | Start read-only, iterate toward write-through API |
| typst on embedded | 🟡 Medium | Server-side PDF generation | | typst on embedded | 🟡 Medium | Server-side PDF generation |
@@ -188,6 +190,7 @@ All three reports collapse into a single question:
## Sources ## Sources
All three reports include extensive source references. See individual documents for full citation lists: All three reports include extensive source references. See individual documents for full citation lists:
- `doc-8 - Opus-4.7-xhigh-analysis.md` - `doc-8 - Opus-4.7-xhigh-analysis.md`
- `doc-9 - GPT-5.5-high-analysis.md` - `doc-9 - GPT-5.5-high-analysis.md`
- `doc-10 - Deepseek-v4-Pro-xhigh-analysis.md` - `doc-10 - Deepseek-v4-Pro-xhigh-analysis.md`
@@ -1,10 +1,11 @@
--- ---
id: doc-8 id: doc-8
title: 'Opus 4.7, xhigh analysis' title: "Opus 4.7, xhigh analysis"
type: other type: other
created_date: '2026-05-04 15:08' created_date: "2026-05-04 15:08"
updated_date: '2026-05-04 15:10' updated_date: "2026-05-04 15:10"
--- ---
# Nerves Deployment Research — Findings # Nerves Deployment Research — Findings
Three parallel research agents covered NIFs, SQLite extensions, and Litestream replication semantics. The user's concerns were well-founded: there are real blockers, but most are solvable with custom Nerves system work. The Litestream question reframes the architecture more than expected. Three parallel research agents covered NIFs, SQLite extensions, and Litestream replication semantics. The user's concerns were well-founded: there are real blockers, but most are solvable with custom Nerves system work. The Litestream question reframes the architecture more than expected.
@@ -12,7 +13,7 @@ Three parallel research agents covered NIFs, SQLite extensions, and Litestream r
## 1. Native libraries (NIFs on aarch64-linux-musl) ## 1. Native libraries (NIFs on aarch64-linux-musl)
| Library | aarch64-gnu | aarch64-musl | Status | | Library | aarch64-gnu | aarch64-musl | Status |
|---|---|---|---| | -------------------------- | ----------- | ------------ | ---------------------------------------------------------------------------------- |
| `mdex` | ✅ | ✅ | Fine | | `mdex` | ✅ | ✅ | Fine |
| `lumis` | ✅ | ✅ | Fine | | `lumis` | ✅ | ✅ | Fine |
| `typst` | ✅ | ✅ | **Nerves-aware** — has `@nerves_rust_target_triple_mapping` in mix.exs | | `typst` | ✅ | ✅ | **Nerves-aware** — has `@nerves_rust_target_triple_mapping` in mix.exs |
@@ -23,7 +24,7 @@ Three parallel research agents covered NIFs, SQLite extensions, and Litestream r
**`vix` is the dominant blocker.** Two compounding issues: **`vix` is the dominant blocker.** Two compounding issues:
- It loads the NIF at *compile time* (macro-driven introspection), which breaks cross-compilation on stock Nerves systems. fhunleth (Nerves core) discussed this in a 2023 ElixirForum thread; the conclusion was "no easy Elixir-level fix." - It loads the NIF at _compile time_ (macro-driven introspection), which breaks cross-compilation on stock Nerves systems. fhunleth (Nerves core) discussed this in a 2023 ElixirForum thread; the conclusion was "no easy Elixir-level fix."
- libvips itself is not in `nerves_system_rpi4` / `nerves_system_rpi5`. You'd need a **custom Nerves system** with `BR2_PACKAGE_LIBVIPS=y` in Buildroot, then `VIX_COMPILATION_MODE=PLATFORM_PROVIDED_LIBVIPS`. - libvips itself is not in `nerves_system_rpi4` / `nerves_system_rpi5`. You'd need a **custom Nerves system** with `BR2_PACKAGE_LIBVIPS=y` in Buildroot, then `VIX_COMPILATION_MODE=PLATFORM_PROVIDED_LIBVIPS`.
This is doable (1530 min Buildroot rebuild, well-documented) but it commits you to maintaining a custom Nerves system. That single decision affects everything downstream — once you're building custom systems, ICU and sqlite-vec become "while we're at it" additions. This is doable (1530 min Buildroot rebuild, well-documented) but it commits you to maintaining a custom Nerves system. That single decision affects everything downstream — once you're building custom systems, ICU and sqlite-vec become "while we're at it" additions.
@@ -51,7 +52,7 @@ Both extensions are blockers on stock Nerves systems for the same root reason
## 3. Litestream replication — this is the architectural pivot ## 3. Litestream replication — this is the architectural pivot
**Headline:** Litestream is *not* a multi-master replicator. It's a one-way backup tool. If the Nerves device writes locally, those writes are lost on the next restore — no conflict detection, no reconciliation, no merge. **Headline:** Litestream is _not_ a multi-master replicator. It's a one-way backup tool. If the Nerves device writes locally, those writes are lost on the next restore — no conflict detection, no reconciliation, no merge.
**Two viable replica modes today (both newer than the docs you may remember):** **Two viable replica modes today (both newer than the docs you may remember):**
@@ -83,6 +84,7 @@ The **biggest non-obvious unknown** is the Litestream architecture decision. Goi
## Sources ## Sources
### NIF / Phoenix-on-Nerves ### NIF / Phoenix-on-Nerves
- [Underjord: LiveView on Nerves](https://underjord.io/liveview-on-nerves.html) - [Underjord: LiveView on Nerves](https://underjord.io/liveview-on-nerves.html)
- [ElixirForum: Cross Compiling with NIF as compilation dependency (vix/Nerves thread)](https://elixirforum.com/t/cross-compiling-with-nif-as-compilation-dependency/59821) - [ElixirForum: Cross Compiling with NIF as compilation dependency (vix/Nerves thread)](https://elixirforum.com/t/cross-compiling-with-nif-as-compilation-dependency/59821)
- [Nerves docs: Compiling Non-BEAM Code](https://hexdocs.pm/nerves/compiling-non-beam-code.html) - [Nerves docs: Compiling Non-BEAM Code](https://hexdocs.pm/nerves/compiling-non-beam-code.html)
@@ -96,6 +98,7 @@ The **biggest non-obvious unknown** is the Litestream architecture decision. Goi
- [Customizing Your Nerves System](https://hexdocs.pm/nerves/customizing-systems.html) - [Customizing Your Nerves System](https://hexdocs.pm/nerves/customizing-systems.html)
### SQLite extensions ### SQLite extensions
- [Releases · asg017/sqlite-vec](https://github.com/asg017/sqlite-vec/releases) - [Releases · asg017/sqlite-vec](https://github.com/asg017/sqlite-vec/releases)
- [Fix for musl compile PR #199 · asg017/sqlite-vec](https://github.com/asg017/sqlite-vec/pull/199) - [Fix for musl compile PR #199 · asg017/sqlite-vec](https://github.com/asg017/sqlite-vec/pull/199)
- [Build and install exqlite with sqlcipher into nerves rpi5 image — ElixirForum](https://elixirforum.com/t/build-and-install-exqlite-with-sqlcipher-into-nerves-rpi5-image/75089) - [Build and install exqlite with sqlcipher into nerves rpi5 image — ElixirForum](https://elixirforum.com/t/build-and-install-exqlite-with-sqlcipher-into-nerves-rpi5-image/75089)
@@ -105,6 +108,7 @@ The **biggest non-obvious unknown** is the Litestream architecture decision. Goi
- [Advanced Configuration — Nerves](https://hexdocs.pm/nerves/advanced-configuration.html) - [Advanced Configuration — Nerves](https://hexdocs.pm/nerves/advanced-configuration.html)
### Litestream ### Litestream
- [Live Read Replication — Litestream (tip)](https://tip.litestream.io/guides/read-replica/) - [Live Read Replication — Litestream (tip)](https://tip.litestream.io/guides/read-replica/)
- [VFS Read Replicas — Litestream](https://litestream.io/guides/vfs/) - [VFS Read Replicas — Litestream](https://litestream.io/guides/vfs/)
- [How it works — Litestream](https://litestream.io/how-it-works/) - [How it works — Litestream](https://litestream.io/how-it-works/)
@@ -1,9 +1,10 @@
--- ---
id: doc-9 id: doc-9
title: 'GPT 5.5, high analysis' title: "GPT 5.5, high analysis"
type: other type: other
created_date: '2026-05-04 15:11' created_date: "2026-05-04 15:11"
--- ---
# Nerves Deployment Research Report # Nerves Deployment Research Report
Date: 2026-05-04 Date: 2026-05-04
+22 -19
View File
@@ -11,6 +11,7 @@ Uses SQLite (via `ecto_sqlite3`) with three databases: one for the app, one for
jobs (Oban), and one for telemetry metrics. All schemas use `binary_id` primary keys. jobs (Oban), and one for telemetry metrics. All schemas use `binary_id` primary keys.
Key capabilities: Key capabilities:
- Browse/search collected and wishlisted records - Browse/search collected and wishlisted records
- Import metadata from MusicBrainz, enrich with Discogs/Wikipedia/Last.fm - Import metadata from MusicBrainz, enrich with Discogs/Wikipedia/Last.fm
- Scrobble tracks to Last.fm, import listening history - Scrobble tracks to Last.fm, import listening history
@@ -50,7 +51,7 @@ when `single_line_logging` is `true` — not a supervised process.
## Database & Repos ## Database & Repos
| Repo | DB file (dev) | Purpose | | Repo | DB file (dev) | Purpose |
|------|---------------|---------| | ----------------------------- | -------------------------------------- | ------------------------------------------------------ |
| `MusicLibrary.Repo` | `data/music_library_dev.db` | All application data | | `MusicLibrary.Repo` | `data/music_library_dev.db` | All application data |
| `MusicLibrary.BackgroundRepo` | `data/music_library_background_dev.db` | Oban job queue | | `MusicLibrary.BackgroundRepo` | `data/music_library_background_dev.db` | Oban job queue |
| `MusicLibrary.TelemetryRepo` | `data/music_library_telemetry_dev.db` | Telemetry metrics history (persistent across restarts) | | `MusicLibrary.TelemetryRepo` | `data/music_library_telemetry_dev.db` | Telemetry metrics history (persistent across restarts) |
@@ -65,7 +66,7 @@ write to it directly; insert/update the `records` table instead.
## Schemas ## Schemas
| Schema | Table | PK | Key Fields | | Schema | Table | PK | Key Fields |
|--------|-------|----|------------| | ------------------------------------------ | ------------------------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `Records.Record` | `records` | `id` (binary_id) | title, type, format, cover_url, cover_hash, musicbrainz_id, genres[], release_date, purchased_at, dominant_colors[], embeds_many :artists | | `Records.Record` | `records` | `id` (binary_id) | title, type, format, cover_url, cover_hash, musicbrainz_id, genres[], release_date, purchased_at, dominant_colors[], embeds_many :artists |
| `Records.RecordEmbedding` | `record_embeddings` | `id` | embedding (Float32 vector), text_representation, belongs_to :record | | `Records.RecordEmbedding` | `record_embeddings` | `id` | embedding (Float32 vector), text_representation, belongs_to :record |
| `Records.SearchIndex` | `records_search_index` | `id` | FTS5 mirror of records (virtual, trigger-synced) | | `Records.SearchIndex` | `records_search_index` | `id` | FTS5 mirror of records (virtual, trigger-synced) |
@@ -84,6 +85,7 @@ write to it directly; insert/update the `records` table instead.
| `Chats.Message` | `chat_messages` | `id` (binary_id) | role, content, position, belongs_to :chat | | `Chats.Message` | `chat_messages` | `id` (binary_id) | role, content, position, belongs_to :chat |
Last.fm schemas (separate, not Ecto-persisted to main DB): Last.fm schemas (separate, not Ecto-persisted to main DB):
- `LastFm.Track` — scrobbled tracks from Last.fm API responses - `LastFm.Track` — scrobbled tracks from Last.fm API responses
- `LastFm.Album`, `LastFm.Artist` — parsed API responses - `LastFm.Album`, `LastFm.Artist` — parsed API responses
@@ -92,7 +94,7 @@ Last.fm schemas (separate, not Ecto-persisted to main DB):
## Contexts (lib/music_library/) ## Contexts (lib/music_library/)
| Context | Schemas | Responsibility | | Context | Schemas | Responsibility |
|---------|---------|---------------| | ---------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Records` | Record, RecordEmbedding, SearchIndex | CRUD, search, import from MusicBrainz, cover/genre/color management, PubSub notifications | | `Records` | Record, RecordEmbedding, SearchIndex | CRUD, search, import from MusicBrainz, cover/genre/color management, PubSub notifications |
| `Collection` | Record (via SearchIndex) | Querying collected records (purchased_at != nil), stats, collected artist IDs, collection summary for AI chat | | `Collection` | Record (via SearchIndex) | Querying collected records (purchased_at != nil), stats, collected artist IDs, collection summary for AI chat |
| `Wishlist` | Record (via SearchIndex) | Querying wishlisted records (purchased_at is nil) | | `Wishlist` | Record (via SearchIndex) | Querying wishlisted records (purchased_at is nil) |
@@ -116,7 +118,7 @@ Last.fm schemas (separate, not Ecto-persisted to main DB):
## Business Logic Modules ## Business Logic Modules
| Module | Purpose | | Module | Purpose |
|--------|---------| | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Records.SearchParser` | Parses search syntax: `artist:X`, `album:X`, `genre:"Y"`, `format:cd`, `type:album`, `purchase_year:2024`, `release_year:2024`, free text | | `Records.SearchParser` | Parses search syntax: `artist:X`, `album:X`, `genre:"Y"`, `format:cd`, `type:album`, `purchase_year:2024`, `release_year:2024`, free text |
| `ListeningStats.SearchParser` | Parses scrobbled tracks search syntax: `record:X`, `album_mbid:X`, `artist_mbid:X`, `artist:X`, `album:X`, `track:X`, free text | | `ListeningStats.SearchParser` | Parses scrobbled tracks search syntax: `record:X`, `album_mbid:X`, `artist_mbid:X`, `artist:X`, `album:X`, `track:X`, free text |
| `Records.Similarity` | Embedding generation and async enqueue (OpenAI, enriched with Last.fm tags, skips API call when text representation unchanged), artist-cascade regeneration when upstream metadata changes, cosine-distance search (sqlite-vec) | | `Records.Similarity` | Embedding generation and async enqueue (OpenAI, enriched with Last.fm tags, skips API call when text representation unchanged), artist-cascade regeneration when upstream metadata changes, cosine-distance search (sqlite-vec) |
@@ -155,7 +157,7 @@ Last.fm schemas (separate, not Ecto-persisted to main DB):
## External API Integrations ## External API Integrations
| Module | API | Rate limit | Purpose | | Module | API | Rate limit | Purpose |
|--------|-----|-----------|---------| | --------------------------------- | -------------------- | ---------- | --------------------------------------------------------------------------------------------------- |
| `MusicBrainz` / `MusicBrainz.API` | musicbrainz.org | 1000 ms | Release/artist metadata, search | | `MusicBrainz` / `MusicBrainz.API` | musicbrainz.org | 1000 ms | Release/artist metadata, search |
| `LastFm` / `LastFm.API` | last.fm | 500 ms | Scrobbling, listening history, artist info (tags, similar artists), user profile/session validation | | `LastFm` / `LastFm.API` | last.fm | 500 ms | Scrobbling, listening history, artist info (tags, similar artists), user profile/session validation |
| `Discogs` / `Discogs.API` | discogs.com | 2000 ms | Artist profiles, images | | `Discogs` / `Discogs.API` | discogs.com | 2000 ms | Artist profiles, images |
@@ -184,7 +186,7 @@ HTTP 429 into `:rate_limit` vs `:auth_error` by reading the body `code`
### Queues ### Queues
| Queue | Concurrency | Purpose | | Queue | Concurrency | Purpose |
|-------|-------------|---------| | -------------- | ----------- | ------------------------------------------------------------------- |
| `default` | 10 | General async tasks | | `default` | 10 | General async tasks |
| `heavy_writes` | 1 | DB-intensive or serialized operations | | `heavy_writes` | 1 | DB-intensive or serialized operations |
| `music_brainz` | 1 | MusicBrainz calls (rate-limited at Req layer via `Req.RateLimiter`) | | `music_brainz` | 1 | MusicBrainz calls (rate-limited at Req layer via `Req.RateLimiter`) |
@@ -195,7 +197,7 @@ HTTP 429 into `:rate_limit` vs `:auth_error` by reading the body `code`
### Plugins (prod) ### Plugins (prod)
| Plugin | Config | Purpose | | Plugin | Config | Purpose |
|--------|--------|---------| | ------------------------ | --------------------------- | ------------------------------------------------------------ |
| `Oban.Plugins.Pruner` | `max_age: 43200` (12h) | Prune completed/cancelled/discarded jobs older than 12 hours | | `Oban.Plugins.Pruner` | `max_age: 43200` (12h) | Prune completed/cancelled/discarded jobs older than 12 hours |
| `Oban.Plugins.Reindexer` | `schedule: "@weekly"` | Weekly reindex of Oban tables for query performance | | `Oban.Plugins.Reindexer` | `schedule: "@weekly"` | Weekly reindex of Oban tables for query performance |
| `Oban.Plugins.Cron` | `timezone: "Europe/London"` | Scheduled recurring workers (see Cron Workers table) | | `Oban.Plugins.Cron` | `timezone: "Europe/London"` | Scheduled recurring workers (see Cron Workers table) |
@@ -203,7 +205,7 @@ HTTP 429 into `:rate_limit` vs `:auth_error` by reading the body `code`
### On-Demand Workers ### On-Demand Workers
| Worker | Queue | Trigger | | Worker | Queue | Trigger |
|--------|-------|---------| | ----------------------------------- | ------------ | --------------------------------------------------------------------------- |
| `FetchArtistInfo` | default | Artist page visit / import (also fetches Last.fm data inline) | | `FetchArtistInfo` | default | Artist page visit / import (also fetches Last.fm data inline) |
| `FetchArtistLastFmData` | last_fm | Manual / batch | | `FetchArtistLastFmData` | last_fm | Manual / batch |
| `FetchArtistImage` | heavy_writes | Artist info fetched | | `FetchArtistImage` | heavy_writes | Artist info fetched |
@@ -229,7 +231,7 @@ HTTP 429 into `:rate_limit` vs `:auth_error` by reading the body `code`
### Cron Workers ### Cron Workers
| Schedule | Worker | Queue | | Schedule | Worker | Queue |
|----------|--------|-------| | ------------------ | --------------------------------- | ------------ |
| Every 12h | `ApplyScrobbleRules` | heavy_writes | | Every 12h | `ApplyScrobbleRules` | heavy_writes |
| Every 12h | `PruneAssetCache` | default | | Every 12h | `PruneAssetCache` | default |
| Daily 2 AM | `PruneAssets` | default | | Daily 2 AM | `PruneAssets` | default |
@@ -248,7 +250,7 @@ HTTP 429 into `:rate_limit` vs `:auth_error` by reading the body `code`
## PubSub Topics ## PubSub Topics
| PubSub | Topic Pattern | Message | Used By | | PubSub | Topic Pattern | Message | Used By |
|--------|---------------|---------|---------| | ---------------- | -------------------------- | ------------------- | ------------------------------------------------------------------ |
| `:music_library` | `"records:#{id}"` | `{:update, record}` | CollectionLive.Show, WishlistLive.Show — real-time record updates | | `:music_library` | `"records:#{id}"` | `{:update, record}` | CollectionLive.Show, WishlistLive.Show — real-time record updates |
| `:music_library` | `"listening_stats:update"` | `%{track_count: n}` | StatsLive.Index, ScrobbledTracksLive.Index — new scrobbles arrived | | `:music_library` | `"listening_stats:update"` | `%{track_count: n}` | StatsLive.Index, ScrobbledTracksLive.Index — new scrobbles arrived |
@@ -259,6 +261,7 @@ HTTP 429 into `:rate_limit` vs `:auth_error` by reading the body `code`
### Router Structure ### Router Structure
All authenticated routes live inside a single `live_session` with three `on_mount` hooks: All authenticated routes live inside a single `live_session` with three `on_mount` hooks:
- `StaticAssets` — detects app updates, shows toast - `StaticAssets` — detects app updates, shows toast
- `GetTimezone` — reads timezone from connect params - `GetTimezone` — reads timezone from connect params
- `ShowToast` — enables `put_toast!/2` in LiveViews - `ShowToast` — enables `put_toast!/2` in LiveViews
@@ -266,7 +269,7 @@ All authenticated routes live inside a single `live_session` with three `on_moun
### LiveViews ### LiveViews
| LiveView | Route | Purpose | | LiveView | Route | Purpose |
|----------|-------|---------| | ------------------------------- | --------------------------------------- | -------------------------------------------------------------------- |
| `StatsLive.Index` | `/` | Dashboard: counts, recent activity, records on this day | | `StatsLive.Index` | `/` | Dashboard: counts, recent activity, records on this day |
| `CollectionLive.Index` | `/collection` | Browse/search collected records (grid/list, paginated) | | `CollectionLive.Index` | `/collection` | Browse/search collected records (grid/list, paginated) |
| `CollectionLive.Show` | `/collection/:id` | Record detail: metadata, scrobbles, similar, colors | | `CollectionLive.Show` | `/collection/:id` | Record detail: metadata, scrobbles, similar, colors |
@@ -286,7 +289,7 @@ All authenticated routes live inside a single `live_session` with three `on_moun
### LiveComponents ### LiveComponents
| Component | Used In | Purpose | | Component | Used In | Purpose |
|-----------|---------|---------| | ------------------------------ | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `RecordForm` | Collection/Wishlist (edit) | Record editing: cover search, genre autocomplete, color picker, file upload | | `RecordForm` | Collection/Wishlist (edit) | Record editing: cover search, genre autocomplete, color picker, file upload |
| `ArtistLive.Form` | ArtistLive.Show | Edit artist image (upload + Brave image search) | | `ArtistLive.Form` | ArtistLive.Show | Edit artist image (upload + Brave image search) |
| `RecordSetLive.Form` | RecordSetLive.Index | Create/edit record set | | `RecordSetLive.Form` | RecordSetLive.Index | Create/edit record set |
@@ -308,7 +311,7 @@ All authenticated routes live inside a single `live_session` with three `on_moun
### Shared Component Modules (lib/music_library_web/components/) ### Shared Component Modules (lib/music_library_web/components/)
| Module | Purpose | | Module | Purpose |
|--------|---------| | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CoreComponents` | Forms, buttons, icons, tables, flash messages | | `CoreComponents` | Forms, buttons, icons, tables, flash messages |
| `Layouts` | Application layout templates, navigation components (`dropdown_nav/1`) | | `Layouts` | Application layout templates, navigation components (`dropdown_nav/1`) |
| `RecordComponents` | Record cards, cover images, artist images, labels, grids, release status icon badge, shared show-page sections (title, external links, genres, releases, timestamps, debug) | | `RecordComponents` | Record cards, cover images, artist images, labels, grids, release status icon badge, shared show-page sections (title, external links, genres, releases, timestamps, debug) |
@@ -322,7 +325,7 @@ All authenticated routes live inside a single `live_session` with three `on_moun
### Web Utility Modules (lib/music_library_web/) ### Web Utility Modules (lib/music_library_web/)
| Module | Purpose | | Module | Purpose |
|--------|---------| | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `ErrorMessages` | Maps internal error terms (atoms, structs) to user-friendly gettext strings via `friendly_message/1` | | `ErrorMessages` | Maps internal error terms (atoms, structs) to user-friendly gettext strings via `friendly_message/1` |
| `Markdown` | Markdown-to-HTML conversion (MDEx with ammonia sanitization) with `[[double bracket]]` link syntax and streaming document support for chat | | `Markdown` | Markdown-to-HTML conversion (MDEx with ammonia sanitization) with `[[double bracket]]` link syntax and streaming document support for chat |
| `Duration` | Milliseconds to human-readable duration formatting | | `Duration` | Milliseconds to human-readable duration formatting |
@@ -336,7 +339,7 @@ All authenticated routes live inside a single `live_session` with three `on_moun
### Controllers ### Controllers
| Controller | Routes | Purpose | | Controller | Routes | Purpose |
|------------|--------|---------| | ---------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| `SessionController` | `/login`, `/sessions/create` | Login/logout | | `SessionController` | `/login`, `/sessions/create` | Login/logout |
| `HealthController` | `/health` | Health check | | `HealthController` | `/health` | Health check |
| `LastFmController` | `/auth/last_fm/callback` | Last.fm OAuth | | `LastFmController` | `/auth/last_fm/callback` | Last.fm OAuth |
@@ -356,7 +359,7 @@ All authenticated routes live inside a single `live_session` with three `on_moun
### JS Hooks ### JS Hooks
| Hook | Type | Purpose | | Hook | Type | Purpose |
|------|------|---------| | --------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------- |
| `FormatNumber` | External (`assets/js/hooks/`) | Client-side number formatting | | `FormatNumber` | External (`assets/js/hooks/`) | Client-side number formatting |
| `UniversalSearchNavigation` | External | Keyboard navigation in search modal (via `create-navigation-hook` factory) | | `UniversalSearchNavigation` | External | Keyboard navigation in search modal (via `create-navigation-hook` factory) |
| `RecordPickerNavigation` | External | Keyboard navigation in record picker (via `create-navigation-hook` factory) | | `RecordPickerNavigation` | External | Keyboard navigation in record picker (via `create-navigation-hook` factory) |
@@ -370,7 +373,7 @@ All authenticated routes live inside a single `live_session` with three `on_moun
All events are namespaced with `music_library:` prefix. All events are namespaced with `music_library:` prefix.
| Event | Action | | Event | Action |
|-------|--------| | -------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `music_library:clipcopy` | Copy text to clipboard | | `music_library:clipcopy` | Copy text to clipboard |
| `music_library:scroll_top` | Scroll window to top | | `music_library:scroll_top` | Scroll window to top |
| `music_library:confetti` | Trigger canvas-confetti animation | | `music_library:confetti` | Trigger canvas-confetti animation |
@@ -391,7 +394,7 @@ All events are namespaced with `music_library:` prefix.
### Test Support ### Test Support
| Module | Purpose | | Module | Purpose |
|--------|---------| | ----------------- | ------------------------------------------- |
| `ConnCase` | HTTP test setup, auto-logged-in session | | `ConnCase` | HTTP test setup, auto-logged-in session |
| `DataCase` | Database test setup with Ecto sandbox | | `DataCase` | Database test setup with Ecto sandbox |
| `LiveTestHelpers` | `escape/1` for HTML-escaped text assertions | | `LiveTestHelpers` | `escape/1` for HTML-escaped text assertions |
@@ -399,7 +402,7 @@ All events are namespaced with `music_library:` prefix.
### Fixture Modules (test/support/fixtures/) ### Fixture Modules (test/support/fixtures/)
| Module | Creates | | Module | Creates |
|--------|---------| | ------------------------------------------- | ----------------------------- |
| `MusicLibrary.RecordsFixtures` | Records with MusicBrainz data | | `MusicLibrary.RecordsFixtures` | Records with MusicBrainz data |
| `MusicLibrary.RecordSetsFixtures` | Record sets with items | | `MusicLibrary.RecordSetsFixtures` | Record sets with items |
| `MusicLibrary.OnlineStoreTemplatesFixtures` | Store templates | | `MusicLibrary.OnlineStoreTemplatesFixtures` | Store templates |
+11 -8
View File
@@ -20,7 +20,7 @@ push via GitHub Actions.
## Hosting ## Hosting
| Component | Technology | | Component | Technology |
|-----------|-----------| | ----------------- | ----------------------------------------- |
| Orchestration | Coolify (self-hosted) | | Orchestration | Coolify (self-hosted) |
| Container runtime | Docker | | Container runtime | Docker |
| SSL termination | Coolify reverse proxy | | SSL termination | Coolify reverse proxy |
@@ -43,7 +43,7 @@ Fluxon UI (licensed dependency) is fetched during build via Docker build secrets
Three separate SQLite databases, each managed by its own Ecto repo: Three separate SQLite databases, each managed by its own Ecto repo:
| Repo | Purpose | Cache size | Pool size | | Repo | Purpose | Cache size | Pool size |
|------|---------|------------|-----------| | ----------------------------- | ---------------------------- | ---------- | ------------------------ |
| `MusicLibrary.Repo` | Application data | 128 MB | `$POOL_SIZE` (default 5) | | `MusicLibrary.Repo` | Application data | 128 MB | `$POOL_SIZE` (default 5) |
| `MusicLibrary.BackgroundRepo` | Oban job queue | 16 MB | `$POOL_SIZE` (default 5) | | `MusicLibrary.BackgroundRepo` | Oban job queue | 16 MB | `$POOL_SIZE` (default 5) |
| `MusicLibrary.TelemetryRepo` | Persistent telemetry metrics | 4 MB | 2 | | `MusicLibrary.TelemetryRepo` | Persistent telemetry metrics | 4 MB | 2 |
@@ -61,7 +61,7 @@ Configured inline in `compose.yaml`. Runs as a separate Docker Compose service
(`litestream/litestream:0.5.11-scratch`) sharing the database volume. (`litestream/litestream:0.5.11-scratch`) sharing the database volume.
| Setting | Value | | Setting | Value |
|---------|-------| | ------------- | --------------------------------------------------------- |
| S3 endpoint | `https://nbg1.your-objectstorage.com` | | S3 endpoint | `https://nbg1.your-objectstorage.com` |
| Bucket | `ffmusiclibrary` | | Bucket | `ffmusiclibrary` |
| Sync interval | 60 minutes | | Sync interval | 60 minutes |
@@ -133,7 +133,7 @@ Fluxon (private dependency) is configured with a dedicated registry entry.
### Deployment credentials ### Deployment credentials
| Secret/Variable | Purpose | | Secret/Variable | Purpose |
|-----------------|---------| | ------------------ | ---------------------------------------- |
| `COOLIFY_TOKEN` | API authentication (GitHub secret) | | `COOLIFY_TOKEN` | API authentication (GitHub secret) |
| `COOLIFY_HOST` | Coolify server address (GitHub variable) | | `COOLIFY_HOST` | Coolify server address (GitHub variable) |
| `COOLIFY_APP_UUID` | Application identifier (GitHub variable) | | `COOLIFY_APP_UUID` | Application identifier (GitHub variable) |
@@ -145,7 +145,7 @@ Fluxon (private dependency) is configured with a dedicated registry entry.
### Required ### Required
| Variable | Purpose | | Variable | Purpose |
|----------|---------| | -------------------------- | ---------------------------------------------------- |
| `DATABASE_PATH` | Absolute path to main SQLite database | | `DATABASE_PATH` | Absolute path to main SQLite database |
| `BACKGROUND_DATABASE_PATH` | Absolute path to background jobs database | | `BACKGROUND_DATABASE_PATH` | Absolute path to background jobs database |
| `TELEMETRY_DATABASE_PATH` | Absolute path to telemetry database | | `TELEMETRY_DATABASE_PATH` | Absolute path to telemetry database |
@@ -158,7 +158,7 @@ Fluxon (private dependency) is configured with a dedicated registry entry.
### Optional ### Optional
| Variable | Default | Purpose | | Variable | Default | Purpose |
|----------|---------|---------| | ------------------------------- | ------------------------- | ----------------------------- |
| `SERVICE_FQDN_WEB` | `example.com` | Application domain | | `SERVICE_FQDN_WEB` | `example.com` | Application domain |
| `PORT` | `4000` | HTTP listen port | | `PORT` | `4000` | HTTP listen port |
| `POOL_SIZE` | `5` | Database connection pool size | | `POOL_SIZE` | `5` | Database connection pool size |
@@ -277,21 +277,24 @@ or browser access. Each extension reads its own environment variables from the p
runtime environment (not server-side config). runtime environment (not server-side config).
| Extension | Tools | Env vars | | Extension | Tools | Env vars |
|-----------|-------|----------| | ------------- | ------------------------------------------------------------------- | ------------------------------------------------------------ |
| `prod-logs` | `fetch_production_logs` | `PI_COOLIFY_HOST`, `PI_COOLIFY_APP_UUID`, `PI_COOLIFY_TOKEN` | | `prod-logs` | `fetch_production_logs` | `PI_COOLIFY_HOST`, `PI_COOLIFY_APP_UUID`, `PI_COOLIFY_TOKEN` |
| `prod-errors` | `fetch_production_errors`, `fetch_production_error`, `/prod-errors` | `PI_API_TOKEN`, `PI_SERVICE_FQDN_WEB` | | `prod-errors` | `fetch_production_errors`, `fetch_production_error`, `/prod-errors` | `PI_API_TOKEN`, `PI_SERVICE_FQDN_WEB` |
**`prod-logs` env vars:** **`prod-logs` env vars:**
- `PI_COOLIFY_HOST` — Coolify server base URL (e.g., `https://coolify.example.com`) - `PI_COOLIFY_HOST` — Coolify server base URL (e.g., `https://coolify.example.com`)
- `PI_COOLIFY_APP_UUID` — Application UUID in Coolify - `PI_COOLIFY_APP_UUID` — Application UUID in Coolify
- `PI_COOLIFY_TOKEN` — Coolify API Bearer token - `PI_COOLIFY_TOKEN` — Coolify API Bearer token
**`prod-errors` tools and command:** **`prod-errors` tools and command:**
- `fetch_production_errors` — List/filter errors via LLM tool - `fetch_production_errors` — List/filter errors via LLM tool
- `fetch_production_error` — Single error detail via LLM tool - `fetch_production_error` — Single error detail via LLM tool
- `/prod-errors` — Interactive TUI for browsing errors (list, detail, filter toggles) - `/prod-errors` — Interactive TUI for browsing errors (list, detail, filter toggles)
**`prod-errors` env vars:** **`prod-errors` env vars:**
- `PI_API_TOKEN` — Must match the `API_TOKEN` env var on the production server (used for Bearer auth on `/api/v1/*`) - `PI_API_TOKEN` — Must match the `API_TOKEN` env var on the production server (used for Bearer auth on `/api/v1/*`)
- `PI_SERVICE_FQDN_WEB` — Production domain with protocol (e.g., `https://musiclibrary.claudio-ortolina.org`, no trailing slash) - `PI_SERVICE_FQDN_WEB` — Production domain with protocol (e.g., `https://musiclibrary.claudio-ortolina.org`, no trailing slash)
@@ -302,7 +305,7 @@ runtime environment (not server-side config).
Mailgun via Swoosh (`Swoosh.Adapters.Mailgun`). Mailgun via Swoosh (`Swoosh.Adapters.Mailgun`).
| Setting | Value | | Setting | Value |
|---------|-------| | ------- | ------------------------------------ |
| From | `postmaster@mailgun.fullyforged.com` | | From | `postmaster@mailgun.fullyforged.com` |
| To | `claudio@fullyforged.com` | | To | `claudio@fullyforged.com` |
+6
View File
@@ -22,3 +22,9 @@ mix gettext.extract --merge
debug_msg "Running shellcheck..." debug_msg "Running shellcheck..."
fd . 'scripts/' --exclude '*.hurl' -t file --exec shellcheck --color fd . 'scripts/' --exclude '*.hurl' -t file --exec shellcheck --color
debug_msg "Running prettier..."
prettier --write '.pi/extensions/**/*.{ts,js,json}'
prettier --write 'assets/css/**/*.css' 'assets/js/**/*.js'
prettier --write 'docs/**/*.md' 'docs/**/*.livemd' 'README.md'
prettier --write 'backlog/archive/**/*.md' 'backlog/completed/**/*.md' 'backlog/tasks/**/*.md' 'backlog/docs/**/*.md'