Cold widget taps waited on TOP-resume (never fires for the
translucent trampoline) plus a DataStore consent read, costing
seconds and spurious app-opens when the read timed out. Fire from
onResume, cache consent in SharedPreferences, keep the first 15s
of banner on the stock template (now with Pause/Save/Discard
actions) until RemoteViews inflation is warm.
- Theme: derive primaryContainer from accent in both schemes (other container-tinted surfaces follow accent now)
- Home FAB: explicit containerColor=primary so button color == accent_color exactly
- Settings: hide 'Reset to default' on non-editable settings (App version showed it and threw 'not editable')
The custom RemoteViews layouts hard-set @color/shonar_widget_text (white)
for the dark navy card, but SystemUI paints the notification card with the
SYSTEM theme: in light mode the card is pale lavender and the title,
file name, and timer rendered white-on-white.
Add @color/notif_text = ink by default, #E6EDF3 in values-night, and use
it in notification_recording, notification_recording_small, and
notification_confirm_delete. The widget keeps its own white text (it paints
its own navy background).
On-device (Pixel 8 Pro, night=no): recording card text is dark-on-light;
zero white text pixels in the card region (was washed-out white before).
- ensureForeground() posts the FGS banner at the top of onStartCommand on a
cold-start START only (was: every action -> raced stopForeground(REMOVE)
and re-flipped the discard-confirm card back to the plain card)
- MediaRecorder prepare()/start() moved to Dispatchers.IO (StrictMode:
disk I/O was on the main thread); start failure reverts UI
- discardRecording() tears the banner down synchronously before stopSelf()
so a scope cancel in onDestroy can't leave a ghost notification
- ensureChannel() memoized + legacy channel cleanup kept (one IPC pass per
process instead of per call)
- restore migrateRestoreTimes() + runCatching around refreshRoot() in
Application.onCreate (dropped accidentally during timing instrumentation)
- Home screen: Column+verticalScroll -> LazyColumn with keyed items
- Notification layouts: explicit textColor for lockscreen/shade legibility
- Widget tap PendingIntent: FLAG_RECEIVER_FOREGROUND for prompt delivery
- All ShonarTiming instrumentation probes removed
On-device (Pixel 8 Pro): widget tap -> banner ~195ms; recording file grows
in .in-progress/; Discard->No survives, Discard->Yes removes service, temp,
and notification with nothing left live; no StrictMode recorder violation.
- buildConfirm had card buttons + addAction row: shade showed
green No/Yes plus black-text No/Yes pair (seen 2026-09-15)
- Keep only the card buttons; no system actions on any recording card
- Test now asserts zero system actions
- Post the foreground notification once on start (skip the duplicate
nm.notify that restarted SystemUI RemoteViews inflation).
- Collapsed shade row is a slim header-only card (lock shade clips
taller cards); expanded keeps the pills card. Restore
DecoratedCustomViewStyle (without it the system drops both custom
views and expansion breaks) plus a custom heads-up view.
- Fallback template leads with the timer; drop the SHONAR subtext
('SHONAR x SHONAR' header) and the system action row duplicating
the pills.
- Recording channel is DEFAULT: no heads-up banner pinning the shade
item expanded on every record start.
- Regression tests lock each behavior in
RecordingNotificationLayoutTest.
Phantom audio (playing with no UI and no way to stop it) is gone:
- New PlaybackController owns the single MediaPlayer process-wide.
Home rows and details mirror the same state; a generation counter
ignores stale async-prepare callbacks from rapid re-taps.
- Ongoing 'Now playing' notification with a Stop action works even
with the app closed (receiver clears card + releases player).
- Details keeps seek/speed UI on top of the shared player.
Fluidity (measured on-device, release build):
- StrictMode caught 192ms DateFormat disk I/O inside composition
(details info card) + library scans on the main thread. Date is
pre-formatted in the ViewModel on IO; scans moved to Dispatchers.IO.
- Detail-open on release: 84 frames, 1 janky (1.2%), p99 31ms
(was 6-8 janky, p99 ~130ms on debug).
Also: storage-access prompt card (fresh installs lose the file
grant; guides to system Settings), release signing via local
keystore (passwords in ~/.gradle/gradle.properties, key ignored
by git), default save name is now 'Recording - <date>'.
Phone is now a pure on-device recorder: no accounts, no servers, no
background uploads (INTERNET permission gone). File sync is the user's
own tooling; the library adopts externally added files.
Android:
- Delete provider package (Nextcloud, custom SHONAR, sync-folder,
registry, auth, TOFU/TLS), sync stack (SyncWorker/Drain/Slots,
MigrationRunner), provider/storage/folder UI, AI-via-server details.
- Home shows a fixed 'on this phone' library; details keep playback,
rename, file info. Settings lose Network/provider/HTTP-logging.
- Library root is the stored folder or Music/Recordings; import is
double-scan proof (mutex + unique filePath index, migration v3->v4
dedupes by path) with regression tests.
- Drop okhttp/work/security-crypto/media deps; delete their tests.
Repo: backend/, desktop/, worker/, deploy/, shared/, server scripts
and docs removed; README rewritten; CI keeps the android job only.
The working tree had reverted RecordingNotificationHelper to plain
system MediaStyle, which renders the small 'SHONAR - SHONAR' text row
with truncated filename and no big timer. Restore the user-verified
custom RemoteViews card (mic + Recording + filename + big timer, X /
pause / check pills, same view collapsed and expanded) and drop the
non-compiling MediaSession dead code from RecordingService
(unresolved activeNotifId / mediaSession / alertOnce references).
Add RecordingNotificationLayoutTest to fail the build if MediaStyle
is reintroduced or the timer/pills leave the layout.
The bug: 'Re-transcribe' uploaded a brand-new recording every click —
the server id lived only in DetailUi and died with the process, so
duplicates piled up server-side and no restart could reuse them.
- RemoteMapping sidecar (<name>.shonar.json) persists recording id +
model + title next to the audio; survives restart, renames (carried
by renameFile), and Syncthing
- openDetail preloads remoteId + override model from the sidecar
- transcribe() re-runs POST /reprocess?job=transcribe&model= in place
when a mapping exists; upload only for never-uploaded files
- new Re-summarize button (mapping-gated) hits /reprocess?job=summarize
- 404 on a stale mapping deletes the sidecar and tells the user to
re-upload (self-heals instead of failing forever)
- 6 unit tests (roundtrip, corrupt json, blank id, rename carry-over)
Validates through validate_model_name (422, never silently substituted)
and persists rec.transcription_model — the worker reads the row, so the
switch survives into the queued job. 2 new inline-queue tests: in-place
re-run with model switch (same id, one extra transcribe call), and
validation/ownership errors (409 nothing-to-summarize, 422 bad model,
404 not owner).
- POST /recordings/{id}/reprocess?job=transcribe|summarize re-runs one
stage on demand (409 if running or nothing to summarize)
- _fail(): summarize failure on a recording with a usable transcript
marks completed with 'Summary failed: ...' instead of failing the
whole recording
Was running on bare MaterialTheme defaults (stock lavender). New
desktop-only Theme.kt: darkColorScheme tuned to the Skills Hub hero
palette — #1A1A1A graphite bg, lifted card surfaces, gold primary
(E0B23A, text-safe contrast on cards), olive pill fills, warm-tuned
error red, cool slate tertiary. GoldInk for text on gold fills.
Status semantics: done=gold primary, failed=error, queued=variant.
Verified by screenshot on the running app (library + settings):
cohesive dark-gold, links legible, no contrast failures.
Rename button per row (hidden while queued/transcribing so the pump
never races it). Dialog validates via LibraryQueue.renameTarget:
extension is fixed, typed extension stripped (case-insensitive),
rejects empty/dot/slash names, collisions, no-ops. The .transcript.md
report moves with the audio; failed/live sets carry over by name.
4 new RenameTest cases; desktop suite 17 green. Verified in-app:
Rename visible on every row.
Backend:
- faster-whisper reports 0-99 percent (segment end / audio duration,
throttled to whole percents); processing.py persists it to the job
row from the worker thread via a best-effort scheduled writer.
- Commit job state (running/stage) before the long CPU/LLM phases so
readers see it (open tx was invisible + SQLite-locked); transient
failures reset the row to queued explicitly before raising for retry.
- summarize jobs carry stage=summarizing; both jobs clear it at 100.
Desktop:
- Library rows show live 'transcribing… 42%' with a determinate bar;
Detail screen busy text updates per poll; poll interval 2s.
- Search matches recording names too (name hits first, openable) —
finds untranscribed files (the 'tycos' miss).
- Engine prefers the LAN H200 openai_compat summarizer when the key
resolves (env or ~/.hermes/.env), falls back to local Ollama, then
none. Key never stored by the app.
Verified: backend 67 pytest green on SQLite and Postgres, ruff clean;
desktop 13 tests green (5 new jobProgress cases); live probe through
the app's engine showed queued→running 33%→100 and summarize running;
H200 summarize call 5.9s vs ~30s local qwen3:4b.
JFileChooser came up with an unusable size under the compositor; pin
1040x700. transcribeFile now appends its exception to
~/.config/shonar-desktop/pump-error.log instead of returning false
silently. Verified live: library lists 49 m4a files from
~/Music/Recordings, existing .transcript.md reports detected as done.
qwen3-family models think by default: a long reasoning chain before the
JSON answer that is brutally slow on CPU and doesn't improve summaries.
Send think:false (ignored by non-thinking models), raise the request
budget to 900s (first call loads a multi-GB model into RAM), and pin
num_ctx=8192. Verified live: spoken standup clip transcribed by
faster-whisper base and summarized by qwen3:4b through the inline queue
in ~20s (warm), recording status completed.
- engine spawns in bundled-lite mode: SQLite at ~/.config/shonar-desktop/
engine.db, SHONAR_QUEUE_BACKEND=inline, auto-migrate; no worker proc
- Ollama autodetect: if 127.0.0.1:11434 answers, LLM=ollama with qwen3:4b
(fallback: any local qwen3); else summaries honestly off
- readiness waits on /readyz database:true (not just /healthz) with early
exit on process death; engine log persists to logs/engine.log
- app auto-starts the engine instead of parking on the Engine screen
- folder watcher (4s poll): drop-ins appear without manual rescan
- auto-transcribe queue (persisted toggle): pump runs one file at a time,
skips files with reports/in-flight/previously-failed (no hot loop);
per-file Transcribe button for NEW/FAILED rows; status badges in list
- pure LibraryQueue logic + 3 unit tests (desktop suite 8 green)
The desktop app now runs the backend as a single embedded process: no
Postgres, no Redis, no Docker. Server deployments are unaffected
(defaults unchanged: postgres + arq).
- dialect-neutral schema: JSON renders JSONB on PG / JSON on SQLite;
new UTCDT type returns tz-aware datetimes from SQLite (naive rows
crashed utcnow() comparisons); PublicIdMixin.updated_at gains an
insert default (PG had silently tolerated NULLs); partial indexes get
sqlite_where; tsvector migration is a PG-only no-op elsewhere
- SQLite connections: PRAGMA foreign_keys=ON, WAL, busy_timeout
- queue_backend=inline: in-process asyncio consumer replaces arq
(shonar/services/inline_queue.py). DB rows remain the queue of record;
startup sweep re-runs pending jobs; transient failures retry with
backoff up to MAX_TRIES, one job at a time (faster-whisper memory)
- SHONAR_AUTO_MIGRATE=1: alembic upgrade head at startup (the desktop
owns its SQLite file end-to-end)
- tests run on either dialect via SHONAR_TEST_DATABASE_URL; 2 new
inline-queue tests (67 green on SQLite AND Postgres, ruff clean)
- verified live: bundled engine on SQLite + inline queue + faster-whisper
base transcribed a real upload end-to-end (queued -> succeeded ->
transcript v1 -> completed)