Skip to content

Harden the backend, modernise the frontend, and make LLM setup a choice from a list - #30

Merged
sim186 merged 4 commits into
mainfrom
claude/feature-improvement-opportunities-vg401x
Aug 5, 2026
Merged

Harden the backend, modernise the frontend, and make LLM setup a choice from a list#30
sim186 merged 4 commits into
mainfrom
claude/feature-improvement-opportunities-vg401x

Conversation

@sim186

@sim186 sim186 commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Started as a review of where AmicoScript was weakest, then grew into fixing those, shipping three requested features, reworking LLM configuration, and running the app end to end to see what that turned up.

79 files changed · 439 tests passing (was 128).

🔐 Access control

AmicoScript now refuses network requests until a password is set. The project documents a Traefik deployment on a public domain, but every API route was open there — anyone who found the hostname could read the library, download the audio, and read the stored Hugging Face token straight out of GET /api/settings.

Requests from the machine AmicoScript runs on behave exactly as before: no password, no prompt. Requests from anywhere else are refused with an explanation until a password exists. Exposing the app unconfigured now fails closed.

  • Security panel to set, change or remove the password, plus an API token for headless clients
  • AMICOSCRIPT_AUTH=auto|always|off, AMICOSCRIPT_PASSWORD for startup
  • Secrets are no longer echoed to clients — settings report whether a token is stored, never its value
  • Login throttling; the loopback check reads the direct peer address, never X-Forwarded-For

🧠 LLM setup that does not require guesswork

  • Nine provider presets — Ollama, LM Studio, Unsloth Studio, llama.cpp, vLLM, Jan, LocalAI, OpenRouter, and anything OpenAI-compatible. Each fills in its address, states whether a key is required and what it looks like, and links to that tool's setup guide.
  • Find running servers scans the well-known ports, reports what answered and which models it has, and adopts it in one click. A server answering 401 still shows up, marked as needing a key — Unsloth Studio's normal state.
  • The /v1 trap is gone. Every one of these tools displays a URL ending in /v1, but AmicoScript appends /v1/chat/completions itself, so pasting what LM Studio showed you produced /v1/v1/… and a 404 that looked like a broken server. All four forms now resolve to the same thing, with the change explained.
  • Docker works out of the box. docker-compose.yml maps host.docker.internal to the host gateway (Docker Desktop provides it, Linux does not), and the app rewrites localhost addresses when containerised, saying so.
  • Hosted providers are gated. Audio never leaves your machine either way, but OpenRouter receives the transcript text — that takes an explicit confirmation, and analyses refuse to run without it.

✨ Features

  • Library export/import — the whole library as one zip you can back up or move between machines. Import matches by id, so re-importing is a no-op. Bundles deliberately exclude settings.json.
  • WebVTT and CSV exports — VTT uses <v Name> voice spans; CSV ships with a BOM for Excel and defuses formula injection in transcript text.
  • Automatic meeting summaries — optional, fires only for captured calls with an LLM configured, once per recording.

🔧 Reliability

  • A restart no longer destroys work in progress. Interrupted recordings were flipped to error with no explanation. Anything whose audio is on disk is requeued; anything unresumable is marked interrupted with a reason.
  • Failed transcriptions can be retried — the audio was always still on disk, but recovery meant delete-and-re-upload.
  • Long transcripts stop being silently truncated. A one-hour meeting is ~12k tokens against Ollama's 4096 default, so the model dropped the beginning and returned a confident summary of the last few minutes. Oversized transcripts are now map-reduced.
  • Search survives punctuation. covid-19, C++, hello "world and a bare AND were FTS5 syntax errors that silently downgraded the query.
  • Versioned migrations replace ad-hoc ALTER TABLE wrapped in except: pass, which left broken databases looking healthy.
  • URL imports download while the previous job transcribes — inference stays serialized, fetching does not.

🧹 Maintenance

  • The frontend is 20 ES modules instead of one 4,800-line inline <script>. Still no build step, no bundler. Split with a real JS parser computing the import graph, then verified against the pre-split page for behavioural parity.
  • Route-level tests. The suite had 128 unit tests over helpers and none that exercised an HTTP route; test_search_escaping.py re-implemented the code it claimed to test.
  • CI runs the whole suite — the workflow named ten files explicitly, so everything added since was never run.

