14 Commits
Author SHA1 Message Date
samiandClaude Sonnet 5 c2eef96175 gui: floating drop target, clipboard global shortcut, theming, UI watchdog
Continues the build order past Options/Scheduler/Speed Limiter/Batch/
Grabber/tray.

- DropTargetWidget: frameless always-on-top drop target (docs/03-gui-spec.md
  §5), position persisted, accepts a dropped http(s) URL or link text and
  opens FileInfoDialog directly (skipping Add URL, since the URL is already
  known). Shown/hidden from general.showDropTarget, live via
  event.settings.changed, same pattern MainWindow already used for
  general.minimizeToTray.
- Clipboard, explicit path #2 (docs/06-risks-and-spikes.md R2):
  GlobalShortcut wraps org.freedesktop.portal.GlobalShortcuts
  (CreateSession -> BindShortcuts -> Activated), triggering the same Add URL
  flow. Guarded end-to-end on `if(TARGET Qt6::DBus)` / VELOX_GUI_HAVE_DBUS
  so a build without the component degrades to "feature skipped," not
  broken (gui/docs/pkg-qa-requests-m1.md R4). Best-effort by design per the
  risk doc: fails silent, never advertised.

  Verified live against the real portal (a real Wayland session, not just
  offscreen): `CreateSession` refuses every caller with "An app id is
  required" — reproduced identically via a bare `busctl` call with no Qt
  involved at all, so this is the portal requiring a sandboxed caller
  identity, not something fixable from an unconfined process. Recorded as
  a partial Spike S2 answer in docs/06-risks-and-spikes.md: this explicit
  path likely doesn't work for Velox as a traditionally-packaged app on
  stock GNOME, only if/when it ships confined. Also fixed a real leak this
  verification caught: QDBusInterface's introspection cache reads as a
  LeakSanitizer leak the first time anything touches D-Bus (tst_rtl went
  red under ASan) — switched to QDBusMessage::createMethodCall, which
  needs no introspection.
- Theming (docs/03-gui-spec.md §7): gui/resources/qss/{idm-like,dark}.qss,
  each with a documented palette block up top (QSS itself has no variable
  syntax), applied by ThemeManager and kept live via
  QStyleHints::colorSchemeChanged. util/Theme.hpp gives the handful of
  inline C++ styles (status dot, offline banner, the eleven identical
  error-label styles across dialogs) named constants instead of a twelfth
  copy of the same hex.
- UiThreadWatchdog: the M1 DoD's 200 ms debug-build watchdog. A background
  std::thread pings the UI thread every 50 ms via a queued invokeMethod and
  warns once (not per-poll) if a ping goes unanswered past 200 ms; no
  QThread, no Qt event loop of its own, so the watchdog itself can never be
  what blocks the thread it watches. No-op in a release build. Proven both
  ways in tst_uithreadwatchdog: fires on a genuinely blocked UI thread
  (synchronous sleep, no processEvents) and stays silent on a responsive
  one.

