-
v2.0.0-rc3
Pre-releasereleased this
2026-09-26 12:58:50 +00:00 | 0 commits to master since this releaseAdded in 2.0.0-rc3
- A route to clearing the cache, in the sidebar directly above Sync. The two-step confirmation, the report of what was removed and the re-sync behind it have all been there since the pre-Fluent menu bar; the method survived the move to the Fluent sidebar and its route did not, so nothing in the application could reach it and there was no way to clear the cache short of deleting files under
~/.config/desecqtby hand. Read off the source, the sidebar's entire action vocabulary was Sync, Logout, the connection status and the last sync time. It goes above Sync rather than into Settings because it is not a setting: it changes nothing about how the application behaves afterwards, it discards the local copy and immediately reads it back, and it ends by running the very sync the item above it runs on its own. That keeps the two items that are about the local copy together, and leaves Logout as the last action before the status readouts. Giving the method a route is also what found it broken, in hand testing of this candidate rather than by the suite, and it is written here rather than as a separate fix because the route and the crash are one piece of work: there has never been a reachable Clear Cache for a user to have lost. Clearing the cache rebuilt the records slot as a plain dict where the constructor builds anOrderedDict, which the records cache needs because it evicts the least recently used domain by ordering, so the first zone opened after a clear died in its callback withAttributeError: 'dict' object has no attribute 'move_to_end', and so did every zone opened after it: nothing was cached again for the rest of the session, until the application was restarted. The structure was written out by hand in two places and the two drifted, and value equality cannot see that drift, because an emptyOrderedDictcompares equal to an empty dict. It survived the Fluent migration unnoticed because the method was unreachable from the moment the menu item went away, so the first person to click the new sidebar item would have been running code that had not executed since. The test that existed asserted that the route reaches the method, against a stand-in whose clear is a counter, so it was green throughout and would have stayed green: a test that proves a button is wired to a name proves nothing about what that name does. There is one builder now and both callers take the structure from it, held by three kinds of test: a behavioural one that clears and then caches a zone's records and reads them back, which is the re-sync the clear itself starts; a structural one comparing the cleared structure to the constructor's, key for key and type for type, so a seventh slot nobody has added yet is covered on the day it is added; and a source-level one that fails if the structure is written as a literal in more than one place, which turns the next drift into a failing test rather than a shipped crash. Six of the ten go red against the code as it stood. Clearing one domain never shared the fault, since it deletes in place rather than rebuilding and a delete works on both types, which is why it went on working while the rest of the cache was broken
Fixed in 2.0.0-rc3
- A bulk delete destroyed records the highlight said were not selected. This is one defect with one cause, and it reached ten modules. A qfluentwidgets table or list does not paint its selection from Qt's selection model: its delegate keeps a set of row numbers of its own, and the highlight and the accent bar are painted from that set alone. The table base class refreshes that set from six places and the list base class from four, and every one of them is a user action, a mouse release, a key press, Select All, selecting a row, clearing the selection, setting the current index. Nothing refreshes it when the rows themselves move. So the two halves of a selection could disagree, and the half every test read was the half that behaves. Reported from a live session: delete records 6 and 7 of nine in bulk, and records 8 and 9 slide up into those row numbers while the highlight stays where it was. Sorting is worse than rebuilding, because a rebuild at least empties the selection model and leaves the delete a no-op, while a column-header sort moves the selection model's rows and leaves the delegate's integers behind: measured on a real window against a ten-row records table, two picked rows read as model
['3', '5']and painted['3', '5'], and one header click made that model['5', '3']and painted['2', '0']. The delete then ran, on records the highlight said were not selected. Three separate investigations had closed this as not reproducible, and all three had read Qt's selection model. The fix is not per call site, because the call sites are what kept being missed: one module now carries two entry points, one said once when a view is built, which wires all four routes by which rows can move so a highlight can never outlive the rows it names, and one said around a rebuild, which holds the selection by identity rather than by row number so it survives a refresh that changed nothing and does not come back for a row that has gone. A record is held by its name and type and only within one zone, so nothing that slid into a deleted record's row number is ever selected again - A token delete after sorting the table destroyed tokens other than the ones highlighted. Both tables on the Tokens page are sortable, and a header click is the same route as above. Driven against a real window with four tokens named after the numbers from the report: rows
['232', '234', '46', '49']with 232 and 234 picked, one header click, rows now['49', '46', '234', '232'], selection model holding['232', '234']and the highlight painting['46', '49']. Delete would have destroyed 234 and 232 while the highlight named two others. On this page that is an API credential that cannot be recovered. The tokens table also loads twice for one visit, cache first and then again when the background refresh lands, and that second invisible rebuild took whatever the user had picked while the highlight stayed up, so Delete came back reading "Delete (2)" for two rows it would not touch. The selection is held by token id now, not by the row, which the sort moves, and not by the name, which the user can edit and which the table re-sorts itself by whenever one does. Policies are held by policy id - Select All on the Export page selected all 140 zones and highlighted none, and pressing Export exported nothing at all. A list view paints nothing for a selection written straight into the model, and typing in the filter box emptied that selection outright, because refilling the list is a model reset and Qt announces the change for neither the reset nor the restore. So the button went on reading "Export (140)" over a selection of none. The same reset defect sat on the DNS page, where a sync that dropped a single zone left the highlight on two rows that by then held other zones with Delete Zone greyed out beneath them, and on the DNSSEC page, where "Verify (2)" became "Verify all" under two rows that still looked picked. All three hold their selection by zone name now, which is unique within an account and the one thing about these rows that outlives a model reset; a zone that has left the account, or that the filter hides, has no row to hold a selection and is not put back
- Search and Replace, the Queue page and version history all acted on rows the user had not chosen. Search and Replace rewrites and deletes records across every zone in the account and rebuilds its table from empty on every search, so a second search left the highlight standing over rows nobody had picked; it is held by zone, subname and type now, which is the unit Replace and Delete act on. The Queue page is the shape that only an identity can catch: neither of its tables empties itself, both write over the rows they already have, and when the row count comes out the same Qt moves nothing, so both halves of the selection keep their row numbers, agree with each other, and name items the user never picked. Measured: two failed items picked for a retry, one new item arrives, and Retry Failed came back holding the newly created item. It is held by queue item id. Version history refreshes its zone list on every showing of the page, so merely leaving it and coming back took the selection while the highlight stayed and Delete Selected went grey underneath it; it is held by zone name
- The wizards painted rows that had gone. Picking twelve domains in the Add Zone wizard and then typing one letter into the filter left the wizard with none selected while all twelve were still highlighted. In the Custom Record Set builder, Remove Selected takes rows out from under the selection and the rows below move up into their numbers, so two rows the user had not picked were drawn as picked while the button that acts on them sat greyed out; there is no identity to hold a selection by there and none is wanted, so the paint simply follows the rows. In the Create Subdomain wizard a row picked before Re-check stayed painted through the whole round trip over a table the selection model had already emptied, and when the run ended early it stayed painted for good. Its selection aims nothing destructive, so this one is misleading rather than dangerous, and a completed re-check still lands on the first row
- Import and export went around the API queue and its rate limiter entirely. The manager took no queue and had nine call sites straight against the API client, so a 12-record import reached the client 14 times with none of them queued and nothing at all showing on the Queue page. What that cost was the 429 handling: a queued item refused with a retry-after over a minute fails, pauses the queue and takes the application offline for the cooldown, and off the queue none of that happens. Driven against the unfixed code, a 200-record import refused on record 4 spent 198 more requests at a limit that could not clear for an hour, and then reported "Import completed: 2 records created, 198 failed" as a success. It stops now, counts what it wrote, what was refused and what was never attempted, and reports a failure. Each request carries a readable action and one of the Queue page's own categories, so it can be found and filtered there. Honest qualifier: the per-second pacing was not broken before this, because both threads already shared one lock inside the client. What was missing is 429 handling, Queue page visibility, priority and cancellation
- A finished import left no restore point, and going back to the zone served the pre-import records again. The completion signal carried nothing and was wired to a plain zone-list reload, so the zone an import had just rewritten kept the cache entry it held before the import, and because that entry was fresh rather than stale, leaving the page and coming back served the old records. No version snapshot was taken either, which made an import the only mutation in this application with nothing to roll back to. Driven against the unfixed code, eight records imported into a zone holding one: nine record sets at deSEC, one in the cache, no cache invalidation and zero version history entries. The signal now names the zones the import wrote to and what it did to them, and the window takes the two steps every other mutation takes, per zone and in this order: record why the zone changed, then invalidate it and read it back, so the commit is taken from the authoritative state rather than from the list this process happens to be holding, which is the state from before the import. A run the queue stopped partway still names its zones, because a half-imported zone is exactly the state somebody wants back; a run that never reached the writing stage names none
- A cancelled import claimed work it had not done. Every progress message was recorded as the last thing written, on a comment claiming the manager always reports after the change it describes. It reports before at five sites, all of them announcements of a phase about to start, so a cancel landing on the first of them was logged as "Cancelled after: Creating zone example.com..." for a run that had sent no request at all. The module log is the only record a cancelled run leaves and it was wrong in the worse direction. Progress calls say which kind they are now, only a report made after the change it names is remembered, and the cancellation message names the last change actually made or says plainly that nothing was written. A cancel is still seen at the first checkpoint after it is asked for, and still never lands mid-write
- The Import page read as idle while it was still writing to deSEC. The confirmation drawer is a child overlay rather than a modal dialog and was measured not to cover Preview Import, which stayed enabled underneath it. Pressing Preview while the confirmation stood open and then confirming left the page's one worker slot pointing at the import thread with the preview thread running unreferenced, and the preview's completion marked the page idle while the import was still writing records. A third operation could then be started over the second, and leaving the page could only retire whatever the slot still pointed at, so the orphan outlived the page. The page is locked from the moment the drawer opens rather than when writing starts, with the progress bar left hidden for that window because nothing has been requested yet; cancelling the drawer by button or by Escape hands the page back. Both buttons follow one rule now, a file chosen and nothing running, so editing the path mid-import no longer switches them back on, and the worker slot retires whatever it already held whenever it is assigned
- A replace wrote whatever the form held at the moment of confirming, not the values that were confirmed. The confirmation captured the checked rows and nothing else, and read Find, Replace, Subname and TTL off the live form when the callback fired, which is a different moment: the drawer is an overlay, not a modal, and none of the four fields is disabled while it stands open. Measured on a 1400-wide window, the drawer occupies x 389 to 1029 while the Replace field sits at x 1040 to 1403, so the form the confirmation is about is not even behind it. Driven: confirm replacing
v=spf1withread-and-confirmed, then typenever-confirmedinto the form, and"never-confirmed mx -all"is what goes to deSEC, past a two-step confirmation whose whole purpose is that the user has seen what they agreed to. The drawer never showed the values either, so "this cannot be undone" was a question the user had no way to answer. The four values are captured at click time now and the drawer lists the changes it is confirming, one line each. The change log had the same cause and would have agreed with the wrong write rather than catching it; it is built from the values the call actually sent. Honest scope: this needs the user to touch the form while the drawer is open, so the exposure is a user action and not a race - Two searches could run at once and the older one painted the table. The Search button greys itself out, but the four filter fields keep their Return key wired straight back to the search and are never disabled, so refining a filter and pressing Return, which is the natural thing to do once the button has gone grey, started a second search over the first. Nothing cancelled the older worker, nothing could reach it again, and the result handler could not tell whose answer it held, so whichever search finished last painted the table, which is usually the older one because it started on a colder cache. Measured: the second search answered with one MX record, the first answered after it with two A records, the Type field said MX and the table showed A. Apply Changes was enabled over that, the drawer offered to apply changes to 2 checked records, and a TTL of 60 went to two A records in two different zones; a subname replace would have renamed them instead, which is a create plus a delete. Leaving the page mid-search cancelled only one of the two and wrote two rows into a page nobody was looking at. There is one live search at a time now and one way it stops, and every result and progress report carries the generation of the search that asked for it. Separately, the search worker fetched the zone list itself when the zone cache was empty, off the GUI thread, around the queue and with no view of its pacing, and the page skipped its queued pre-fetch precisely when the queue was paused for a rate-limit cooldown, so the request went out at the one moment it must not: measured with the queue running and with it paused, 1 request, 0 of them through the queue, 0 queue history entries. The worker holds no API client at all now and reads only the cache; with nothing cached and the queue paused the search is refused and says why, and with the zones already cached it still runs, because reading the cache asks nothing of the API
- Pressing Search on a profile that had never synced raised an error instead of searching. The zone cache answers with a pair of empty values when no cache file exists, and that pair is truthy, so it satisfied both halves of the guard in front of it and then failed on the GUI thread the first time Search was pressed. Every read of the zone cache on that page goes through one accessor now
- A confirmation drawer's Delete button rendered as "De". The drawer applied its 340 pixel minimum width outside the clamp to the room its parent actually has, so a parent narrower than that floor, which the DNS page's zone list pane is at its own 220 pixel splitter minimum, still got a 340 pixel drawer centred on a negative x, and Qt clipped both ends of it with the destructive button's own label among them. The clamp to the parent is the outermost operation now, so the parent is always a hard ceiling. Four kinds of Settings card had the same shape: each gave its control a minimum width with nothing capping it, so a card narrower than the icon plus the title and description plus that minimum ran the control past the card's own right edge, taking a dropdown's arrow with it and squeezing the title on the way, since the title was the only other shrinkable thing in the row. The title and description column gets its room reserved first now and the control takes what is left, down to a 40 pixel floor, recomputed on every resize. The test that was meant to cover this only ever read the description label, which was never the widget actually breaking
- A wizard run reloaded the records of a zone it had not touched. Both of the wizard's completion signals carried nothing in all three modes, and a signal that names nothing leaves its listener one safe answer, which was to refresh whatever was on screen. Measured with the on-screen zone's record cache older than the sync interval, which is the ordinary case for somebody who has just been through a four-step wizard, reads of the zone on screen that no run below writes to: Custom with one zone written 1 before and 0 after, Custom with three zones written 1 and 0, Create Subdomain 3 and 0, Reverse DNS 2 and 0. One wasted read per run rather than per zone, always landing on the zone the user was reading. Both signals name their zones now, so the records pane refreshes only when the zone being read is one of them, and the zone list is no longer repainted from a cache that is known to be wrong at that exact moment, since the signal fires because zones were created and none of them is in it
- Opening a profile froze the window for a round trip before it had drawn anything. The API client ran a connectivity check from its own constructor, on the GUI thread, at every profile open, and saving a token ran a second one the same way. Measured against a 10 ms heartbeat with a 2 second stand-in round trip, the loop made 0 of the 200 ticks it owed, and that is the floor rather than the worst case: the rate limiter sleeps up to 2 seconds at the default 0.5 requests per second before the request is even sent, and the request itself is given a 10 second timeout. Neither call was a legitimate exception, the routine check having been queued at high priority all along. The constructor makes no request at all now, the window makes the same one check per profile open through the queue, and it respects offline mode, which the constructor's did not. The token slot goes the same way, which also means its answer reaches the sidebar instead of being discarded
- Every sync sent the user back to the first zone in the sidebar. The five minute background timer, the sidebar's own Sync item and the sync a wizard asks for on finishing all did it. Measured on the shipped tree with the third of four zones selected, the selection and the records pane both came back on the first zone, and the run spent a record fetch nobody had asked for. The guard reading "only auto-select the first zone if nothing is currently selected" has been in place for a long time and could never work, because the line that hands the fetched zones to the model empties the selection three lines above it, and the answer to "is anything selected" after a model reset is always no. Both zone-loading paths read where the user is before the rebuild and put them back after it now, cursor as well as selection, and silently, so restoring the cursor does not itself trigger the records read this change exists to remove. Two cases are asserted rather than assumed: a sync that drops the selected zone from the account lands on the first zone, because the pane is showing something that no longer exists, and a first launch with nothing selected and nothing cached lands there too
- Scrolling the zone list stuttered while a delegation and DNSSEC sweep was running, and went smooth again the moment the sweep finished. The cause is the spinner beside the zone being checked, not the worker doing the checking. The spinner ticks every 90 ms for the length of a sweep and every tick was announced to the view as a change of data, and a list view answers any change of data by laying the whole list out again and repainting the whole viewport: Qt's
QListModeViewBase::dataChangedcallsdoDelayedItemsLayout, and neither that nor the repaint under it is narrowed by naming one row or two roles. So one spinning row cost a full relayout and a full-viewport repaint eleven times a second, whether or not that row was anywhere near the screen, and with one zone in flight out of hundreds it usually is not. Measured on a 400 zone list scrolled for 3 seconds with one zone in flight, against the same list idle: list layouts 0 per second idle, 11.7 before and 0.7 after; painted pixels 11,988 thousand per second idle, 2,141 thousand per second above that before and 268 thousand above it after. The extra paint worked out at 183,000 pixels per repaint for a row of about 14,000, so thirteen times the paint the frame needed, arriving inside a 16.7 ms budget. What it is not was ruled out with numbers rather than by inspection: the worker burns 0.15 ms of CPU per zone and, at the 0.3 second floor between zones, runs under 0.05% duty, where a background thread only begins costing the GUI thread frames at about 25%, and results landing, the sync timer and model rebuilds during a scroll were each driven and measured without degrading the heartbeat. The spinner hands back the rows it is animating now instead of announcing anything, and the widget repaints each of those rows through its own rectangle, skipping any row the viewport does not intersect; a genuine change of state still goes out as a change of data. Skipping an off-screen row is safe because the model reads the spinner and the padlock afresh on every call and caches no pixmap, so a row skipped while off screen is already correct the first time it is drawn. Only a real profile with the checks switched on reproduces this, and that setting is off by default, which is why it went unnoticed; a demo profile cannot reproduce it at all, because its sealed worker answers a whole batch on the GUI thread in one turn, so the timer starts and stops without ever animating. The three new tests are relations rather than constants, layouts against ticks, repainted area against the viewport, and nothing at all for a row off screen; reverting the one connection point turns four of them red - Flipping Debug Mode did nothing at all until Save Settings was pressed. It was reported as toggling debug mode leaving no log entry, and that was the smaller half of it: the switch wrote no setting and changed no log level either, so debug mode was never actually being enabled, while the card directly below it, Delegation and DNSSEC Checks, applies the instant it moves. Two switches side by side in one group, behaving differently, with nothing on the page to say which was which. The switch is live now, on the pattern the four DNSSEC settings here already follow, where the page persists the value and says the line and the window carries it out. That is a change in how the page behaves and worth knowing: flipping the switch raises or lowers the log level there and then, and a Save is no longer any part of it, which is the behaviour the rest of the group already had. The one method that sets the level takes the new state as an argument and is still the only caller of
setLevelin the application, with the startup path folded into it, so the level is decided in one line of code rather than being set from the same configuration key in two files. Three tests hold it and the third is the one that matters: a runtime test drives a real settings page against the window's real handler bodies, wired from the connections parsed out of the window's own source rather than written down in the test, so deleting the connection turns the suite red instead of leaving a test that passes over a switch the user cannot see working - Changing a setting left nothing in the Log Console. The Settings page was the one page interface here without the signal that carries a line to the on-screen console, and nothing else feeds that console: a module logger writes to the log file and to the terminal, and the console is fed by that signal alone, so anything this page had to say went somewhere the user was not looking. It carries the signal now and the window listens to it, and each of these says one line when it actually changes: the API URL, the sync interval, the rate limit, queue persistence, history retention and the theme. Each is compared against the stored value first, so pressing Save having touched nothing still says nothing. Connecting the signal also exposed a doubling that had to go with it, because the debug mode and sync interval handlers were both writing to the console from the apply step unconditionally, on every Save and whether or not the value had changed; both of those lines are gone and the page's own gated line replaces them, and a test fails if any of the window's apply handlers writes to the console again. What is deliberately left is the connectivity check a Save triggers, which still logs, so a Save is not literally silent: those lines report a request the window went and made rather than claiming that a setting changed, and not re-checking is wrong when the API URL is the thing that changed
- The documentation called each profile's version history shared. Version history stopped being installation-wide in 2.0.0-rc2 and the old wording survived in the passphrase panel, the roadmap, the README and three of the reference documents, which is the one word a user reads while deciding whether a passphrase is enough. It says per profile now, and names the read-only installation-wide archive beside it; the log genuinely is one shared file for the whole installation, so that half of the sentence is unchanged. A test pins the passphrase panel's wording, since the wrong word shipped through a whole release unnoticed. The README was cut back to an introduction in the same pass, from 377 lines to 215, with every section that left landing in the document that owns the subject rather than being dropped: the install reference to
doc/INSTALL.md, the signing key and its fingerprint toSECURITY.mdnext to the hash-pinning it already documents, the privacy statement to a file of its own because somebody deciding whether to hand this thing an API token reads it, and the rate limit, DNSSEC and record-management material to the documents that already covered most of it. What you get now sits directly under the introduction instead of third down the page, and covers the demo account and the wizard's reverse DNS mode, neither of which the README had mentioned at all.SECURITY.mdalso gained the one fact nothing in the repository stated anywhere: every valid deSEC token can read every zone in its account, and RRset policies restrict writes only
Technical Improvements in 2.0.0-rc3
- Eight invariants are now enforced by detectors that run in the test suite. Seven audits swept the codebase for classes of defect and four waves of fixes landed what they found, but fixing the instances is the smaller half: without a check, the class reopens in code written next month, and the audits had left their detectors in
/tmp. They live inscripts/now, are discovered by the test runner and are enforced on every push. Six are hard gates throughout, which fail on any finding outside a reviewed table; a seventh gates the application source and ratchets its tests, pinning a count for a review queue rather than a list; the last is three ratchets and a report, for a rule whose output is mostly correct code and where failing on the count would fail the build for writing documentation. Each carries a negative control in both directions, because a detector that cannot recognise the fixed shape is how one of them arrived reporting 59 source sites for a class with 2 open instances, and a report that is mostly wrong is one nobody reads twice; each also carries a historical control that scans the tree from before the fix and requires the confirmed instances to still be found there, so a detector that has quietly stopped detecting fails rather than passing. A stale entry is a failure too: five suppressions were found still switched on for defects that had already been fixed, two reviewed sites were being reported as new findings because the allowlist was keyed on a line number that had moved, and an entry can now record the exact guard text it rests on and stops applying when that text leaves the module, so a fix that deletes the guard cannot be waved through by the review the guard was the whole reason for.doc/STYLE-GUIDE.mdcarries all eight, each with the shape it is about, the fix shape, and the file that enforces it. Porting them also corrected one of the measurements they make: the rule requiring a number stated in prose to be reproducible had been searching a window that mostly contained the prose itself, so nearly every number was counted as anchored by its own sentence; excluded properly, it is 39 anchored against 228 bare, and 228 is the baseline - The shared code survey now tells two same-named signals apart. Six audits asked which sites connect to a given signal and were handed every signal of that name in the tree:
src/declares twelve signal names on more than one class, and the worst of them collapsed twenty-one connect sites across five worker classes into one answer. The receiver expression is resolved to a class now and walked up its bases to the one that declares the signal, so a panel'scancelledno longer collects the four othercancelledsignals, and every site the resolution cannot reach carries the reason, because a wrong identity is worse than a missing one. Of the 96 connect sites whose name matches a signal declared insrc/, 59 resolve; the rest are receivers static reading cannot type and say so. A companion script runs the CI check job locally in the same container image the runner uses, against a clean export rather than the working tree, because the last release was verified on a machine with fonts and Python 3.14 and went red on the runner on one pinned pixel constant - Twenty-one unreachable methods removed. A consistency register found 25 members with no caller anywhere in the application, found by resolving calls to a class and iterating to a fixpoint, and each was independently reconfirmed before deletion rather than trusted from the list. Ten were leftovers from the menu bar the Fluent sidebar replaced, and one of those is the reason a fixpoint was needed: its only caller was another dead method. Eleven were accessors elsewhere, among them five configuration setters superseded by the generic pair, which wrote to the in-memory configuration without ever saving it, a silent no-persist trap for anyone who reached for one, and a zone loader that built a worker thread with no queue check at all, harmless only because nothing called it. Two exceptions are deliberate and both are written down: the cache clear is no longer dead, having been given a sidebar route in this release, and the keyboard shortcuts dialog stays unrouted by the owner's decision, the shortcuts themselves being live and documented, and now says so in its own docstring so the next sweep does not quietly finish the job. Two links of one chain are left in place with the product question they are waiting on recorded beside them, kept apart from the reviewed allowlist so that landing the decision is a deletion rather than an argument. A test fails if any removed name reappears
- A test harness was leaving its git repositories behind. Each of the 54 tests in the version-snapshot file built a version manager over a fresh temporary directory, closed the manager and left the directory; four and a half thousand of them had accumulated, the oldest months old, and a full run of that file added 53 more. The harness takes its own directory away now, and not through a context manager, because the order matters: the manager runs git on a worker thread, and removing the repository under a live subprocess is a race this project has already hit twice, so the close is awaited and the worker is asked whether it actually stopped before the tree goes. The fix belongs in the harness and not in the manager's own close, since in the application the caller's directory is the user's entire version history
- 1634 tests across 64 files, up from 1394 across 46
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- A route to clearing the cache, in the sidebar directly above Sync. The two-step confirmation, the report of what was removed and the re-sync behind it have all been there since the pre-Fluent menu bar; the method survived the move to the Fluent sidebar and its route did not, so nothing in the application could reach it and there was no way to clear the cache short of deleting files under