🐛 Found by actually running the app

Drove the real application in a browser — upload, live transcription, editing, all six exports, folders, tags, search, backup round trip — with only Whisper inference stubbed (huggingface.co is blocked in the dev sandbox).

  • Creating a duplicate tag returned HTTP 500; now a 409 naming the clash
  • API errors were shown as raw JSON: Save failed: {"detail":"…"}
  • Password fields were not inside a form and had no username field
  • No retry path, invisible status reasons, no origin badge on recordings

⌨️ The TUI

Auditing it turned up a regression this branch introduced: masking secrets broke the settings form, so opening it and pressing Save erased the stored Hugging Face token. Fixed, plus /retry, /backup export|import, /llm-providers, /llm-detect, VTT/CSV export, the new status marks, and readable transport errors.


Reviewer notes

  • Auth trusts loopback by default. On a shared multi-user machine you'd want AMICOSCRIPT_AUTH=always.
  • GET /api/settings changed shapehf_token is now hf_token_set + hf_token_preview; GET /api/llm/settings reports llm_api_key_set. Any other client reading those fields needs the same treatment the TUI got.
  • Not included: the speaker library and library-wide RAG from the original review. Both are at the top of docs/ROADMAP.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JmgRm98BjMyfRok7eN9L12


Generated by Claude Code

claude added 4 commits August 5, 2026 09:39
Addresses the weak spots found in the review, plus three requested features.

Security
- Refuse API requests from off-machine until a password is set. Loopback is
  unchanged (no password, no prompt), so local use is unaffected, but the
  documented Traefik deployment no longer serves the library, the audio and the
  stored Hugging Face token to anyone who finds the hostname.
- Stop echoing secrets: settings report whether a token/key is stored, never
  its value, and an unedited field cannot overwrite a stored credential.
- Add a Security panel, an API token for headless clients, and login throttling.

Reliability
- Requeue interrupted transcriptions on restart instead of flipping them to
  'error' with no explanation; mark the unresumable ones with a reason.
- Replace ad-hoc ALTER TABLE statements wrapped in `except: pass` with numbered
  migrations recorded in schema_version, which fail loudly.
- Escape FTS5 query terms so ordinary punctuation ("covid-19", "C++") stops
  raising syntax errors and silently downgrading the search.
- Chunk LLM analysis that exceeds the context budget, so long recordings are
  summarised in parts and merged rather than truncated from the front.
- Prefetch URL downloads concurrently while keeping model inference serialized.
- Keep a tombstone for expired jobs so their routes answer 410 with the
  recording id rather than a 404.

Features
- Export/import the whole library as a zip bundle (settings excluded).
- WebVTT and CSV exports, in the web UI, bulk menu and TUI.
- Optional automatic summary when a captured meeting finishes.

Maintenance
- Split the 4,800-line inline script into 20 ES modules under frontend/js/,
  loaded natively — no build step, no bundler.
- Add route-level tests (128 -> 329) and make CI run the whole suite instead of
  ten named files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JmgRm98BjMyfRok7eN9L12
Configuring an analysis backend meant typing a base URL and hoping. Three
things went wrong often enough to fix in code rather than documentation.

Provider presets
- Ollama, LM Studio, Unsloth Studio, llama.cpp, vLLM, Jan, LocalAI, OpenRouter
  and a generic OpenAI-compatible option. Each fills in its address, states
  whether a key is required and what it looks like, and links to its setup docs.
- Unsloth Studio always needs a key (sk-unsloth-); LM Studio needs none. Model
  downloading is advertised only for Ollama, which is the only one that has it.

Finding a server
- GET /api/llm/detect probes the well-known ports and reports what answered,
  which models it has loaded, and whether it wants a key. One click in the UI
  adopts it. A 401 still counts as found — that is Unsloth's normal state.