Full non-conformance suite (55 tests across every lane, `ctest -LE
conformance`) passes clean at this point, including the whole gui label
under ASan+UBSan.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
2026-09-12 21:30:13 +04:00
samiandClaude Sonnet 5 1fd2e0a0db gui: Options — Capture tab, saveTo.allowedRoots, lock coverage to the schema
Options was missing 8 of the 43 real settings.* keys: capture.enabled,
monitoredExtensions, monitoredMimeTypes, minSizeBytes, excludedHosts,
bypassModifier, autoStartTypes, and saveTo.allowedRoots. The capture.* keys
were dropped in an earlier pass on the mistaken read that the spec's "File
Types" tab meant per-category extension lists (which do live on Category,
not settings.*) — they're real settings.* keys for a real daemon feature
(the extension's auto-capture policy), so the tab exists now, named
"Capture" to match what it actually configures rather than the spec's
label.

New tst_optionsdialog case (allKeysMatchesTheSchemaExactly) loads
Settings.schema.json itself at test time and diffs its property set against
OptionsDialog::allKeys() — this drifted silently once already, so the
regression is now a build-time gate an unused import or a future key
addition would trip, not something that needs re-discovering by hand again.

Verified against a real veloxd (not just mockd): settings.get across all
43 keys, a settings.set/get round trip on a scalar (connection.timeoutSec)
and on array-valued keys in the shapes OptionsDialog::currentValues()
actually produces (capture.monitoredExtensions, proxy.bypassHosts,
saveTo.allowedRoots), and event.settings.changed fanning out to a second
subscribed client — all round-tripped and restored to their original
values afterward. The real OptionsDialog widget also loads and renders
correctly against that same daemon's live defaults with no crash under
ASan+UBSan.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
2026-09-12 21:29:19 +04:00
samiandClaude Sonnet 5 755d85964e gui: M1 DoD harness — scroll-60fps, rss-flat, unhappy-path
gui/docs/pkg-qa-requests-m1.md R3, filed by the previous session: three GUI
M1 DoD items (10k rows at 60 fps, flat RSS over 10 minutes,
--slow/--flaky/--drop-connection recovery) had nowhere to run in CI. This is
the harness — `gui/tests/dod/run.sh <gate> [--json <path>]`, exactly the
path/invocation contract tests/integration/README.md already specified —
plus `gui/tests/dod/dod_harness.cpp`, the Qt/RpcClient-driven binary that
actually runs each gate against a real mockd run.sh starts and tears down
itself.

- scroll-60fps: an eased scripted scroll over the whole loaded table,
  timing each step's synchronous repaint; p99 against a 16.6 ms budget
  (auto-scaled 4x under a sanitized build — ASan/UBSan overhead, not a
  loosened bar, see the harness's isSanitizedBuild()).
- rss-flat: samples this process's own VmRSS at 1 Hz across the run,
  discards a warm-up window, checks post-warm-up growth against a stated
  20 MiB slack.
- unhappy-path: three phases (slow/flaky/drop-connection), each its own
  mockd instance; passes when the client reaches and holds Connected with
  no crash or hang. A watchdog (the harness's own QTimer, backstopped by
  run.sh's external `timeout`) turns a genuine hang into a bounded non-zero
  exit rather than needing the CI caller to timeout(1) around it.

Every gate honours the exit-code and --json contract PKG/QA's pre-drafted
CI job expects unchanged (one addition needed: the build step must also
build the `gui-dod-harness` target, noted in the R3 update). No leaked mockd
processes on any exit path (`trap cleanup EXIT INT TERM`); no writes outside
a tempdir except the caller's own --json path.

Verified live end-to-end (not just unit-level): all three gates run against
a real mockd under the exact `ASAN_OPTIONS=detect_leaks=1:halt_on_error=1`
`.github/workflows/ci.yml`'s sanitizers job already sets, all pass, and
scroll-60fps was forced red once on purpose
(VELOX_DOD_FRAME_BUDGET_MS=1) to prove the fail path and exit code actually
work. Building this is also what surfaced the two RpcClient bugs fixed in
the previous commit, and one real gap in mockd itself — --drop-connection
never worked over the Unix socket transport (only WebSocket) — filed as
gui/docs/proto-requests-m1.md since tools/mockd is PROTO's file.

gui/docs/pkg-qa-requests-m1.md R3 and R4 (an unrelated, non-blocking Qt6::DBus
CMake hygiene note filed while wiring the clipboard global-shortcut path)
are updated with the concrete findings above.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
2026-09-12 21:28:36 +04:00
samiandClaude Sonnet 5 c6f864ea30 gui: fix RpcClient double-free on stop() and the 1000-row list cap
Both found live while building gui/tests/dod's DoD harness, not from reading
the code — see that commit for how.

RpcClient::stop() left conn_ dangling after joining the worker thread: the
thread's own finish() flushes the DeferredDelete stop()'s
connect(&thread_, &QThread::finished, conn_, &QObject::deleteLater) already
posted, so conn_ is gone by the time stop() returns, but nothing cleared the
pointer. Any caller that calls stop() and later lets the client destruct
(the harness's own client.stop() at shutdown; also plain, correct API usage)
hit a double-free in the destructor's leftover `delete conn_`. Caught by
ASan on the very first run that actually exercised the stop-then-destroy
path.

requestInitialList() also called download.list with a hardcoded
`{"limit": 1000}`, silently capping the table at 1000 rows no matter how
many the daemon actually has — download.list.schema.json's own description
says "the GUI pages", not "the GUI takes it all in one call". The
scroll-60fps DoD gate refused to run against mockd --tasks 10000 rather
than "pass" against a 1000-row table, which is what surfaced it.
requestInitialList() now pages (5000 per call, the schema's own max) until
`total` is satisfied, then resets the model once with everything.

Separately: RpcConnection's session.subscribe list never included
event.settings.changed or event.grabber.progress, even though RpcClient has
carried signals for both since the Options/Grabber work — session.subscribe
"replaces the previous selection" and "nothing is delivered until this is
called", so both events were being silently dropped by any real daemon that
enforces the subscription (mockd does; verified live with a second
subscribed client actually receiving event.settings.changed after this
fix, round-tripped through a real veloxd's settings.set). GrabberWizard's
5 s poll fallback is exactly why this went unnoticed until now — it covered
for the missing push the whole time.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
2026-09-12 21:27:28 +04:00
samiandClaude Sonnet 5 de83ee3cce gui: Options, Scheduler, Speed Limiter, Batch, Grabber, and the tray icon
Continues the build order past the Add-URL/File-Info/Progress dialog flow.

- OptionsDialog: General/Save To/Connection/Downloads/Proxy/Sounds tabs, every
  control bound to a real settings.* key (contracts/schema/types/Settings.
  schema.json). The spec's File Types and Site Logins tabs have no settings.*
  backing (categories go through category.upsert, credentials through the
  Secret Service) so they don't exist here — a tab either binds to a real key
  or isn't shipped. diffChanged() sends only what actually changed, matching
  settings.set's "changed[] names exactly what took effect" contract.
- SchedulerDialog: per-queue schedule (schedule.get/set) plus maxConcurrent/
  onComplete (queue.upsert), Start Now/Stop. Queue.schema.json already carries
  the schedule so queue.list alone seeds the window.
- SpeedLimiterDialog: the live global limiter (limiter.get/set) — a different
  thing from Options' downloads.speedLimit* default. buildParams() enforces
  the schema's "0 with enabled true must not be offered".
- BatchDialog: clipboard-blob and {start..end}-wildcard tabs sharing one
  category/queue/start-mode footer into download.addBatch.
- GrabberWizard: 4-step QWizard (project label -> start URL/depth/filters ->
  file-type filter -> review), grabber.start feeding a poll+event.grabber.
  progress-driven review page, Finish = grabber.harvest for the checked files.
- TrayIcon: active-count tooltip, Show/Add URL/Pause All/Resume All/Speed
  Limiter submenu/Quit. Quit only closes the GUI — there is no RPC to stop
  veloxd itself, filed as a new gap in daemon-requests-m1.md. MainWindow now
  also hides to tray instead of closing when general.minimizeToTray is set.

Every dialog's non-widget logic (diffChanged, buildSchedule, buildParams,
parseUrlBlob/expandWildcard/buildAddBatchParams, buildFileTypes/
buildStartParams/buildHarvestParams) is a static pure function with its own
test, same shape as FileInfoDialog::buildSpec from the previous round.

Verified end-to-end against a running mockd under ASan+UBSan: all five
surfaces render real data (settings.get values, queue.list's two seeded
queues, limiter.get, a live grabber.start/status crawl returning 3 files) with
no sanitizer reports. gui-check (non-ASan) and dev (ASan+UBSan) presets both
build the whole repo clean; all gui-labeled ctest targets pass.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
2026-09-11 17:17:43 +04:00
samiandClaude Sonnet 5 39c69f3871 gui: rpc plumbing for settings.changed and grabber.progress
Protocol.hpp gains method-name constants for every RPC the next batch of dialogs
needs (settings.*, limiter.*, schedule.*, queue.*, download.addBatch, grabber.*)
and RpcClient re-broadcasts the two events nothing consumed yet:
event.settings.changed and event.grabber.progress. No behaviour change on its
own — OptionsDialog, SchedulerDialog and GrabberWizard are what actually call
these, landing next.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
2026-09-11 17:17:23 +04:00
samiandClaude Sonnet 5 a71d904a1f gui: finish Add URL -> File Info -> Progress dialog flow
Wires up the three dialogs from build order step 5 (docs/03-gui-spec.md
§§2-3) and the MainWindow slots that were declared but never implemented:

- AddUrlDialog: clipboard prefill is the explicit path docs/06 R2 calls
  for (no passive monitoring, not advertised).
- FileInfoDialog: async download.probe never blocks the UI; ends by
  calling download.add itself (Now / Later / Add to Queue). buildSpec()
  is a pure static so the optional-field-omission logic is unit-testable
  without touching a widget.
- ProgressDialog: non-modal, WA_DeleteOnClose, driven by the taskProgress/
  taskStateChanged signals RpcClient already re-broadcasts; download.get
  seeds state once for a dialog opened mid-transfer. Hosts SegmentBarsWidget
  and SpeedGraphWidget.

MainWindow: openAddUrlDialog/openPropertiesForSelection/showTableContextMenu
now have bodies; category.list/queue.list responses are cached so File Info
can populate its category combo and queue menu without a second round trip.
The row context menu covers what already exists (Resume/Pause/Stop/Delete/
Properties) and deliberately leaves out Open/Open With/Move-Rename/
Redownload/Add to Queue — those need dialogs later build-order steps haven't
reached yet.

util/Format.hpp: pulled the bytes/rate/eta formatting out of MainWindow and
DownloadTableModel once the dialogs wanted the same strings a third time.

Verified end-to-end against a running mockd (category.list/queue.list,
download.probe, download.add, download.get, and live event.task.progress/
event.task.state) under ASan+UBSan: all three dialogs render correctly
against real fixture data and the flow runs clean with no leaks or
sanitizer reports.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
2026-09-11 12:11:13 +04:00
samiandClaude Sonnet 5 41bce91770 gui: add SegmentBarsWidget and SpeedGraphWidget
Per-connection progress bars and the 60 s rolling speed graph the progress
dialog needs (docs/03-gui-spec.md §3). Both reuse row/widget state across
ticks instead of rebuilding, matching the discipline DownloadTableModel
already uses for progress patches.

SpeedGraphWidget keeps a fixed ring buffer and one reused QPainterPath —
no allocation in paintEvent or addSample. Fixed a real bug found while
writing tst_speedgraphwidget: the elapsed timer was started in the
constructor, so the very first sample after construction would silently
wait up to 1 s to be recorded instead of landing immediately.

Tested against mockd (both offline via QTest/offscreen, and manually
against a running mockd instance through ProgressDialog once that lands).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
2026-09-11 12:10:06 +04:00
samiandClaude Sonnet 5 0af4a5c4fc gui: verify against the real veloxd; file one daemon gap
Pointed the existing RPC client at a real veloxd instance (isolated
HOME/XDG_RUNTIME_DIR/XDG_DATA_HOME — never touched the real ~/Downloads),
added a real download.add against tools/testserver, and watched it render
live end to end with no GUI code changes:

  - handshake, subscribe, category.list/queue.list all match what DAEMON
    reported
  - the full event.task.state sequence and batched event.task.progress
    both applied correctly by DownloadTableModel
  - the written file's SHA-256 matches the server's reference

Filed gui/docs/daemon-requests-m1.md: TaskSummary.sizeBytes is never
populated by this daemon build, even in download.get after the task
completes with the exact byte count already on disk. Not a GUI bug —
ProgressDelegate and the model already do the documented right thing
when size is unknown (fall back to plain text, no bar) — but it means
every task renders without a percentage against the real daemon today.
mockd always supplies sizeBytes so this doesn't block current GUI work;
flagging before the M1 GUI<->daemon integration pass.

mockd stays the primary harness for the unhappy paths
(--slow/--flaky/--drop-connection) that a real daemon won't misbehave on
command for.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016Ne28kx4VreeBWZv82Nksd
2026-09-11 07:55:06 +04:00
samiandClaude Sonnet 5 51fa1201bd gui: category tree, menus, toolbar, splitter — build-order step 3
- CategoryPanel: the left tree — All Downloads / Unfinished / Finished,
  then Categories and Queues populated from category.list / queue.list,
  with per-node task counts. Selecting a node emits a TaskSelection.
- DownloadFilterProxy: QSortFilterProxyModel keyed off that selection.
  Client-side for M1 (the whole list fits); asTaskFilter() exposes the
  equivalent TaskFilter for a server-side download.list once paging lands.
- MainWindow: menu bar (Tasks / Downloads / View / Help) sharing QAction
  objects with the toolbar; QSplitter [panel | table]; Delete with a
  confirm; Resume/Pause All; View menu toggles the panel. Actions
  disabled while offline.
- Counts are computed off a throttled 400 ms timer, not the 4 Hz progress
  path. Fixed a debounce-vs-throttle bug found in the first screenshot:
  restarting the timer on every progress tick meant it never fired and
  the status bar sat at "0 of 0 downloads".
- First-run column widths that fit the content.
- tst_downloadfilterproxy: nodes filter to their own rows, the
  Finished/Unfinished split is correct, and the filter is dynamic (a row
  that finishes leaves the Unfinished node with no re-list). Verified
  against mockd --tasks 400 (screenshot: tree counts 75/84/89/83/69 sum
  to 400, status bar "400 of 400, 21 active").

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016Ne28kx4VreeBWZv82Nksd
2026-09-10 15:41:29 +04:00
samiandClaude Sonnet 5 8844dd616b gui: amend PKG/QA request (R1 escalation, R3 restated) + EXT grep-check note
Review feedback on the filed request:

- R1: gui/CMakeLists.txt is on main now, so root's
  find_package(Qt6 ... Svg REQUIRED) is live — a missing SVG dev package
  is a hard configure failure for the whole project, not a skipped guard.
  Added the reason CI hasn't caught it: ubuntu-latest is 24.04 (where
  libqt6svg6-dev likely resolves), the project targets 26.04 (where it
  does not). Wrong name + runner/target release mismatch = the class of
  bug PKG/QA owns is currently unobservable in CI. That's the argument
  for R2, folded in.
- R3: corrected — CI does build the GUI and runs its three ctests under
  the ci preset. What has no home is the non-unit-test DoD: 10k-row
  60fps, flat RSS over a 10-minute run, and mockd
  --slow/--flaky/--drop-connection recovery. Asked for those specifically.
- gui/docs/ext-requests-m1.md: CLAUDE.md §3 says the no-download-logic
  rule applies to extension/ too; GUI made its half an executable ctest,
  EXT's half is still prose. Suggested the ESLint equivalent for the
  existing extension-lint job.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016Ne28kx4VreeBWZv82Nksd
2026-09-10 13:53:51 +04:00
samiandClaude Sonnet 5 2959b0f707 gui: real RTL + no-download-logic checks; split into velox-gui-lib
Follow-up hardening after a review noted the RTL "check" verified nothing
(a .ts stub that no test loads), matching a session-wide pattern of
checks written against what should be true rather than what would break.

- Split the non-main() code into velox-gui-lib (STATIC) so tests link the
  real widgets/models, not a reimplementation.
- tst_rtl: builds the real MainWindow, flips layoutDirection, asserts the
  direction propagates to the central widget AND that the offline-banner
  QHBoxLayout actually mirrors (label x-position LTR vs RTL differs by
  >100px). Verified it fails when the banner is pinned LtR.
- gui_no_download_logic: a ctest that greps gui/src for curl_*/pwrite/
  sqlite/QSqlDatabase/QNetworkAccessManager and fails on a hit — CLAUDE.md
  §3 as an executable check. Verified it fails when a curl_ token is added.
- Still uncovered (noted, not claimed): that the translation catalogue
  loads and the right context/strings resolve at runtime.

Not covered here because the files are PKG/QA-owned: tools/bootstrap.sh
ships a package name that does not exist on 26.04 (libqt6svg6-dev; the
real one is qt6-svg-dev), and --check validates pkg-config outcomes
rather than the apt names it would install. Both, plus the same name in
AGENT-PKG-QA.md and the README, are written up apply-ready in
gui/docs/pkg-qa-requests-m1.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016Ne28kx4VreeBWZv82Nksd
2026-09-10 01:12:06 +04:00
samiandClaude Sonnet 5 2a87abe96d gui: RPC client, download table model, and a live main window
First vertical slice of velox-gui, built entirely against tools/mockd
(no daemon dependency):

- rpc/: RpcConnection runs a QLocalSocket on a worker thread with
  newline-delimited JSON-RPC framing, drives the session.hello /
  session.subscribe handshake, and reconnects with exponential backoff
  (250 ms -> 8 s). RpcClient is the main-thread face: marshals calls onto
  the worker, delivers replies as main-thread callbacks, re-emits server
  notifications as typed Qt signals, and issues the one-shot download.list
  on reaching Connected.
- models/DownloadTableModel: QAbstractTableModel over TaskSummary. A
  progress batch is a row patch with a narrow dataChanged over the value
  columns only; beginResetModel() is reserved for the initial load and a
  reconnect resync.
- widgets/ProgressDelegate: in-cell progress bar for the Status column.
- mainwindow/MainWindow: the table, a status-bar connection dot, an
  offline banner instead of a modal, dialog-free pause/resume/stop
  actions, and QSettings column/geometry persistence.
- gui/CMakeLists.txt links velox::proto (never velox::core, ADR 0009) and
  self-guards on the veloxproto target so main keeps configuring if it is
  ever absent again.
- i18n from the first commit: every string via tr(), plus an Arabic .ts
  stub for the RTL check.
- tests/: headless QTest for the model — proves the progress patch is a
  narrow dataChanged and never resets the model.

Verified end-to-end against `mockd --tasks 300`: handshake, initial list,
live progress batches applied to the model, and a clean
Reconnecting -> Connected recovery when mockd is bounced mid-run.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016Ne28kx4VreeBWZv82Nksd
2026-09-10 00:58:53 +04:00
samiandClaude Opus 5 8bb683b09d scaffold: project structure, wire contract, roadmap and agent briefs
Lays out Velox Download Manager (IDM-class download manager for Ubuntu
26.04) as a monorepo ready for parallel lane development. No implementation
code by design.

- docs/: architecture, roadmap M0-M7, IDM-parity GUI spec, engine design,
  Firefox extension spec, risks/spikes, packaging
- contracts/: wire-contract skeleton (JSON Schema + fixture templates) —
  the single synchronization point between lanes
- docs/agents/: one brief per lane (PROTO, CORE, DAEMON, GUI, EXT, PKG/QA)
  with owned directories, build order and definition of done
- CLAUDE.md: rules of engagement — lane ownership, layering, non-negotiables
- CMake scaffolding with dev/tsan/release/ci presets

Two environment findings shape the design: Firefox here is the Mozilla snap
(native-messaging risk, so the extension carries a loopback-WebSocket
fallback), and Wayland forbids passive clipboard monitoring (so clipboard
capture is explicit-action-first).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-09 18:21:11 +04:00