-
v2.0.0-rc2
Pre-releasereleased this
2026-09-22 17:38:43 +00:00 | 0 commits to master since this releaseAdded in 2.0.0-rc2
- Every profile now has its own version history. Zone snapshots lived in one installation-wide git repository keyed by domain name, so two profiles holding the same domain shared a single history: restoring the oldest version of
example.comin one account could write another account's records over it. Each profile keeps its snapshots in its own directory now, and the version manager refuses to be constructed without being told which one, so a writer over the shared repository cannot be built by leaving an argument out. The old repository is neither deleted nor merged, because folding two accounts' histories of one name together would invent a history neither of them had. It opens read only instead, through a class that has no method capable of running a writing git command, and the History page lists what it holds under Earlier shared history, hidden when there is nothing in it, with Restore disabled and the reason beside it - An account summary on the Settings page: the profile, the account email, how many domains it holds, how many of those are verified, and how many record sets are cached. The verified count is green only when every domain is verified and red when one is not, because deSEC stops raising the domain limit automatically while any domain is unverified, which the card's tooltip explains. The email and the domain limit arrive in a response the application already asked for on every sync and then discarded, so neither costs a request. Reading them needs a token carrying the manage tokens permission; without it the request is refused, which the card used to report as the account not having synced yet. That is a different thing and it was not true. It names the missing permission now, and stops asking: a refusal that reflects a permission the token does not have cannot answer differently a minute later, and it was being retried every sixty seconds forever. A server error or a network failure is still retried, and changing the token clears the latch
- Qt's own messages now reach the Log Console. The Wayland and X11 platform plugins, QSslSocket and QProcess log through a channel entirely separate from Python's logging, and previously reached only the terminal that launched the process, which is nowhere at all for anyone starting this from a desktop menu. A handler is installed as the first thing done after the application object exists, buffering whatever is logged before the Log Console is built and flushing it once it is. Qt calls that handler on whichever thread produced the message and this application runs several, so it only ever emits a signal and the console is written on the GUI thread. One category is dropped outright rather than rate limited: typing a 43-character sentence into a plain text field on a GNOME Wayland session produced 589
qt.qpa.wayland.textinputmessages in 0.67 seconds, and nothing else came close - A demo profile says what it is in the Log Console, as the first line there, ahead of the opening sync. It says the two things the banner across the top of the window does not: the data is generated, and the live delegation and DNSSEC checks do not run here, with every padlock derived from the stored records instead. Someone looking at a silent padlock column had nothing to explain it. "Retrieved N zones from API" was untrue on a demo profile as well, printed directly under a banner saying nothing here reaches the network; it names the demo store now, and is untouched on a real profile
Fixed in 2.0.0-rc2
- Help tooltips turned up anywhere on the screen under Wayland, nowhere near the question mark they belong to, with "This plugin does not support setting window opacity" repeating on the console. The tooltip was a top-level window, and on Wayland a top-level window is an
xdg_toplevelsurface, which the xdg-shell protocol gives a client no request to position: only a popup can be placed, through a positioner. So the move was a number Qt kept client-side and the compositor never read, and the box landed wherever the compositor's own policy put it. Reading the position back does not show this, on either platform, which is why the arithmetic always looked right. The tooltip is an ordinary child widget of the window now, positioned in that window's coordinates and clamped to the window's own edges rather than the screen's, which is Qt's own arithmetic with no compositor in it and behaves identically on Wayland, on X11 and on the offscreen platform the tests run under. Nothing else about the box changed: the theming, the gap from the icon, the drop shadow and the width cap are all as they were. Verified on a real session: zero opacity warnings where there had been eleven, and no second toplevel surface under Wayland protocol logging - A one-word help tooltip was drawn as a wide slab sitting to the left of its icon. Every tooltip label was forced to the same 320 pixel width and the box is centred on the button it belongs to, so a short tooltip's box began well left of a small icon. The label is sized from its own text now, capped at the same 320 so a long tooltip still wraps exactly as before
- A help tooltip's background was black on some pages and right on others. Qt resolves a native tooltip's style against the widget it is shown for, so any ancestor carrying its own stylesheet defeats an application-wide rule, and one of them does; which tooltips looked correct therefore depended on where in the window they happened to sit. qfluentwidgets ships a themed tooltip and the filter that installs it, and this application used neither. Every help icon uses them now, so the styling no longer depends on the widget tree above it
- An apostrophe in a help tooltip was painted as
'. The text is escaped for HTML and the escape was quoting apostrophes too, which only matters inside an attribute and this text never is one. A single-line tooltip with no other markup is classified as plain text, so the entity was rendered literally; a second line hid the defect by making the same string read as rich text. Ampersands and angle brackets are still escaped, since those can genuinely start a tag or an entity anywhere - Every version history entry described a change it did not contain. The snapshot was taken from the record list the widget was already holding, which is the state from before the write: after a delete it still held the record being deleted, after a create it did not yet hold the one just created. So every commit carried the state from one mutation earlier under a subject naming a change that was not in it. Where that stale list happened to equal what was already committed, git found nothing to commit and the entry was skipped in silence, which is the "only the first record I created was ever recorded" of the report. Restore worked throughout, so the safeguard looked present and was not. The commit is taken from what the API hands back on the refresh that already follows every mutation, so it costs no extra request, and the reason it is filed under is keyed to the zone it names, so a fetch for a zone the user has clicked away to cannot commit the wrong records under this one's message
- A zone emptied of all its records could not be recorded at all. Two guards tested the record list for truth rather than for existence, and an empty list is false, so deleting the last record in a zone wrote nothing. The state a user is most likely to want back was the one state the history could not hold
- A bulk delete left no version history entry whatsoever, neither a record of the state before it nor anything to restore back to. It files one entry for the batch now, naming what was actually removed rather than what was attempted: up to three records by name, "Delete A record for 'mail', MX record for '@'", and past that a count with a sample, "Delete 5 records: www A, mail MX, git SSHFP and 2 more", because the History page shows a subject on its own and one running to a dozen names is one nobody reads. A batch the API refused in part records the accepted count with the refusals named beside it; a batch refused in full records nothing, since nothing changed. One record deleted through Delete Selected and the same record deleted from its own row used to leave two differently worded entries, and the one that said less came from the button most people use
- A record opened for editing while its zone was still loading was saved into the wrong zone. Selecting a zone that was not cached queued the fetch and left the previous zone's rows on screen under the new zone's name, so opening one of them edited it against the zone now selected, and saving created the old zone's record in the new one. The window was however long the queue took, seconds normally and minutes behind a rate limit, and it was reachable on any uncached zone, which includes a fresh profile and anything after clearing the cache. A second fault made it permanent: a failed result carrying no message matched neither branch of the handler and did nothing at all, so the wrong rows stayed. Rows are dropped the moment the selection changes, an answer for a zone already left is discarded, and every row carries the zone it was built for, checked at the one place editing and deleting read a row
- A bulk delete confirmed after switching zones deleted from the wrong zone. The confirmation drawer is a child overlay rather than a modal dialog, so the sidebar stays live while it stands open, and the callback read whichever zone was selected at the moment of confirming rather than the one the rows had been collected from. Records were then deleted from the new zone wherever a name and a type coincided. The per-row check could not catch it: it had already passed, against the right zone, before the drawer opened. The zone is captured with the rows now
- Everything a queued mutation did afterwards followed the user rather than the write. A save, an edit, a single delete, a restore and a bulk delete all answer a round trip after the click, and clicking away while the queue sits behind a rate limit is ordinary rather than contrived. The zone that had actually changed kept a cache from before the change, so the next visit was a cache hit, not stale by the clock, showing the record as it used to be, which reads as the save having been lost. The zone the user had moved to lost its cache and was read again in full, a request spent on a zone nothing had happened to, its table blanked to "Loading..." on the way. And the history entry, correctly keyed to the written zone, had no read of that zone to be taken from, so it waited and was absorbed by the next change made there: one entry covering two actions taken far apart, under the timestamp of the later one. Every mutation names the zone it wrote to now, and that zone is the one invalidated and the one read back, off screen when it is not in view. A zone delete blanked the records pane unconditionally for the same reason, telling a user their view had been cleared as part of removing a zone it had nothing to do with
- Two bulk deletes running at once shared one set of counters. A batch answers one record at a time, so it stays alive as long as its slowest request, and a second batch started before the first finished reset the zone and the three counters underneath it. The first batch's answers were counted into the second's tally, its completion fired on one of the second batch's answers rather than on its own last one, and fired again on every answer after that. The zone that was emptied got no history entry at all and the zone that was not touched got several, saying three, four and five records deleted for a batch of three. Each batch owns its own state now, and a second is refused while one is running, said out loud in a notification and in the Log Console with the busy zone named and how far along it is, because a keystroke that silently does nothing is what makes somebody select the rows and press Delete again
- A multi-row Delete went past the API queue and its rate limiter entirely. The toolbar button and the Delete key ran two copies of one flow and the key's copy had drifted, reaching a worker thread directly instead of the central queue that orders and paces every other write in this application. The button's guard was not a guard either: it greyed the bulk buttons out, and the handler that runs on every change of selection re-enabled them from the selection alone, so they came back by themselves the moment the table was touched. Both ways in run one implementation behind one guard now
- Saving a token could write its name and permissions onto another token's row, after which deleting that row deleted the wrong token. The save goes through the API queue, which handles one item at a time and holds everything behind a rate-limit pause, so the answer arrives seconds to minutes after the click and the user is free to select something else in between; the callback read the selected row at that moment instead of the token it had been given. Delete builds its list out of exactly what was written there, so the next Delete destroyed the token that had just been renamed and left the selected one alone. An API token cannot be recovered once deleted. The answer is matched to the row carrying the id it was saved for, and does nothing at all when no row carries it. Reloading the list instead would have been simpler and costs a request through the same limiter to fetch what the answer already contains, and clears the table, dropping the user's place mid-edit. A token's policy panel had the same shape and now carries the token it was opened for
- A refused policy list was displayed as a token having no policies. In deSEC a token with no policy is unrestricted, so a tightly scoped token whose policies could not be read showed as having no restrictions at all, which is the opposite of the truth for anyone deciding whether it is safe to use. A refusal says so now, and is structurally a different thing from an empty list rather than the same thing differently worded. Creating a token also creates its default policy, and that second request was the only one in the application built with no callback: if it failed, the secret was shown and the token reported as created while it in fact had no policy, and therefore unrestricted write access to every zone. That failure is reported now, stays on screen until it is dismissed rather than fading, and says what state the token is in and what to do about it
- Leaving Search and Replace mid-search froze the window for two seconds and then carried on searching. The page asked a thread with no event loop to quit, which does nothing, then waited two seconds for it: measured at a full 2.00 seconds of frozen interface, after which the search continued anyway. Its callbacks were never invalidated either, so one landing afterwards started a fresh search on a page nobody was looking at, and the progress bar was still running on return. Import and export used a terminate instead, which stops a thread wherever it happens to be, and for an import that is potentially between two writes, leaving the account half changed with nothing saying where it stopped. All three cancel between items now and report what was actually written. A cancelled merge import was also reported as nothing written, because the cancellation was caught by a handler meant for real errors
- Text on the Queue page was unreadable on the light theme. It used one set of colours for both themes where every other module pairs them: measured against the light background, Completed read at a contrast ratio of 2.10 and Pending at 1.95, where the floor for the weight they are drawn at is 4.5 and 3.0. They reuse the pairs the record editor already defines, each checked against the floor that applies where it is actually drawn. Links were a flat blue at 2.51 on light. They stay blue rather than taking the accent, which is now a strong red: a red link reads as an error beside ordinary text, which is the same reason the version number is not accent coloured. One pair serves the About page, the notification drawer and the confirmation drawer, which had been three copies of the same wrong blue
- Toasts in the profile window cut their own message in half. Deleting a profile raised a success message with its second line clipped off the bottom of a 50 pixel bar. Two wrapping mechanisms were fighting and nothing recomputed the height: the library wraps the text itself and then sizes the bar to it, which is what every other toast in the application uses, and a second pass layered on top of that turned on the label's own word wrap and pinned the bar narrower than the first had assumed, so the label wrapped again at the new width, needed more height for it, and nothing asked for any. Measured in a 720 pixel window, the shipped delete message had 20 pixels of room for text that needs 34, and a four-line message 34 for 102; only a one-line message was ever safe. The bar sizes itself to its text first now, exactly as the rest of the application's toasts are sized, and caps its width only when that does not fit, measuring the chrome off the bar it has just laid out rather than counting up the library's margins. A short message is no longer stretched to the cap, and a display name with no space or hyphen in it is broken across lines rather than pushing the bar out past both edges of the window, which a 60-character name did at 644 pixels inside 600
- The raw body of a failed API response was printed in the log, where a JSON error object reads as line noise to someone looking for what went wrong. The line names the subsystem, what was attempted and the cause now, a 404 names the URL that was not found, and the body moved to a debug line, one setting away
- A zone created or delegated by a wizard waited up to ten minutes to be checked. Two subdomains created with the Create Subdomain wizard, with both checks switched on in Settings, still had no verdict a minute later; verifying by hand worked. The wizard cleared the zone's stored override and said in its own docstring that the zones were due for a recheck, but never asked for one, and nothing reads the store looking for work except the background sweep. The plain Add Zone path has checked its new zone at once all along, so the two ways of creating a zone behaved differently for no reason a user could see. A newly created zone is also registered rather than merely enqueued, because it is not in the cached zone list until the next sync lands and a result for a zone the checker does not know about is discarded, which would have left the blank padlock exactly as reported. The other two wizards had the same gap: Reverse DNS mode reported nothing at all, so a batch of reverse zones carried no padlock until a sync and then a sweep had reached them, and Create Subdomain left out a zone it created but could not delegate, which with no icon reads as a zone nobody can resolve rather than the undelegated one it actually is. Each zone is named once per pass, so a row created and then delegated is one check rather than two
- Nine pixels of misalignment on five pages, wherever a help button faced a bare caption: Tokens, Queue, History, Search and Replace, and Import and Export
Technical Improvements in 2.0.0-rc2
- Every record mutation read its zone back twice. The mutation's own callback invalidates the zone and reads it back, which is where the history entry is taken from, and the signal it then emitted carried no zone, so the window refreshed whatever was on screen. The first read is still queued at that point, so the cache is still empty, the second misses as well and a second request goes out. One wasted request on every save, edit and delete, against a daily rate limit this application does reach, and it grew worse once the read started following the mutation rather than the user, because a save to one zone while another was on screen then re-read the zone on screen. The signal carries its zone now, and carrying it is the promise that the emitter has already invalidated that zone and read it back
- A restore read the account on the GUI thread. Working out which record sets a restore has to drop was done straight against the API client from the thread that was about to repaint, so the window froze for a round trip and the request never passed the rate limiter every other call goes through. It is queued now, with the write built and enqueued from the read's callback, so the read still precedes the write
- The record selection is cleared deliberately when the zone changes, rather than by accident. It was cleared in fact, but only as a side effect of the table being rebuilt, so a property that guards a destructive action rested on how a widget happens to drop its rows. Whoever later made a rebuild preserve the selection, which is a reasonable thing to want for a plain refresh of the zone already in view, would have handed Delete Selected a set of rows nobody chose without ever seeing the connection. Removed alongside it: two guards reading attributes that are assigned nowhere in this application, and so had been false on every run there has ever been, and the two methods under them, which nothing called and nothing was connected to
- The guard that keeps raw response bodies out of the log could not see most of them. It recognised a payload only when it was bound by unpacking a method call and then interpolated into an f-string. Fed nine variants of the same defect it caught one, and the miss that mattered is that every queue callback in this application takes its payload as a parameter, which the guard was structurally unable to see, so the next person to write one would have shipped it green. It catches all nine now, with the callback rule narrowed against the real call sites first, because a guard that reports false positives gets switched off
- Geometry assertions no longer assert which fonts are installed. A build failed on two comparisons a single pixel out, both of heights Qt computes from a font, in a container that has fontconfig and no font packages. The ten text-derived comparisons allow two pixels now, which is useless to a real defect and sufficient for a font: every misalignment these tests were written to catch was an order of magnitude larger, between 11 and 45 pixels. Pixmap arithmetic is left exact, since rendering at a height times a ratio has a right answer no font can change
- 1394 tests across 46 files, up from 976 across 35
Downloads
-
Source code (ZIP)
19 downloads
-
Source code (TAR.GZ)
17 downloads
- Every profile now has its own version history. Zone snapshots lived in one installation-wide git repository keyed by domain name, so two profiles holding the same domain shared a single history: restoring the oldest version of