aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules
AgeCommit message (Collapse)AuthorFilesLines
6 dayssettings: the assistant's daemon config, tools, and Ask presetsDanilo M.7-0/+1109
Two settings pages for desktop-assistant. Assistant edits the daemon's config.toml: server, temperature, tool rounds, thinking, the system prompt, and the [env] values its tools read; a switch per tool renames tools/<name>.toml to .toml.off, which the daemon's glob skips. The daemon reads all of it at startup only, so the page says when a change is pending and Restart runs assistant.sh restart, showing its status when the daemon does not come back. The file is edited line by line, like autostart.lua, so the comments the example ships with survive: unset keys stay commented out in place. Every write parses the whole new file with tomllib first and replaces the old one atomically, so no edit can leave a config the daemon will not start with. Defaults are read from assistant.py --defaults rather than copied into QML. Ask presets edits the canvas's canned questions, which Prefs already stored for exactly this page: label, question, and the kinds each is offered for. They apply live. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 daysicons: cursor previews load againDanilo M.1-1/+0
Moving the appearance files into the desktop added import "../.." after the last line of each file that began with "import". In Icons.qml that line is inside the embedded Python cursor script, so the import landed there: python3 died on a SyntaxError, printed nothing, and every cursor card stayed empty with nothing in the log. Icons.qml uses no Theme, so the line goes. Every other moved file has it above its root object. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 dayswallpaper: the preview above the listDanilo M.2-111/+108
The mock screens took a third of the width beside the list, which left both cramped in the settings window. They now sit on top, sized to half the width and capped so the upright screen stays under 200px, with the notes and the actions beside them; the folder list takes the full width and the rest of the height below. The actions are a Flow, so they wrap instead of clipping in a narrow window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 daysuser: the details page moves to the settings windowDanilo M.2-5/+9
The drawer's greeting row stays, and clicking it now closes the drawer and opens the user page in the settings window: Drawer.show already hands a settings module to the window, so the row's handler is unchanged. "More git settings" switches the window to the git page through the same go() it used in the drawer. The page is searchable by the fields it edits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 dayssettings: HyprSunset and HyprIdle, named after what they configureDanilo M.3-6/+6
The labels and page titles only; the module names, and so the IPC verbs and search keys, stay sunset and idle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 daysappearance moves into the settings windowDanilo M.22-16/+3136
The appearance shell existed because a wallpaper picker needed more room than the 600px drawer, so it ran as its own process with a full-screen overlay. The settings window now gives it that room, and its five tabs become five settings pages in the desktop process: Wallpaper, Theme, Icons & cursor, Sunset and Idle. appearance/ is gone, and with it a process, a keepalive window and a layer blur rule. The files move to desktop/modules/appearance/ unchanged in what they do. Each tab gets a small *Module.qml; the window's refresh-on-open calls the singletons' refresh(), which the panel's show() used to. The two tabs that were inline Components in AppearancePanel become ThemeTab and WallpaperTab, owning the notices the panel printed. Sunset, Idle and Icons drop their own Flickable so the page scrolls them and search can reveal a row; the wallpaper list virtualises its rows and scrolls itself, so it takes the page's new viewHeight instead. Search finds a colour scheme and an icon or cursor theme by name. The appearance IPC target moves to desktop/shell.qml with the same verbs, so udt-steps and any keybind change only the path. Git now follows the appearance pages and autostart in the sidebar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 daysdesktop: settings search finds rows, and a hit reveals its rowDanilo M.4-9/+39
Search used to stop at the page: clicking a result opened it at the top and left the setting to be found by eye. Now each settings module keeps its rows as searchItems, {text, key}, reread on every window open since the page itself is not loaded until shown: git runs gitcfg list, and autostart watches its file. A row hit opens the page, and Page.reveal() finds the row whose objectName is the key, waiting up to 3s for the page's own data, scrolls to it and flashes it in the accent. The reveal lives in Page, so a page only names its rows. Git's long keyword list was a stand-in for exactly this and is cut to "gitconfig". The autostart page takes its path from the module now that both read the file. Also corrects the AGENTS note on missed hot reloads: it blamed sed -i replacing the file, but the Edit tool writes a new inode too and usually reloads. The cause is unknown; the note now says how to detect it and that a process that stops watching needs a restart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 daysdesktop: settings search, and an About landing pageDanilo M.2-0/+7
The landing page's tiles repeated the sidebar. It now says what the shell is and where things live, which is what a landing page is for. The sidebar gains a search. A page is a Component loaded only when opened, so its contents cannot be indexed without loading every page; instead a module lists keywords for the settings buried in its page, and the search matches those and the label. An entry matched by a keyword shows which one, Enter opens the first match, Escape clears and then steps out as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 daysdesktop: git config moves to the settings windowDanilo M.4-8/+8
The global git config is set once and left alone, which is what the settings window is for. The user row and its page stay in the drawer; "More git settings" there now closes the drawer and opens git in the window: Drawer.show hands any settings module to the window, so every deep link, IPC included, lands in the right place. Git was the only user of Module.up, the page back() returned to, so up goes with it and back from a drawer page is always the grid. Also gives autostart its rocket glyph, which the first commit wrote as an empty string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 daysdesktop: a settings window, and autostart as its first pageDanilo M.5-0/+729
The drawer's tiles were turning into a full settings app. The drawer stays the quick area, live state and things toggled often; pages set once and left alone go to a settings window, a normal Hyprland toplevel listing them down the left. A module joins with settings: true, which also drops its tile. A gear tile and ipc call settings open|toggle reach it. Autostart edits hypr-theme's autostart.lua directly, line by line: an hl.exec_cmd line is an entry, behind -- it is disabled, everything else passes through. Edits refuse when their line moved since the parse, and writes are atomic because Hyprland reloads on every change. A wrong entry only shows at the next login, so adding is guided rather than a free text field: pick an installed app or type a command whose program must resolve on PATH before Add enables, then a workspace and silent, composed into a [workspace X silent] rule. Try runs the line through hyprctl dispatch so the rule applies as at login. Existing rows get no Try: they ran at login, and a second start stacks an instance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysnarrow the sound module's real player listDanilo M.1-1/+9
7 daysnotes: an empty note body gets a file, and the second click writesDanilo M.3-31/+54
Reported from use: a note created with only a title then saved left a db.json entry with no file beside it, and reopening it said the file was missing. Cause: FileView.setText("") on a path that view has not loaded creates no file at all and reports no error. The index write (always non-empty JSON) was fine, so only the body was lost, which is why it looked like a missing file rather than a failed save. Bodies are now written by a Process running cat > <file> with the text on stdin: verified against empty and non-empty payloads, consecutive writes on one Process, and a store with no db.json at all. The exit code is the real outcome, so a refused write reports it. Second report in the same flow: the editor stays editable after the first click, so typing more and then pressing Commit & push dropped the edit, the push carrying only what the first click wrote. The second click now writes the current title and body before it commits and pushes.
7 daysnotes: delete asks before it destroysDanilo M.2-16/+25
The confirm was on the second click, after the first had already removed the file and the entry, so the 'Confirm?' label promised a check that had already happened and one stray click lost the note. The first click now only arms the button; the second removes and pushes the removal.
7 daysnotes: write and commit failures are reported, not assumedDanilo M.5-73/+153
A review of the branch found four ways the page could report success for work that never happened, all reachable in ordinary use. Writes: setText is asynchronous unless blockWrites, and its saved()/saveFailed() do not fire for every write (a second setText to the same path loses the signal, which stranded the callback), so a save was reported before it landed and a failed db.json or body write was never seen. Both views now block and carry the outcome, so writeDb and writeBody know the result when they return; a failed create therefore leaves no index entry. The create path also loads the body before the index, so a failed body write cannot leave an entry with no text. Commits: the sync chain swallowed every commit failure with '|| true', so a failing pre-commit hook or a signing failure pushed an unchanged HEAD and reported success. It now stages, commits only when something is staged, and lets 'set -e' fail the chain, with stderr folded in for the whole chain rather than just the push. Duplicate notes: the 'awaiting commit' state lived only in a 3 second timer, so a slower second click re-entered create() and appended a second note. It is now a page property that outlives the timer, and the button follows it. Preview: the shared Switch writes checked itself, dropping the binding, so the switch stopped following page.preview; resynced on change, the way StatusRow.qml does. Also validates a note's file name as digits before read, write or rm, and clears the index error once the index reads again.
7 daysnotes: component READMEDanilo M.1-0/+53
7 daysnotes: in-drawer editor with commit-on-save and git pullDanilo M.4-75/+555
The editor, and the write and git path behind it. Save is two clicks and each does one thing: the first writes the note and only then arms the button as 'Commit & push?', so a quick second click cannot commit a note the first has not written yet. The write path refuses when db.json cannot be read as the index. That is not hypothetical: git pull --rebase --autostash resolves a remote db.json change against a local uncommitted one, the stash pop conflicts, git still exits 0, and the file is left full of merge markers. Reading that as 'no notes' and writing it back deleted every title in a probe; Notes.valid now distinguishes it from an empty store and the write reports instead. parse stays tolerant for display. Stamping lastpull also only follows a pull that worked, and only when git is on at all. Db reads that a write is based on go through reload() plus waitForJob(): blockLoading does not make reload() blocking, contrary to the note in AGENTS.md, and reading text() straight after reload() returns the old contents. A body is written with a plain setText, which needs no prior load; only reading text() before a load would.
7 daysnotes: drawer tile with count and the note listDanilo M.2-0/+243
The module resolves notes.sh's own paths by sourcing its rc, so a changed BASEDIR moves the page with it, and notes.sh's git test is the same one the page uses. A Module is a plain QtObject with no default property, so the config Process and the db.json FileView are typed properties, the shape UpdatesModule uses for its own Process. db.json is watched rather than read on open: the tile's count has to move when a note is added from either side, the CLI included, which is the same call mail makes for its unread count. The list reads db.json.notes rather than walking notes/, because that is where the titles are; entries sort newest first on the file timestamp and a title search filters them.
7 daysnotes: pure db.json helpers, node-testedDanilo M.2-0/+183
The parsing, sorting and transforms the notes page needs, kept free of any file or process so node can test them. Ids and files stay strings: notes.sh writes them with jq --arg and looks a note up with select(.id == $i), so a number would stop the CLI finding a note the page wrote. sorted() copies rather than reorders, and an entry whose file is missing or non-numeric sorts last and formats as blank instead of NaN.
7 dayschore(assistant): retire the voice overlayDanilo M.1-2/+1
desktop-assistant is a text daemon now (its voice version is the tag voice-v1), and the canvas in desktop/assistant/ is its front end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfeat(git): add keys per section with a + beside each headingDanilo M.2-102/+131
The single Add line at the bottom took a full section.key and left the user to type the section right. Each heading now carries a + that opens a key and value row under it; git config --add writes the key into that existing section, among its neighbours, rather than at the end of the file. The typed text lives on the page, not in the fields, so a failed add keeps it across the reload that rebuilds every row. A new section is made in the editor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfix(git): refuse duplicate values, show list errors, Escape on AddDanilo M.4-9/+71
Review findings. With a key holding the same value twice, --fixed-value --replace-all rewrote both and merged two rows into one, and --unset failed on both; gitcfg now counts the matches and refuses anything but exactly one, pointing at the editor. A file git cannot parse made list print nothing, which the page drew as an empty config; list now prints git's error as an error= line and the page shows it. Escape in the Add fields clears the typed text before it leaves the page, as a row's edit is undone first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysdocs(git): the git config page, and replace-all appending on no matchDanilo M.2-1/+24
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfeat(user): open the git config page from the Git sectionDanilo M.2-1/+11
A "More git settings" button under the Git fields. The page emits moreGit and the module turns it into Page.go("git"), since the page itself has no reach into the drawer. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
7 daysfeat(git): the global git config pageDanilo M.2-0/+287
Every global value as a row under its section, in file order: Enter saves, Escape undoes, Remove drops that one value, and an Add row takes section.key and a value. Each write goes through gitcfg and is followed by a fresh list, so a row shows what git holds; a failure shows git's reason under its row. "Edit in editor" opens ~/.gitconfig in Sublime Text. No tile: up is "user", so back returns to the user page it opens from. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
7 daysfeat(git): parse git config -z --list for the pageDanilo M.2-0/+63
NUL between entries and the first newline between name and value, so values holding = or newlines survive. The key is after the last dot, so color.branch.remote groups under color.branch. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
7 daysfeat(git): gitcfg, global git config through git itselfDanilo M.2-0/+133
list, set, add and unset over git config --global. --fixed-value lets a write touch one value of a multi-valued key. set and unset check the old value is still there first: --replace-all with no match appends and exits 0, and the file may have changed in an editor since the page read it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
7 daysfeat(user): a random greeting on each drawer openDanilo M.4-4/+45
The greeting row picks one at random from ~/.config/quickshell/greetings, one per line with {user} for the login name, # comments and blank lines skipped; without the file, five built-in ones. The row is rebuilt on every drawer open, so the file is read then with no watch, and the last pick is kept in User.js, a shared .pragma library, so the same greeting never shows twice running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysdocs(user): location suggestions and the Git section in the READMEDanilo M.1-0/+6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfeat(user): keyboard for the location suggestionsDanilo M.1-6/+29
Up and Down move through the list, wrapping, and light the row as hover does; Enter takes it; Escape closes the list. With no list open the keys pass on, so Escape still leaves the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfeat(user): location suggestions from OpenStreetMapDanilo M.3-4/+110
There is no place database on this machine, and the user chose Nominatim over a tz city list for coverage. It is asked 1s after typing stops (its policy: one request a second, an identifying User-Agent, nothing personal sent), from three characters, in the desktop's language, and a reply to an older query is dropped. places() turns its JSON into 'place, region, country' labels and treats an error page as no suggestions; test_user.js covers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfeat(user): a Git section with the values git will carryDanilo M.1-16/+35
The one-line preview became a section: the git name and email as read-only fields that follow the Name and Email edits as they are typed, and the signing key, which is git's too, moved under them. A derived field is computed from the draft and never saved on its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfeat(shared): one faded glow for hovers across the shellsDanilo M.1-17/+37
Hover and attention effects were solid: a flat accent ring round the user page's avatar, a flat quarter-disc in the hot corner, and thick accent or white borders on window cards, keycaps and knobs. Glow.qml is a RectangularShadow in the accent, fading out from the shape's edge and in over 140ms, linked into desktop, window-switcher and keybinds like Theme.qml, and all five places use it. Selection borders in the appearance tabs stay: they mark state, not hover. The avatar also gains the hover cue in the first place: the hand cursor never showed under a hyprcursor-only theme, so without it the picture gave no sign it could be clicked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysstyle(user): a larger avatar and dropdowns in the themeDanilo M.1-5/+22
The avatar had room to spare at 96px; 160 fills the page better. The two ComboBoxes rendered in the default style's light colours on a dark page; a palette takes the theme into the control and its popup, and the background matches the text fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysstyle(user): the greeting is a header, not a cardDanilo M.1-3/+3
With a surface and a border it read as one more notification in the list under it. Only a faint hover tint remains. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfix(user): the avatar is ~/.user-icon.jpg, as hyprlock and SDDM useDanilo M.3-7/+32
The page showed AccountsService's icon, an older picture than the one the rest of the desktop uses: hyprlock, the udt SDDM theme (pointed at it by udt-accent) and the rofi power menu all read ~/.user-icon.jpg. get now prefers that file, and set converts the pick to JPEG there, through a temp file and a rename, before handing it to AccountsService so both agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfix(user): the page retries partial saves and survives leaving mid-saveDanilo M.3-7/+15
A key whose write half-failed (AccountsService took it, git did not) reloads as unchanged and Save went dead. A key with an error now counts as changed, and editing a field clears its error. Processes were parented to the page, so leaving it mid-save killed userctl between stores. They hang off the module now, passed in as owner (not the module's id, which would bind to itself). busy now clears when the reload lands, so Save cannot be clicked again before saved is fresh, and the fields and avatar picker are disabled while busy. Avatar paths with # or % broke the preview under a bare file:// prefix; each segment is now URI-encoded for the Image source (page and row), while draft.avatar stays a plain path for userctl. Dropped a redundant saved clear in load. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
7 daysdocs(user): the module's README and the busctl escaping noteDanilo M.1-0/+30
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfeat(user): a drawer page for the user's detailsDanilo M.2-0/+285
Name, email, avatar, language, location and the git signing key, each saved through userctl to every store that holds it. Only changed fields are sent; a field whose write failed keeps its edit and its error, so the next Save retries it. A failed userctl get reports a single error= line. The page shows it in red under the avatar, leaves saved and draft untouched, and disables Save until a later load succeeds. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
7 daysfeat(desktop): a greeting row above the notificationsDanilo M.1-0/+102
Avatar, Hello <login> and the real name, re-read whenever the grid view shows again so an edit is visible on the way back from the page. Module gains tiled, so a module can own a page without a tile. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
7 daysfeat(user): helpers for the user pageDanilo M.2-0/+93
Parsing userctl's output and locale -a, and the git name preview, kept out of QML so node can test them. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
7 daysfix(user): userctl survives leading dashes and reports failed readsDanilo M.2-15/+37
busctl and kwriteconfig6 parse a value starting with '-' as an option, so a location like '-x' failed; '--' before the value ends option parsing (checked against both binaries). get ran without pipefail, so a failing busctl gave empty fields and exit 0, which the page would show as a blank form. It now reads all fields first and, on failure, prints one error= line and exits 1. keys listed revoked, expired or invalid primaries, keys that cannot sign, and could label a key with a revoked uid. It now skips those. Unsetting the signing key uses --unset-all, since git exits 5 on a multi-valued key with --unset. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
7 daysfeat(user): userctl reads and writes the user's detailsDanilo M.2-0/+258
One writer for the stores other programs read: AccountsService (and with it the passwd GECOS LibreOffice prefills from), KDE's emaildefaults and the global git config, where the surname goes in as an initial. busctl is asked for JSON because its plain output escapes non-ASCII as octal. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
7 daysfeat(perf): the performance page moves to the drawer, with ~/.cacheDanilo M.6-62/+161
The island's performance icon and panel are gone: the drawer page covers them. With only one host left, the panel drops its three-column grid for one card per row at a larger size, since the page scrolls; temperatures get a card of their own as tiles. The files move from island/perf to modules/perf. A cache card ports conky's: ~/.cache and its five largest children, measured with du each time the page opens (28ms over 2 GiB here) rather than read from conky's 15-minute sampler file. The largest is yellow, red past half the total, as conky had it. The warning dot now shows only on the drawer tile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysdocs(spec): user details in the drawerDanilo M.4-0/+979
A drawer row greets the login user and opens a page that edits name, email, avatar, language and location, written where other programs already read them: AccountsService (and through it the passwd GECOS LibreOffice reads), KDE's emaildefaults and the global git config, with the surname shortened to an initial for git. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfeat(sound): per-app volume on the drawer page, output to 150%Danilo M.3-84/+157
The island spec put per-app volume on the drawer page, and it was never built. Each application playing audio now gets a row with its name, what it is playing (two browser tabs are otherwise identical rows), a click-to-mute level and its own slider. Streams are matched on PwNodeType.AudioOutStream. Quickshell reports isSink true for a playback stream, so the obvious isStream && !isSink filter found nothing while Firefox played. Their properties are empty until bound, so the page carries a PwObjectTracker for them, which stops with the drawer. speech-dispatcher-dummy is always present and never audible, and is hidden. The output slider runs to 150%, the ceiling the volume keybind already uses (wpctl -l 1.5), with a tick at 100%. Input and apps stop at 100%: a boosted microphone only clips. The meter became an inline component shared by both, and wheel-up never lowers a level already past its maximum. Now playing is dropped from the page: the OSD already carries it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfeat(updates): a page that reads at a glanceDanilo M.1-30/+106
The first cut was small type in one long list: 471 Slackware upgrades pushed everything below them out of sight, and the repo tag, the versions and the name ran together across the row. Type is 2px larger and rows taller. A section over ten rows starts folded behind a clickable heading with its count. The version being moved to is bright, the one being left dim, both in Inconsolata so digits line up. Every other row is shaded to carry the eye across, and the repo is a pill in one colour per repo, never red, green, yellow or the accent, which mean something elsewhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysdocs(updates): the module's READMEDanilo M.1-0/+32
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfeat(updates): the drawer tile and pageDanilo M.2-0/+228
The tile counts what can be installed now (Slackware and the binary repo) and what only needs a build, and goes accent when there is something to install. It runs updatesctl while the drawer is open and not otherwise; a result over 30 minutes old is followed by a refresh that runs nvchecker, never after a refresh, so a failing nvchecker cannot loop. The page has one section per next action: Slackware upgrades, new and removed first, then upgrade here, awaiting build, upstream newer, app updates, the order the work is done in. Each row names the package, the repo it comes from and the one version jump its action covers. A source that could not be read says so under the section it empties instead of reading as nothing pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfix(updates): review findings in updatesctlDanilo M.2-12/+27
A git hash starting with a letter (3.1-a59215d) ordered behind its base, because any trailing alphabetic chunk counted as a pre-release; only alpha, beta, rc, pre and dev do now. Trailing zeros (1.14 against 1.14.0) compare equal instead of reading as an upstream bump. A bad line in the slackpkg blacklist is skipped rather than crashing the script and leaving the drawer with nothing. An AppImage keeps its key over a flatpak of the same name, as the spec orders them, and flatpak list has a timeout. A Slackware upgrade compares version and build, so an arch change alone is not a row. The spec's prose now matches the code: the upstream comparison includes the installed version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfix(updates): drop a redundant clause, test the mirror blacklistDanilo M.2-2/+3
The awaiting-build branch also required the build to be older than upstream, which cannot fail there: a build newer than installed was already taken as an upgrade, so the build is at most the installed version, which is behind upstream whenever a row is emitted. Removing it left every test passing, which is how it was found. The blacklisted testing/ entry sat after the main one in the fixture, so setdefault kept the main one and the mirror-side blacklist was never exercised; it now comes first, and dropping the check fails the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>