All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Plugins can contribute to the sidebar and the header bar through helpers that clean up after themselves:
MiAZPlugin.add_sidebar_widget(widget, widget_key=None),add_headerbar_widget(widget, position='left'|'right', widget_key=None)andadd_sidebar_dropdown(dropdown). The last one replaces five setup steps and their five teardown steps: sizing, the shared size group, theplugin-dropdownslist, the widget key, and the icon row every filter dropdown was building by hand. APluginWidgetRegistryrecords one undo step per contribution andunload_pluginruns them, most recent first, skipping any that fails so one detached widget cannot strand the rest.MiAZNotes,MiAZProjectMgt,MiAZPeriodicityandMiAZFullscreenmigrated. These are additions, not restrictions: reachingsidebar-plugin-sectionorheaderbar-left-boxdirectly still works. - New
scripts/devel/check_plugin_ui.py: drives a real disable/enable cycle in the running app and reports what every shared container (workspace stack, sidebar section, both header bar boxes, the dropdown list) held at each step. Covers what unit tests cannot, since it needs a display and a loaded repository. With no arguments it checks the four plugins that contribute UI. - The plugin system now owns the workspace pages plugins contribute.
MiAZPlugin.add_workspace_pagerecords the page against the plugin (PluginPageRegistry), andunload_pluginremoves it, next to where it already removes that plugin's web content and rename-dialog tabs. Covered by 9 new tests intests/test_pluginsystem.py. - New backend module
MiAZ/backend/gate.py(UpdateGate) andMiAZWorkspace.suspend_updates(): hold back workspace refreshes during bulk work and collapse them into one. It is reference counted and returns a handle, so it nests, survives two operations overlapping, and works across a thread boundary (GLib.idle_add(handle.release));release()is idempotent, and a callback that raises cannot leave the gate shut. Covered bytests/test_gate.py(15 tests). - New
MiAZConfigStoreinMiAZ/backend/config.py: one owner for the eight configurations of a repository and for the cache they share, created byrepository.load()and disposed when another repository is loaded.MiAZRepository.get_config_store()exposes it;app.get_config(name)is unchanged, since the store publishes its configs into the same registry. Covered bytests/test_configstore.py(14 tests) plus 6 new repository-switch tests intests/test_repository.py. - New backend module
MiAZ/backend/tasks.py(run_in_background(fn, on_done=None, on_error=None, name=None)): runs work in a daemon thread and delivers the result back throughGLib.idle_add, so callbacks can touch widgets. Without anon_errora failure is logged with its traceback, which is the reason to prefer it over a rawthreading.Thread: an exception in a worker thread dies where nobody sees it unless every caller remembers to wrap its own body, and most did not. Covered bytests/test_tasks.py(12 tests) which pump the real GLib main context rather than mocking the marshalling. - New
tests/test_boundaries.py: the two architectural rulesAGENTS.mdstates are now enforced instead of documented.test_backend_imports_no_gui_toolkitfails if anything underMiAZ/backend/imports Gtk, Adw, Gdk, Pango, GdkPixbuf or WebKit (GObject, GLib and Gio stay allowed, they are the backend/frontend signal contract).test_frontend_does_not_touch_the_filesystem_directlyfails if a frontend module callsos.rename,os.unlink,shutil.copy,shutil.rmtreeand friends instead of going through the util service. Two modules are allowlisted with a written reason, and a third test fails when an allowlist entry stops being needed, so an exemption cannot quietly outlive its cause. The detectors are themselves tested, so a passing rule cannot mean a broken detector. - New backend module
MiAZ/backend/query.py(DocumentQuery): the workspace filter as a value instead of the combined state of six widgets. It carries the free-text search, the concept substring, the five field selections, the date mode and its bounds, the review flag and two lifted checks (ignore_date,ignore_active), and answersmatches(item)as a pure function that reads no widget and no app.to_dict()/from_dict()round-trip it through plain JSON types, which is what saved searches will need;from_dictignores unknown keys so a search written by a newer version does not break an older one. Covered bytests/test_query.py(37 tests), including a matrix of 2016 comparisons against a verbatim transcription of the widget code it replaces. MiAZWorkspace.set_query(query)now writes the query into the sidebar controls and reads it back, so the view and the sidebar always agree. It used to set the filter in memory only: the sidebar kept showing something else, and the next change to any control rebuilt the query from those controls, silently discarding the applied filter. It returns the fields it could not represent rather than dropping them quietly.DocumentQuerygaineddate_preset, a stable token naming which sidebar date entry produced the range; the entries are relative to today and their labels are translated, so neither the resolved dates nor the visible text can identify one later. Reading the query back is what resolves the token, so a saved search storing "this month" means the month it is opened in, not the month it was saved in.models.Datecarries the token.MiAZWorkspace.register_query_hook(name, callback)/unregister_query_hook(name). The callback receives the query after it is read from the widgets and may adjust it. This is the missing half ofregister_filter_view, which can only AND an extra condition in and so could never relax one.- The Projects tab of the rename dialog has a Manage projects button. Its empty state told the user to create a project "from Projects management" without saying where that is: the only way in was the workspace menu (
Projects > Manage projects,Ctrl+Alt+P), which is not reachable while the rename dialog is open. The button opens the same manager over the rename window, and the tab rebuilds its list when the manager closes, keeping the boxes the user had already ticked and leaving new projects unticked.MiAZProjectMgt.show_settings()returns its dialog so the tab can wait for it to close. - New backend module
MiAZ/backend/index.py(MiAZDocumentIndex, registered as theindexservice): the in-memory view of the repository. It owns the only path from a filename to aMiAZItem(build_item), the description cache, the field index and the pending set, and exposesreload(),apply_change(path, event, other=None),documents(),document(id),pending(),invalid(),field_index(),concepts()andinvalidate_cache(config_name=None, key=None). It emitsindex-loadedafter a full scan andindex-changedwith a list of(action, payload)operations after a single-file change. No GTK, so it is testable headless:tests/test_index.pyadds 50 tests where that code previously had none.
-
A plugin adding a workspace page no longer has to clean up after itself.
MiAZWorkspace.remove_stack_pageremoves the child instead of hiding it, so the name is free again and a re-activated plugin just builds a fresh page.add_stack_pagereplaces any other child holding the same name rather than silently showing it.MiAZNotesdropped both halves of the workaround: the branch that looked for a hidden page by name and adopted it, and the block that hid the page on deactivate.AGENTS.mddocumented that dance as a rule every page-adding plugin had to follow; it now documents the opposite. -
MiAZStatusnow means one thing. It was doing three jobs through one process-wide flag: "a repository is being switched" (MiAZWorkflow), "a workspace scan is in flight" (MiAZWorkspace.updateset it and_apply_parse_resultscleared it), and "a plugin is doing bulk work, hold the refreshes" (four plugins).BUSYnow only means the first; the scan re-entry guard is a private_scan_in_flighton the workspace, and the plugins take a handle fromsuspend_updates().MiAZAddFromDir,MiAZImportFromZip,MiAZOCRandMiAZAutoScanno longer importMiAZStatusat all. -
Three background workers moved onto
run_in_background: the workspace repository scan, the external-libraries venv install and theMiAZInsightspage rebuild. Each dropped its own try/except-and-log wrapper. -
Every remaining background task now runs through
run_in_background. The eight raw threads left in the plugins are gone:MiAZAddFromDir(the directory import),MiAZImportFromZip(the ZIP import),MiAZAutoScan(scanner-source detection and the scan itself),MiAZOCR(the OCR run) andMiAZAIAssistant(the suggest call, the chat text extraction and the chat request). Workers that ended inGLib.idle_add(callback, ...)now return their result and leton_donemarshal it, so_finish_import,_finish,_build_source_menu,_set_documentand_on_answerare ordinary methods again rather than idle callbacks returningFalse. Two threads stay raw on purpose and now say why in a comment:webserver.pyruns a serve loop rather than a task, andutil.check_remote_directory_syncstarts a thread only to join it with a timeout. -
MiAZImportFromZipdisables the file watcher on the main loop before its worker starts, instead of as the worker's first statement. There was a window where the watcher was still live and reacting to the files being copied. -
MiAZConfig'sused-updatedandavailable-updatednow carry the set of keys that changed, instead of nothing.save()diffs what it is about to write against what is on disk, which is the only version that survivesset()mutating the cached dict in place.Nonemeans the previous contents could not be read, so the receiver must assume everything changed. The workspace uses it to drop one description-cache entry (index.invalidate_cache(config, key)) rather than the whole cache: renaming one country used to make every document in the view rebuild all six of its field descriptions. Handlers that only repopulate a dropdown ignore the payload through the newactions.dropdown_repopulateadapter, sodropdown_populatekeeps its signature for its many direct callers. Covered by 9 new tests intests/test_config.pyand 8 in the newtests/test_config_cache_invalidation.py. -
The workspace filter moved out of the widget into
DocumentQuery._refresh_filter_cachenow builds a query (_read_query) instead of copying widget state into eight_cached_*attributes, and_do_filter_view_mainis one line:return self._query.matches(item). The five_do_eval_cond_matches_*methods and theself.datetimesmemo are gone; date parsing is anlru_cached module function inquery.py. -
The core filter no longer knows MiAZProjectMgt exists. It used to look up
plugin-MiAZProjectMgt-dropdownby name and override two of its own conditions when a project was selected. The plugin now registers a query hook that setsignore_dateandignore_activeitself. Theregister_filter_viewhook it already used could not express this, because it ANDs conditions and this one has to relax them. -
The document parse moved out of
MiAZWorkspaceinto the new index. It used to exist twice inside that 1398-line widget, in_parse_files_worker(full scan) and_build_item_for_file(single file), with the same seven-field loop, cache lookup and config validation written out separately in each. The two had already drifted: the full scan batched cache updates and built a field index, the incremental one wrote the cache directly and refused any document whose name was not fully valid. Both now callMiAZDocumentIndex.build_item, andtests/test_index.py::test_incremental_and_full_scan_agreekeeps them honest.workspace.pydrops from 1398 to 1211 lines. -
The workspace no longer writes
util._field_indexfrom a re-scan of its own;_apply_parse_resultshands overindex.field_index(), which the scan has already built. -
The concept vocabulary the rename dialog completes against (
ENV['CACHE']['CONCEPTS']) is now refreshed on single-file changes too, through_publish_concepts. The old incremental path never updated it, so after adding or renaming a document the dialog kept offering concepts from the last full scan. -
MiAZWorkspace.initialize_cachesand_on_application_finishednow read and write the index cache instead of a private copy on the widget.
- The .rpm and the .deb pass policy checks and now carry the same payload. The .deb had been shipping 94
.pycfiles compiled by the Fedora build host's CPython 3.14, which Debian never loads, and was missing thecopyrightandchangelog.Debian.gzfiles Policy requires: the cross-distro build path uses plaindpkg-deb, so debhelper never ran. The .rpm shipped/usr/share/doc/miaz/READMEas a dangling symlink, since%docstored the repository root symlink rather than its target, and gave its own%changeloga version it disagreed with, because the build counter was written intoVersion:instead ofRelease:. RPM artifacts are now namedmiaz-0.1.50-13.fc44.noarch.rpm, with the release version inVersion:and the build counter inRelease:, which is what that field is for. Across both packages, 99 library modules carried a shebang while being mode 0644, two of them naming/usr/bin/python, a path Debian does not have, and/usr/bin/miazwas installed group-writable. Newmiaz.rpmlintrcdocuments the two rpmlint tags that do not apply to a noarch package with no ELF objects. - Every package is built from one export of one commit, so the
.rpmand the.debof a run always declare the same version and carry the same files. They did not before: the.rpmtook its sources fromgit archive HEADbut its spec and version from the working tree, and the.debtook everything from the working tree, so the two could disagree about their own version and about which files they shipped. Newscripts/packaging/lib/source_export.shexports the commit once;create_rpm.shandcreate_deb.shread sources,miaz.spec,debian/*and the version from that export, andbuild_all.shhands both formats the same one. It accepts--pushed(fetch, then the last commit pushed to the tracked branch) and--ref REF(any tag, branch or SHA), defaulting toHEAD. The export prints what it is building and warns when uncommitted files are being left out. Two consequences: the package version now comes from the commit rather than the working tree, so a build-counter bump reaches a package once it is committed; and a build no longer writes anything into the repository, since staging and build directories live inside the export. - New
scripts/checks/verify_packages.shchecks a built.rpmand.debwithout installing either. Structure and payload run natively (metadata, dangling symlinks, compiled bytecode, desktop file, AppStream metainfo, GSettings schema, shebangs on non-executable modules, raster images in scalable icon directories, syntax);lintianruns in a podman or docker container when it is not installed; andapt-get install --simulateagainstdebian:stable-slimandubuntu:24.04proves theDependsnames exist in the target release, which is the one thing static inspection cannot do. It also diffs the two payloads against each other, which is what caught the bytecode only the .deb carried.build_all.shruns it with--no-containerand fails the build on a regression. - The rename dialog's Rename button is insensitive while the fields cannot make a valid filename, instead of looking clickable and doing nothing.
_on_rename_responserefused the rename and focused the offending field, which is invisible when the field is already on screen, so the button read as broken.MiAZRenameDialogemits a newfields-changedsignal from_on_changed_entry, and the dialog enables the response fromis_valid(). Date, Country, Sent by, Concept and Sent to are the blocking fields; Group and Purpose stay advisory (a warning in the live preview). A document whose stored values are not in the repository configuration opens with those dropdowns on "Any", which is now visible in the button rather than only in the row colours. - Dialog bodies no longer open with all their text highlighted. The body label of every dialog is selectable so an error message or a path can be copied, and a selectable
Gtk.Labelselects everything the first time it takes the focus, which is what the dialog does while opening: the rename confirmation came up with "You are about to rename this document. Are you sure?" fully selected. A focus controller drops that first selection, one idle later so it lands after the select-all, and removes itself; selecting by hand is untouched. - A failed background operation no longer freezes the workspace for the rest of the session.
MiAZImportFromZip,MiAZAutoScanandMiAZOCReach take an update-gate handle before starting their worker and release it in the callback that reports the result. An exception outside the worker's own try block killed the thread silently, so the handle was never released, the view stopped refreshing and every later update was swallowed with nothing in the log.MiAZImportFromZiphad one waiting:get_temp_dir()ran after the watcher was disabled but before thetry. Each of them now passes anon_errorthat releases the gate, re-enables the watcher and reports the failure. MiAZAddFromDirreports a directory import that stopped early instead of ending in silence.MiAZAutoScanstill installs its Add-menu entry when scanner detection fails. Detection ran in a thread whose only job was to build that menu, so an exception left the plugin with no entry at all: no way to scan and no hint that anything had gone wrong. A failure now installs the plain entry, which reports the real problem when used.- The AI assistant's "Suggest with AI" button no longer stays greyed out reading "Thinking…" after an unexpected failure, and the chat no longer waits forever for an answer that will not arrive. Both had error handling that covered the provider call but not the code around it.
- The "Since last year" date filter covered the wrong period. It resolved through
util.since_date_this_year, which returns 1 January of the current year, so the label said "last year" while the range was the calendar year to date: seven months on 7 August, and two days on 2 January. It now usessince_date_last_n_months(now, 12), the last twelve months, consistent with its "Since last 3 months" and "Since last 6 months" neighbours. The same window was wrong inMiAZInsights, which mirrors the same preset list, and is fixed there too so the two views agree. - Lint is clean again:
ruff check MiAZ data/resources/plugins testsreported three unused imports.timedeltainbackend/util.pyand a straypytestintests/test_boundaries.pywere removed. The third,make_usagein the AI plugin'ssuggestion.py, is a deliberate re-export thattests/test_ai_plugin.pyasserts on, so it is now declared in__all__instead: applying the fix ruff suggested would have deleted it and broken that test. - The
MiAZFullscreenbutton now comes back after the plugin is disabled and re-enabled.do_deactivatedetached the button from the header bar but left it registered underheaderbar-togglebutton-fullscreen, so the next activation looked the key up, found the detached button, took that as "already set up" and skipped the whole block. The button was gone for the rest of the session. Widget keys are now unregistered along with the widget. - Disabling and re-enabling
MiAZNotesno longer leaves the "All notes" page wired to the previous plugin instance. The page was kept in the stack and re-adopted by the new instance, which reassignedstore,backupand the two compute callbacks but noton_open_document. That callback stayed bound to the deactivated instance, so opening a note from the page called into a plugin that was no longer running, and the dead instance could never be collected. The page is now rebuilt, so every callback belongs to the live plugin. - Workspace pages of an uninstalled plugin no longer stay in the
Adw.ViewStackfor the rest of the session as hidden children. - Two overlapping background operations no longer let the first one to finish re-enable workspace refreshes for both. Every setter of the old
MiAZStatus.BUSYflag reset it unconditionally, so starting an OCR run while an AutoScan import was still going meant the remaining work refreshed the view on every file, which is what the flag existed to prevent. The reference-counted gate stays shut until the last holder releases.MiAZAutoScancould also release twice (itsfinallyblock and_on_scan_errorboth ran on a failed scan); the handle makes the second release a no-op instead of an unbalanced reset. - Switching to a repository no longer reads configuration left behind by a previous one.
MiAZConfig.cachewas a class attribute, shared by every config instance in the process and keyed by absolute filepath. Sharing was deliberate (SentBy, SentTo and Person all readpeople-available.jsonand must agree), but it tied the cache lifetime to the process instead of the repository:repository.load()built eight new config objects per switch while the old entries stayed, so a repository edited outside MiAZ, or simply switched away from and back to, was read from the stale copy. The cache now belongs to theMiAZConfigStoreof one repository and is emptied when that store is disposed. Two of the new tests fail against the previous code. MiAZConfig.set()no longer writes its cache bookkeeping to the wrong key. It calledsave()with no filepath;save_dataresolved the default and wrote the correct file, but the cache invalidation and the signal check both saw the empty string, soself.cache['']was marked dirty, the real entry was left looking clean, andused-updatednever fired. It worked only becauseload()returns the cached dict by reference andset()mutates that same object: any caller saving a freshly built dict would have read stale config afterwards.save()now resolves the default before doing either.MiAZConfigApp.save()now invalidates the cache. It overridessave()to emitrepo-settings-updated-appinstead of the base signals and skipped the cache bookkeeping entirely. This is the config that records the active repository, so it sits on the switch path.- Deleting a document from the rename dialog now goes through
util.filename_deleteinstead of callingos.unlinkdirectly, so thefilename-deletedsignal fires. Without it the workspace and the new document index both kept an entry for a file that was already gone, until the next full re-scan. Found by the new boundary test, which is the only violation of that rule in the tree. - A failed workspace scan no longer leaves the application stuck.
update()sets the status toBUSYand only_apply_parse_resultscleared it, so an exception in the worker thread left the app busy for good: every later update logged "App is busy. Workspace update deferred" and rescheduled itself, forever, with the thread already dead and nothing in the log to say why. The scan now runs throughrun_in_backgroundwith anon_errorthat logs the failure and releases the status. - A deleted file that was not in the view no longer decrements the document counter. The old incremental handler removed unconditionally, so the count could drift below the real number until the next full scan.
- Controlled-vocabulary field descriptions are now localized to the user's language. Country, Group and Purpose descriptions previously always showed in English (they ship as English JSON in
data/resources/conf/and are copied into each repository, bypassing gettext). A newutil.humanize_value(gtype_name, description)translates them at display time: Country through the standard iso-codesiso_3166-1gettext domain (so every language iso-codes ships is covered with no per-language work, e.g. Spain to España), and Group/Purpose through the app catalog. The stored value and the on-disk filename keep the original code, only the shown label changes, and any user-customized or user-typed value (people, concepts) passes through unchanged. The helper is called from every display surface: the configuration views, the sidebar and workspace filter dropdowns (dropdown_populate), the workspace columns, and the rename "Suggest metadata" view. The default Group and Purpose labels are listed in the newMiAZ/backend/vocabulary.pyso they are extracted into the catalog for translation (kept in sync with the JSON bytests/test_vocabulary.py). Theiso_3166-1domain is bound at startup;iso-codeswas added as a dependency (deb and rpm; the GNOME Flatpak runtime already includes it). - Translation infrastructure refreshed and seeded for new languages.
po/POTFILESwas stale (it listed removed plugins and the deleted widgets, and omitted every plugin added since, including both AI plugins, Notes, Newspaper, OCR and others); it now lists all 88 source files that contain translatable strings. The templatepo/miaz.potwas regenerated from it (825 messages) and merged intopo/es_ES.po, which surfaces the real translation debt that the stale template was hiding. French (po/fr.po) and German (po/de.po) were seeded from the template and added topo/LINGUAS; all three locales compile. The actual message translation is left to native reviewers now that the extraction is correct. - AI plugin API keys are now stored in the system keyring instead of plaintext JSON. A new backend service
secrets(MiAZ/backend/secrets.py,MiAZSecretStore) stores secrets through a cascade: libsecret (the Secret Service) first, then the pythonkeyringlibrary, and only if neither is available does the key stay in the plugin's JSON config (with a warning toast). Keys already saved in plaintext are migrated into the secure store on plugin load and stripped from the JSON file. Both AI plugins (MiAZAIChat,MiAZAIAssistant) store per provider under an account likeMiAZAIChat/claude; the API-key field now saves on apply rather than on every keystroke. As defence in depth,MiAZPlugin.set_config_keyno longer logs config values (only the key name). Flatpak manifests gained--talk-name=org.freedesktop.secrets; packaging gainedgir1.2-secret-1(deb) /libsecret(rpm) withpython3-keyringrecommended, and akeyringoptional dependency inpyproject.toml. Covered bytests/test_secrets.py(9 tests, injected fake backends). - Per-type configuration import and export. Each configuration view (Countries, Groups, Purposes, Sender, Recipient, Repositories, Plugins) gained an Export/Import menu button on the available-items toolbar (
MiAZConfigView._add_config_menubutton,frontend/desktop/widgets/configview.py). Export writes the available pool of that type to a JSON file throughGtk.FileDialog; import reads a JSON file and merges it into the pool (existing entries are kept), then refreshes the view through theavailable-updatedsignal. This replaces the previous "not implemented yet" placeholders; the orphaned, never-wiredimport_config/export_configstubs inservices/actions.pywere removed in favour of the config-view implementation, which has direct access to the configuration object. CONTRIBUTING.mdis now tracked in the repository.- Single version sync script
scripts/devel/sync_versions.sh. It reads the base release version frommeson.build(the single source of truth, build metadata stripped) and propagates it to every packaging file: it overwrites the single version fields inpyproject.tomlandmiaz.specidempotently, and prepends a new entry todebian/changelog, the spec%changelog, and the AppStream<releases>list only when the release version actually changed (so plain build-counter bumps touch nothing and prior release history is preserved). The prepended note is a placeholder pointing toCHANGELOG.md.scripts/devel/build.shnow runs it right after bumping the meson build counter, so every build keeps the metadata consistent. - Restored unit tests for the mass-rename concept transforms (
tests/test_massrename.py, 42 tests). Coverage was lost whenMiAZMassRenamemoved from a plugin (with a standaloneconcept_opsmodule) to a core service and the oldtests/test_massrename_concept.pywas deleted. The new tests import the pure module-level functions fromMiAZ.frontend.desktop.services.massrename(parse_positions,keep_tokens,remove_tokens,add_prefix,add_suffix,find_replace,change_case,set_value, and theapply_concept_opdispatcher) and cover the renamed dispatcher plus new branches (empty-concept prefix/suffix,titlecase, blank separator fallback, unknown op). They set thegiversions before import, the same waytests/test_util.pydoes, so they run headless in CI. - Optional external libraries. Plugins that need third-party Python libraries (for example the AI provider SDKs) can now have them installed into a private per-user virtualenv at
~/.MiAZ/opt/venv, never into the system Python. Each plugin declares its dependencies in arequirements.txt. After the first repository, MiAZ offers to download the libraries once; the same action lives in Settings under "External libraries", and enabling a plugin installs its libraries when the feature is on. When a library is missing, the AI error dialog and the chat offer an "Enable external libraries…" action. - Plugin load failures are now visible in the app instead of only the log. When an enabled plugin fails to load, a startup toast reports how many failed and points to Settings > Plugins, where a banner names each failed plugin and its reason. The "Plugins activated: N of M" count now excludes plugins that failed to load. The banner connection follows the same map/unmap pattern as the other configuration views, so reopening Settings does not leak signal handlers. Covered by
tests/test_pluginsystem.py. - Plugins can contribute tabs to the single-document rename dialog. The dialog is now an
Adw.ViewStackwhose first page, Fields, holds the seven filename fields; the current and new filename move out of that list into a footer that stays visible from every tab. A new core servicedocument-tabs(frontend/desktop/services/doctabs.py) holds the registrations, andMiAZPlugingainedregister_document_tab(name, title, factory, icon_name=None, weight=100)andunregister_document_tabs(), alongsideinstall_menu_entryandadd_workspace_page. A registry rather than a signal, because the dialog is built fresh on every rename: plugins no longer have to track handler ids to avoid contributing the same thing twice. Tabs are ordered by(weight, title)with Fields always first; registering a name twice replaces it, andunload_plugindrops a plugin's tabs even if it forgets to. The tab widget answers a duck-typed contract:set_document(doc_id)when the dialog opens,apply(old_id, new_id)after the rename succeeded, optionalis_valid()to veto and optionaldiscard(). Tabs hold their edits: nothing is written while the user works, so Cancel discards tab edits the same way it discards field changes, and by the timeapplyruns thefilename-renamedhandlers have already moved plugin data to the new name. Every call is wrapped in try/except, so a failing tab logs and never blocks a rename. With no tab registered the view switcher is not installed and the dialog looks exactly as before.MiAZWindowDialoggainedset_title_widget()for the switcher, and therename-dialog-builtsignal stays as it is for header-bar additions such as the AI "Suggest" button. Covered bytests/test_doctabs.py(7 tests). - MiAZProjectMgt shows a Projects tab in the rename dialog: one check row per enabled project, ticked for the projects the document belongs to. Applying clears the document from every project and re-adds the ticked ones, falling back to the default bucket when none is ticked, mirroring what the workspace assign and unassign actions do.
- MiAZPeriodicity shows a Periodicity tab in the rename dialog: one dropdown over the enabled vocabulary, showing the document's current value and writing it through the same code path as the workspace action.
- Every plugin now has an icon, and the rename dialog tabs wear it. A plugin ships
icon.svgoricon.pngnext to its module;MiAZIconManager.register_file_icon(name, filepath)exports it into the new~/.MiAZ/opt/icons(ENV['LPATH']['ICONS'], added to the icon theme search path at startup) under the unique namemiaz-plugin-<module>, because widgets take icon names and the theme resolves names rather than paths.MiAZPlugin.get_icon_name()returns that name, or the bundled generic plugin icon when the plugin ships none, and never an empty value.register_document_tabuses it when the caller passes noicon_name, so a plugin gets a recognisable tab without doing anything about it. The Fields tab carries the rename icon so the switcher does not mix bare labels with icons. - New
util.filename_rename_needed(source, target, upper=True): whether a rename would really move the file, comparing the target after the same uppercasingfilename_renameapplies.filename_renamereturnsFalseboth when the target already exists and when there is nothing to do, and callers with work to do alongside the rename have to tell those apart. Both now share one_rename_targethelper so the normalization cannot drift.
- Renamed the
MiAZYearReportplugin toMiAZInsights. The name stopped matching the plugin: a year is one scope out of eleven now, since_periods()builds nine rolling windows (this month through ten years ago) on top of the per-year views and the all-years overview, and "report" suggests a document you print once when the page is interactive (hash routing, period selector, year combo, clickable heatmap and trend, theme toggle, foldable tables). The new name says what the page delivers and what the Workspace columns cannot: busiest month, longest streak, rank movers, where documents come from. Renamed the plugin directory,MiAZInsights.py, theinsights/support package,miazinsights.pluginandtests/test_insights.py;Module,Name,Description,PLUGIN_DIR_NAME, the class and its__gtype_name__followed. The page heading, the browser tab title and the menu entry now read "Insights" and "Open insights"; the deadtitle_yearlabel ("Year report {year}", never rendered since the single-page redesign) was dropped.po/POTFILESpoints at the new paths and the two renamed messages are marked fuzzy inpo/es_ES.pofor review, while the obsolete "Printable yearly summary report" description was removed (849 to 848 messages).PLUGIN_DIR_NAMEis the WWW page key, so the Browser dropdown entry changes and any~/.MiAZ/var/www/html/MiAZYearReportleft from an earlier build is orphaned; it is removed on upgrade here but a manualrm -rfclears it elsewhere. Per-repositoryplugins-used.jsonandplugins-available.jsonare keyed by plugin name, so the plugin comes back disabled and has to be enabled once per repository. - MiAZInsights gained a period selector and a world map. The year pills became two dropdowns side by side: Year (All years plus every year in the repository) and Period, which offers the same windows as the workspace date filter (this month, since past month, since last 3 and 6 months, since last year, since two, three, five and ten years ago) with the same titles and the same lower limits, computed from the same
util.since_date_*helpers. Both select a date range, so choosing in one clears the other, and the selection still lives inlocation.hash. A period view is built like a year view: hero total with the change against the window of equal length right before it, a timeline (monthly up to two years, yearly beyond that, so a ten year window is eleven columns and not a hundred and twenty), ranked senders, concepts and purposes. A new Where it comes from section renders in every view, including All years: a world map with one path per country shaded on the same sequential blue scale as the activity heatmap, with the ranked country list beside it. Countries too small to see at this scale (Luxembourg, Malta, the island states) carry a marker circle that switches on when they hold documents. The outlines ship asstatic/worldmap.svg(120 KB, 174 countries, Natural Earth 1:110m public domain data projected with Robinson), generated by the newscripts/devel/gen_worldmap.pyand inlined into the page at build time, so the report still fetches nothing and still works offline overfile://. The map is markedaria-hiddenbecause the country list next to it carries the same values as text. Covered by 8 more tests intests/test_insights.py(range aggregation, bucket switching, previous window, period payload shape, country counts), 30 in total. - MiAZInsights is now a single page with a selectable year row and an all-years overview. It used to publish one HTML file per year plus an
index.htmlcopy of the most recent one, with a full page reload on every year change and no view of the archive as a whole. It now publishes one self-containedindex.htmlthat carries every aggregated number as an embedded JSON payload and renders views from it: clicking a year in the pill row (or pressing Left/Right/Home/End) swaps the content in place, and the selection is mirrored inlocation.hashso the Browser tab's Back button walks the year history and a refresh restores the view. The new All years view leads with lifetime totals (documents, years covered, span, distinct senders, purposes, concepts, groups, countries), then a per-year trend with year-over-year deltas, a years-by-months activity heatmap, rank movers (all-time leading senders and purposes with how their rank moved in the latest year: new, up, down, gone), and firsts and lasts (first and latest document with their sender, busiest and quietest year, longest run of consecutive active months, senders first seen or gone silent in the latest year). The per-year view keeps its four stat cards and gains a twelve-column month chart, a share-of-year percentage per purpose with the one-off tail folded into "Other", and a "New this year" block for senders and purposes. Charts follow the MiAZ data-viz palette in selected light and dark step sets, switched byprefers-color-schemewith an in-page toggle that overrides it (WebKitGTK does not reliably pass the app theme through); every chart has hover tooltips and a foldable data table, and printing opens those tables, drops the chrome and renders light. The plugin was split intoMiAZInsights.py(lifecycle),insights/aggregate.py(pure arithmetic),insights/render.py(payload and page) andstatic/report.css+static/report.js(inlined at build time), covered bytests/test_insights.py(22 tests). Still nostatcall per file: everything comes from the filenames, and document count remains the proxy for "biggest purpose" since the seven-field scheme carries no amount. - MiAZNotes marks documents that have notes with a dedicated leftmost workspace column (a pin icon plus the note count) instead of a CSS row highlight. The column is added to the workspace view at runtime when the plugin activates and removed when it deactivates, so it only exists while MiAZNotes is enabled. This replaces the old
.miaz-has-noteshighlight, which embedded a hardcoded absolute path to the pin SVG and so broke on any installed system; the new cell uses a theme icon name. - Moved
MiAZImportDoc("Add new document(s)") from a togglable plugin to a core service (services/importdoc.py, serviceimportdoc, following the same pattern asMiAZMassRename). Every repository needs a way to add its first document, so that action should not live behind an optional, disableable plugin; a fresh repository previously had to hardcode it into its defaultplugins-used.jsonto avoid shipping with no way to add documents at all, which is no longer needed now that the feature cannot be disabled. The headerbar "Add" menu (headerbar-add-menu) now always shows the core action first, followed by any loaded Import-subcategory plugin (MiAZAddFromDir,MiAZImportFromZip,MiAZImportFromScan), and the Add button is now always visible instead of only when an Import plugin happened to be enabled. - The workspace no longer opens on an empty view when the default date filter has no documents. Right after a new month starts, "This month" is usually empty; the workspace now selects the nearest date filter that actually contains documents (walking to wider windows: "Since past month", "Since last 3 months", and so on, falling back to "All documents"). It runs only as the initial default, at load, repository switch, and the day/month rollover, and never overrides a date filter you selected by hand. The chooser (
pick_date_presetinwidgets/workspace.py) is covered bytests/test_workspace_datepick.py. - The rename dialog is now keyboard-friendly. It focuses the first field that needs attention when it opens, the date is an editable
YYYYMMDDentry (the calendar remains as an assist),Ctrl+Enterapplies from any field andEsccancels, and in the concept entryEnterpicks the highlighted autocomplete while the popover is open. Applying now refuses to build an invalid filename: when a required field (date, country, sent by, concept, sent to) is missing or invalid the dialog stays open and focuses the field to fix. - Merged the
MiAZAIChatplugin intoMiAZAIAssistant. There is now a single AI plugin that offers both features: suggesting the seven filename fields from a document (theSuggest filename…menu entry and the rename-dialog button) and chatting about the selected document (theChat with document…menu entry). Both share one provider layer and one settings dialog, so API keys and the active provider are configured once. Internally themiazaicpackage was folded intomiazai: every provider (Claude, OpenAI, Gemini, Ollama) now implements bothsuggest()andchat()on a shared base,make_usagehas a single definition inmiazai/usage.py, and the chat dialog moved tomiazai/ui/chat.py. No configuration is migrated, so anyone who had configured the oldMiAZAIChatplugin re-enters that provider's API key once. The aggregateddata/resources/plugins-requirements.jsonno longer listsmiazaichat. Covered bytests/test_ai_plugin.py. - Three workspace-view efficiency fixes from a performance review (
responses/2026-08-04-workspace-view-performance-analysis.md), none of them behavior changes. (1) The workspace Type column no longer stats the file on disk to pick its icon:Gtk.ColumnViewrebinds a cell every time its row scrolls back into view, so the oldMiAZIconManager.get_mimetype_icon(os.path.exists+Gio.File.query_infoper bind) repeated that disk I/O on every scroll. It is replaced byget_mimetype_icon_for_extension, which resolves the icon from the extension alone viaGio.content_type_guessand caches it, no filesystem access at all. (2)_parse_files_workerbuilt the field-value index (used by "how many documents use this Country/Group/…") in a second full pass over the file list that recomputedutil.get_fields()for every file from scratch; it is now folded into the existing parse loop, which already has those fields in hand. (3) The workspace filter's project-dropdown bypass check (used whenMiAZProjectMgthas a project selected) looked up the dropdown widget and its selection on every item on every filter pass; it is now resolved once per pass in_refresh_filter_cache, alongside the other cached filter inputs, and read fromself._cached_project_bypass. - Four more workspace-scan efficiency fixes, benchmarked against a real 1231-document repository. The biggest one: Country/Group/Purpose/SentBy/SentTo display labels are now cached per (field, raw value) in
self.cache, the same way the Date label already was; onlyhumanize_value()(a gettext lookup) was uncached, and re-running it for every field of every document on every scan measured at 122.78ms for this repository, dropping to 0.32ms once cached (values repeat heavily across real documents, e.g. 7 distinct countries across 1231 files). The cache is invalidated by the sameused-updatedsignal that already resets it, so edited vocabulary still shows up correctly. Also:util.get_files()now lists the repository withos.scandir()instead ofglob.glob()+ a separateos.path.isfile()stat per entry (7.66ms to 1.53ms for 1231 files); the parse loop's ownfields = util.get_fields(filename)is reused instead of callingfilename_validate()right after (which re-parsed the same filename from scratch); and the workspace's default Date column sort uses a dedicated comparator with no.upper()call, since aYYYYMMDDvalue has no case to fold, while every other sorted column (group, purpose, sender/recipient labels, concept) keeps its case-insensitive compare since those can hold real mixed-case text. Measured together, this repository's ~224ms workspace-update time is expected to drop to roughly 90-100ms; the remaining time is theMiAZItemGObject construction and GTK's own model/view work, both inherent to theGio.ListStorearchitecture. - The "Review" toggle triggered two full workspace rescans per click instead of one.
show_pending_documentscallssidebar.clear_filters(), which itself callsworkspace.update(), and then separately reselects the Date dropdown to "All documents", whose selection signal independently callsupdate()again. The second reselection now blocks that signal (the samehandler_block/handler_unblockpattern already used in_update_dropdown_dateand_auto_select_date_presetfor the same purpose), so toggling Review still shows every date range, but scans the repository once instead of twice. - MiAZNotes' "All Notes" view now defaults its status filter to "All statuses" instead of "In Progress", so it opens showing every note rather than hiding done/planned ones by default.
- The
"Workspace updated in {dt}s"debug log now includes the document count:"Workspace updated in {dt}s ({n} documents displayed)",nbeing the count after the active filters are applied, not the repository total. The log line moved from_apply_parse_results(end of the background parse, before the store is spliced or filtered) to_idle_view_update(afterview.update()applies the splice, whenself._num_displayed_itemsis the real post-filter count), so the timing now also covers the store splice and filter pass, not just the parse. - Incremental workspace updates for large repositories. A single file change (add, delete, rename) no longer re-scans and re-parses the whole repository; it updates just the affected row. The watcher now forwards the changed path and event through a new
repository-changed(path, other, event)signal (coalescing a burst to one settled event per path, and falling back to a fullrepository-updatedwhen more than 25 files change at once). The workspace applies the change with the columnview'supdate_incrementalprimitive and a_build_item_for_filehelper that mirrors the full parse. The existing full re-scan (repository-updated) is kept as the safety net for the Notes plugin and for anything the incremental path does not confidently handle (pending or invalid names, unknown events); a skip flag suppresses that re-scan only when the per-file update fully applied the change. Because the fallback does a complete splice-replace, an imperfect incremental update can never corrupt the list, only cost a redundant re-scan. Covered bytests/test_watcher.py(7 tests). This turns the common single-document case from O(N) to roughly O(1); bulk changes still take the full path. - GNOME HIG polish (P1 batch). The sidebar's "Search in all fields" and concept search now use
Gtk.SearchEntryinstead of a bareGtk.Entry, so they get the search icon, a clear button and Escape-to-clear (the config selector already usedGtk.SearchEntry). Label typography inservices/factory.pymoved from manual Pango markup to theme-correct CSS classes (heading,caption,dim-label) and, importantly, the dropdown and column-view cell labels now useset_text(item.title)instead ofset_markup, which removes a real markup-injection risk for values containing&,<or>(a company name, a concept). The "No documents found" status page dropped its redundant<big>markup wrapper. The keyboard-shortcuts UI was migrated from the deprecatedGtk.ShortcutsWindow(deprecated since GTK 4.18) toAdw.ShortcutsDialog(libadwaita 1.8+, present at the GNOME 50 floor), which also removed the inline builder XML inservices/actions.pyand the now-unusedservices/help.py. - Accessibility: the icon-only buttons in the configuration selector (
frontend/desktop/widgets/selector.py) now have tooltips: Add, Remove and Edit on the available side, Edit on the used side, and Enable/Disable on the transfer controls. The Enable/Disable tooltips were also hardcoded English strings ('enable'/'disable'); they are now translatable. A broader accessibility audit stays on the 0.2.x backlog. - CI (
.github/workflows/ci.yml): the test job now runs a Python matrix (3.11 and 3.12), lintstests/in addition toMiAZand the plugins, and reports coverage (pytest-cov, informational). 3.9/3.10 compatibility is enforced statically by ruff'starget-version = "py39", since the system PyGObject bindings follow the runner's Python and cannot be matrix-tested against pip. - Clarified the backend architecture rule in
CLAUDE.mdandAGENTS.md: the backend forbids GTK/Adw/Gdk widget imports, butGObject/GLib/Gioare allowed and used on purpose, since the backend exposes its events through GObject signals (the backend/frontend contract). The old "zero GTK imports" wording read as a violation when it was the intended design. - Extended
.gitignoreto cover build artifacts and local working files (AppDir/,builddir*/,*.AppImage,*.deb,*.flatpak,*.tar.gz,*.snap,repo/,logs/,linuxdeploy*) and session files (commit.txt,responses/,.claude/,.opencode/). - The Flatpak manifest
flatpak/io.github.t00m.MiAZ.jsonnow builds themiazmodule from local sources ("type": "dir", "path": "..") instead of cloning a pinnedgithub.com/t00m/MiAZ.gitbranch (0.1.26). The build now uses whatever versionmeson.builddeclares, so there is no version string to keep in sync in the manifest. Both Flatpak manifests (.jsonand.local.json) now build from the local tree.
- Flatpak packaging is deprecated on purpose and no longer built by default. A Flatpak runs sandboxed on
org.gnome.Platform, which cannot see or run the host command line tools that plugins depend on (ocrmypdffor OCR,scanimagefor the scanner), so those features never work in a Flatpak build and users saw "OCR tools not installed" even with the tool present on the host. MiAZ is a native desktop app; use the deb, rpm or AppImage package instead. The Flatpak build code is kept for reference and is gated behindMIAZ_ALLOW_FLATPAK=1inbuild_all.shandcreate_flatpak.sh.
- Dropped the
MiAZNewspaperplugin and everything tied to it. It rendered the documents visible in the Workspace as a "MiAZ Times" broadsheet published to the Browser page, but every sentence on that page was generated from the seven filename fields the Workspace columns already show, so it produced no information the user did not have. Its costs were real: 36 translatable messages (out of 885) in the hardest register to translate, prose hardcoded to the default purpose codes (INV,REQ,STM,INF,CON,TKT) so any renamed purpose fell back to a generic sentence, a fullrmtreeplus a recopy of 244 KB of web fonts on every filter and sort change, and 275 lines oftemplates/*.htmldesign mockups that no code ever read. It also declaredDependencies: HelloWorld, a leftover from templating off the demo plugin, which silently enabled HelloWorld whenever the newspaper was enabled. Removed:data/resources/plugins/MiAZNewspaper/, itspo/POTFILESentry, and its 36 messages frompo/miaz.pot,po/es_ES.po,po/de.poandpo/fr.po(885 to 849 messages, all four in sync, all compile).MiAZInsights(formerlyMiAZYearReport) is now the single reference example for the WWW-publish + Browser-page pattern inAGENTS.md, and it earns the pattern by aggregating (counts, heatmap, rank movers, country map) instead of restating. Bundled plugin count is now 19. Historical release notes inCHANGELOG.md,debian/changelogandmetainfo.xml.instill name the plugin on purpose; those record what shipped at the time. - Removed the per-repository change journal (
MiAZHistory,backend/history.py, servicehistory). It appended every add, rename and delete to<repo>/.history/<YYYYMM>.jsonl, but nothing read it back and the feature was unused, so it was dropped along with itsapp.pyservice registration and its tests. MiAZ is a curated tool for a small set of documents, not an archive of record, so change tracking, undo and recovery are not part of its scope. Git-based repository tracking may return later as an optionalMiAZGitplugin. Thefilename-importedsignal and theoriginprovenance argument onutil.filename_import, which existed only to feed this journal, were removed as well:filename_import(source, target)now just copies and emitsfilename-added, and the ZIP and scan importers no longer build provenance objects. - Removed two unused GSettings keys,
sidebar-widthandlast-repository, fromdata/io.github.t00m.MiAZ.gschema.xml. Nothing read or wrote them. The sidebar lives in anAdw.OverlaySplitView(fixed 300-360 px range, no user-draggable resize), so there is no sidebar width to persist; and the active repository is already restored from the App JSON configcurrentkey, so a second GSettings copy would only add a competing source of truth. Removed now, before 0.2 freezes the schema. The Countries configuration keeps its add/remove/edit buttons hidden by design (countries are a fixed ISO 3166-1 vocabulary with bundled flag icons); the staleFIXMEthere was replaced with a rationale comment. - Dropped the Snap and Windows packaging paths for the 0.2 release. Removed
snap/snapcraft.yaml(stale at 0.1.27, never tested) andscripts/packaging/win/, plus their sections and the snapPATHshim inscripts/packaging/build_all.sh. The officially supported formats are now deb, rpm, flatpak and AppImage. Both are recoverable from git history if revived later. - Removed dead and obsolete files: unused widgets
MiAZ/frontend/desktop/widgets/about.py(the About dialog usesAdw.AboutDialoginservices/actions.py) andwidgets/searchbar.py(never imported); obsolete packaging scriptsscripts/packaging/deb/create_deb_last.sh(setup.py + stdeb) andscripts/packaging/AppImage/miaz_appimage_repackage.sh(hardcoded 0.0.5), plus the now-unused rootstdeb.cfg; the deprecatedscripts/translations/directory (superseded byscripts/i18n/); and tracked junk (Changelog.md, and untrackedCHANGELOG.md.backup,s1.txt,s2.txt,damnsign.txt, rootbuild.sh). - Removed an unused
gi.repository.GObjectimport indata/resources/plugins/MiAZAIAssistant/miazaiassistant.py(was failing the CI ruff step).
- Plugin icons never showed up anywhere, because
MiAZPlugin.get_source_dir()looked for a folder named after the plugin'sModule(projmgt) while plugin folders are named after itsName(MiAZProjectMgt). For most bundled plugins it therefore returnedNone, soget_icon_path()found nothing and every caller silently fell back to no icon, including the sidebar filter rows in MiAZProjectMgt and MiAZPeriodicity. It now triesNamefirst andModulesecond, in the system directory then the user one. - Opening the rename dialog only to set a plugin value (a project, a periodicity) and pressing Rename showed "Another document with the same name already exists" and lost the edit. With no filename field touched there is nothing to rename, and
util.filename_renamereturnsFalseboth for "nothing to do" and for "the target already exists", so the dialog reported a name clash and never committed the tab edits. It now checks whether a rename is due first: when it is not, it skips the confirmation, writes the tab edits against the unchanged name, shows a toast and closes. When a rename is due, nothing changes. - The "Suggest with AI" button from MiAZAIAssistant could appear several times in the rename dialog.
inject_suggest_buttonconnected a newrename-dialog-builthandler on every call but tracked only the latest handler id, so any repeated call leaked the earlier handler and stacked another button on every rename. The injection is now idempotent: it disconnects any previously installed handler before connecting a new one, and clears its tracking slot on removal, so exactly one button is installed no matter how many times it runs. - Plugins that rely on system command line tools no longer fail with a "tools not installed" error when MiAZ is launched from the GNOME desktop icon rather than a terminal. A desktop launcher can start the app with an empty or stripped
PATH, soshutil.whichand subprocess found nothing (MiAZOCR reported ocrmypdf missing, MiAZAutoScan reported scanimage missing) even though the tools live in/usr/bin; running from a shell worked because the shellPATHis rich. MiAZ now appends the standard system tool directories (/usr/bin,/usr/local/bin,~/.local/bin, and thesbinvariants) toPATHat startup if they are missing, so tool detection and execution behave the same regardless of how the app was started. - Dialog body text is now selectable and left-aligned. The
Adw.AlertDialogbody label was read-only and centred, so a user could not copy the command or message shown in a dialog such as "OCR tools not installed". This applies to every error, warning, info and question dialog. - The workspace date filters "Since past month", "Since last 3 months" and "Since last 6 months" excluded documents from the earliest month they should cover.
since_date_last_n_monthssubtracted a fixed 30 days per month and forced the day to the 1st, which drifts at month boundaries: on the 31st of a month, 30 days back stays in the same month, so "Since past month" on Jul 31 started on Jul 1 and hid all of June (and the 3 and 6 month options were each off by a month). It now steps back whole calendar months. Covered by new tests intests/test_util.py. - The workspace date filter no longer hides documents dated today when MiAZ has been left running past midnight. The relative date presets (This month, Future, and so on) baked in the day the workspace was first loaded and were never refreshed, so once the calendar day rolled over a document dated today fell outside every today-bounded range and only appeared under "Future" (which had been defined as the old day plus one).
update()now rebuilds the presets with the current day whenever it detects the day changed, preserving the selected preset by title (its key changes daily, so it cannot be matched by key). Same-day updates skip the rebuild, so there is no added cost in normal use. - Disabling or uninstalling a plugin that publishes to the MiAZ Browser now removes its web content. Web cleanup moved from each plugin's
do_deactivateinto the plugin manager (unload_plugincalls a new_remove_plugin_www), which deletesLPATH/WWW/<plugin folder basename>for any plugin, bundled or user-space, on disable or uninstall. Previously onlyMiAZNewspaperandMiAZInsightscleaned up after themselves, so user plugins such asKB4MiAZandMiAZdeasleft their published directories behind and they kept showing in the Browser. The duplicatedrmtreein the two bundled plugins was dropped. A plugin that publishes under a name different from its folder is not covered centrally and must still clean up in its owndo_deactivate. - The MiAZNotes plugin failed to load from the rpm (and any
git archivebased package) withModuleNotFoundError: No module named 'lib'. The plugin keeps its own code in a bundledlib/subpackage (data/resources/plugins/MiAZNotes/lib/), but.gitignorehad an unanchoredlib/rule (from the standard Python template, meant for a build directory at the repo root) that matched that path at any depth. The subpackage was therefore untracked and excluded from thegit archive HEADsource tarball, so the installed plugin hadnotes.pybut nolib/. Anchored the rule to/lib/and/lib64/so it only ignores a repo-root build directory, not nested plugin source. TheMiAZNotes/lib/files must now be added to git so they ship. This was a packaging problem, not a missing third-party dependency. - Config and repository JSON is now written atomically.
MiAZUtil.json_saveand the repository bootstrap (repository.pyinit/_init_default_plugins) share a new module-levelatomic_json_save(MiAZ/backend/util.py) that writes to a temporary file in the same directory,fsyncs it, thenos.replace()s it onto the target. A crash mid-write can no longer leave a half-writtenrepo.json,plugins-used.json, or config file. - The disaster-recovery restore paths (
MiAZ/backend/dr.py) no longer risk masking the real error or crashing during rollback.restore_configandrestore_repositorydefine the.oldsidecar path before thetry, so a failure inunzip(before anything is moved) rolls back cleanly instead of raisingNameError. The rollback is wrapped so a rollback failure is logged rather than replacing the original exception, and the original error is logged before re-raising. Covered by the newtests/test_dr.py(8 tests: backup/restore round-trips plus both rollback paths). MiAZUtil.filename_renameno longer silently swallows the two skip cases. A rename whose target already exists now logs a warning (the caller already surfaces the skip; mass rename counts it in a toast), and a no-op rename where source equals target logs at debug. Removed the stale# FIXMEmarkers; the Windows-path doubt is resolved as "Linux-only for 0.2" with a comment.- Version numbers now agree across every static config file.
meson.buildis the single source of truth (0.1.28+build.29).miaz.spec(Versionand%changelog),pyproject.toml(version),debian/changelog, and the AppStream<releases>list indata/io.github.t00m.MiAZ.metainfo.xml.inwere stale (0.1.26/0.1.1); all now read the release version0.1.28. The+build.NNsuffix is left tomeson.buildalone, sincescripts/devel/increase_meson_version.shauto-increments it and the packaging scripts append it to artifacts at build time. - AI plugins used Ollama even after configuring another provider. The active provider is now inferred from the first configured provider when the selector was never changed.
- AI provider failures are shown with full, selectable, copyable details.
- The Export/Import menu button in repository settings now links visually with the neighbouring toolbar buttons.