Addresses
- Every one of these tools shows a URL ending in /v1, but AmicoScript appends
  /v1/chat/completions itself, so pasting what LM Studio displayed produced
  /v1/v1 and a 404 that looked like a broken server. Base URLs are now
  normalized, and the UI reports what changed.

Docker
- docker-compose.yml maps host.docker.internal to the host gateway, which
  Docker Desktop provides and Linux does not.
- The app detects that it is containerised and rewrites localhost addresses to
  the host alias, saying so; scanning probes the host, not the container.

Hosted providers
- OpenRouter and other remote endpoints are gated behind an explicit
  confirmation, because they receive the transcript text. Manual analyses and
  automatic meeting summaries both refuse to run until it is given.

Also: connection failures now explain which tool is not running, whether the
key was rejected, or whether the address has a stray /v1; and LLM Settings moved
to the main sidebar, having previously been reachable only after transcribing
something.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JmgRm98BjMyfRok7eN9L12
Drove the real application in a browser — upload, live transcription, segment
editing, all six exports, folders, tags, search, backup round trip, the LLM
setup panel — with only Whisper inference stubbed (huggingface.co is blocked in
this environment, so no model could be downloaded). Everything else ran for
real: the routes, the job queue, the worker, SSE, SQLite and the whole UI.

Bugs it found
- Creating a tag that already exists returned HTTP 500: the unique constraint
  reached the client as a server error. Now a 409 naming the clash, with the
  same treatment for renaming a tag onto an existing name.
- API errors were shown to users as raw JSON — `Save failed: {"detail":"…"}`.
  Folder, tag and library actions now surface the sentence the server sent.
- The password fields were not inside a form and had no username field, so
  browsers warned and password managers had nothing to bind a credential to.
  The Hugging Face and LLM key inputs are now marked so password managers skip
  them: they are tokens, not logins.

Gaps it exposed
- No way to retry. A failed transcription was a dead end even though the audio
  was still on disk — the only recovery was deleting the recording and
  re-importing the file. Adds POST /api/recordings/{id}/retry plus a button on
  failed, cancelled and interrupted rows, reusing the original options. The
  requeue logic is now shared with startup recovery rather than duplicated.
- The reason a recording was interrupted only existed as a tooltip, invisible on
  a touch screen. It is shown in the card.
- Recordings never showed their origin, so an auto-captured call and a dragged-in
  file looked identical. Captures and link imports now carry a badge.
- Automatic summaries were indistinguishable from ones the user requested; they
  are labelled "automatic".
- Library rows carry a data-recording-id so a row can be identified without
  counting its position.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JmgRm98BjMyfRok7eN9L12
Auditing the terminal UI against this session's work turned up a regression I
had introduced, plus a set of features it never gained.

The regression
- Masking secrets in the API broke the TUI's settings form: it still read
  `hf_token`, so the field came back empty, and saving wrote that emptiness
  back. Opening settings and pressing Save erased a stored Hugging Face token.
  The form now shows the masked preview, tracks that it is a placeholder, and
  sends the __unchanged__ sentinel unless the user actually types something.
  The LLM API key gets the same treatment.

Parity with the web UI
- /retry and Ctrl+R re-run a failed, cancelled or interrupted transcription.
- /backup export and /backup import for the library bundle.
- /llm-providers lists the supported backends; /llm-detect scans for a running
  server and offers to adopt it.
- Settings gained provider, context budget, cloud consent and the
  auto-summarise toggle.
- /export accepts vtt and csv and rejects unknown formats with the valid list.

Display and errors
- interrupted and cancelled have their own marks; captured meetings and link
  imports are prefixed in the list.
- Transport errors are explained: a 401 says which token to set and where it
  comes from, a 410 points at the library, an unreachable backend says so.
- Durations under an hour keep their seconds (a 22s clip read "0h 00m"), and the
  MODEL column reads the model out of transcription_options, where it lives.

Verified by driving the real TUI headless against a running backend: the retry,
backup round trip, provider list and VTT export all go through, and a save with
an untouched token leaves it intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JmgRm98BjMyfRok7eN9L12
@sim186
sim186 merged commit dd05d8e into main Aug 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants