Commit graph

64 commits

Author SHA1 Message Date
avi
d6b55d79d6 Add Shonar Android launcher icon
Some checks failed
CI / android (push) Has been cancelled
2026-10-06 14:35:36 -05:00
avi
6cb5797990 Widget instant start: sync consent cache, plain banner with actions
Some checks failed
CI / android (push) Has been cancelled
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.
2026-10-06 14:31:08 -05:00
avi
c35e6935b5 Checkpoint: current Shonar state
Some checks failed
CI / android (push) Has been cancelled
2026-09-23 18:58:28 -05:00
avi
cd173d80b8 Widget mirrors in-app Record button; consent-gated start, save while active 2026-09-18 18:17:37 -05:00
avi
673330293a Widget trampoline waits for foreground recording; fixes 10s notification delay 2026-09-18 17:24:03 -05:00
avi
1f46d2a0e4 Single-action recording rows, S.H.O.N.A.R. header branding, drop storage pill 2026-09-18 16:11:34 -05:00
avi
47f7cb9336 Reskin to dark-navy + cyan design system; tidy recording rows, settings access on top 2026-09-18 15:32:41 -05:00
avi
9c866c858b Record FAB paints with accent (primary); Material3 default primaryContainer muted it
- 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')
2026-09-17 22:06:38 -05:00
avi
b6d4fd3cb2 Theme + accent settings now apply live (were decorative: ShonarTheme ignored them) 2026-09-17 21:25:20 -05:00
avi
c942865879 Notification channel v3: HIGH + 60ms tick, lockscreen public (silent channel delayed the heads-up pill) 2026-09-17 20:51:19 -05:00
avi
8489c407cc Widget tap: invisible trampoline with own task affinity (was revealing MainActivity under it) 2026-09-17 20:51:19 -05:00
avi
7f9b9c2e5c Widget: render mic+badge as bitmaps (Lawnchair themed-icons flattened drawable resources to gray) 2026-09-17 20:51:19 -05:00
avi
d8441ab385 Notification text follows system theme (fix white-on-white in light mode)
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).
2026-09-17 13:48:43 -05:00
avi
645e466b2f Instant record banner from widget tap; fix ghost notification + main-thread recorder prep
- 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.
2026-09-17 13:23:38 -05:00
avi
1d53d0f00c Notification: drop duplicate system No/Yes on delete-confirm card
- 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
2026-09-15 14:39:19 -05:00
avi
ffea797eba Settings: remove search field
- Settings list is small; search added noise
- Drop OutlinedTextField + query filtering from ViewModel
2026-09-15 11:34:54 -05:00
avi
1887256fbe Update README.md 2026-09-15 16:01:59 +00:00
avi
a005d4d922 Home: add Discard with confirm to in-progress recording card
- RecordingStatusCard: Pause/Resume + Finish + Discard (red)
- Discard shows confirm dialog ('Delete current recording?')
  then discards temp via ACTION_DELETE; Cancel keeps recording
2026-09-15 10:41:17 -05:00
avi
5235707de5 Library folder setting + title-time restore + notification heads-up/lockscreen + version stamp
- Settings: editable Library folder (blank = default Music/Recordings);
  bad/unusable paths keep previous folder, Current path shown, toast on update/reset
- Repository: refreshRoot never resets behind user; migrateRestoreTimes()
  restores HH-mm from createdAt for date-only titles, handles (2) collisions, once
- Notification: fresh v2 channel IMPORTANCE_HIGH (banner on record start),
  PRIVATE + publicVersion for secure lockscreen (single mic row, no pills)
