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
The audit report shows:
- Total number of scrobbled tracks
- Artists with missing MusicBrainz IDs (grouped by artist name)
- 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.
2. **Apply Rules**: Use the "Apply Rules" button in the Scrobble Rules page to update existing tracks, or run in IEx:
```elixir
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.
The application also provides helper functions in the `MusicLibrary.ScrobbleActivity` context:
- `count_tracks_missing_artist_musicbrainz_id/0`
- `count_tracks_missing_album_musicbrainz_id/0`
- `get_artists_missing_musicbrainz_id/1`
- `get_albums_missing_musicbrainz_id/1`
## Deployment
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 &);
/* 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 {
a,
@@ -37,7 +40,7 @@
@theme {
--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-shine: shine 3s ease 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";
// 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
const maxItems = 3
const maxItems = 3;
const liveToastHook = createLiveToastHook(duration, maxItems)
const liveToastHook = createLiveToastHook(duration, maxItems);
let Hooks = FluxonHooks;
Hooks.FormatNumber = FormatNumberHook;
@@ -56,8 +56,8 @@ const liveSocket = new LiveSocket("/live", Socket, {
params: (view) => {
return {
_csrf_token: csrfToken,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
}
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
};
},
hooks: { ...Hooks, ...colocatedHooks },
dom: {
@@ -121,32 +121,39 @@ window.liveSocket = liveSocket;
// 2. click on elements to jump to their definitions in your code editor
//
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.
// Disable with reloader.disableServerLogs()
reloader.enableServerLogs()
reloader.enableServerLogs();
// 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 "d" key pressed to open at function component definition location
let keyDown
window.addEventListener("keydown", e => keyDown = e.key)
window.addEventListener("keyup", e => keyDown = null)
window.addEventListener("click", e => {
let keyDown;
window.addEventListener("keydown", (e) => (keyDown = e.key));
window.addEventListener("keyup", (e) => (keyDown = null));
window.addEventListener(
"click",
(e) => {
if (keyDown === "c") {
e.preventDefault()
e.stopImmediatePropagation()
reloader.openEditorAtCaller(e.target)
e.preventDefault();
e.stopImmediatePropagation();
reloader.openEditorAtCaller(e.target);
} else if (keyDown === "d") {
e.preventDefault()
e.stopImmediatePropagation()
reloader.openEditorAtDef(e.target)
e.preventDefault();
e.stopImmediatePropagation();
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
+5 -2
View File
@@ -1,6 +1,7 @@
export default function banner() {
const version = document.querySelector('meta[name="version"]').content;
console.log(`%c
console.log(
`%c
███╗ ███╗██╗ ██╗███████╗██╗ ██████╗ ██╗ ██╗██████╗ ██████╗ █████╗ ██████╗ ██╗ ██╗
████╗ ████║██║ ██║██╔════╝██║██╔════╝ ██║ ██║██╔══██╗██╔══██╗██╔══██╗██╔══██╗╚██╗ ██╔╝
██╔████╔██║██║ ██║███████╗██║██║ ██║ ██║██████╔╝██████╔╝███████║██████╔╝ ╚████╔╝
@@ -8,5 +9,7 @@ export default function banner() {
██║ ╚═╝ ██║╚██████╔╝███████║██║╚██████╗ ███████╗██║██████╔╝██║ ██║██║ ██║██║ ██║ ██║
╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═════╝ ╚══════╝╚═╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝
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 {
mounted() {
this.selectedIndex = -1;
@@ -89,13 +93,16 @@ export default function createNavigationHook({ getContainer, inputId, onSelect }
if (searchInput) {
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];
selectedResult.setAttribute("aria-selected", "true");
selectedResult.scrollIntoView({
block: "nearest",
behavior: "smooth"
behavior: "smooth",
});
}
},
@@ -105,6 +112,6 @@ export default function createNavigationHook({ getContainer, inputId, onSelect }
if (!container) return [];
return Array.from(container.querySelectorAll('[role="option"]'));
}
},
};
}
+2 -2
View File
@@ -4,5 +4,5 @@ export default {
},
updated() {
this.el.innerText = parseInt(this.el.innerText).toLocaleString();
}
}
},
};
+1 -1
View File
@@ -9,5 +9,5 @@ export default createNavigationHook({
} else {
hook.navigateUp();
}
}
},
});
+1 -1
View File
@@ -9,5 +9,5 @@ export default createNavigationHook({
} else {
hook.navigateUp();
}
}
},
});
+1 -1
View File
@@ -28,7 +28,7 @@ export default {
onEnd: () => {
const items = this.el.querySelectorAll("[data-sortable-item]");
const recordIds = Array.from(items).map(
(item) => item.dataset.recordId
(item) => item.dataset.recordId,
);
const payload = { record_ids: recordIds };
@@ -2,5 +2,5 @@ import createNavigationHook from "./create-navigation-hook";
export default createNavigationHook({
getContainer: () => document.getElementById("universal-search-root"),
inputId: "universal-search-input"
inputId: "universal-search-input",
});
@@ -1,9 +1,10 @@
---
id: doc-10
title: 'Deepseek v4 Pro, xhigh analysis'
title: "Deepseek v4 Pro, xhigh analysis"
type: other
created_date: '2026-05-04 15:11'
created_date: "2026-05-04 15:11"
---
# Nerves Deployment Feasibility Analysis for Music Library
> 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.
| 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. |
| **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. |
@@ -46,8 +47,8 @@ excluded — they never reach the firmware.
### Targetarchitecture specifics
| 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. |
**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:
| 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. |
| **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)
| Capability | Litestream | Needed for device ↔ cloud sync |
|---|---|---|
| ----------------------------------- | ---------- | ------------------------------ |
| Unidirectional backup (app → S3) | ✅ | ❌ |
| Disaster recovery (restore from S3) | ✅ | ❌ |
| Bidirectional sync | ❌ | ✅ |
@@ -191,7 +192,7 @@ path. This is independent from the production Litestream instance.
## Summary of risks
| Risk | Severity | Mitigation |
|---|---|---|
| -------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------- |
| **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). |
| **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. |
| **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. |
| **dominant\_colors** | 🟢 Low | Tiny Rust NIF. Straightforward crosscompilation. |
| **dominant_colors** | 🟢 Low | Tiny Rust NIF. Straightforward crosscompilation. |
---
@@ -2,8 +2,9 @@
id: doc-11
title: Nerves Deployment Research — Consolidated Summary
type: other
created_date: '2026-05-04 15:25'
created_date: "2026-05-04 15:25"
---
# 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.
@@ -113,6 +114,7 @@ Litestream is asynchronous backup and disaster recovery. It streams WAL pages to
#### D. Offline-Capable Writes (Most Complex)
Options if offline writes are required:
- **Application-level outbox**: Local command log + idempotency keys + push to production on reconnect.
- **CRDTs** (Automerge, Yjs): True offline-first, no Elixir/SQLite bridge exists.
- **Event sourcing**: Append-only mutation log, replay to reconstruct state. Complex.
@@ -150,7 +152,7 @@ Options if offline writes are required:
## 5. Risk Summary
| Risk | Severity | Mitigation |
|---|---|---|
| ----------------------------- | --------- | ------------------------------------------------------------------------- |
| 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 |
| typst on embedded | 🟡 Medium | Server-side PDF generation |
@@ -188,6 +190,7 @@ All three reports collapse into a single question:
## Sources
All three reports include extensive source references. See individual documents for full citation lists:
- `doc-8 - Opus-4.7-xhigh-analysis.md`
- `doc-9 - GPT-5.5-high-analysis.md`
- `doc-10 - Deepseek-v4-Pro-xhigh-analysis.md`
@@ -1,10 +1,11 @@
---
id: doc-8
title: 'Opus 4.7, xhigh analysis'
title: "Opus 4.7, xhigh analysis"
type: other
created_date: '2026-05-04 15:08'
updated_date: '2026-05-04 15:10'
created_date: "2026-05-04 15:08"
updated_date: "2026-05-04 15:10"
---
# 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.
@@ -12,7 +13,7 @@ Three parallel research agents covered NIFs, SQLite extensions, and Litestream r
## 1. Native libraries (NIFs on aarch64-linux-musl)
| Library | aarch64-gnu | aarch64-musl | Status |
|---|---|---|---|
| -------------------------- | ----------- | ------------ | ---------------------------------------------------------------------------------- |
| `mdex` | ✅ | ✅ | Fine |
| `lumis` | ✅ | ✅ | Fine |
| `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:
- 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`.
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
**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):**
@@ -83,6 +84,7 @@ The **biggest non-obvious unknown** is the Litestream architecture decision. Goi
## Sources
### NIF / Phoenix-on-Nerves
- [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)
- [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)
### SQLite extensions
- [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)
- [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)
### Litestream
- [Live Read Replication — Litestream (tip)](https://tip.litestream.io/guides/read-replica/)
- [VFS Read Replicas — Litestream](https://litestream.io/guides/vfs/)
- [How it works — Litestream](https://litestream.io/how-it-works/)
@@ -1,9 +1,10 @@
---
id: doc-9
title: 'GPT 5.5, high analysis'
title: "GPT 5.5, high analysis"
type: other
created_date: '2026-05-04 15:11'
created_date: "2026-05-04 15:11"
---
# Nerves Deployment Research Report
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.
Key capabilities:
- Browse/search collected and wishlisted records
- Import metadata from MusicBrainz, enrich with Discogs/Wikipedia/Last.fm
- Scrobble tracks to Last.fm, import listening history
@@ -50,7 +51,7 @@ when `single_line_logging` is `true` — not a supervised process.
## Database & Repos
| Repo | DB file (dev) | Purpose |
|------|---------------|---------|
| ----------------------------- | -------------------------------------- | ------------------------------------------------------ |
| `MusicLibrary.Repo` | `data/music_library_dev.db` | All application data |
| `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) |
@@ -65,7 +66,7 @@ write to it directly; insert/update the `records` table instead.
## Schemas
| 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.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) |
@@ -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 |
Last.fm schemas (separate, not Ecto-persisted to main DB):
- `LastFm.Track` — scrobbled tracks from Last.fm 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/)
| Context | Schemas | Responsibility |
|---------|---------|---------------|
| ---------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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 |
| `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
| 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 |
| `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) |
@@ -155,7 +157,7 @@ Last.fm schemas (separate, not Ecto-persisted to main DB):
## External API Integrations
| Module | API | Rate limit | Purpose |
|--------|-----|-----------|---------|
| --------------------------------- | -------------------- | ---------- | --------------------------------------------------------------------------------------------------- |
| `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 |
| `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
| Queue | Concurrency | Purpose |
|-------|-------------|---------|
| -------------- | ----------- | ------------------------------------------------------------------- |
| `default` | 10 | General async tasks |
| `heavy_writes` | 1 | DB-intensive or serialized operations |
| `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)
| Plugin | Config | Purpose |
|--------|--------|---------|
| ------------------------ | --------------------------- | ------------------------------------------------------------ |
| `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.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
| Worker | Queue | Trigger |
|--------|-------|---------|
| ----------------------------------- | ------------ | --------------------------------------------------------------------------- |
| `FetchArtistInfo` | default | Artist page visit / import (also fetches Last.fm data inline) |
| `FetchArtistLastFmData` | last_fm | Manual / batch |
| `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
| Schedule | Worker | Queue |
|----------|--------|-------|
| ------------------ | --------------------------------- | ------------ |
| Every 12h | `ApplyScrobbleRules` | heavy_writes |
| Every 12h | `PruneAssetCache` | 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 | Topic Pattern | Message | Used By |
|--------|---------------|---------|---------|
| ---------------- | -------------------------- | ------------------- | ------------------------------------------------------------------ |
| `: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 |
@@ -259,6 +261,7 @@ HTTP 429 into `:rate_limit` vs `:auth_error` by reading the body `code`
### Router Structure
All authenticated routes live inside a single `live_session` with three `on_mount` hooks:
- `StaticAssets` — detects app updates, shows toast
- `GetTimezone` — reads timezone from connect params
- `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
| LiveView | Route | Purpose |
|----------|-------|---------|
| ------------------------------- | --------------------------------------- | -------------------------------------------------------------------- |
| `StatsLive.Index` | `/` | Dashboard: counts, recent activity, records on this day |
| `CollectionLive.Index` | `/collection` | Browse/search collected records (grid/list, paginated) |
| `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
| Component | Used In | Purpose |
|-----------|---------|---------|
| ------------------------------ | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `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) |
| `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/)
| Module | Purpose |
|--------|---------|
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CoreComponents` | Forms, buttons, icons, tables, flash messages |
| `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) |
@@ -322,7 +325,7 @@ All authenticated routes live inside a single `live_session` with three `on_moun
### Web Utility Modules (lib/music_library_web/)
| Module | Purpose |
|--------|---------|
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `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 |
| `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
| Controller | Routes | Purpose |
|------------|--------|---------|
| ---------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| `SessionController` | `/login`, `/sessions/create` | Login/logout |
| `HealthController` | `/health` | Health check |
| `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
| Hook | Type | Purpose |
|------|------|---------|
| --------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------- |
| `FormatNumber` | External (`assets/js/hooks/`) | Client-side number formatting |
| `UniversalSearchNavigation` | External | Keyboard navigation in search modal (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.
| Event | Action |
|-------|--------|
| -------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `music_library:clipcopy` | Copy text to clipboard |
| `music_library:scroll_top` | Scroll window to top |
| `music_library:confetti` | Trigger canvas-confetti animation |
@@ -391,7 +394,7 @@ All events are namespaced with `music_library:` prefix.
### Test Support
| Module | Purpose |
|--------|---------|
| ----------------- | ------------------------------------------- |
| `ConnCase` | HTTP test setup, auto-logged-in session |
| `DataCase` | Database test setup with Ecto sandbox |
| `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/)
| Module | Creates |
|--------|---------|
| ------------------------------------------- | ----------------------------- |
| `MusicLibrary.RecordsFixtures` | Records with MusicBrainz data |
| `MusicLibrary.RecordSetsFixtures` | Record sets with items |
| `MusicLibrary.OnlineStoreTemplatesFixtures` | Store templates |
+11 -8
View File
@@ -20,7 +20,7 @@ push via GitHub Actions.
## Hosting
| Component | Technology |
|-----------|-----------|
| ----------------- | ----------------------------------------- |
| Orchestration | Coolify (self-hosted) |
| Container runtime | Docker |
| 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:
| Repo | Purpose | Cache size | Pool size |
|------|---------|------------|-----------|
| ----------------------------- | ---------------------------- | ---------- | ------------------------ |
| `MusicLibrary.Repo` | Application data | 128 MB | `$POOL_SIZE` (default 5) |
| `MusicLibrary.BackgroundRepo` | Oban job queue | 16 MB | `$POOL_SIZE` (default 5) |
| `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.
| Setting | Value |
|---------|-------|
| ------------- | --------------------------------------------------------- |
| S3 endpoint | `https://nbg1.your-objectstorage.com` |
| Bucket | `ffmusiclibrary` |
| Sync interval | 60 minutes |
@@ -133,7 +133,7 @@ Fluxon (private dependency) is configured with a dedicated registry entry.
### Deployment credentials
| Secret/Variable | Purpose |
|-----------------|---------|
| ------------------ | ---------------------------------------- |
| `COOLIFY_TOKEN` | API authentication (GitHub secret) |
| `COOLIFY_HOST` | Coolify server address (GitHub variable) |
| `COOLIFY_APP_UUID` | Application identifier (GitHub variable) |
@@ -145,7 +145,7 @@ Fluxon (private dependency) is configured with a dedicated registry entry.
### Required
| Variable | Purpose |
|----------|---------|
| -------------------------- | ---------------------------------------------------- |
| `DATABASE_PATH` | Absolute path to main SQLite database |
| `BACKGROUND_DATABASE_PATH` | Absolute path to background jobs database |
| `TELEMETRY_DATABASE_PATH` | Absolute path to telemetry database |
@@ -158,7 +158,7 @@ Fluxon (private dependency) is configured with a dedicated registry entry.
### Optional
| Variable | Default | Purpose |
|----------|---------|---------|
| ------------------------------- | ------------------------- | ----------------------------- |
| `SERVICE_FQDN_WEB` | `example.com` | Application domain |
| `PORT` | `4000` | HTTP listen port |
| `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).
| Extension | Tools | Env vars |
|-----------|-------|----------|
| ------------- | ------------------------------------------------------------------- | ------------------------------------------------------------ |
| `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-logs` env vars:**
- `PI_COOLIFY_HOST` — Coolify server base URL (e.g., `https://coolify.example.com`)
- `PI_COOLIFY_APP_UUID` — Application UUID in Coolify
- `PI_COOLIFY_TOKEN` — Coolify API Bearer token
**`prod-errors` tools and command:**
- `fetch_production_errors` — List/filter errors 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` env vars:**
- `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)
@@ -302,7 +305,7 @@ runtime environment (not server-side config).
Mailgun via Swoosh (`Swoosh.Adapters.Mailgun`).
| Setting | Value |
|---------|-------|
| ------- | ------------------------------------ |
| From | `postmaster@mailgun.fullyforged.com` |
| To | `claudio@fullyforged.com` |
+6
View File
@@ -22,3 +22,9 @@ mix gettext.extract --merge
debug_msg "Running shellcheck..."
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'