- Build: versionCode = git commit count, versionName 0.1.<count>
- README: library folder note
- Tests: LibraryRootTest, TitleRestoreTest, updated layout + settings tests
2026-09-15 10:41:17 -05:00
avi
8cd8861695 Recording notification: correct card, fast, no pop-banner
- 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.
2026-09-15 05:56:34 -05:00
avi
d68aab5336 Playback you can always stop + release build + storage-access prompt
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>'.
2026-09-14 14:30:04 -05:00
avi
a445105d86 Android: optional local release signing (keystores ignored; passwords via ~/.gradle/gradle.properties, debug unaffected) 2026-09-14 12:29:14 -05:00
avi
d9f3d0b5c4 Android: StrictMode (debug) + off-main-thread IO — root resolver, file import, and DateFormat moved off composition/main; details label preformatted 2026-09-14 12:29:14 -05:00
avi
8c510f4ab9 Local-only app: remove network, uploads, providers, backend, desktop
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.
2026-09-14 11:56:20 -05:00
avi
7b3436837a Desktop: always-visible work indication — indeterminate bars while running w/o pct, summarize job row, upload phase on library rows; dev-loop watcher fixes 2026-09-14 11:20:55 -05:00
avi
a5201c1f2f Android: lock ongoing-recording notification to custom card + regression test
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.
2026-09-14 11:14:53 -05:00
avi
31e03262fd Desktop: dev loop (desktop-dev.sh, auto-restart on source edits), SHONAR_DEV engine --reload, build stamp in window title, detail screen reloads report rewrites + surfaces fetch errors 2026-09-14 11:10:36 -05:00
avi
67b1eb65c7 Android WIP: recording notification layout + service updates (in-flight from prior session) 2026-09-14 10:37:09 -05:00
avi
0fefd044ba Engine: inline queue reclaims running-job corpses at startup (single-process engine has no live workers) 2026-09-14 10:37:09 -05:00
avi
7f447188b5 Desktop: upload-path transcribe mirrors live progress to library rows 2026-09-14 10:24:52 -05:00
avi
5b39c1de4f Desktop: honest Transcribe/Summarize labels — 'Re-' only when the output exists; Summarize always visible, gated on transcript 2026-09-14 10:19:42 -05:00
avi
0fa558b1a7 Desktop: adopt server recording by title for pre-mapping files; refresh stale reports missing a summary 2026-09-14 09:53:40 -05:00
avi
e3c4c06d23 Desktop: reprocess path mirrors live progress onto library rows 2026-09-14 09:41:13 -05:00
avi
f73f12056f Desktop: live progress bar + % on the detail screen; openDetail polls running jobs 2026-09-14 09:09:28 -05:00
avi
1460d767f7 Desktop: exclude new Android-only sources (widget, ui/rename, recording helpers) from shared srcDir 2026-09-14 08:58:36 -05:00
avi
6824d9fbf1 M9: search endpoint + list filters, exports (audio/txt/md/zip), retention sweep 2026-09-14 08:51:24 -05:00
avi
4944fcd162 Desktop: app-wide zoom (Ctrl+=/-, Ctrl+0 resets) via scaled Density 2026-09-14 08:03:33 -05:00
avi
ac77f89ffa desktop: in-place reprocess via .shonar.json remote-id sidecar
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)
2026-09-13 20:38:12 -05:00
avi
b121d1a2e3 provider: reprocess(recordingId, job, model) against /reprocess
Server detail message surfaces through ProviderError.Transient so 409
reasons ('already running', 'transcribe first') reach the UI.
2026-09-13 20:36:54 -05:00
avi
0cba721ada api: reprocess accepts ?model= to switch the saved override on re-run
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).
2026-09-13 20:36:54 -05:00
avi
e7610d3901 desktop: ellipsize long file names in the library list 2026-09-13 19:20:33 -05:00
avi
5b4a111960 api: /reprocess endpoint; summarize failure keeps usable transcript
- 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
2026-09-13 19:20:33 -05:00
avi
b1df46ef19 Desktop: detail screen shows the saved report (summary was unreachable) 2026-09-13 16:28:12 -05:00
avi
9618067fc4 Desktop: warm-graphite theme (dark charcoal + gold accent)
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.
2026-09-13 02:58:20 -05:00
avi
9906a5af00 Desktop: rename recordings from the library
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.
2026-09-13 02:35:34 -05:00
avi
86e18c6b52 Progress reporting end-to-end + name search + LAN summarizer
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.
2026-09-13 02:04:08 -05:00
avi
3241bd50cf Desktop: size the folder picker dialog (tiny on HiDPI/XWayland) + log pump failures
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.
2026-09-13 00:27:56 -05:00
avi
535a96a1f0 Backend: Ollama CPU-friendly summarize (think=off, 900s timeout, 8k ctx)
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.
2026-09-12 23:41:25 -05:00
avi
1475f48329 Desktop: bundled-lite engine launch, folder watch + auto-transcribe queue
- 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)
2026-09-12 23:30:03 -05:00
avi
eea51c6741 Backend: bundled-lite engine mode (SQLite + inline queue) for the desktop app
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)
2026-09-12 23:15:41 -05:00