Skip to content

Change log

What changed in each Dédalo v7 release, newest first. Every change is filed under who it affects — users of the application, administrators who install and run it, and developers who build on its API. A release that needs something from you when you update says so first, in an Action needed box.

Each release's Wire contract list names the deliberate changes to the API's wire shape by the id under which the source tree records each one (engineering/wire_contract/).

Unreleased

Merged since the last release; these ship with the next one.

Action needed when you update

  • Dédalo now runs on Bun 1.4.4, the latest stable release.
  • Irreversible maintenance actions now require the superuser, maintenance mode and a recent backup.

For users

Changed

  • Sorting a portal by a column header is now a temporary view; Save makes it the stored order.

    Before, clicking the sort arrow of a portal column header (on columns configured with sort_by_column) reordered the linked records and saved that order at once. Now the click only changes how the list is shown: every page follows the chosen column and direction, and nothing is written. A line under the list shows the active order ("Order: ↓") with two buttons: Save stores the shown order as the portal's order (recorded in Time Machine, as before), and Reset returns to the saved order. While a temporary order is shown, rows cannot be dragged; they can again after Save or Reset. Linking, unlinking or adding a record keeps the temporary order. Users with read-only access can sort the view and reset it, but not save it.

    On a portal whose ontology declares a permanent order, the same line shows that order. A header click there still gives a temporary view with Reset, but no Save, and rows are never dragged: the declared order is applied automatically on every save.

    Wire contract: WC-2026-10-31-portal-order-model.

  • On a phone, portal tables now show as labelled cards like section lists, with the drag handle always visible.

    On a phone-sized screen a portal table (the default and content views, and every field that shows records the same way, such as related records) now shows each linked record as a card, the way section lists already did. Each field in the card carries its name. The drag handle that moves a row sits in the card's Id band and is always visible, because a touch screen has no hover. The delete button stays inside the card, so the page is no longer wider than the screen. Field names containing an apostrophe, such as Catalan "Data d'alta", now show on the cards too.

    Wire contract: WC-2026-10-30-section-record-row-box.

  • The field you are editing is now outlined by an orange frame instead of a light orange background.

    Before, the active field was painted a very pale orange that was hard to tell from the rest of the form, with its tools floating above it in a separate orange pill. Now the field keeps its normal background inside an orange frame: its name sits in the frame's top edge in dark, bold text, and its tools are a tab joined to the frame's top-right corner (on a field too narrow for both, the tools float just above it so the name is never cut short). In relation fields (portals) the search bar is now the frame's bottom edge, exactly as wide as the field, and the list of results continues below it. Nothing moves on the page when a field becomes active. Works the same in the dark theme and on phones.

  • A portal whose ontology declares an order now stores its records in that order.

    A portal column can declare a fixed order in the ontology (for example, linked records sorted by name). That order used to be applied only on screen, so exports, publication and the edit view could disagree, and dragging records inside such a portal was refused. Now every save keeps the stored list in the declared order, and editing the value a linked record is ordered by re-sorts every portal that holds it. Because the declared order is fixed, those portals cannot be reordered: they offer no drag handle and no change-order dialog, and the order bar above the list names the declared order. Clicking a sortable column header still sorts the list on screen only, with Reset to go back; there is no Save there. Lists saved before their order was declared keep their old order until they are saved again, or until an administrator runs the portal_declared_order reconcile (Maintenance › Reconcile). The reconcile also runs by itself once an ontology update has added or changed an order (once, after the whole update), and in the background after an order is edited in the ontology editor; stopping the update, the background process or the server stops it at the next record, and a later reconcile run finishes the job. A record it cannot save is listed in its report and does not stop the others from being sorted.

    Wire contract: WC-2026-10-31-portal-order-model.

  • Section lists and portals now place their rows with the row box. Long lists stay fast, scrolling never loses an edit, and screen readers announce each list as a table or a list.

    Every section list, every portal view (default, content, line, tree, indexation, mosaic, and the list views) and the two tool mosaics (cataloguing, coin ordering) now draw one row per record, in order, through one shared engine. A page of more than 200 records keeps only the rows near the screen built. The others wait as empty, sized placeholders and are built when you scroll to them, so a "show all" page of 1000 records opens without freezing the browser. A row holding a value you typed but have not saved yet is never thrown away while you scroll. A page change, a filter or a reorder first saves what you typed. When it cannot save it, it asks before discarding, and Cancel leaves the list exactly as it was. When you print a long list, a note says how many rows were not printed (only the rows built on screen are). Use the print or export tool to get the whole list. Screen readers now announce list grids as tables, with a header row, and line, mosaic and tree views as lists. A portal row can be dragged by its handle, by touch with tap-to-pick and tap-to-place, or with the keyboard (Enter on the handle opens move-to-position).

    Wire contract: WC-2026-10-30-section-record-row-box.

  • JSON fields are lighter and faster

    The JSON component loads its search and mini/text/collapse views only when used, list cells preview a large value without serializing all of it, typing in the editor no longer re-parses the whole value on every keystroke, closing a record releases its JSON editors, and the JSON download no longer builds a percent-encoded copy of the value.

  • Geolocation maps load faster

    The map component fetches fewer files before showing a map: the colour picker library loads only when a layer's colour swatch is first opened, turf and the language table load in parallel with Leaflet, and the search and mini/text views load only when used.

  • The text editor toolbar stays in view while you scroll a long text.

    In both plain and rich text fields, the editing toolbar now stays visible just below the top menu while you scroll through a long text, so the formatting tools no longer disappear off the top of the page. When you first click into a field, the toolbar appears over the field's label without moving the text, and the page no longer jumps: clicking inside a long rich text used to scroll the page to the top. The toolbar also always appears when the field is selected, including when you select it by clicking its label or button bar.

  • Audiovisual records load faster and reuse cached media.

    The audiovisual field no longer downloads its poster image and video again every time a record is shown. Media links now change only when the file itself changes (a new upload, a new transcode or a new poster image), so the browser reuses what it already has. Poster images below the visible part of the page load only when they scroll into view, the quality selector appears only when there is more than one quality to choose, and the player, viewer and search displays load only when they are used.

    Wire contract: WC-2026-10-29-posterframe-url-version.

  • Pages open noticeably faster for returning users on slow connections.

    On HTTPS installations the browser keeps Dédalo's program files in a local cache after login, but three files were still fetched from the server on every page load, one after another, before the page could show anything: the page's entry file and two small helper libraries (tooltips, and a decompressor only used when opening a tag link). Now the entry file comes from the local cache and the two libraries load only when needed. Measured on a simulated slow mobile connection (Slow 4G), a section list appears in 2.8 s instead of 3.9 s and an edit form in 4.5 s instead of 5.7 s; on Fast 4G a list appears in 0.9 s instead of 1.3 s. First visits and installations without HTTPS are unchanged.

  • AI entry points are hidden when the installation has no model.

    The assistant icon is no longer shown unless the installation has an AI model enabled. Semantic search is shown only when a real embedding sidecar is configured; without one it is hidden instead of falling back to a placeholder. Object identification is hidden for now while it is being redesigned. Nothing else changes for installations that already have a model.

  • Link fields show their hints only on hover or when selected.

    An empty link (IRI) field in the edit form no longer shows its "Label", "Title" and "http://" hints all the time. Like the date field, it stays blank until the pointer is over it or the field is selected, so empty fields no longer look filled in.

  • Images, PDFs, SVGs and 3D models load faster and reuse cached files.

    Every media field now behaves like the audiovisual one: images, PDFs, SVGs and 3D models are no longer downloaded again each time a record is shown. A file's link changes only when the file itself changes (a new upload, a rotation, a regenerated version or a saved drawing layer), so the browser reuses what it already has. Thumbnails in lists and small previews load only when they scroll into view, the image quality selector appears only when there is more than one quality to choose, and the viewer, search and other rarely used displays load only when they are used.

    Wire contract: WC-2026-10-29-posterframe-url-version.

  • The search panel opens in one smooth step instead of stuttering in.

    Opening the search panel used to show a loading line, then the empty panel, then each saved filter row popping in one by one, pushing the list down each time — most noticeable on slow connections. Now the panel's data loads in parallel, starts loading as soon as the pointer rests on the Search button, and the panel fades in once, complete (a row slower than 1.5 s fills in afterwards). Nothing extra loads with the page, so lists and counters are unaffected.

  • Tools that open in a dialog appear about half a second sooner on slow connections.

    The first time a tool opened in a dialog on a page, its stylesheet only started loading after its code had finished, adding one extra network round trip to the wait. Both now load at the same time, which saves about 0.6 s per first open on a slow connection (measured on Slow 4G: for example 2.96 s → 2.40 s for the posterframe tool). Tools that open in their own window are unchanged.

  • The Transcription and Translation tools' text panes now look like the Indexation tool's.

    In the Transcription and Translation tools, the field name is now a header row across the top of the text pane, as in the Indexation tool. While you edit, the editor toolbar takes over that row, with the full-screen and save buttons at its right end. The text no longer moves or gets covered when the editor is selected, and in the Translation tool the language selectors stay visible above it.

  • Tree-picker portals show their linked records like Parent and Children.

    Portals that link terms through the tree picker (for example "Connected to" in an ontology record) spread each linked record across wide columns. They now show it on one line, term | tld | id | model, with the same edit, tree and remove buttons as the Parent and Children fields.

  • An empty date field is now blank until you hover or select it.

    An empty date field in the edit form showed its input format (DD/MM/YYYY), the <> range separator and the period labels even when it held no value. It now stays blank, like text fields and selectors, and shows them when you hover the field or select it. A range with only one date filled in keeps its separator. On a phone or tablet, the calendar button now appears once you tap the field.

  • The inspector's Relations, Latest changes and Component history panels now page the same way.

    The Relations panel's old Total / Page line with two arrows is replaced by the compact strip used in Latest changes and Component history (« ‹ › » 1-10 of 4.683), so long relation lists can jump to the first or last page as well as step one page at a time. All three panels now page identically: the strip stays put, only the rows change, the new page slides the block to its height instead of the panel jumping, and a page that takes a moment dims the block after a short beat — a fast turn does not blink.

  • Developer builds are named for the next version, for example 7.0.1-dev.

    Before: an untagged developer build reported its version as 7.0.0.dev, the name of the release it followed, and its archive was 7.0.0-dev.zip. Now it reports the next version it is heading to, written with a hyphen (7.0.1-dev), and its archive is named the same way. A developer build sorts above every prerelease of its version and below that version's release, so an install running it is offered the stable release 7.0.1 once that exists. Two developer builds of the same line share one name, so the version shown does not change between them; the Installed archive value shows which build is running. Moving from a developer build to a release candidate is not offered, and no channel moves an installation to an earlier release: an urgent developer build is followed by the next stable release. A release tag names its own version, so a code-only patch tag such as 7.0.1 over data 7.0.0 reports 7.0.1, not a developer build.

Added

  • Each user can now choose how the field being edited is highlighted: Frame, Lift or Margin.

    The user panel (click your username in the header) has a new Active field style choice. Frame is the default thin orange frame. Lift raises the field as a card with a soft shadow, an orange dot by its name and quiet grey tools on the name's row; where that row cannot hold them (text areas, narrow fields) the card grows upward to hold the tools. Margin marks the field with an orange bracket in its left margin and stands its tools as a vertical orange rail in a margin on its right (fields leave that margin free while this style is chosen). The choice applies at once and is saved in the new User interface preferences field of your own user record, so it follows you to any browser and every login. The record number (Id) is no longer highlighted when clicked: it is read-only. Requires the ontology update that adds that field (dd1206).

    Wire contract: WC-2026-11-02-ui-preferences.

  • Press ⌘K (Ctrl+K) to jump to any section or open a tool.

    A search box opens anywhere in the application with ⌘K on a Mac or Ctrl+K elsewhere. Type part of a name (accents and case are ignored) and press Enter. It lists the tools of the section you are in, your recently visited sections, and every section in your menu, so moving between sections such as Image and Audiovisual to open the same tool no longer means browsing the menu each time. Arrow keys move through the list; Esc closes it. On the Maintenance page the maintenance tools are listed first.

  • Indexation, transcription and translation tools — time machine button on the text

    The text being edited in the indexation, transcription and translation (target language) tools now shows the time machine button next to full screen and save, so earlier versions of the text can be opened without leaving the tool. In the transcription tool it replaces the header's separate time machine button.

Fixed

  • Uploading a very large file no longer reports an error while the server is still joining it.

    After all parts of a large file (several GB) had arrived, the server needed minutes to put them back together. The browser stopped waiting after about half a minute and showed an error, although the file was saved in the end; trying again then failed because the parts were already gone. The browser now keeps waiting (up to 30 minutes, or until you cancel), and asking again for a file that is already joined returns it instead of an error. A part that hits a temporary gateway error (502, 503, 504) is now retried too.

  • The Projects checklist in edit mode fills its whole field instead of stopping halfway.

    In edit forms (e.g. a user record), the Projects checklist was capped at a fixed height and left the lower half of its field empty, so long project lists needed scrolling. It now fills the space the form gives it; the old height stays the minimum.

  • Wide portal tables in edit mode scroll sideways instead of sliding under the inspector.

    In edit forms with a wide related-records table (e.g. Informants in an Oral History record), a narrow window pushed the whole form underneath the inspector panel. The table now scrolls horizontally inside its own field and the form keeps its width.

  • Screen readers now announce the list delete and edit buttons.

    The trash-can button on each row of a section list now has an accessible name that says which record it deletes, such as "Delete 7". The pen buttons that open a record from a portal line view and from a graph list are named as well. Before, a screen reader announced these icon-only buttons only as "button".

  • Screen readers now announce the language and dashboard selectors.

    The data-language selector in the menu, and the sort and time-range selectors on area dashboards, now have accessible names. Every language selector built by the client gets a name by default. The interface and data language selectors have different names, so a screen reader can tell them apart.

  • The menu no longer offers the retired Graphs area.

    The Graphs area (dd630) was still listed in the menu, but opening it only showed an error: the area was retired and the server refuses to open it. The menu now leaves it out. Nothing else changes: the area still appears in the Maintenance area-visibility settings and in profile permissions.

    Wire contract: WC-2026-11-01-menu-dead-area.

  • Area dashboards, the thesaurus and ontology toolbars, the Maintenance map and the edit inspector fit a phone screen.

    On a narrow screen (about 360 pixels wide) these pages were wider than the screen, so the whole page, menu included, scrolled sideways. Their toolbars, chart headers, chart-type switchers and legends now wrap onto a second line, the Maintenance tool buttons and forms wrap inside their panel, the record title in the inspector is shortened with an ellipsis, and the Maintenance system map scrolls inside its own frame.

  • A field whose save failed is no longer forgotten — it stays marked unsaved and is saved again.

    Before, when saving a field failed (a network interruption, a server error, a session that changed on re-login), the field showed its orange "modified" bar and nothing else: leaving the page did not warn, the automatic save on navigation did not retry it, and a later successful edit of the same field quietly cleared the warning while the earlier change was still not stored.

    Now a failed save keeps the field marked as unsaved. Leaving the page warns about it, the automatic save on navigation tries it again, and the next save of that field sends the earlier change along with the new one. A field whose row scrolls away or is rebuilt while its change is still unsaved keeps that change until it is saved or you choose to discard it. If you edit the same value of that field again in the rebuilt row, your newer value is the one that is kept; the older, unsaved one is dropped instead of being written over it later. Unsaved changes to OTHER values of the same field (another line of a multi-value field, another linked record) are kept and saved. Typing a value back to what is stored no longer clears the warning while another change of the field is still unsaved. A change refused while an earlier save of the same field was still on its way (the session changed on re-login in the meantime) stays unsaved when that earlier save completes.

    Wire contract: WC-2026-10-30-section-record-row-box.

  • Dragging a related record on the second page of a portal now moves it where it is dropped.

    Reordering the records of a portal by drag and drop counted the page twice from the second page on, so a record dragged on page 2 or later was sent to the wrong position (or refused). It now lands exactly where it is dropped, on every page. A drag that is not a portal record — a file, a thesaurus term, a card from a tool — is no longer swallowed by a portal it passes over and reaches the place it was dropped on. When a reorder cannot be confirmed, the message now says so and asks for a reload instead of claiming the new order was not saved. That message, the one for a reorder that was not saved, and the one for a list that changed during the drag are shown in the user's language. A portal no longer highlights a drop place for a record it would refuse, for example a portal whose order is declared in the ontology, so a drop no longer looks accepted and then does nothing.

    Wire contract: WC-2026-10-30-section-record-row-box.

  • Components with language versions show the other version beside the value, and offer the translation tool

    Some fields are not translated but can keep a version per language beside their base value — a personal name written once and transliterated into Greek or Russian, for example, or any IRI field. These fields are marked with with_lang_versions in the ontology.

    Before, such a field behaved like a plain untranslated one. The translation tool was not offered on it, and the version in your working language was not shown next to the value.

    Now:

    • The translation tool appears on these fields and on every IRI field. Inside the tool, the version you edit shows the base value beside it.
    • In the edit form and in lists, the version in your working data language is shown beside the base value, for example "Augustus (Αύγουστος)".
    • Opening an IRI field's version in the translation tool no longer fails.
    • Searching one of these fields also finds the record by any of its language versions, not only by the base value.
    • The edit form keeps editing the base value even when your working data language is another one; only the translation tool edits a version.
    • When published, these fields give each language its own version and fall back to the base value when a language has none. IRI fields no longer publish the base and all its versions joined together in every language.
    • The options of selectors, radio buttons and check boxes are always labelled in your working data language, also right after saving. Before, they could appear in the installation's default language.
    • In the Time Machine history of one of these fields, a change to a language version shows that version as the row's value, no longer as a greyed missing-translation hint.

    Wire contract: WC-2026-10-31-transliterate-value-emitted.

  • Records open when a portal's list layout names a field from an uninstalled ontology.

    A portal's list layout can name fields from optional ontologies (for example tchi). On an installation without that ontology, opening the record failed with "The identifier is not a valid ontology tipo". Those fields are now left out of the layout, and the server logs a warning naming each one.

  • Catalog terms show the coins of their type again, and legend symbols follow their legend.

    Before, cataloguing a term (dropping a type onto it in the cataloging tool) left the term's Coins and Ordered coins portals empty, and the symbol lists derived from a coin legend never updated when the legend's tags changed. Both kinds of field are calculated, and the server did not know how to calculate them. Now it does: a term's coins are a copy of its type's coins, in the type's order, and they update when a coin is linked to the type as well as when the term is catalogued; a legend's symbol list is the symbols tagged in the legend text, in the order they appear. See Server-side observers — the value laws.

    The update button of a calculated portal works again. It recomputes that portal on that record from the current data and shows the result; anyone who can read the record may use it, because the value is the same whoever asks. If nothing changed, nothing is saved. See Refreshing a mirror by hand.

    Existing stored values are not rewritten by the update: they are corrected the next time their source is saved, when the button is pressed, or by an administrator running the observer reconcile (dry run first — on large catalogues review the proposed removals before applying them).

    For developers: the button now sends source.build_options.get_data_external: true; the old get_dato_external flag is ignored. A calculated value is only created and maintained on records whose section declares the field; a value stored on any other record is left exactly as it is, and the observer reconcile reports such values as left untouched instead of correcting them.

    Wire contract: WC-2026-10-30-observer-external-value-laws, WC-2026-10-30-observer-target-resolvers, WC-2026-10-30-observer-target-not-host, WC-2026-10-30-observer-law-derived-edges, WC-2026-10-30-portal-get-data-external-refresh.

  • Portals without a configured page size show their paginator again.

    A portal in an edit form whose ontology declares no page size showed only its first 10 records, with no paginator to reach the rest. It now pages at the installation's default rows per page (DEDALO_MAX_ROWS_PER_PAGE, 10 unless changed) and shows the paginator when there are more records.

    Wire contract: WC-2026-10-30-request-config-default-page-size.

  • Open relationships now follows every found record, with exact counts per related section.

    Before, Open relationships (the inspector button, and a portal's List from component data) used only the first 1000 found records, without saying so, and sent the list of related ids back and forth on every page of the opened window. Now the dialog lists every related section with its exact number of records — the number the opened window shows, with your permissions applied — and says so when nothing is related. The opened window is an ordinary filtered list: it counts and paginates normally however many records are related, and opening relationships again from it follows the chain further. The related records are read from what the found records themselves store, so the answer no longer depends on the relation index being up to date, and the activity log can be a starting point too. Counting has a 30-second limit, and so does each page and count of the opened window; past it a timeout error is shown instead of the server working on indefinitely. One count runs per session: opening the dialog in another tab or window replaces the count still running in the first, which then offers Reload. A found set that includes recursive children (the search's Children option) is refused with a message instead of being counted without its children. See inspector — Open relationships.

    For developers: the window's search carries a new referenced_by filter leaf, and the counts come from a new read-only relation_targets action (one deadline for the whole census, db.statement_timeout past it). Every search statement carrying the leaf has the 30-second limit wherever it runs (read_raw, the MCP search, tools acting on a search selection, diffusion and export selections), not only the opened window's — see SQO — relationship filter leaves and rqo — relation_targets.

    Wire contract: WC-2026-10-30-search-referenced-by-leaf.

  • Search panel no longer forced tall by the fields list

    The Fields, Type and Preset columns of the search panel now take the height of their content (up to half the screen, scrolling past that) instead of always reserving half the screen.

  • Reordering a related record to an invalid position no longer removes it.

    Moving a record inside a portal to a position outside the list (for example a drag on the second page, or a malformed request) used to drop the moved record from the field and still report the save as successful. The server now refuses such a move with an error and leaves the field exactly as it was; moves to a valid position behave as before.

    Wire contract: WC-2026-10-30-sort-data-key-range.

  • The export now writes the data language you select in the tool, not always the main one.

    Before, the tool export always wrote values in the main language of the install, whatever data language the menu showed. Now the tool has a Data language selector, starting on the menu's data language, and the export is written in the language you pick. Thesaurus terms (toponymy included) show the term in the data language when it exists, and otherwise in the thesaurus's own language, never translated. To get every language in one file, choose Dédalo (Raw); the selector is disabled for that format.

  • Indexation — the text pane's header now matches the selected field.

    In the Indexation tool, selecting the transcription left the header row above it grey, and the editor toolbar and its buttons sat slightly too low: a grey strip showed above them and they overlapped the first line of text. The header now takes the field's highlight colour while it is selected, as in the Transcription tool, and the toolbar covers the header row exactly. The pane also has the same space above it as in the Transcription tool.

  • Indexation shows the orange unsaved-changes bar on its text boxes.

    In the Indexation tool, the orange bar that marks unsaved changes was hidden behind the editor toolbar on both the transcription text and the tag note's description. It now shows across the top of the toolbar, as it does in the Transcription tool.

  • The side panel of a record that does not exist no longer offers record actions.

    Opening a link to a record that does not exist (or was deleted) showed an empty edit page whose side panel still offered "Latest changes", the component history, and the duplicate and delete buttons, all acting on no record. Those parts of the panel now appear only when a record is open.

  • The search panel's side lists now always end level with the search fields.

    With many fields added to a search, the centre area grew taller than the Fields, Type and Preset lists, so their bottoms no longer lined up. Now the centre area sets the panel's height and the side lists fill it, scrolling on their own. An open side list still gets at least half the screen height.

  • A list could show the previous search's record total.

    When a new search was started while the record total of the previous one was still loading (common on slow totals such as Activity or Time Machine), the list could show the old search's total ("1-10 of 50000") under the new results. The total now always belongs to the search on screen.

  • The thesaurus search Type list no longer grows taller than the Fields list.

    On short screens a long Type (typology) list stretched the search panel past the Fields list, so the two scrolled out of step. The Type list now has the same height limit as Fields and scrolls on its own; its selector stays visible at the top.

  • Clicking inside a tall field no longer scrolls the page.

    The first click in a field taller than the window, such as the permissions tree of a user profile, scrolled the page to the top of the field, so the pointer was no longer over the option just chosen. The page now stays where it is; it only scrolls when the activated field is entirely off screen.

  • An active text field keeps its label visible above the editor toolbar.

    When a text field (such as Description / transcription) was selected, the editor toolbar covered its label, so it was no longer clear which field and language were being edited. The label now sits as a small tab on the field's top edge, in the field's highlight colour. The text does not move when the field is selected.

  • A Time Machine note can be written again on every change in Component history.

    Opening the note of a change in Component history and closing it without typing left an empty note behind, and from then on that change never showed its note: each click opened a new blank one, and the text saved there never appeared (the icon stayed grey). Notes written in another language had the same problem. A change now always reopens its own note, and where earlier attempts left several notes on one change, the one with text is shown.

  • The cataloguing tool shows only its own thesauri, marks used types, and shows every configured coin again.

    Four defects of the cataloguing tool are fixed, restoring the behaviour of version 6:

    • Only the configured thesauri load. When the tool's configuration limits the thesaurus panel to some hierarchy typologies (for example hierarchy_types: [8]), only those hierarchies are loaded; thesauri of any other typology no longer appear.
    • Used types are marked again. A record already used as a term shows the highlighted background behind its drag handle. The "Relation list" (component_inverse) field now carries the records that point to each record, as in version 6, wherever it is shown. The mark now follows the thesaurus: deleting a type, or emptying its link, clears it without reloading, and changes made in another window are picked up when you return to the tool.
    • Portals inside portals keep their configured limit. A nested portal declared with, for example, limit: 4 (the ordered coins) showed only one item after any refresh or save; it now shows the configured number.
    • Thumbnails in the tool's record list are whole — no longer clipped or overlapping.

    Wire contract: WC-2026-10-29-area-declared-hierarchy-types.

  • Translation tool — full screen works again, and the read-only source has no save button.

    In the Translation tool, the full-screen button on either text showed a blank page, because the tool's header and action bar were drawn on top of the editor. It now fills the screen as expected. The source text is read-only there, so its button bar no longer offers a save button. This applies to any read-only text editor.

  • Tool save notices are readable again and no longer cover the close button.

    In tools such as Transcription, "saved" messages appeared as bare text over the top-right corner of the header, on top of the close button. They now show as solid green (saved) or orange (not saved) cards just below the header, and repeated identical saves merge into one card with a ×N count.

  • The menu no longer offers Maintenance or Development to users whose profile does not grant them.

    An administrator or developer whose profile did not grant the Maintenance area still saw it in the menu, and opening it showed the no-access page. The same happened to developers with the Development area. Each now appears only when the profile grants it, as every other menu entry already did. The main administrator is not affected.

    Wire contract: WC-2026-10-28-menu-role-areas-need-grant.

  • A saved search whose field no longer exists in the ontology no longer breaks the search panel.

    A search filter stores each field as it was when it was saved. After an ontology update removed a field, or changed its type, opening the search panel with that filter left an empty row and could stop the panel from loading the rest. Now the row shows Invalid component in the field's place, the field is left out of every search, and your current (unsaved) filter is saved again without it, so the next load is clean. A field that only failed to load (a network drop, a timeout, a server error) also shows Invalid component, but its condition is kept. Named presets are not rewritten for you: load one, check it and save it again to remove the field for good. Until the current filter has been saved again, the panel may offer the error report once for the missing field.

  • Three sections with ontology defects now open, or say clearly why they cannot.

    The Ontology and Ontology commons lists (in Development) failed to open on development servers and showed an empty column elsewhere, because their column list includes a program label. Labels are no longer treated as columns, so both lists open.

    The Text structuration list (in Auxiliary) failed with an internal error. Its definition points at a table from Dédalo 6 that version 7 does not have. It now shows the message that an ontology definition is unusable, which is the real cause. The section is marked deprecated in the master ontology and no longer points at that table; installations receive the change with their next ontology update.

    Wire contract: WC-2026-10-28-section-definition-defects.

  • Four tools that failed to open, or did more than asked, are fixed.

    • The Transcription tool is offered only on media fields set up for transcription. On other images, audio and PDFs (such as a user's picture) it used to appear and fail to open.
    • The Upload tool works again on JSON fields: uploading a .json file replaces the field's value. A file that is not valid JSON is refused and nothing changes.
    • The Time Machine tool opens from inverse-relation fields; it failed with an error before.
    • Opening the Diffusion tool no longer retries pending deletions behind the scenes. It only counts them for the "Pending deletions" badge; the retry runs when you press Retry.

    Wire contract: WC-2026-10-28-tool-open-defects.

  • Your user settings show your profile, and selection lists no longer show another user's labels.

    In the user settings tool (your name in the menu), an administrator other than the main administrator was refused their own user number, user name and profile, and the profile was often empty for everyone. The tool now shows all three, read-only. The profile shows only the name of your own profile, even if your permissions do not include the Profiles section.

    Selection lists could show the option names of whoever opened them first since the server started: empty names for users who should see them, or names a user is not allowed to read. Each user now sees the names their own permissions allow.

    Wire contract: WC-2026-10-28-own-user-record-read-floor.

  • Opening a record you cannot access shows the no-access page, and the Relations panel appears only when you may read it.

    Opening a record by its address (a bookmark or a shared link) when the record belongs to a project you do not work in showed an empty form, and the side panel then showed an error message. It now shows the no-access page. A record that does not exist gets the same page, so the page never tells you whether a record you cannot see exists. Administrators are not affected.

    The Relations panel in the side panel of a record is now shown only to users whose profile grants it for that section. Before, it was shown to everyone and then refused to open for users without that permission. If the panel cannot be loaded, it now shows a short message instead of an empty list.

    Wire contract: WC-2026-10-27-start-deep-link-record-scope, WC-2026-10-27-relation-list-grant-stamp.

  • The inspector's Component history keeps all its rows after you add a note, and pages through long histories.

    After adding or editing a Time Machine note in the inspector's Component history, the list showed only the newest change, or went empty. It now keeps the full history and the note icon updates in place. Histories longer than one page show the compact paginator (1-10 of 47) in the Component history and Latest changes panels, and paging there no longer changes the page address or adds browser-history entries.

  • Uploading an image from the text editor toolbar works again.

    The "Upload image from computer" button of a rich-text field failed with upload: missing file_to_upload. The image is now stored in the image web folder (DEDALO_IMAGE_WEB_FOLDER) and inserted into the text. Only JPG, PNG, GIF, WebP and AVIF images can be inserted. An image with the same name as an earlier one gets a new name instead of replacing it, so other texts that show the earlier image keep it. A resized image now opens at its resized size, without first appearing at full size while the editor loads.

  • Paging to the last records of a large section is fast for users limited to their projects.

    A user who is not an administrator, and who sees a section through their projects, waited around half a second or more for every page near the end of a large section (for example stepping through records in edit mode from the newest one). Administrators were already fast there. Those pages are now read from the nearer end of the list, as they already were for administrators, so the last records open in milliseconds. Pages in the middle of a very large section are no slower than before. The records and their order are unchanged.

  • User profile editor opens for ordinary users

    The user profile editor (avatar menu) failed with "Insufficient permissions" for any user whose profile grants no access to the Users section. Your own name, email, password and image are readable and editable again; other users' records stay protected.

For administrators

Security

  • Ontology updates now check every downloaded file against the master's digest before importing it.

    An ontology master now records the sha256 of every file it exports in its ontology.json, and an installation updating from it refuses any file whose bytes do not match, before anything is decompressed or imported. A master that has not re-exported since this release publishes no digests: its files are still accepted, and the server log says once per update that they were not verified. Re-export the ontologies on the master to publish digests. See Updating the ontology.

    Wire contract: WC-2026-10-30-ontology-file-digests.

  • The relations of a record are only shown to users who can open that record.

    A user with access to a section could ask which records point at any record of that section, including records outside their projects. The same was true of the thesaurus "show indexations" list for a term. Dédalo now refuses that request unless the user can open the record itself, and answers the same way when the record does not exist. Nothing changes for records you can open.

    Wire contract: WC-2026-10-28-related-host-record-scope.

  • A link to a single field of a record outside your projects is refused like a link to the record.

    Opening a link to one field of a record outside your projects used to show an empty page instead of the "no permission" page that a link to the record itself shows. No data was shown, but the two answers did not match. Both links now show the same "no permission" page, which is also what a link to a record that does not exist shows.

    Wire contract: WC-2026-10-28-start-component-deep-link-record-scope.

Changed

  • Dédalo now runs on Bun 1.4.4, the latest stable release. (action needed)

    The pinned runtime (.bun-version) moves from Bun 1.4.3 to 1.4.4. No defect drives the move; the runtime behaviours the engine depends on were re-checked on 1.4.4 and are unchanged. The update panel refuses this release until the server runs Bun 1.4.4.

    Action needed, before you apply this update:

    • Host installation: as root, install the verified 1.4.4 runtime at /opt/dedalo/bun/1.4.4/bun exactly as the host_runtime step does (with PIN=1.4.4, checked against this release's .bun-sha256), point the instance's ExecStart at it (the host_units step), run systemctl daemon-reload, restart the instance, then apply the update.
    • Container installation: nothing extra; the new image carries Bun 1.4.4 (Upgrading).
    • Publication hosts: each site's Bun must move to 1.4.4 as well, or its Bun version row turns red (Upgrading a site's Bun).
  • /health shows only the state of an update run and of the data version, not their identifiers.

    /health answers without a session, so its update_run and data_version entries now carry only state. The run id, target release and version pair are shown by bun run update status and the maintenance panel. The health verdict is unchanged.

  • Irreversible maintenance actions now require the superuser, maintenance mode and a recent backup. (action needed)

    The maintenance actions that change stored data with no undo (the move tools when not in dry-run mode, resetting hierarchies, the data update, restoring a previous code version, and restoring the ontology from a recovery file) now run only for the superuser, only in maintenance mode, and only when a recent verified database backup exists. They also cannot run while an update is in progress, or while another of these actions is running. Before, any global administrator could run them at any time. Dry runs of the move tools are unchanged. On a code server, building a release archive for other installations is now reserved to the superuser. Make a backup in Make backup before using them.

    Wire contract: WC-2026-10-30-maintenance-door-classes.

  • The ontology update lists the nodes it will remove or re-model, and a failed update says exactly what it restored.

    Update ontology now reports, for each ontology it imports, the nodes the new version removes and the nodes whose model changes. When an affected node is a section, it counts the records stored under it. The report does not stop the update. If an update fails and its automatic restore runs, an ontology that was empty before the update is emptied again; before, the imported rows stayed. The message now says whether the restore was complete, partial or incomplete. See Updating the ontology.

    Wire contract: WC-2026-10-30-ontology-import-census-restore.

  • The section-id repair script now checks for a recent backup before it writes.

    scripts/migrate_section_id_locators.ts --apply rewrites stored records without Time Machine history, so a recent database backup is the only way back. It now refuses to apply unless a recent verified backup exists; --waive-backup proceeds anyway and records who waived it in the log. It also cannot run while an update or another irreversible maintenance action is running. Dry runs are unchanged.

  • The TOOLS_ENABLE_REGISTRY_IMPORT setting is retired; a code update now always re-registers tools and keeps each one's Active choice.

    TOOLS_ENABLE_REGISTRY_IMPORT no longer exists; a line left in .env is ignored. A code update now always reconciles the tools registry from the release's tool packages, so a tool added by the release appears without pressing Register tools. Each tool keeps the Active value an administrator set, even one changed while the update runs.

    Wire contract: WC-2026-10-30-tools-registry-import-switch-retired.

  • Update ontology says whether a database backup is in place before you press the button.

    An ontology update is refused without a recent, verified database backup. Before, the Update ontology panel showed only a checkbox labelled "Update without a recent database backup", out of line with its text, and you learnt about the rule only when the update was refused.

    The panel now has a Database backup block above the button:

    • With a recent, verified backup, it shows the backup's age and no checkbox.
    • Without one, it says the update will be refused and how to fix it: make a backup in Backups and reload the panel. The checkbox to update without a backup sits below that, with what it costs: if the update goes wrong, the database cannot be restored, and the waiver is recorded with your user. Replace selected ontologies stays disabled until you make a backup or tick the box.

    Wire contract: WC-2026-10-30-update-ontology-backup-gate.

  • An ontology update only touches the shared ontologies.

    "Update ontology" now refuses any TLD that is not one of the installation's active shared ontologies (ACTIVE_ONTOLOGY_TLDS), the core TLDs or the shared value lists. Local TLDs and localontology overrides can no longer be overwritten by an update. On a master, the ontology export files are now written atomically: a failed export keeps the previous file instead of serving a truncated one.

    Wire contract: WC-2026-10-30-update-door-refusals.

  • A code update is now one journaled update run that also applies the ontology, schema, data and tool steps, and confirms only after verifying them.

    Update code in the maintenance panel now starts an update run. The server reads the code server's release list itself, plans every step of every release between the installed version and the chosen one, and records the run in a journal under the private directory. Maintenance mode stays on from the start of the run until it is confirmed. After the new code starts, the run applies the ontology, the database migrations, the data update and the tools in release order. It then verifies the result, and only then confirms the update. A run that is interrupted resumes from its journal on the next start. If a step fails within one data model, the previous code is restored automatically. If the run has already moved the database to a new data model, it stops and names the backup to restore. bun run update status, check, plan <release> and abort show and manage runs from the command line. See Updating code.

    Wire contract: WC-2026-10-30-update-run.

  • Update runs refuse ontology sources without file digests and resume more safely.

    An update run (Update ontology, or a code update's ontology phase) refuses an ontology source that publishes no file digests; re-export on the master. A data change that is resumed after it already committed is not run again. If the server loses its database connection during a run, the run stops before its next phase and the next start resumes it; at start the server waits up to two hours for a lock still held by a dead session. A run that has no schema phase after the code swap now ends with one, so a development build's migrations are applied. Maintenance jobs that need the update lock are refused while an update run is in flight.

    Wire contract: WC-2026-10-30-update-run.

  • After an update that needs a database restore, no new update starts until the run is closed.

    When an update run ends asking for a database restore, every new update run is refused until you have restored the database and closed the run with bun run update abort <note>. Each release's migrations now run in that release's own phase, after its ontology step. A code update on an installation with no ontology source no longer fails on the ontology phase: the step is skipped and the skip is recorded. Restore in the code update panel now offers to proceed without a recent database backup when the backup is stale.

  • Updates check what the master offers, and masters must use https.

    "Update ontology" now reads the master's ontology list itself. It refuses an ontology exported for another data model, and a database that still needs "Update data" first. Each file is taken from the master's own list, not from the browser. With several ontology masters, each entry in ONTOLOGY_SERVERS lists the ontologies it provides in tlds, and every ontology must come from exactly one master. "Update code" now checks that the selected master really offers the chosen release with that checksum before downloading it. Code and ontology masters must use https://: an http:// entry is ignored, with a message in the server log. Plain http works only for a master on the same machine.

    Wire contract: WC-2026-10-30-update-sources-server-held.

  • AI tools are hidden when no AI model is configured.

    The assistant appears only when the agent HTTP endpoint is enabled and at least one model is configured. Semantic search appears only when a real embedding model (the sidecar endpoint) is configured. Object identification is hidden until it is redesigned. On an installation without a working model, these entry points no longer show buttons that fail when clicked.

    Wire contract: WC-2026-10-29-ai-capabilities-page-globals.

  • The Update code panel shows its options only once loaded, and maintenance mode is switched on from the update dialog only.

    In Maintenance → Update code, the panel no longer repeats the maintenance-mode warning, its switch and its row: the dialog opened by "Check available updates" already offers the switch where the update is started. While the panel loads, a spinner stands where the status will appear, and the source, offer and "Check available updates" controls appear together once loading finishes. The "Published releases" option now carries a short description, like the other two. Every maintenance widget's loading spinner now takes its own space instead of floating over the content.

  • The publication tool asks for the number of pending deletions without using the retry action.

    The publication tool shows how many deletions are still waiting to reach the publication server. It now gets that number from a separate, read-only request instead of the request that retries the deletions. Nothing changes on screen; read-only checks of the installation no longer block it.

    Wire contract: WC-2026-10-28-count-pending-deletions.

  • The code update panel tells you about maintenance mode up front and lets you switch it on there.

    A code update is refused while maintenance mode is off, but the update code panel only said so after you had chosen a version and pressed Update. The panel now shows this at the top, under the readiness verdict, with an Enable maintenance mode button. The version window also keeps Update disabled until the refusing check is resolved, and offers the same button. Update data version gets the same button in place of its old message.

    The maintenance widgets also look more consistent:

    • Folded sections use the same arrow as the rest of the application, on the right.
    • Every key/value table in a widget uses the same label column width, so stacked tables line up. On a narrow screen the label sits above its value.
    • In update code, the server list, the Offer choice and Check available updates are grouped under Source at the end of the panel. Choosing an option no longer moves the other options: its note appears below them.
    • Warnings are now amber instead of a red-orange that was hard to tell from "blocked". An unknown state is a grey pill like the others, so a column of states lines up.
    • In a key/value table, the key now lines up with the first line of its value, even when the value runs to several lines.
    • In the System Map, a destructive or irreversible tool is marked only by a red dot. Its name and border are no longer red. Hovering the chip still says it is destructive.
  • The Docker install guide says the Official image comes from GitHub Container Registry only, and that the Docker Hub mirror is reached through a custom image.

    Before: the registry table listed the Docker Hub mirror as available, with no word on how an installation reaches it, and the host install.sh --status --json was described as reading only the checkout. Now: the page says the Official choice pulls from GitHub Container Registry only, that Docker Hub is reached only through the custom choice (--image-repository docker.io/dedalia/dedalo), and that a custom image is trusted as pulled, not signature-checked. The host --status --json reports the same install record as the in-container command.

  • The Docker install's System checks show cosign, and the Image version help names only the tags the installer accepts.

    Before: cosign was checked only at the Review, and the Image version help listed 7.0.0 as an accepted tag, which the installer refuses. Now: the System checks show a cosign row, a warning when cosign is missing (only the official image needs it), and the Image version help names the accepted tags: the installer's own release, or the next release's -dev tag.

  • The installer shows that the image pull is alive, and says when docker has gone silent.

    Before: while docker pulled the image, the installer printed one line and then nothing for as long as the pull ran, so a stalled pull looked like a hung install. Now: a "still pulling" line with the elapsed time every 30 seconds. When docker has produced no output for 3 minutes, the installer says so and names the resume command (sh install.sh --route <route> --resume), so you can stop the pull and resume it.

  • Resuming an interrupted Docker install asks for the root password once, not twice.

    Before: install.sh --resume asked for the root password twice, the same as a fresh install. Now: a resume asks it once, because the install already set the password. A fresh install still asks twice, to catch a typo in a password that is set for the first time. The password rules and the refusals apply in both cases.

  • The Docker install guide lists the curl, unzip, sha256sum and cosign prerequisites, and documents the updater's --check dry run.

    The Docker install guide now lists curl, unzip and sha256sum among the host tools, with the install commands for Debian, Ubuntu and the RHEL family, and gives the release-binary steps for cosign, checked against the release's checksums file. The update section documents --check, the read-only dry run of an image update.

  • The installer Review lists the cosign verify command for an official image, and the registry check reports success.

    Before: the installer's Review of an official image showed the pull, but not the signature check it would run after it, and a successful registry check was silent. Now: the Review lists the cosign verify command that checks the pulled image's digest against Dédalo's signing identity, so the operator reads the check before the install runs it. When the registry confirms the image is published, the run prints a success line.

Added

  • The Publication server API panel can open the v2 API documentation.

    The "Open Swagger UI" buttons of Maintenance → Publication → Publication server API open the legacy v1 documentation, a separate application Dédalo does not serve: on an installation whose web server does not route /dedalo/publication/ to it they answered "not found". A new optional setting, DEDALO_PUBLICATION_API_V2_URL (the v2 API address, BASE_PATH included), adds an "Open API v2 docs" button that opens the v2 API's own documentation page (<address>/docs). The v1 buttons are unchanged.

  • Profile permissions can be discarded before saving.

    The permissions tree of a user profile now shows a Cancel button next to Save once a value changes. It asks for confirmation, then reloads the stored permissions, so a value changed by accident can be undone without saving it. Setting a value back to its saved level hides both buttons and the "Unsaved changes!" warning again.

  • The permissions tree filter can show the siblings of a found item.

    While filtering a profile's access permissions, the tree shows only the path to each match. A parent that hides other children now carries the arrow: click it to show all of them around the match (still highlighted) and set their permissions in place; click again to go back to the matches only.

  • Older images in rich text can be made visible on published pages, from Maintenance.

    Images inserted into rich text before this update keep their original file name and stay visible to logged-in users only. Maintenance → Reconcile status has two new entries. rich_text_images lists them and, when applied, renames a copy of each after the record that uses it (the original file is kept, and every change is a normal save with a Time Machine entry; a text that cannot be saved safely is listed and left alone). Both apply only for root. web_image_exposure lists any image a visitor can open although the published record it is named after does not use it, and, applied by root, makes it private. The second one checks by itself at every start. See the upgrading guide for the steps.

    Wire contract: WC-2026-10-26-rich-text-image-legacy-reconcile.

  • The container updater gains a --check dry run that reports whether an update would be accepted, without changing anything.

    deploy/dedalo-image-update.sh --version <version> --check runs the version checks, the engine's check and the image check, then stops. It takes no lock, makes no backup, creates no rollback tag, changes no file (.dedalo.env included), re-pins nothing, and starts, stops or recreates no container. It pulls no layer: the image is checked by its manifest, and with DEDALO_IMAGE_VERIFY=cosign its signature is read from the registry.

    When the update would be accepted, it prints check: would update <from> -> <to> and exits 0. When it would be refused, it prints the same refusal a real run gives and exits non-zero. See Updating the image.

Fixed

  • The root user can now open their own account panel by clicking their name in the header.

    Clicking the username in the header opens the account panel (full name, password, email, image and the active field style). For the root user the name was not clickable, so root had no way to change their name, email or interface preferences from inside Dédalo. It now opens for root as for every other user. Root still sees and changes only their own account. Root's image and password are shown but cannot be changed there: a password change asks for no current password, and root has no emailed recovery, so a session left open could lock the real root out for good. In this panel the user name and profile are now read-only for everyone, administrators included; administrators edit them in the Users section.

  • The newest update run is always the one shown as newest.

    Update run ids now carry the start time to the millisecond. Before, two runs started in the same second (for example a refused submission followed by the real update) could be ordered by the random part of their id, so bun run update status could report the older run as the newest one.

  • A code update on an installation without an ontology server no longer stops in "needs a database restore".

    A release that declares no update steps of its own runs the default ones; its ontology step is skipped, and recorded, when the installation has no ontology server configured (ONTOLOGY_SERVERS=[]). The master's update list lost that distinction on the way to the installation, so the installation treated the step as required: after the new code started, the ontology step failed and the update ended asking for a database restore. The installation now receives it and skips the step as intended. An update refused for want of a recent, restorable database backup again says which backup did not verify and why, and is reported as a refusal: make a backup (or waive it) and start again. A refused archive (its checksum does not match the one the code server publishes) is also reported as a refusal with its reason, no longer as a bare "code failed". And a healthy update that crosses the data model no longer logs "NOT CONFIRMED — NEEDS A HUMAN … a migration failed" when it starts that step: the log now says the rollback is disarmed until the update confirms.

    Wire contract: WC-2026-11-01-update-plan-declared.

  • The data update refuses a step meant for a newer version than the running code.

    A development or nightly build could already list the data update of the next release and run it. The database then stood at a newer data version than the code serving it, and the health check turned red. "Update data" now refuses such a step before running anything and says which code version it needs. Install a build of that version first.

    Wire contract: WC-2026-10-31-update-rung-above-code.

  • An interrupted Docker image update now rolls back instead of leaving the stack pinned to an unverified version.

    Stopping deploy/dedalo-image-update.sh after it re-pinned DEDALO_VERSION now puts the previous version back and reports interrupted. If it was killed outright, a .dedalo-image-update.pin file blocks later runs until --recover (which the host updater runs on its own) rolls back. A request for the version already installed is reported as same version, and a cancel can no longer race the host updater's claim.

  • Restoring a code copy asks for confirmation whenever the code changes, also between betas.

    "Update code" used to compare a restore point with the running code by data version only. Every beta of a version shares it, so restoring 7.0.0-beta.5 over 7.0.0-beta.7 asked for no confirmation. Restore points are now listed, named (dedalo_<release>[_<digest>]_<timestamp>) and compared by their release name (7.0.0-beta.5, 7.0.0, 7.0.1-dev), and the restore asks for the confirmation when the release differs, or when the same release comes from another archive. The rollback sentinel names releases too. The status payload adds data_version to each restore point.

    Wire contract: WC-2026-10-30-restore-point-release-identity.

  • A code update whose database migration fails is no longer confirmed.

    When the new code started but one of its boot database migrations failed, the update was still marked as confirmed and /health stayed green, so the automatic rollback never ran. Now /health answers 503 with boot_schema: "failed" and the update is not confirmed. If nothing had been applied yet, the watchdog rolls the code back as before. If a migration had already changed the database, the update is marked needs_restore and nothing is rolled back automatically: restore the pre-update database backup together with the previous code. /health also reports data_version, and a code update is refused while a data update is still pending.

    Wire contract: WC-2026-10-30-health-boot-facts.

  • Older development builds are offered the nightly again

    A development build made before 26 October 2026 reports its version as 7.0.0.dev. The code server could not read that form, treated the installation as the stable release, and so listed no nightly in its update panel. The code server now reads the older form as 7.0.0-dev, so these installations can update through the panel again.

  • Patch releases are offered when the data version did not change.

    The list of available code updates is now worked out from the installed release (for example 7.0.2), not only from its data version. A patch release that did not change the data version was not offered before.

  • The test ontology is no longer shipped or offered to installations.

    The ontology files shipped with the code no longer include the test TLD, which belongs to the test suite. The installer also stops offering it when an ontology source contains it.

  • After login, users open on the default section set in their user record.

    The "Default section" field of a user record (for example numisdata6) was saved but never used. Opening Dédalo without a section in the address now shows that section, provided the user is allowed to read it. If the field is empty, names something that is not a section or area, or names one the user cannot read, Dédalo opens as before: the installation's main section, or the menu.

    Wire contract: WC-2026-10-29-start-user-default-section.

  • Menu entries a user may open no longer disappear when their parent area is hidden from that user.

    When a user's profile granted a section but not the area that contains it in the menu, the section was missing from that user's menu even though they could open it. It now appears under the nearest area the user can see, or at the top of the menu. Sections inside the Ontology, Development and Maintenance areas stay hidden from users who cannot open those areas.

    Wire contract: WC-2026-10-28-menu-orphans.

  • The root user's "My profile" tool opens again.

    For the root user, the "My profile" tool (user administration) opened with an error panel ("The server answered with an unreadable response") instead of the account's fields, so root could not change its own password, email, name or picture from the application. It now opens normally. No other user gains access to the root account.

    Wire contract: WC-2026-10-28-own-account-read.

  • The host --status --json on the Docker and trial routes reports the install record the run wrote.

    Before: sh install.sh --route docker --status --json read the private directory of the host process, which on the Docker route holds no record, and answered exists: false for a complete install. Now: on the docker and trial routes it reads the stack's host record, the one --resume reads, and answers its outcome (complete after a finished install).

  • A commit with a release tag is never built as a development build, and Docker says plainly that it has no nightly images.

    Before: a developer build of a commit that carries a release tag (for example v7.0.1) could be built under the nightly name, even when the tag was asked by name, and a Docker install asked for a -dev image was told only that its image version was not accepted.

    Now: a developer build of a tagged commit is refused and the message names the tag, whichever way the commit was asked for. A tagged commit is a release or beta and is named by its tag. A Docker install asked for a development image is refused with the sentence "Docker has no nightly images": nightlies reach tree installs through the code update path only. On a development checkout the Image screen refuses Official and Build (a local build of a development checkout is not an image install) and preselects Custom, the operator's own registry.

  • The reconcile panel no longer reports "OK" when it found images it cannot fix.

    Running the rich-text images check when the referenced image files are not in the media web folder reported "OK. the two stores agree (drift 0)", as if no old image link existed, and Apply then changed nothing. The message now says how many references are missing, held or failed, and the report lists each one, so the files can be put back in place before the check is run again.

  • The permissions tree filter no longer freezes after several searches.

    In a profile's access permissions, typing several searches in a row in the tree filter could leave the page slow and unresponsive, with areas stuck on "Checking...". Each search left background work behind for the areas of the searches it replaced, and that work kept running after the tree had changed. Background work for a replaced search is now dropped, and the per-search scans of the permission list are cached, so the filter stays responsive however many searches you type. The tree now also waits until you pause typing before it updates, instead of redrawing on every keystroke.

  • Setting permissions on a large area is faster, and a failed change no longer locks the permissions tree.

    Ticking an area or a permission in a profile's access permissions updated every child by searching the whole permission list again, so large areas took long to apply. Each update is now immediate. And if applying a change ever failed, the permissions tree stayed greyed out and ignored clicks, filter included, until the page was reloaded; it is now always released, and a failed save no longer leaves the tree locked either.

  • The container updater reports a refused re-install of the pinned release as its own outcome, same_version, instead of downgrade_refused.

    Before: asking the updater for the release the stack already runs was refused as downgrade_refused, which read as a step back. Now: it is refused as same_version in the outcome file, and the message says the version is the pinned one. Only -dev builds are re-installed at the same version. A step back is still downgrade_refused.

  • The installer's --plain mode refuses answer flags before it asks any question, and names the flags it refused.

    Before: combining --plain (the interactive front end) with an answer flag such as --answers, --plan, --execute or --root-password failed with one generic sentence, and only after some questions had been asked. Now: the combination is refused at the start, before any prompt, and the refusal names each flag that caused it. Give those answers on the interactive screens instead.

  • Ontology update and export now say why the ontology directory cannot be created.

    When the engine could not create the ontology exchange directory (ONTOLOGY_DATA_IO_DIR/<major.minor>), Maintenance → Update ontology answered only unable to resolve the ontology IO directory, and the export actions answered Unable to create directory: false. They now name the directory and the system error, for example EACCES when the service user cannot write to it. The usual fix is to give the service user ownership of that directory, or to point ONTOLOGY_DATA_IO_DIR at a writable one.

  • The Reconcile panel's table is readable: each reconcile's name sits above its description, headers read as headers, and Check / Apply stay on one line.

    Maintenance → PostgreSQL → Reconcile listed every registered reconcile in a table whose first column squeezed the name into a few letters per line while the description ran beside it, and whose buttons wrapped their labels to Che ck / App ly. The name now reads as a heading with its description underneath, the header row is styled as a header, and the two buttons keep their width. A run whose last check found drift is marked on its Last run cell, which previously carried the mark on an element that painted nothing.

  • Images inserted into rich text are visible on published pages.

    An image inserted into a rich-text field from the editor toolbar was visible only to logged-in users: a visitor of the published page saw a broken image. New images are now named after the record they are inserted into and follow its publication — public while the record is published, hidden again when it is withdrawn — and they are copied to a separate publication server like any other published media. Images inserted before this update keep their old names and stay visible to logged-in users only. Only users who may edit the text can insert an image into it. As with any other media of a published record, an image is visible from the moment it is inserted, and stays visible after it is removed from the text while the record remains published.

    For administrators: on nginx, reload it once after updating so the regenerated media rules take effect (Apache needs nothing). If you set DEDALO_MEDIA_PUBLIC_QUALITIES yourself, add image/web to it, or these images stay private.

    Wire contract: WC-2026-10-26-rich-text-image-record-keyed.

  • Profile permissions search: a matched section or area now shows its permission radios and can be opened.

    Searching the permissions tree of a profile (e.g. rsc832) showed the matched section without its x / r / rw radios and without a way to expand it, so its permissions could not be set from the search result. A match with no deeper match now shows its radios and its children, collapsed, as in the unfiltered tree.

  • The updater's same-version refusal suggests the skip flag only for a stack pinned at a bare version.

    Before: a refused update that went back to an older version suggested --skip-version-check for every stack, including one already pinned to a prerelease or a developer build, where the suggestion did not apply. Now: the suggestion appears only when the stack is pinned at a bare X.Y.Z version, the case of a beta built before 7.0.0-beta.7. A stack pinned to a prerelease or -dev build is refused without it.

  • Installing or updating no longer leaves a hidden .<hash>-<pid>.node-gyp folder in the Dédalo folder.

    The production installer and the Update code panel installed the dependencies with package scripts enabled. Each run left a small, empty-of-data helper folder (a one-file node-gyp shim) inside the Dédalo folder. Dependencies are now installed with scripts disabled, as every other install path already did, and nothing is left behind. A leftover folder from an earlier install holds no data and can be deleted. The manual's bun install commands now include --ignore-scripts too.

  • The official Docker image now carries its signature in every registry that publishes it

    The 7.0.0-beta.7 image reached GitHub Container Registry and Docker Hub without its signature, so cosign verify (and the installer's Official check) refused it there. The release process now signs the image in each registry it publishes to, with the same official identity, and a release already published unsigned is signed in place when its signature can be proven from the build. Verify an image as described in Checking the signature.

    Mirroring an image into a registry of your own no longer suggests cosign copy, which is deprecated and drops current signatures: verify at a Dédalo registry, then copy that digest unchanged (A registry of your own).

For developers

Changed

  • Database migrations are frozen by content hash.

    Each schema migration is now recorded together with the hash of the exact file that ran. If an applied migration file is changed later, the server refuses to migrate and says which file differs. The fix is to restore the file and ship the change as a new migration. Every migration file is listed in install/db/migrations.lock.json with its hash, its class and its release. The class is compatible (the previous code still runs on the new schema) or incompatible. A migration is compatible only when it is provably additive, such as an index, a new table or a nullable column. An incompatible migration can only ship in a minor release.

  • A release tag needs its catalog entry and its seed.

    Pushing a release tag is refused unless the tagged commit lists the release in install/update/catalog.json and carries an install seed compiled for the commit's data version. Without the catalog entry, no master would ever offer the release.

  • Four read-path comparison tests now expect the current ontology definitions.

    After the ontology refresh, four tests that compare the engine with frozen answers saw definition changes made on the master ontology: a section tool's "coins" role points at the portal again instead of an alias, the mint section is called "Ceca", and two more components are now cloned into the test ontology. Each test keeps the old frozen value and checks the new one explicitly.

    Wire contract: WC-2026-10-28-parity-definition-drift.

Added

  • Section lists, portals and the tool mosaics place their rows through one client module, the record window, with a fixed row DOM contract.

    A new client module, core/section_record/js/record_window.js, places the rows of every section list, every portal row view and the two tool mosaics, through one door, window_section_rows in section.js. Code that renders or reads list rows can rely on this contract (see The row box):

    • content_data.children holds exactly one .section_record per entry, in order: children[i] is row i with data-index="i", at every page size, 0 included. Nothing else goes into content_data. A "no records" node, a footer, back-references, the print notice and a portal's order indicator go into the one .list_after sibling (hand it over as content_data.list_after; common.js place_list_after places it).
    • Table views mark their list_body with layout_table: header, content_data and every row are column subgrids with table roles, so each row is one box.
    • Above 200 rows only the rows near the screen are built; the rest are childless placeholders (.section_record.placeholder). Find a row with content_data.children[i] or row_of(), and its entry with the window's entry_of(i) — never a node map of your own. caller.ar_instances holds the built rows only.
    • Row gestures are delegated: one listener set per content_data, bound to the window's signal (bind_row_drag, bind_tool_row_drag, delegate_row_hover), never a listener per row.
    • Any operation that replaces rows takes the owner row lock first (common.js lock_rows), which commits typed-but-uncommitted values and asks before discarding one.
    • A portal reorder sends absolute keys: paginated_key is the index in the stored array; never add the paginator's offset.

    The module is also part of the files the browser pre-caches.

    Wire contract: WC-2026-10-30-section-record-row-box.

  • The parity test baseline can be re-recorded in the CI image.

    bun run ci:local --docker --record-parity-baseline re-records engineering/parity_baseline.json inside the CI image, the same way --record-unit-baseline does for the unit tests. Recording it on a developer machine froze that machine's own configuration into the baseline. It can be combined with --record-unit-baseline --new <file> so a single green run records both.

  • The test suites refuse to run at the same time on one test database.

    bun test, bun run test:client and bun run test:crawl reseed the same test fixtures, so running two of them at once made each fail in confusing ways. Each now takes a lock on the test database first and refuses, naming the run that holds it, while another one is running. A lock left by a run that died is cleared automatically, and a test that starts its own bun test inside a run is let through.

  • bun run test:crawl checks every feature as an administrator and as a project-scoped editor.

    Checking Dédalo by hand as an administrator and again as an editor took days. A new browser check does it on the test database: three users — the superuser, an administrator who is not the superuser, and an editor limited to one project — each open everything their own menu offers. For every section it opens the list and a record, checks one field of each kind at the permission level the server grants, opens every inspector panel and every tool offered, and fails on any script error, refused request or error panel.

    It then tries what a user must NOT be able to do, by typing the address directly: a section without permission, a record of another project, a tool not granted. Each must be refused. Finally it replays real user actions — logging in through the form, logging out, editing and saving, creating and deleting a record, exporting a CSV — and checks the result on the server. An export by the editor must contain only their project's records.

    Run it with bun run test:crawl (--out <dir> keeps a report and a screenshot of every problem). Known problems are listed with their reason in test/helpers/feature_crawl_ratchet.ts; a listed problem that gets fixed must be removed from the list.

Fixed

  • The test ontology's "Countermark descriptions" section tool now opens its target section.

    The test ontology's copy of the "Countermark descriptions" section tool (test6402) used an old setting name that the engine does not read, so it could not open. It now uses the current name, matching the fix on the master ontology. Tests that need a section tool without a target build their own.

    Wire contract: WC-2026-10-28-section-tool-test6402-config.

  • The section read example in the API field mapping names the section in source.tipo and lists it in sqo.section_tipo.

    Before: the read request example set only source.section_tipo and no source.tipo, which a section read refuses with request.invalid_source. Now: the example sets source.tipo to the section's own tipo and sqo.section_tipo to the list of sections to read, and the field list states both as required for a section read: source.tipo, and sqo.section_tipo in every sqo.

Wire contract — 44 entries
  • WC-2026-10-26-rich-text-image-legacy-reconcile
  • WC-2026-10-26-rich-text-image-record-keyed
  • WC-2026-10-27-relation-list-grant-stamp
  • WC-2026-10-27-start-deep-link-record-scope
  • WC-2026-10-28-count-pending-deletions
  • WC-2026-10-28-menu-orphans
  • WC-2026-10-28-menu-role-areas-need-grant
  • WC-2026-10-28-own-account-read
  • WC-2026-10-28-own-user-record-read-floor
  • WC-2026-10-28-parity-definition-drift
  • WC-2026-10-28-related-host-record-scope
  • WC-2026-10-28-section-definition-defects
  • WC-2026-10-28-section-tool-test6402-config
  • WC-2026-10-28-start-component-deep-link-record-scope
  • WC-2026-10-28-tool-open-defects
  • WC-2026-10-29-ai-capabilities-page-globals
  • WC-2026-10-29-area-declared-hierarchy-types
  • WC-2026-10-29-posterframe-url-version
  • WC-2026-10-29-start-user-default-section
  • WC-2026-10-30-health-boot-facts
  • WC-2026-10-30-maintenance-door-classes
  • WC-2026-10-30-observer-external-value-laws
  • WC-2026-10-30-observer-law-derived-edges
  • WC-2026-10-30-observer-target-not-host
  • WC-2026-10-30-observer-target-resolvers
  • WC-2026-10-30-ontology-file-digests
  • WC-2026-10-30-ontology-import-census-restore
  • WC-2026-10-30-portal-get-data-external-refresh
  • WC-2026-10-30-request-config-default-page-size
  • WC-2026-10-30-restore-point-release-identity
  • WC-2026-10-30-search-referenced-by-leaf
  • WC-2026-10-30-section-record-row-box
  • WC-2026-10-30-sort-data-key-range
  • WC-2026-10-30-tools-registry-import-switch-retired
  • WC-2026-10-30-update-door-refusals
  • WC-2026-10-30-update-ontology-backup-gate
  • WC-2026-10-30-update-run
  • WC-2026-10-30-update-sources-server-held
  • WC-2026-10-31-portal-order-model
  • WC-2026-10-31-transliterate-value-emitted
  • WC-2026-10-31-update-rung-above-code
  • WC-2026-11-01-menu-dead-area
  • WC-2026-11-01-update-plan-declared
  • WC-2026-11-02-ui-preferences

7.0.0-beta.7 — 2026-10-25

278 commits: git log --no-merges v7.0.0-beta.6..v7.0.0-beta.7

Action needed when you update

  • Dédalo prereleases are now published releases — from 7.0.0-beta.7 on, each beta is a fixed, signed version you can install and update.
  • The Docker and Trial installer installs the image of its own release — 7.0.0-beta.7 for a beta — and a stack built from an earlier beta needs --skip-version-check once on its next image update.

For users

Security

  • The change history of a password field no longer shows the stored password hash.

    The Value column of a record's change history showed, for each past change of a password field, the stored (encrypted) password itself, to administrators and to any user allowed to read that field. It now shows the mask ****************, as the field does everywhere else. Restoring a past password from the Time Machine is unchanged.

    Wire contract: WC-2026-09-30-password-hash-never-served.

Removed

  • The hidden 'Calm Scholarly' design preview is removed.

    Dédalo carried a second, optional interface design that only a developer could reach, by adding ?design_preview=1 to an address or by setting one browser value. It was a temporary vehicle for a redesign, and that work is now part of the standard interface.

    The preview is gone: the design switch in the menu, the startup script that applied it, and the second palette behind it. Nothing changes for a user who never opted in. A user who did opt in returns to the standard design, and their browser keeps a setting that no longer has any effect.

    Wire contract: WC-2026-10-19-design-line-removed.

Changed

  • Image quality selector now appears only on hover or on an active component; touch screens keep it visible.

    Before, the quality chip ("1.5MB ▴▾") sat permanently on top of the image in the edit view, overlaying the picture's top-right corner. Now it stays hidden while the component rests: it appears when the pointer is over the component, when the component is active (clicked), or when it takes keyboard focus, and it fades back out.

    The chip is still keyboard-reachable (it keeps its place in the tab order, and focusing it reveals it), and at rest it no longer intercepts clicks in that corner of the image. On touch screens — which have no hover — the chip keeps working as before, always visible.

    Applies to the edit view of component_image only.

  • Opening a rich-text field no longer downloads the editor on the first click.

    Clicking into a description, transcription or any other rich-text field used to start the download and parsing of the editor itself, so the first click sat there for a moment before the cursor appeared.

    The editor's engine is now fetched in the background while the edit form settles — once per page, only when the form actually shows an editable rich-text field — and the first click only creates the editor for that field. Fields are still not loaded until used, so a form with many text areas does not carry one editor per field. On a metered connection (browser "data saver") the background fetch is skipped.

  • CSV import guesses the target section from more file names and remembers your correction.

    In the Import Dédalo CSV tool, the target section tipo is now read from any word of the file name that has the tipo shape, so names such as export_Mint_07-10-2026-numisdata6-raw.csv resolve to numisdata6 instead of raw. When a name holds several tipos, the field offers them as suggestions.

    A section tipo typed by hand is remembered in your browser for that file name and comes back after a reload, marked remembered; click the mark to forget it. Nothing is stored on the server.

  • The Import files tool's options panel reads as three clear blocks, and the import button stays in reach.

    The panel at the top of Tools → Import files spread five groups of different heights and widths across the bare page, each label on its own line height, so the options read as unrelated fields. It is now one card above the file queue, in three columns: what the files go into (Processor, Target field and Quality, stacked), Replace existing files, and New files. Each option is a label above its own control, and each group heading has room under it. When the tool is narrower than three columns allow (below 1024 px wide, or in a narrow window), the columns become one, in that same order.

    The Import button used to sit far below the last field, off screen on a long queue. It now stays in a bar along the bottom of the tool while you scroll.

  • The inspector's Data box keeps its size when you select another component, and you can resize it.

    Before, the inspector rebuilt the component information each time you selected a component. The Data box briefly collapsed, then grew or shrank to fit that component's value, which moved Project, Relations and Latest changes up and down.

    Now the Data box has a fixed height of 10rem whatever the value. Drag its bottom-right corner to make it taller or shorter: the height is kept across reloads. Double-click the box to fit it to its content for a moment. The Copy button sits in the Data header, so you can reach it without scrolling, and it also works when a value is too large to display. An empty value now reads "Empty".

  • A relation table is framed, with no rules between its column titles

    The column titles of a relation table sat in a box each, so every pair of neighbours drew two vertical rules between them and the titles read as a grid of sealed cells. Those boxes stopped at the header too, leaving the left rule hanging above the rows.

    The titles are labels, not fields, so the rules between them are gone. The table's own edges carry the frame instead: one rule down each side, from the titles to the last row, plus the line above the titles. No column moves.

  • Time machine tool list on phone renders as one card per version

    The time machine list (tool_time_machine) on small screens no longer stacks every field of every version into one unbounded run of label/value rows. Each version now renders as a distinct bordered card with its ID header on top, the restore icon takes the tool colour, and fields the snapshot does not carry are hidden (they remain visible in the desktop table).

  • On a phone, the time machine marks the picked version across the whole row and brings its preview into view.

    Two things got in the way of reading a long history on a phone.

    The picked version is marked in orange, but only the value half of its Id row was filled, so pale strips of card showed at either side of the mark and the row read as a loose chip rather than the row you are on. The mark now runs the full width of the row, Id label included.

    And picking a version deep in the list left what you picked out of sight above you: the preview of the chosen version and the Apply and save button both sat over the fold. Picking a version now scrolls its preview to the top of the tool, just under its header, so the version you are about to restore is the first thing you see.

  • On a phone, the time machine's two version panels, its Apply action and its page counter now stack into one readable column.

    The time machine band at the top of the tool compared the live version and the chosen snapshot in two half-width columns. On a phone that left the value hugging the left edge with an empty half beside it, floated the Apply and save button up into the version labels, and wrapped the page counter's chevrons onto a line of their own.

    Now every block gets the full width: the live value first, the chosen snapshot under it, then the actions. Each version is announced by a strip above its value — Now for the live one, the snapshot's date and time for the chosen one. The still-empty snapshot panel stays out of the way until a version is picked, Apply and save takes a row of its own, and the page counter keeps its chevrons on one line. On a desktop nothing changes.

  • Checkboxes and radio buttons fill with a deeper orange so the white tick shows in every browser

    A ticked checkbox or a picked radio button no longer depends on which browser you use. The checked fill was the bright brand orange, and each browser picks the tick's colour from it: Chrome and Edge drew a near-black tick, Firefox and Safari a white one, so the same box disagreed from one browser to the next.

    The checked fill is now a deeper Dédalo orange (#db760f), dark enough that every browser draws the tick — and the radio dot — in white, and it clears the 3:1 contrast floor a checked control needs to read as checked. The app's own drawn boxes in the dark theme take the same white tick. Unchecked boxes, focus rings and the rest of the bright accent are unchanged.

  • The date field's calendar now writes the dates the catalogue actually stores — including year-only, year+month and BCE (negative) years — with localized month names and full keyboard and touch support.

    Before: clicking the calendar opened a third-party picker that only understood complete CE dates in a four-digit year (it also showed English month names regardless of the interface language). Opening it on a year-only, year+month or BCE value fell back to today, and picking a day there replaced the value with a full modern date.

    Now: the calendar is built into the component and speaks the same date model the catalogue stores. The year field accepts any signed year (with a CE/BCE toggle and decade steps), the month and weekday names follow the interface language, and the footer commits the precision you need — the bare year (-205), the year and month (04/2022) or a full day. The picker is fully keyboard driven (arrows, PageUp/PageDown, Home/End, Escape; the calendar button is a tab stop, and Alt+ArrowDown opens it from the field; pressing the calendar button again closes the open calendar instead of reopening it), its touch targets grow on phones and tablets, and it follows the light and dark themes. In the search panel the calendar button appears on the field's top-left corner when the field takes focus, a range's two dates share the row in full and its <> hangs under the gap between them, so the card keeps the same height as every other search field. The separate clock picker now uses the operating system's own time wheels on touch devices, and its button closes it on a second press too.

Added

  • The profile permission tree can be filtered by name or tipo.

    A profile's Access and permissions tree lists the whole ontology, and a node several levels down (a component inside a section inside an area) was hard to find: you had to open the branches one by one.

    The tree now has a search box above it. Typing part of a node's name or its tipo keeps that node together with the path to it, and opens the branches on the way down, so the permission radios are in front of you instead of hidden.

    • Matching ignores case and accents (administracion finds Administración).
    • Several words must all match the same node (user name, not one each).
    • Matching nodes are highlighted in yellow, so a result is easy to spot among the open branches that lead to it.
    • The counter shows how many nodes match; a very broad query renders the first 500 matches and says so — type more to narrow it.
    • The filter only changes what is shown. Permission values, and the propagation of a checkbox or a global x / r / rw radio, still apply to every node, visible or not. Clearing the box restores the tree exactly as you had it.
    • Jumping to a section from Latest changes clears the filter: that jump exists to show you the section, and a query could hide it.
  • The address bar now names the open record, so an edit page can be copied and reopened

    While editing a record, the address bar now carries its id (?tipo=…&mode=edit&id=…), so copying the URL and opening it later — or in another tab — lands on that record. Before, the URL only named the section, and reopening it showed whatever record the browser's stored edit position happened to point at.

    The same URL is also the reliable form for a bug report or an assistant prompt: it identifies the section and the record without the search that produced them.

    Alongside it, two long-standing traps around pasted record links are gone: a link that named a record could come up with an empty form ("no records found") when the browser had a stored edit position, and leaving such a page for the list could show a list of one. The list itself is unchanged, and paginating inside an edit page still works; a reloaded or shared record link simply shows that record, one at a time.

  • The image vector editor now supports undo and redo

    Cmd+Z (Ctrl+Z on Windows) undoes the last change in the image vector editor, and Shift+Cmd+Z / Ctrl+Shift+Z (Ctrl+Y on Windows) redoes it. Drawing, moving or resizing a shape, deleting, colour changes, moving the raster image, resetting the raster and adding/deleting layers are covered. Layer visibility, layer names, the colour swatch and zoom are editor settings and are not part of the history. See component_image.

  • The inspector shows again who created, last modified and published a record, and when.

    In edit mode, the inspector's Information panel always showed - for Created and Modified and Never for Published. It now shows the date and user of each, and for publication the first and last time the record was published. If the record was later withdrawn from publication, the panel says so. The values load just after the record appears, so opening a record is not slower.

    Wire contract: WC-2026-10-19-inspector-record-info.

  • Export: columns are added and reordered by tap on a phone

    The Export tool built its column list by dragging: a field dragged from the available-fields list onto the Active elements list to add a column, and a selected column dragged up or down to reorder it. A finger cannot drag, so on a phone neither could be done at all — the tool opened, but its columns could not be chosen.

    On a touch screen each field in the available-fields list now carries a + that adds it as a column on a tap, and every selected column carries up / down buttons and a ✕ that move or remove it. Both do exactly what the drag did (a field already added is pointed at rather than duplicated). On a computer nothing changes: the drag is still the gesture there.

    The phone view is also straightened out around that. The fields list fills the whole width (it shared its row with the other, empty drill column), the empty column list names the gesture it has (it points at the + instead of saying "drag a field here"), the +, the remove ✕ and the option checkboxes are full 44px tap targets, the export preview fills its panel, and the result area no longer reserves a blank half-screen before any export is run.

    The Export presets editor (and its Search twin) opens as the full-width sheet on a phone — maximum room for the form — while staying the small card on a computer. And a saved preset is one tidy line in the panel on a phone — the apply ring, the name, then Edit and Delete — instead of the desktop columns squeezed to slivers with their labels stacked one letter per line.

  • On a phone, a drilled search field level shows its path

    In the search Fields list, drilling into a link field (a portal) opens the target section as a new level. On a phone each level replaces the previous one, so the list header now shows where you are: the section path, e.g. People / Projects, instead of only the current section name. When the path is too long for the header it is shortened on the left (… / Level two / Projects), so the deepest — most relevant — terms stay readable. On a desktop, where the levels stay side by side, the header is unchanged.

  • On a phone, moving between search field levels now slides

    In the search Fields list, opening a link field (a portal) to reach a deeper section — or returning with the level's × — now animates: the incoming level slides in from the side it comes from (right when drilling in, left when going back) instead of the list swapping with no motion. It makes it clear that you are moving between levels of the field selector, not replacing its contents. Desktop (side-by-side levels) is unchanged.

Fixed

  • The autocomplete result list no longer opens as a blank band

    An autocomplete field painted its dropdown the moment the search answered, but before the matched records had been built: every result was an empty row. On a slow render the open list became a tall blank panel over the form, with rows appearing one by one — which read as broken layout.

    Rows are now appended only once their record is rendered, in result order, and the picker answers only when the whole list is ready: the open list is either empty or complete, never a band of blank rows. A row that fails to render is skipped and named in the console instead of leaving another blank band. The fix is in core/services/service_autocomplete/js/view_default_autocomplete.js.

  • component_iri edit row lays out on one aligned line

    The component_iri edit row mixed wrapped widths (30% + 20% + 50% plus the active checkbox forced an overflow) so the label, title and url fields scattered across misaligned lines. The three now share one line with stable proportions when the container is wide, and wrap to neat full-width lines when it is not. The active checkbox also joins the component's hide-at-rest rule: it is only visible on hover, on the active component or on keyboard focus. Checkbox and remove/link icons render inside one flow cluster at the row end (content_actions) that wraps with the row, so the icons no longer overlay the inputs on narrow, wrapped rows.

  • A read-only field's value starts under its own label again

    When a text field is not editable — a view-only user, a Time Machine preview, a print view — its value sat about half a character to the left of the label above it, while an editable field's value sat exactly on it. The two looked like two different grids in the same record.

    The value now starts on the same left inset as its label, editable or not. The alignment covers every single-line field that renders as plain text (text, number, date, e-mail, URL), not just the one that was reported.

  • Rich-text fields in a language with no bundled editor translation now open in English instead of not opening at all.

    The editor that backs the html_text view ships a UI translation only for some languages (70 of the 138 application languages). For the rest — Georgian, Welsh, Icelandic, Tamil and others — the editor refused to start, so those fields could not be opened or edited at all.

    The editor now falls back to its English interface when its translation file is not available; the field opens and works as usual. Content and data language are unaffected.

  • Change history no longer fails, and restore no longer writes a broken value, when an old history entry holds a malformed link value.

    Opening the history of a linked field (for example a user's profile in the inspector's Component history) could answer with an unexpected error, and no entry at all was shown, when one very old history entry stored the value in a shape earlier versions used (a bare record number instead of a link). The history now opens: every well-formed entry shows as usual, and an entry whose stored value cannot be read as a link is shown empty instead (the response flags it with the reason, for developers). The same entry previews empty in the Time Machine tool, and restoring it (the field alone, the whole record, or a deleted record) is refused with a message saying the entry is in an old format, instead of writing the broken value into the record (before, the restore stored it and the field showed nothing usable). The current value stays as it is. The stored history itself is not changed.

    Wire contract: WC-2026-10-24-tm-malformed-relation-cell.

  • Image derivatives are encoded at full quality again — sharper web and list images, and somewhat larger files

    Every generated image size (1.5MB, 6MB, thumb, the alternative-format twins) was compressed at quality 82, a value inherited from the first version of this engine. The previous engine encoded images at 90, and inspection of the same picture side by side shows the difference plainly: the web size looked softer and smoothed-over, most visibly on fine texture and lettering.

    Derived images are now encoded at 90 again, matching the previous engine. Expect the 1.5MB and other jpg sizes to grow by roughly 40–75 % for the same pixel dimensions (on the reference record, 55,988 → 97,453 bytes); thumbnails grow much less. The original quality's viewable copy was already written at 100 and is unchanged.

    Nothing is rebuilt automatically: existing records keep their current files until a master is uploaded again or the record is regenerated from the media versions tool (Regenerate files after deleting the size you want rebuilt).

  • Deleting an image's last file now clears its vector drawing, so a new upload starts fresh

    Deleting every quality of an image used to remove the rendered SVG overlay but keep the vector layers in the record; uploading a new image afterwards showed the old shapes over the new picture. The delete that removes the last file now clears the stored layers too. The cleared drawing is kept in the Time Machine, so the previous version can be recovered. See component_image.

    Wire contract: WC-2026-08-09-image-svg-envelope-preserved.

  • An image uploaded in a format the browser cannot show (TIFF, PSD, PNG…) keeps its full-resolution original AND regains the viewable copy, so the "original" quality is selectable again

    Dédalo stores an image in tiers, and the best one is the original. Uploading a .tif (or .psd, or any format a browser cannot draw) left that tier holding only the file you uploaded: the engine indexed it and let you download it, but it had no picture to show. The image component's quality selector only lists tiers that hold the type's standard format (jpg), so the original quality disappeared from the selector altogether, and the media-versions panel's preview cell for it stayed empty.

    The original tier now also carries the standard-format copy beside your upload — rsc29_rsc170_1.tif and rsc29_rsc170_1.jpg side by side, the pair Dédalo has always stored. The copy is made when the file is uploaded into original or modified (including a colleague's retouched master), and it is what every reader resolves when it picks a tier's picture: the quality selector, the media versions panel, list and mosaic views.

    Your uploaded file is never modified or replaced, and the copy is only a viewable companion: every derived size is still built from the original file itself. It is encoded at maximum quality (not the lighter web compression), so a large scan keeps its detail when you open the original tier.

    Records uploaded before this fix heal with one Regenerate files click in the media versions tool (or the next cache update sweep).

    One visible consequence of restoring the established behaviour: a media export or download that asks for the original quality of such a record now gives the viewable .jpg copy (the standard format), not the raw .tif. The raw file remains listed and downloadable as its own version in the media versions panel.

  • The propagate tool on a Project filter now offers the same options as the component.

    Opening Propagate component data on a Project (filter) component showed the selector empty — no checkbox, so nothing could be propagated. The tool's editable panel is a throwaway copy of the component that is saved once before it renders, and the server's answer to that save carried the component's value but not its option list (the projects you are authorized to use). Only the Project filter family was affected: selects, radio buttons and the rest kept their options.

    The server now answers every save of a filter component with the same options its edit view offers — the propagate tool panel, the record edit after a save, and the staged forms all show the project tree as before.

  • An image's drawing envelope is removed with the record's last file, not with one quality

    Every image is displayed through a small SVG envelope that wraps the record's web-quality picture. Removing the record's files never removed that envelope, so with every quality deleted the editor kept loading a wrapper whose picture was gone and the browser painted its broken-image icon — in the edit view and in the media versions panel.

    The envelope now follows the record's files, with the same rule the files themselves follow: it is moved to a deleted folder (recoverable, exactly like a deleted quality file) only when the record has no file left in any quality — no original, no retouched master, no web size, no thumbnail, no alternative format. Deleting one quality, even the web size, leaves the envelope where it is, because the record still holds a picture.

    A record emptied before this fix keeps its leftover envelope until it is regenerated: one Regenerate files click in the media versions tool clears it, and the record then shows the normal empty placeholder.

  • Turning pages in Latest changes no longer fails with a permission error for users who are not administrators.

    A user allowed to see a section's change history could open the inspector's Latest changes, but moving to the next page of the list answered with a permission error. History lists are read-only, so they no longer take part in field locking at all, and paging through them works for every user the profile allows.

    Wire contract: WC-2026-10-23-own-lock-cleanup-ungated.

  • The image vector editor now deletes shapes and layers reliably

    Deleting a selected shape with Delete/Backspace did nothing when several shapes were selected or when focus was on the image-quality selector. Deleting a vector layer could leave it in the saved drawing, and the raster layer's trash button could duplicate its contents. Delete now works for single and multiple selections from the quality selector, the base photograph cannot be deleted by accident, a deleted layer is removed from the saved drawing, and the raster trash empties the annotations while keeping the photograph. See component_image.

  • Saving a drawing in the image vector editor keeps the image visible

    Clicking save in the image vector editor no longer replaces the picture with a broken-image placeholder. The drawing was saved correctly all along; only the editor's live view lost the image until the record was reloaded. Switching the image quality now also keeps working on images that carry a vector drawing.

  • The acquisition tools no longer start an import while a new Preview is loading

    In the auction-catalogue and journal acquisition tools, clicking Preview again left the previous review on screen, with its Confirm import still enabled, until the new preview arrived. An import started in that moment was then hidden by the new review, and the new review's Confirm import could start a second import beside the first. The previous review now disappears as soon as you click Preview, and Confirm import does nothing while a preview is loading.

  • Remove background works again in the image rotation tool.

    Remove background failed every time, with a bare Worker error in the panel and nothing else. Three faults sat behind it, all client-side: the worker was requested at a path one directory above the application root, so the browser never loaded it; the model it asked for (briaai/RMBG-1.4) is not loadable by the bundled Transformers.js runtime; and the model's output shape had changed under it, so even a successful run would have thrown instead of returning the cut-out.

    It now measures the subject with onnx-community/ISNet-ONNX, the same DIS/IS-Net family as before, and any failure reports its actual reason (missing model file, no WebGPU, worker error) instead of a blank banner. A failed run no longer claims "Background removal completed" and no longer leaves the button disabled.

    Administrators: the weights come from the install's own model store — never a CDN — so a store seeded before today has to add onnx-community/ISNet-ONNX (176 MB fp32: config.json, preprocessor_config.json, onnx/model.onnx) to <private>/ai_models/. Until it is there the tool says so plainly instead of hanging.

  • Unpublishing every record of an RDF, XML or Markdown publication now also removes its combined file and ZIP

    An RDF, XML or Markdown publication writes one file per record. It also writes a combined document (RDF and XML only) and a ZIP archive, both built from the records the run published. If a run published nothing, for example because every record in it had been unpublished, the run removed the per-record files but kept the combined document and the ZIP from the previous run. Those files still held the unpublished records, so anyone could still download them from the public files directory. The run still reported that it had completed. Now a run that publishes no records removes the previous combined document and ZIP as well.

  • A refused side panel no longer takes over the whole page.

    When a part of the page that a user may not open — for example a history block in the inspector — was refused, the whole page switched to the "You don't have permission to access this page" panel and stayed there: every other block showed the same panel and navigating elsewhere did not recover. The refusal now stays inside the part that was refused, the rest of the page keeps working, and moving to another section clears a page-level refusal. See errors.

  • The References panel and its graph open for every user who can read the record, and their page counts add up

    A user who was not the superuser got Insufficient permissions when opening a record's References panel or its relations graph, even with read access to the record. They now open for anyone who can read the record, listing only the referring records that user may see. The total shown by the panel's paginator now matches its pages: before, a user who could not see some referring records got short pages and could never reach the total, and counting those records grew slower with every record the user could not see. See relation_list.

    Wire contract: WC-2026-10-22-related-search-one-scope.

  • Users who are allowed to see a section's change history can open Latest changes and Component history again.

    A user who is not an administrator but whose profile grants a section's history list got a permission error in the inspector's Latest changes and Component history. The history now opens for every user the profile allows, also when the history list sits inside a group of the section. Users without that permission no longer see the two blocks at all, and a block that cannot load shows one short line inside the block instead of a full-page error. The record counter of a history list follows the same permission. The history follows the same rule as the Time Machine tool: a user allowed to see a section's history sees the changes of every record of that section, also of records outside their projects and of deleted records. A history list shows only the section it was opened for, for every user: a search of another section remembered in the session no longer replaces it after a reload. An administrator's own Time Machine list, opened with no section, keeps its saved search across a reload. The history never shows the past values of a field the user's profile does not let them read on the record: those cells stay empty, the rest of the change (who, when, which field) is shown.

    Wire contract: WC-2026-10-22-tm-scope-whole-request.

  • The acquisition tools no longer let you start a second import while the first one's outcome is unknown

    In the auction-catalogue and journal acquisition tools, Confirm import stays disabled while the server has not said what an import did. Three ways around that could still start a second import beside the first:

    • Running Preview again replaced the review, including its disabled Confirm, with a new one you could click. Preview is now disabled until the import's outcome is known, or until you choose to try again.
    • An import that ended without saying what it did turned Confirm back on by itself. That happens when the server process running it stopped, when it was stopped partway, or when the server could no longer find it. The panel now shows the server's own message, asks you to check the records, and turns Confirm back on only when you say so.
    • Check my jobs offered "It did not start: allow a new attempt" even when it listed an import of the same kind that had just finished, which might be the one you started. You now open that import first to see what it did. When no import of that kind is running or finished recently, the button now says only that, and leaves the decision to you.
  • The acquisition tools no longer go silent when a job's progress is lost, and the move panels show a refused run.

    In the auction-catalogue and journal acquisition tools, if the connection that reports a preview's or an import's progress dropped before the job finished, the panel showed nothing more and re-enabled its button, although the job could still be running on the server. Clicking again could start a second import beside the first. The panel now says that it lost the job's status and that the job may still be running, keeps the button disabled, and offers Check again, which reconnects to the same job.

    In the auction-catalogue tool's company picker, a slow first search for the auction house could answer after a newer search you had typed, and replace your choice of Entity with the old one. Only the latest search now counts. A search that fails is shown as an error, not as "no candidates" with Create new Entity already selected. Confirm also waits until the Entity is chosen, so an import never drops the auction's company link without telling you.

    In the maintenance move panels (move TLD, language, locators, to portal, to table), a run the server refused, for example because of a bad option, used to show nothing in the panel. The panel now shows the reason, the server's own sentence, and the request id.

    For developers: with SHOW_DEBUG on, the browser console's request line now lists only the names of a request's options and hides their values. Before, it hid only some secret keys and printed the login password.

  • The acquisition tools show why a preview or an import failed, instead of "Done."

    In the auction-catalogue and journal acquisition tools, a preview or an import that failed on the server was reported as "Done.", and the panel then stopped responding for that run. Pasting a URL the tool does not support, a page that could not be read, or an import the server refused all looked like a success.

    The panel now shows the reason, in your language, with the server's own sentence when it says more — for example Only OAI-PMH journal/repository URLs are supported right now. If your session expired while the job was running, the login window opens. A job you stopped is no longer reported as done either: the panel says it ended without a result.

    The per-record lines of an import summary state their failures the same way.

    In the maintenance move panels (move TLD, language, locators, to portal, to table), when two previews of the same files run at once, only the latest one can now reveal the Execute button. Before, an older clean preview could reveal it after your newer preview had failed.

  • The selected field is fully tinted in a light orange — its list surfaces included — and its label stays readable in both themes.

    When you clicked a field, only its label turned orange: the tint behind the component was #fffaf5, 1.02:1 on the light card — invisible, so an active field looked much like an inactive one.

    The active field now paints the palette's orange field tint on the whole component (label and value): a light cream, #fff7ef, in the light theme and a warm brown, #4a3424, in dark, following the theme a reader has chosen. The search panel's highlight of a field with a value uses the same tint, which is lighter than before (it was #fce8d2). Text you select is not painted in this tint: a selection has its own colour, which stands out against it (see the entry on selected text).

    The label on that tint is the palette's deep orange — 5.6:1 in both themes. The bright Dédalo orange it was painted with before is a fill colour: on the tint it measures 2.3:1, which is why the header of a selected Date or Text field was hard to read.

    Nothing paints a slab of its own over that tint any more. A portal's list draws a card — white in light, near-black in dark — for its rows to sit on, and each row carries a grey remove column; both sat as boxes in the middle of the lit field. Inside a selected field those surfaces no longer paint, at every depth, so a portal nested in a row behaves like the one holding it, and an empty portal reads as the fields around it. The rows, their columns and every control keep their place: only the paint goes, so nothing moves.

  • A record restored from an archive no longer shows the history of a deleted record that had the same id.

    Restoring an archive re-creates each record with the id it had when it was archived. If this installation once held a different record with that id, deleted since, the restored record's history used to list that deleted record's changes as if they were its own, and they could be restored onto it. A restored record's history now starts with the restore, as it already did for records imported with an explicit id. The deleted record's history is kept.

  • The assistant's find-or-create no longer duplicates a record under concurrent calls

    When two assistant requests asked for the same record at the same moment (find it by its fields, or create it), both could miss it and both create it, leaving two records with the same identity. The search is now asked again under a lock before anything is created, so the second request finds the record the first one created.

  • The "Report a problem" tab only appears for users whose profile grants the error-report tool.

    An administrator whose profile does not include Error report still saw the purple "Report a problem" tab on the right edge; clicking it only answered "The tool is not active or not authorized for your user".

    The server now tells the page whether the tool is granted (no extra request), and the tab shows only then, so it appears exactly for the users who can use it: those whose profile includes the tool, and the superuser. To give an administrator the tab, add Error report to their profile's tools.

    Wire contract: WC-2026-10-21-error-report-granted.

  • Stopping a background export now stops it at once, even while its record selection is still being computed.

    Pressing Stop on a background export used to wait until the database had finished computing the export's whole record selection — on a large selection, minutes in which the export kept a database connection busy for nothing. Now Stop (and the export lane's deadline, and a server restart) interrupts that computation in the database itself: the export ends as stopped straight away and its connection is freed.

    Wire contract: WC-2026-10-11-export-buffered-cap-and-admission.

  • The acquisition tools and move panels no longer re-enable an action when the server did not confirm whether the job started.

    In the auction-catalogue and journal acquisition tools, a preview or an import whose start the server never confirmed (the request timed out, the connection dropped, or the server answered that the same request was still in progress or its outcome was unknown) used to re-enable Confirm as if it had been refused. Clicking again could start a second import beside the first. The panel now says the job may be running and keeps the button disabled. Check my jobs lists your jobs of this kind: Show this job follows the one that started, and a new attempt is offered only when none of them is running or queued.

    The action buttons are now truly disabled while a job runs, so the keyboard can no longer start the same job twice. A job status that falls silent, or stops halfway through, is declared lost after 15 seconds, with Check again to reconnect to the same job.

    In the maintenance move panels (move TLD, language, locators, to portal, to table), a run whose start the server did not confirm is no longer shown as refused: the panel says it may have started and asks you to check before running it again.

  • Pasting into the search panel's Id field adds to what is already there instead of replacing it.

    In the search panel, pasting into a section's Id field replaced the whole field with the clipboard, so ids you had already typed were lost, and a single pasted line went in unfiltered, letters included.

    A paste now goes where the cursor is, or replaces only the selected text, like a paste anywhere else. A single line keeps only the characters the field accepts (digits and the search operators . , > < =). A list pasted from a spreadsheet column, one id per line, still becomes a comma-separated list, and a comma is added between it and the ids already on either side.

  • The reference picker of a text area separates the term and its thesaurus part (… | …), instead of gluing them.

    In the reference picker of a text area, a term whose record carries no ancestor chain showed the thesaurus part glued to it (LatinaAgrupador). The picker now separates the columns that actually render, so the same entry reads Latina | Agrupador, like the rest of the list.

  • A text area reference can link the same term from several references, and deleting a reused reference keeps the others linked.

    In the reference picker of a text area, giving a second reference a term that another reference already used was refused as a duplicate, yet the reference was still written into the text, pointing at a link the database never saved. The same term can now be linked from any number of references.

    Opening an existing reference (or draw tag) showed an empty New tag field instead of its current term, and pressing Apply there deleted the link of the reference. The picker now shows the current term, and Apply without changes keeps it.

    Reusing a reference (the Reuse tag list) is also clearer and safer: each entry shows its tag number and an icon, selecting one shows its term in the New tag field, and deleting or moving one of several text fragments that share a reused reference no longer unlinks the term the other fragments still point at. A reused reference also takes the label of the reference it reuses.

    Draw tags: Apply in the draw tag picker saves the chosen term again (it failed before), and Delete no longer fails on a record without draw links.

  • Update cache never overwrites an edit saved while it runs

    Update cache regenerates a field by saving its current value again. It used to read each record once, at the start of that record's turn, and save that copy later, possibly minutes later when a media field of the same record was rebuilt first. A change someone saved to the field in between was silently replaced by the old value, and a record deleted in between was brought back. Now each field is read and saved again in one step, with the record locked, so a change saved meanwhile is kept and a deleted record stays deleted. A record that stays locked by someone else is skipped and listed, and a re-save the system refuses is listed instead of being counted as done. A record whose stored value the system refuses to save again (for example an old value in a shape the field no longer accepts) is now listed and left untouched, and the run goes on with the next record: it used to stop the whole run at that record.

    Wire contract: WC-2026-10-21-update-cache-locked-resave.

  • Translatable fields in tool forms show the value in your data language, not always in Spanish

    Some forms build their fields by asking the server for each field's definition — the Hierarchy tool's Name, for example. When that request named no language, the server answered Spanish, so a record holding Unofficial toponymy in English and Toponimia no oficial in Spanish showed the Spanish value to a user working in English, and an edit there was saved into the Spanish value.

    Those fields now use your current data language, as every other field does.

    Wire contract: WC-2026-10-20-element-context-default-lang.

  • Clicking a field puts the cursor in it, every time

    Clicking into an Email or Number field while another field was selected sometimes selected the field but left no cursor in it: typing went nowhere until you clicked a second time. The same happened to Date, IRI and colour fields, and when moving into a field with the Tab key from a related-records list.

    Now the click or the Tab always leaves the cursor where you aimed it. Clicking the second value of a field no longer sends the cursor back to the first one, and clicking the label or the empty space of the selected field puts the cursor back in its first box. Lists of related records, check boxes, radio buttons and text editors keep their own behaviour, and the buttons of a field act as before.

  • Turning a list page no longer jumps the layout

    Every paginated list (section lists, portals, the inspector's Latest changes and Component history) replaced its rows instantly, and the new page is often a different height: a value that took one line on one page takes six on the next. The block snapped to the new height in one frame, so the page content below it jumped — on a phone, the whole form under your finger.

    The rows container now grows or shrinks over the page turn (200 ms, the same motion as the rest of the interface), and it is clipped while it does, so no row paints over the paginator. Users who ask their system for reduced motion get the instant swap instead. The paginator's own dim is deferred too: a fast page turn no longer blinks the arrows, and a slow one still fades.

  • Search: the drag-a-field slot is the same height as the field cards beside it

    The search panel's third slot (Drag a field here) was taller or shorter than the field cards next to it, so a row of filters looked ragged. Every card in a row now takes the row's height: the tallest field card, or the drop slot, sets it for all of them. A drop slot alone on a row still matches a field card. Its + and the one on Add group are the light outlined glyph, not the filled disc.

  • Thesaurus lists are sorted by name in the reader's language.

    The installer's Thesauri step and the Add hierarchy maintenance panel now list thesauri in the same order: alphabetically by their name in your language, with accents ignored, inside each typology group. Before, the installer sorted by code, so Spain (es) appeared between Eritrea and Ethiopia, and the panel used a third order that put accented names after Z.

  • Selected text is visibly highlighted in every field, active or not

    Dragging over the text of a field used to paint the range in the same pale orange as the surface behind it — exactly the tint of the field you had just activated, and nearly the white of every other field. You could not tell whether the text was selected.

    A selection now paints as a clear amber slab with dark ink (11.8:1) in the light theme, and as a stronger warm brown with white ink (5.1:1) in dark. It reads clearly against the active field's tint and against the plain card, in inputs and text editors alike.

  • Opening a record by link no longer lands on a one-row list.

    A link that names a record — ?tipo=<section>&section_id=<id>, which is what the inspector's Local Ontology pen, a list row's edit button and a job's record link all produce — used to open the record as a list of one: the columns of the section list, no edit form, no inspector. It now opens the record for editing, as it did before the engine rewrite.

    The cause was on the server: the first-boot request (start) received the record id but described the page element in list mode, and the page then inherited that mode into its own data request. A section link that carries a record id is now always answered in edit mode, whatever mode the link asks for. Links to a component or an area keep the mode they ask for.

  • Inspector history panels read as blocks on a phone or tablet

    Below the desktop break the inspector drops its rail and renders inline, where Latest changes and Component history kept a narrow multi-column table shape designed for the side panel. The column labels were squeezed into 15px-wide strips (one letter per line) and the values were pushed out of their cells.

    Each history entry is now one block: the date (orange) and author on the first line, what changed next, and the snapshot value last, separated by a hairline. The desktop side panel is unchanged.

  • The Section id search field fills its card like every other search field.

    In the search panel, the Id field of a section rendered its value box at its content width: the box stayed short and left dead space inside the card, while every other field's box spanned the card. It now fills the card like the rest, in both the empty and the valued (highlighted) state.

  • Orange warning buttons are readable — white text on a deeper orange in the light theme.

    Warning buttons were painted in the bright brand orange, with dark text. White text on that orange measured 2.44:1, under the readable minimum, so it could not be used.

    Warning buttons now use the same orange as the warning messages: white text and icon on a deeper orange in the light theme (4.62:1), near-black on a bright orange in the dark theme (6.76:1). Hovering deepens the colour and keeps the text readable (5.21:1 light, 7.86:1 dark). The section toolbar's warning buttons, transparent until you hover them, light up in the same colours.

  • Dark mode: the page canvas no longer sits above the cards, and empty-field hints are legible

    In dark mode, the page background was painted with the light theme's canvas grey. Because the dark palette inverts the grey ramp, that grey became lighter than every content surface: cards, the list body and the login card sank into a lighter field, and the search band around the filters merged with it. The canvas is now the role token meant for it, which is darker than every surface in dark mode and the same colour as before in light mode — the light theme renders byte-for-byte identically.

    Empty-field hints — a portal or dataframe's empty value (Etiqueta, Título, http://, the date DD/MM/YYYY masking), and the browser's own placeholder in every other input — were below the WCAG AA floor: 1.9:1 in light and 2.7:1 in dark for the portal hint, 3.1–3.9:1 for the rest, because the browser paints a placeholder with its own light-scheme grey and the app never themed it. Hints now carry the muted-text token in both themes: 8.1:1 on the dark card and 5.7:1 on the light one.

    The edit view's field placeholders were also dimmed by an extra 30% opacity that took them under AA; they now render at their label ink's own contrast.

  • Tool Export — the presets panel no longer shows a blank band between its header and the presets.

    The Export tool's presets panel kept an empty pagination block (39px of blank space) between the "Export presets" header and the first row, and between the header and the "no presets" message on an empty list. The collapsed paginator now takes no space; a preset list long enough to paginate still renders its pagination.

  • Plain-text filter values join terms with ', ' when the section map defines no separator

    component_filter in the 'text' list view smashed its selected terms together when the section context supplied no fields_separator ("MIB AntigüedadBibliografíaMIB medieval-moderno"). An absent separator now falls back to ', '; a separator the context does define — empty included — is respected as before.

  • List headers stay readable in Safari, even when rows have different heights

    In Safari, after paging through a list whose rows have different heights (for example the interviews list, with its identifying images and information widgets), the column headers could collapse into a thin, clipped strip and the first rows below them were hard to read. The header cells no longer let Safari recompute the header height from the data rows after a page change, so paging, sorting and late-loading widgets leave the header intact in every page.

    No action is needed on update.

  • The line under a list row is unbroken across empty columns

    A list row draws its separator as a 1px line under each cell. A cell that held no value was given a zero width, so it painted no line: in a relation table the separator stopped where the record's first empty column began, and the row read as if it ended there.

    An empty cell now keeps the width of its column, so the line continues to the end of the row. No column moves: the column widths come from the grid, not from the cells.

  • Mosaic hover: the record id is no longer cut off, and the publication switch is centred

    Hovering a mosaic tile in edit mode shows a small overlay on its top-left corner: the record id with its pen, and — in portals that carry one — the publication switch on the other side. Two things were wrong there.

    The id chip was a fixed cell, narrower than an id of five or more digits, so the leading digits of the number were cut off by the tile's edge: 104299 read 04299. The chip is now as wide as the id it holds, so the number is whole.

    The publication switch sat slightly left of and above the middle of its cell. Its value is centred in the cell now, so the switch rides the tile's corner evenly, and the id chip's own drag handle stays where it was: a band under the chip, revealed when you hover it.

    Nothing else moved: tile size, the record's image, the pen, the switch's colours and every other control are the same.

  • A portal row's edit cell now fills the row, like its trash cell.

    Hovering a row of a portal list — a component like Autoridad, or Monedas — lit two cells: the id cell turned green behind the number and its pen, the last cell turned red behind the trash. They were not the same box. The red covered the row's whole height; the green stopped at the icon's own height, leaving a strip of untouched row under it — more of the row on a two-line value, most of it on a tall media row.

    Both cells are the row's height now, so a row reads as one band and the pen and the trash are equally easy to hit. The number and its pen hold the row's first line, level with the trash icon at the other end, so a row still reads along a single line. Nothing else moved: rows, columns, their widths, the height a row takes and every other control are the same.

  • The date field's calendar button is a full-size tap target and no longer covers the field label

    On a touch screen the date field's small round calendar button — the one that opens the date picker — sat on top of the field's own label (for example Fecha de captación), so part of the label was hidden behind it, and the button itself was too small to tap reliably.

    On a phone the button now sits inside the field row, at the end of the date box, at the size of the app's other touch targets. The label and the date value are fully readable, and the button is easy to hit.

  • The Import files tool's options panel lines up on a phone

    The panel at the top of Tools → Import files — Processor, Target field, Quality, and the matching / new-files switches — was laid out for a wide window. On a phone its labels and their drop-downs landed at different left edges, the drop-downs ran off the right edge of the screen, the section headings were centred over left-aligned content, and each switch caption sat a full space away from its own switch.

    On a phone the panel is now one option per row: the label sits above its drop-down, every drop-down spans the full width and starts at the same left edge, the headings read left like the rows beneath them, and each switch row is a full-height tap target with its caption beside its switch. The suffix-to-field legend ("- A -> Obverse") drops to its own full-width line instead of squeezing in beside its caption. On a wider screen the panel is the three-block card described in its own entry.

  • Ontology and thesaurus search: the result highlight and the loading state are now in sync.

    Searching in the Ontology and Thesaurus areas showed a confusing sequence: the loading state ended before the results were highlighted, so the page looked finished and only afterwards the matched term popped in and the page scrolled, sometimes re-centering even when the term was already on screen.

    Now the whole sequence is one coherent transition: the loading dim lasts until the matched terms are highlighted, the page scrolls only when the first match is out of view (and never shifts sideways without need), a brief pulse marks the highlighted term even when no scroll happens, and the search panel's Apply button shows a busy state while the search runs.

  • Search on a phone: the fields panel's level header is as easy to tap as the fields themselves

    The Search panel's fields list lets you drill into a related section, and the grey band at the top of the list names the level you are in ("People / Projects") and carries the × that takes you back out of it. On a phone that band was about a third shorter than the field rows under it, so the × was the smallest thing on screen — a thumb aimed at it usually missed, and the way back out of a level was easy to lose.

    The band is now the same height as the field rows, and the × has a full-height tap target of its own at the end of it — the same target the rows give their + — so closing a level is as easy as adding a field. The group bands between the rows ("Identification"…) take the same height, so the whole list is one rhythm. On a computer nothing changes.

  • On a phone, the section list buttons and their show/hide arrow no longer overlap or jump around.

    The list toolbar of every section has a small arrow that shows or hides the row of action buttons (New, Delete, Tools). On a phone the action row used to wrap onto its own line and push that arrow onto a third line, alone and stuck to the left edge, while the collapsed state kept it on the right of the search row — so the arrow jumped across the screen as the row was shown or hidden.

    Now the arrow keeps the right edge of the search row in both states, the action buttons keep their own full row underneath, and the arrow is a normal (no longer rotated) button that is big enough to tap comfortably. On a desktop nothing changes.

    Screen readers also get the state of that button (aria-expanded) and the region it controls (aria-controls), which they did not before.

  • Tools on a phone use the phone's own spacing and header height; only part of the phone adjustments were reaching the screen.

    Every tool panel carries a set of adjustments for small screens: tighter page padding, a shorter header, a smaller tool name. Half of them did not reach the browser — the shared stylesheet declared them before the styles they were meant to override, so the desktop values won and each tool kept its desktop padding and header height on a phone. The result was a panel whose content started lower and whose header (which stays pinned while you scroll) took more of the screen than it was designed to.

    The panel now uses the phone measurements throughout: the body keeps a half-rem gutter instead of a full one, the header is a little shorter, the tool name sits at the phone size, and the header's controls band still spans the full width with its bottom edge flush with the header. Desktop layouts are unchanged.

  • The Update cache tool reads properly on a phone: one component per row, its options and model under it, all lined up.

    On a phone, Update cache squeezed three columns (component · regeneration options · model/tipo) into 360–600px. The component name wrapped over two or three lines, the long model names spilled past the right edge of the panel, and the column headings no longer sat above the columns they name — so the list read as a ragged staircase and the tap row for a component was about 30px tall.

    A phone now shows one component per row: the checkbox and its name on a 44px tap row, the model/tipo as a small subtitle under it, and any regeneration option (e.g. delete normalized files) as the last line of the row. Every secondary line hangs under the name's text, and the group headings keep only the group name, since the two column captions describe a layout a phone no longer has.

    The same three columns also line up with their headings again on wide screens: a column is now sized by the whole list rather than by each row separately, so the headings and the values beneath them cannot drift apart, and no model name is cut off or broken mid-word.

  • Upload buttons keep a clear gap between their icon and their label on a phone

    On a narrow screen the icon inside a labelled button was pushed to within two pixels of its text, so the button read as ⤒Start upload with the glyph touching the word — and the gap looked uneven from one button to the next, because one icon's drawing fills its box and another's does not.

    The upload tools' buttons (Add files, Start upload, Cancel upload, Delete file, and the same controls on each queued file) now keep the same icon-to-label spacing they have on a wide screen, so the icon reads as a separate mark before the word and the buttons line up as one set.

  • Each file in the upload queue is a card, so you can see where one file ends and the next begins

    In the upload queue (Tools → Import files, and the MARC 21 and Zotero importers) each file's thumbnail, name, option menus and buttons ran straight into the next file's, with nothing between them — on a phone the list read as one long column and it was easy to lose which name, which menu and which Delete button belonged to which file.

    Every file is now a bordered card: a white panel with a rounded border and a gap before the next one. A file's start and end are unmistakable, and its menus and buttons sit inside its own card.

  • Uploaded-file rows stack on a phone instead of running off the side

    The file rows in Tools → Import files (and in the MARC 21 and Zotero importers, which use the same upload widget) were laid out as a single desktop row — a thumbnail, the filename, the file's two option menus and its Start / Cancel / Delete controls side by side. On a phone that row is wider than the screen, so the part past the right edge was simply not there: the row's menus and its Delete and Cancel controls could not be reached at all, and a long filename was squeezed into a column so narrow it broke one letter per line.

    Each uploaded file now takes its own block on a phone: the thumbnail, then the filename and size, then the file's option menus full width, then its controls, one under the other. Nothing runs off the side, and every control is on screen.

  • The upload queue's "select all" checkbox says what it does again

    Above the list of files waiting to be uploaded there is a checkbox that ticks every file's Delete box at once. It had lost its caption, so it showed as a bare square with nothing to say what it selected — easy to miss and easy to misread.

    The checkbox carries its label again — All (in the interface language) — and the label is part of the control, so tapping the word ticks the box just like tapping the box itself. Each file's own Delete checkbox is unchanged.

  • The search panel keeps the values you typed when the page reloads.

    The search panel saves what you are building as you go, so a reload brings it back. That automatic save could store only the fields of the search and drop their values: after a reload the panel showed the right fields, all empty, and the search had to be typed again. This happened when the panel's working copy carried the save values switch set to No, a switch that belongs to named presets and cannot be changed on that working copy.

    The automatic save now always keeps the values. The save values switch still decides, as before, whether a named preset you save stores its values or only its fields (a template).

  • The autocomplete options panel no longer clips its section list

    In a search or edit field that offers an autocomplete, the gear panel that lists the searchable sections (and the related-record filters) laid its options out as a two-column table. A long section name such as Tipologías de ontología or Causas de restauración made that table wider than the panel itself, so the right-hand column was cut off at the panel edge: some sections were half-visible and, because the overflow was hidden rather than scrollable, some checkboxes could not be reached to deselect them one by one.

    The list is now a grid that fits the space it is given. It shows a single readable column on a narrow field and two or more columns once there is room for whole names, so no option is clipped and the panel stays inside its card.

  • Search option tooltips are legible in dark mode.

    Hovering a search field showed the "Search options" help tooltip as a bright, near-white panel against the dark interface. It now follows the theme like every other floating panel: a dark elevated card with a subtle border in dark mode, a light card in light mode.

  • The Tools menu in a section list opens its tools again on a phone.

    On a phone or any touch screen, opening Tools in a section list showed the menu, but tapping a tool in it (Export, Import, Diffusion, Update cache…) did nothing: the menu closed and no tool opened.

    The menu was dismissed by the first touch of the tap, before the tap had reached the row; on a touch screen that left the press landing on the list behind the menu instead of on the tool. It now closes on the same press that opens the tool, so a finger works as the mouse and the keyboard already did.

For administrators

Security

  • Names and values typed by one user can no longer run script in another user's browser

    Several places in the work interface inserted a stored value into the page as markup instead of text: the name of an image annotation layer in its Delete confirmation, the field labels of a saved or shared search preset, the record terms and field values in the graph view's node panel, the selected record's title in the autocomplete choice grid, the people offered by the transcription tool's Assign speakers dialog, and the user and record names listed by the Lock components maintenance panel. A value containing markup — for example a layer named <img src=x onerror=…> — ran as script for any other user who opened it. These values are now always shown as plain text, and the same rule now holds for every label, count and message the interface builds around a value: a confirmation dialog's title and its question, for example, are always text. Two internal lookups keyed by text a user controls (URL parameters and the autocomplete search cache) were also hardened so such a key cannot alter them. No action is needed on update.

  • Server hardening from the CodeQL review — the tag-badge address refuses over-long requests, rich text keeps only raster-image data: links, and literal &lt; text is no longer turned into <.

    • The address that draws the small tag badges in transcriptions (…/component_text_area/tag/?id=…) needs no login, and a long run of < in it made the server spend over a second of processing per request. Such an address (longer than 4096 characters, far beyond any real tag) is now refused with a 400 before it is read, and the tag clean-up itself runs in linear time — as do the other places that strip markup from text (search ordering, publication values, thesaurus empty checks, list previews, subtitle files, external-record markup, image annotation envelopes, merged RDF publications).
    • Rich text keeps a data: link in an attribute only when it is a raster image (PNG, JPEG, GIF, WebP, AVIF, BMP). Any other data: link — an SVG image, XML, plain text — is replaced by # when the text is saved. Descriptive attributes (alt, title, …) are not affected, so an alternative text that starts with "Data:" is kept.
    • Text in which a cataloguer literally typed &lt; is now shown, counted and indexed as &lt;, not as <: the indexation grid, the transcription character count, the semantic index and the import code lookup decode each entity once. The numeric spellings of the five special characters (&#60;, &#x27;, …) are decoded there too, exactly as the previous engine did.
    • Two small file races are closed: the update log and the media access markers are created in one step that never empties an existing file, and the staged upload name is size-checked and read through the same open file.

    Wire contract: WC-2026-10-25-tag-id-length-cap, WC-2026-10-25-rich-text-data-url-raster-only.

  • Third-party packages updated to their latest stable releases, closing every known advisory against them.

    The engine and the publication packages (the v2 publication API, the site builder and the publication-host agent) now install with no known security advisory, measured against the public advisory feed at release time. Twenty-one advisories are closed: body-parser, qs, undici, fast-uri, serialize-javascript, hono, ip-address, vite and zod move to their fixed releases, and nodemailer, which sends Dédalo's mail, moves to 10.2.1.

    Other notable moves in this release:

    • zod 4.6.7, the validation library, is now the only copy in every package.
    • Anthropic SDK 1.3.0 (was 0.131) for the Dédalo assistant. It speaks the newer API version 2026-10-21 and sends the API key in the Authorization header instead of X-Api-Key. Nothing changes for an installation that reaches the Anthropic API directly. If outbound traffic goes through a proxy or gateway that reads X-Api-Key, make it read Authorization instead.
    • Swagger UI 5.34.0 (was 5.33.1) and Scalar API reference 1.76.0 (was 1.72.4) on the v2 publication API's documentation pages. The legacy v1 API's Swagger page moves from 5.32.14 to the same 5.34.0 (see vendored library versions).
    • OpenTUI 0.6.8 (was 0.6.1), the terminal installer's interface library.
    • Biome 2.6.3 (was 2.5.15), the code checker. Nothing changes for an installation; this only affects development checks.
    • actions/upload-artifact 7.0.2, in the release and CI workflows.

    The Bun runtime pin stays at 1.4.3.

  • SQL the engine runs through psql no longer appears on the psql command line

    Several maintenance and installation doors (ontology import and export, ontology update snapshots, thesaurus export, the restore door, the installer's probes and the seed compiler) passed their SQL to psql as a command-line argument. Any local user could read those statements in the process list while they ran, and on Linux a statement over 128 KiB failed to start at all (E2BIG). The SQL now reaches psql on its standard input; the command line carries only connection and output options. The terminal installer's host-preparation plan keeps its short catalog probes on the command line, where its Review shows them.

  • A user can only assign a record to projects they belong to

    The Projects field lists only the projects the logged user belongs to, but the server used to store any project it was sent. A modified or out-of-date request could therefore place a record in a project the user is not a member of, or, on a user's own Projects field, grant a project the editor cannot see. Saving now refuses a project the user is not a member of with a message, and nothing is stored. Global administrators can still assign any project. Projects already on a record, including those added by colleagues in other projects, are never removed by this check. See component_filter.

    Wire contract: WC-2026-10-22-filter-project-pick-membership.

  • Site builder: a Claude Code turn turns off its auto-updater and telemetry in every confinement mode, not only when confined

    A site builder turn run by Claude Code now always starts the agent with its auto-updater and its non-essential traffic (telemetry, error reporting) switched off. Before, those two switches were applied only to a turn confined under its site identity: on an instance declared AGENT_CONFINEMENT=none, a Claude Code turn ran with the auto-updater on — which could replace the very binary the daemon had checked before the turn — and sent traffic to hosts the instance's plan does not name. opencode turns already had their equivalent switches in every mode. Nothing to configure.

  • Site builder: provision remove and provision adopt can no longer be hung by a pipe planted at a turn HOME's .gitconfig, a site's site.json or an instance marker

    The site builder provisioner reads a few files that an agent or a site identity can write — a turn HOME's .gitconfig when an instance is removed, a site's site.json and the instance marker (.dedalo_site_instance) at the root of an install when it is adopted. A named pipe planted at any of those names made the root command wait forever. Those reads now open without waiting and read only a regular file; anything else is treated as unreadable (a marker that is not a regular file counts as no marker), which each command already reports. Regular files — including an operator's symlinked password file — are read as before.

  • Outbound fetches judge an IPv6 address through every NAT64 prefix that holds it, so nested prefixes cannot reach an internal address.

    Before the server fetches a URL on a user's behalf, it judges an IPv6 address that carries an IPv4 address through NAT64 by that IPv4 address. NAT64 prefixes can sit one inside another: two translators, a name server answering with two addresses, or an outer and an inner prefix declared in DEDALO_NAT64_PREFIXES. The same address then carries a different IPv4 address through each, and the check read only the first prefix in its list. Depending on the order, an address could pass as a public site while, through the inner prefix the network actually routes by, it reached the cloud metadata endpoint.

    Now the address is read through every prefix that holds it, the most specific first, and it is refused if any of those readings is not a public address. Nothing to configure; the prefixes are declared as before (see Declaring the NAT64 prefixes).

  • Bun no longer fetches packages by name outside the lockfile.

    Running a Bun command in a checkout where bun install had not been run let Bun's auto-install download any imported package straight from the registry by name, ignoring bun.lock — so neither the pinned versions nor their integrity hashes applied. Auto-install is now off in the engine and in each publication package: a missing install stops with Cannot find package, and dependencies come only from bun install against the lockfile.

  • A local certificate authority can now only certify the install's own addresses, and its site certificate can be renewed without touching the staff computers.

    Every staff computer trusts the local certificate authority an install creates, and until now that authority could sign a certificate for any website: whoever obtained its key on the server could impersonate any site to those computers. A new authority is now restricted to the addresses it was issued for (the server name, any extra --host addresses, localhost) — exactly those names, not the names below them, so an authority for museo.lan cannot certify mail.museo.lan (a list where one name lies inside another is therefore refused) — and the key of the authority itself is deleted as soon as it has signed; the key that stays on the server can only sign under that restriction. The authority is also limited to website certificates, so that key cannot sign e-mail or software either.

    deploy/dedalo-tls-rotate.sh gains --mode renew: it re-issues the site certificate (now valid 397 days) from the same authority, so staff computers change nothing. It does nothing until the certificate is inside its last 30 days. On the Docker route, run it yourself once a year or from a monthly cron job (docs/install/docker.md, "Renewing the local-CA certificate"). --host can be given several times, for a LAN name or an IPv4 address beside the server name. When the new certificate is in place but the web server could not be reloaded (for example docker missing from cron's PATH), the script now exits with status 3 instead of reporting success, and every later run retries the reload until it works.

    An authority created before this change is not restricted. It keeps working and its certificate can be renewed (--mode renew --host <name>). Your next rotation (--mode local-ca) replaces it with a restricted one, which each staff computer then trusts in place of the old entry.

Changed

  • Ontology files move out of the code tree by default

    Unset, ONTOLOGY_DATA_IO_DIR now resolves to <private dir>/import/ontology. The in-tree install/import/ontology/ holds the release that ships with the code and is only read. Before, every install resolved to the in-tree directory, because the shipped release made it look non-empty. As a result, ontology updates wrote their staging files and recovery snapshots into the code tree, and the code updater refused every update with "Runtime data lives inside the code tree".

    An install whose in-tree directory holds anything that is not an ontology release file (a recovery/ folder or .staging from an earlier update, any other file), or that is an ontology server, keeps that directory and logs a warning at start-up. Nothing is moved. The decision is taken once and recorded. The check goes by file name: an export of your own that carries a release file's name counts as release content. It can be rebuilt from the database, and a code update keeps it in the backup of the old code.

    Setting ONTOLOGY_DATA_IO_DIR= to an empty value now means the same as leaving it unset. Before, the code updater read the empty value as the code directory and refused.

    One manual step for installs on an earlier release: the refusal is checked by the code that is already running, so an existing install still refuses this update once. Before updating, run mkdir <private dir>/import/ontology as the service user. If you want to keep the old install/import/ontology/7.0/recovery/ folder, move it there first.

    On an ontology server, copy before you create. It serves its exports from that directory, and the moment <private dir>/import/ontology exists it serves from there instead. Copy every <major.minor>/ directory from install/import/ontology/ into it, exports included, then restart. Never create the directory empty.

  • The installer field Database role is now Database user.

    Same field, same value: the PostgreSQL user (a login role) Dédalo connects as and that owns the database — the name you pass to psql -U. The install summary says database user too. Nothing to change on an existing installation.

  • Dédalo prereleases are now published releases — from 7.0.0-beta.7 on, each beta is a fixed, signed version you can install and update. (action needed)

    Until 7.0.0-beta.6, a beta was never published: its code reached an installation only as a developer build, and its Docker image only by building it yourself. From 7.0.0-beta.7 on, every prerelease (-alpha.N, -beta.N, -rc.N) is published the way a release is, under its own name: a code archive with a checksum on the code server, and a signed official image :7.0.0-beta.7. Neither is ever rebuilt, so a bug report that names a prerelease names exact code. An installation is offered prereleases only when it chooses Prereleases (or Developer builds) in the Update code panel, and only from a code server that publishes them; the default, Published releases, never offers one.

    Action needed for an installation running 7.0.0-beta.6 or an earlier beta: it reports the bare version 7.0.0 and does not know about prereleases, so it is offered neither 7.0.0-beta.7 nor, later, 7.0.0. To move on:

    • a Docker installation updates its image to 7.0.0-beta.7, as described in a stack installed from a beta before 7.0.0-beta.7;
    • an installation updated from the panel chooses Developer builds and installs the developer build of 7.0.0 the code server offers (see testing development work on a real installation). With Developer builds still chosen, it is then offered every published build of 7.0.0 — 7.0.0-beta.7 and the later prereleases (from a code server that publishes prereleases), and 7.0.0 once it is released — because a developer build cannot tell which of them are older than it. Once it runs one of them, its panel's Prereleases and Published releases choices work as usual.

    Wire contract: WC-2026-10-15-release-identity-from-tag, WC-2026-09-29-code-release-channel-refs.

  • The Docker and Trial installer installs the image of its own release — 7.0.0-beta.7 for a beta — and a stack built from an earlier beta needs --skip-version-check once on its next image update. (action needed)

    The installer now knows which release it is from the git tag of the checkout or release archive it runs from, and says so in its header and on the System check: 7.0.0, 7.0.0-beta.7 (prerelease), or 7.0.0.dev (development build) for a commit with no release tag. That decides the image of a Docker or Trial install:

    • A stable release or a prerelease installs the image tagged with its own name. A beta asks the registry for :7.0.0-beta.7, never for :7.0.0 — after the stable release that tag is a different image. The Review still asks the registry before yes and refuses an official image that is not published yet, with Build and Custom offered.
    • A development build has no official image: Build is preselected, and Official is refused on the Image screen without asking any registry.

    A local build is tagged and labelled with that name, and .dedalo.env pins it as DEDALO_VERSION (7.0.0-beta.7, or 7.0.0-dev for a development build).

    The image updater (deploy/dedalo-image-update.sh) accepts prerelease versions and orders them as releases are ordered: 7.0.0-beta.6 < 7.0.0-beta.7 < 7.0.0-rc.1 < 7.0.0. In build mode it checks out v7.0.0-beta.7 for a prerelease (or the ref you name with --ref) and refuses a commit that is not that release — a tag that is not annotated, or names another version — before anything is built; a local build's version label is always what the engine inside it reports. --skip-version-check now also waives a step back within one data version, and never one to an older data version. A running engine that cannot read a prerelease version is asked about its data version instead (7.0.0-dev for 7.0.0-beta.7), so a skipped X.Y.Z is still refused.

    Action needed for a stack installed from a beta up to 7.0.0-beta.6 with Build: it is pinned at the bare DEDALO_VERSION=7.0.0, and a prerelease comes before its stable release, so the move to 7.0.0-beta.7 reads as a downgrade. Its update script is still the old beta's, which refuses any prerelease version, so first move the stack's checkout to the new release (git fetch --tags && git checkout v7.0.0-beta.7), then run that one update with --skip-version-check; later updates need nothing. See a stack installed from a beta before 7.0.0-beta.7.

    Wire contract: WC-2026-10-15-release-identity-from-tag.

  • A prerelease build now calls itself by its tag (7.0.0-beta.7), and its data version is published beside it.

    Before, a build made from a beta tag reported the bare version of the release it precedes (7.0.0), so a beta could not be told apart from the stable release in /health, the About panels or the Update code panel. Now the program names itself by its git tag: 7.0.0 for a stable release, 7.0.0-beta.7 (or -alpha.N, -rc.N) for a prerelease, 7.0.0.dev for any untagged build.

    The data version is unchanged and still the bare 7.0.0: databases, updates and code servers keep comparing that. API consumers get it beside the name — page_globals.dedalo_data_version, and /health and the update status carry a new release_kind (stable, prerelease or dev) — so nothing needs to parse a version out of the name. A beta asking a code server for updates keeps sending the bare data version, which every code server, older ones included, understands.

    A code server reads the release name from the tag when it builds an archive, which needs git 2.35 or newer on the code server: with an older git, Build release now refuses (before, the archive was built and every installation of it called itself a developer build).

    Wire contract: WC-2026-10-15-release-identity-from-tag.

Added

  • The profile tool-permissions panel explains each tool — an info button unfolds its description.

    In a profile's Tool permissions field, every tool row now carries a small info button. It unfolds the tool's description (in the interface language) under the row and folds it again on a second click, so the panel stays compact while an administrator decides which tools to grant. Tools without a description show no button.

    Wire contract: WC-2026-10-21-tool-permissions-description.

  • "A code server can publish a prerelease (Build prerelease), and a published release is never rebuilt."

    The serve code panel has a third button, Build prerelease, beside Build release. It builds the newest prerelease tag (for example v7.0.1-beta.2) that is newer than the newest release tag, into <X>/<X.Y>/7.0.1-beta.2.zip with its .sha256 file. The Prerelease tag row names the tag before you press the button. When there is no such tag, the row says so and offers no button. Build release still builds only stable vX.Y.Z tags.

    A built release or prerelease cannot be built again. If its .zip and .sha256 are already on disk, the build is refused and the files are not changed. To publish different code, tag a new release. Before the code server publishes an archive, it checks that the archive gives the tag's name as its version. A tag made without -a (a lightweight tag) fails this check, so the build is refused. A prerelease tag made before this check existed (up to v7.0.0-beta.6) cannot pass it either, so the panel does not offer it; the first prerelease that can be built is one tagged after it.

    For developers: the pre-push hook now refuses a lightweight release tag. It also asks for a full green gate for a prerelease tag, in the same way as for a stable tag. bun run push:release-tag now takes the pushed tag object as a third argument.

    Wire contract: WC-2026-10-15-release-identity-from-tag.

  • "Code updates move between prereleases of one version: beta to later beta, beta to its release."

    An installation that runs a prerelease can now install a later prerelease of the same version, or that version's release, from the Update code panel or with deploy/dedalo-image-update.sh: 7.0.0-beta.6 → 7.0.0-beta.7 → 7.0.0-rc.1 → 7.0.0. These moves need no data update, and prereleases may be skipped. Moving back (7.0.0-beta.7 → 7.0.0-beta.6, or from 7.0.0 to one of its prereleases) is refused. A move to the next version still goes one version at a time; its data update runs once, on the first build of that version you install.

    A prerelease needs the Prereleases (or Developer builds) choice of the panel. On a container installation the panel can now request a prerelease image too, and the running engine checks a prerelease version itself instead of refusing it as malformed.

    After the restart, the panel reports an update as landed only when the server answers with the exact version it installed; a server that answers with another build of the same version is reported as rolled back.

    An update is refused, before anything is swapped, when the archive it would download is not the release it names, or when the downloaded archive calls itself another release.

    Wire contract: WC-2026-10-15-release-identity-from-tag.

  • "Prereleases have an official image: :7.0.1-beta.2, signed, and never under a release's tag."

    Pushing a prerelease tag (for example v7.0.1-beta.2) now publishes the official Docker image :7.0.1-beta.2, in the same way as a release: built from the tag's code, signed, with the same digest in every registry, and never rebuilt once published. The image is published only under its own name. It never replaces :7.0.1, which is the image of the release, and no image is tagged latest, 7 or 7.0.

    So the installer of a prerelease can now use the Official image: it asks the registry for :7.0.1-beta.2, and finds it once the image is published.

    The signature check accepts prerelease tags. If you verify images by hand with cosign verify, use the identity printed in the Docker installation guide, which now includes them. The old command still verifies release images, but it refuses a prerelease image.

    Wire contract: WC-2026-10-15-release-identity-from-tag.

  • "The update panel can ask for prereleases: choose published releases, prereleases or developer builds."

    The Developer builds checkbox of the Update code panel is now a choice of three: Published releases (the default), Prereleases and Developer builds. Each choice also offers what the one before it offers. A prerelease (for example 7.0.1-beta.2) is a fixed, checksum-verified build of a version that is not yet stable, and its row shows that full version. An installation that asks for published releases is never offered a prerelease.

    A code server offers prereleases only when it sets the new DEDALO_CODE_SERVER_PRERELEASE_CHANNEL=true, which is separate from DEDALO_CODE_SERVER_DEV_CHANNEL. When the code server does not offer part of what you chose (it is not set up for it, or it runs an older Dédalo version), it lists only what it does offer and the panel names the missing part above the list.

    An installation that runs a prerelease is also shown the later prereleases and the release of its own version, and can install them (see the entry on moving between prereleases).

    Wire contract: WC-2026-10-15-release-identity-from-tag.

  • The production installer can now make a local certificate authority, for an intranet with no public name.

    A museum that installs Dédalo with internet access and then runs it on an isolated intranet has no public name, so Let's Encrypt cannot reach it, and until now the only other choice on the production route was to make certificates by hand. The Web server & TLS screen now offers Local CA (--tls local_ca), as the Docker route already did. Other addresses (--tls-extra-names dedalo.lan,archivo.museo.lan) adds the other names staff type: the certificate names them and the site answers to them.

    On an intranet with no local DNS, Other addresses can also list the server's IPv4 addresses (--tls-extra-names dedalo.lan,192.168.1.20), on both routes: staff then open https://192.168.1.20/, and http:// to it redirects to https://. A browser sends no server name for an IP address, so on the production route the instance claims the address in the installer's host-wide catch-all, which hands a handshake to that address the instance's certificate. Every other site on the server keeps answering by name, on that address too, and a handshake to an address no instance claims is refused as before. The System check refuses an address that is not this server's, one another instance already claims, one a site of yours listens on itself, a site of yours on port 443 that loads before the catch-all (it would answer visits by IP address), and IPv6. On nginx the claiming instance's site key becomes readable by the web server's group, because nginx loads that certificate per connection; on Apache, one instance per server can claim addresses. See Reaching an instance by IP address.

    The installer makes the authority and the site certificate in /opt/dedalo/instances/<i>/tls/, with the same script the Docker route and every rotation use. The authority can only certify the install's own addresses, exactly. When the install ends, the To do on this host list names the authority's file and its SHA-256 fingerprint, how to trust it once on Windows, macOS, Linux and Firefox, and the curl command that checks it from a staff computer. The site certificate then renews itself from the same authority (dedalo-tls-renew@<i>.timer), so staff computers never trust anything again. --abort removes only what this install made: the timer, its drop-in and the certificate directory.

    On both routes, an install over a certificate directory that already holds an authority stops at the System check when that authority cannot serve the answered addresses, and names the rotation command. That happens when it was made for other addresses, or when it was made before this release and its certificate does not name them all. The production route also refuses an authority made before this release, because it is not restricted. See Production install; its last section is a recipe for trying the production route in a virtual machine with no domain name.

Fixed

  • Aborting an install (--abort) no longer fails with a deadlock when something else is connected to its database.

    When --abort (or a reinstall over an aborted install) resets the install's database, any other session still connected to that database — a server left running, an open psql, a monitoring tool — could hold a table the reset had to drop. The reset then failed with deadlock detected, or waited indefinitely behind a session idle in a transaction.

    The reset now ends every other session on that database only (never another database, never the installer's own lock) before it starts, waits until they are gone, and runs again when the server still picks it as a deadlock victim or a lock wait times out — up to four times, each attempt one transaction that rolls back whole. If it still cannot finish, it stops with a line naming the reason and any session the installer's database role is not allowed to end (Troubleshooting); when it completes despite such a session, its result line names that session too.

  • The installer no longer crashes when a check's helper process stops before reading its request.

    Each installer check (System, database, mail, …) runs in a helper process of its own. If that process stopped before reading what it was asked to check — failing at start, or stopped at its time limit — the installer itself could exit with a "broken pipe" error. The check now simply fails, with a sentence saying the helper did not read its request, and the installer stays open.

  • A maintenance job whose progress cannot be followed now says so instead of waiting forever.

    When the progress of a long job (a backup, a code update, a move of terms or languages) could not be followed at all — the connection failed, the server refused the request, or it answered without any content — the panel kept showing what it showed before, and a move whose run could not be followed kept its form locked ("an execute is still running") until the page was reloaded. The panel now states the failure inside the panel, with the request id to look up in the server log, and the form is released. A refusal there never turns the whole page into an error page.

  • The publication host's install.sh no longer hangs on a stalled Bun download.

    Before, install.sh downloaded Bun with a size limit only: a mirror (or GitHub) that accepted the connection and then stopped answering — during the TLS handshake or mid-download — left the install waiting forever, holding its lock. Now every download has a connect deadline and a total deadline; when one passes, install.sh stops with a line saying so and naming the ways on (retry, --mirror <https base url>, or --offline <bun zip>). See the publication host guide.

  • Installing or resetting a thesaurus no longer waits without limit behind another session's lock

    Maintenance › Install hierarchies (both Install and Reset to seed) imports a thesaurus in one transaction. If another session held a lock on one of its terms (for example, an editor's unfinished save, or a database session left idle in a transaction), the import waited for that lock with no limit. While it waited, it kept the terms it had already reached locked, so edits to those terms queued behind it, and nothing could stop it. Now each lock wait gives up after 5 seconds, the same limit every other maintenance operation uses, and the whole import is retried a few times. If the lock is still held, the import stops with a message saying that another session holds a lock on the thesaurus. Nothing is changed, and you can run it again once the lock is released. If the import is stopped, it ends at once and nothing is changed. The result names any case where it could not confirm whether the import was applied.

  • The publication-marker index is now repaired every 10 minutes as well as at startup, and a repair held off by a running publication is retried instead of skipped.

    The web server decides whether anonymous visitors may open a record's media by a marker the engine keeps in .publication/pub. That set is re-derived from the per-publication truth after every start, to repair a marker pair a crash left half-written. When a publication was writing at that moment for longer than two minutes, the repair was skipped and not tried again until the next restart, so a published record's media could stay unreachable — or a withdrawn one stay public — for as long as the server kept running. The repair is now retried a minute later until it completes, and it also runs every 10 minutes, which heals a pair left half-written by a publication process that died while the server stayed up. The periodic repair holds the publication targets only when it actually has something to repair, and when it did hold them it then completes the removals of records deleted meanwhile, so a record deleted while it ran leaves the public site with that run instead of waiting for the next publication. In the maintenance area, Reconcile shows the schedule as boot + every 10 min and marks a run that had to wait as DEFERRED, with the reason; GET /api/v1/counters carries it as last_deferred.

    Wire contract: WC-2026-10-22-reconcile-deferred-rearm.

  • A running move execute in Maintenance can no longer be hidden by a new Preview

    In the five Maintenance move widgets (move languages, locators, TLD, to portal, to table), clicking Preview while an Execute was still running replaced the execute's progress panel and the record that lets a reloaded page find it again. The result of the execute (completed, rolled back, partly applied, or unknown) was never shown, so it was easy to run the same move a second time. While an execute runs, Preview and Execute now explain why they wait instead of starting a new run. A reloaded page reconnects to a running execute and keeps it in front. If the server did not confirm an execute, the widget waits for a page reload before it starts another run.

  • The publication host installer never puts a cut-off file in place on a full disk

    When the disk of a publication host was nearly full, the root provisioner could write only part of a systemd unit, an environment file, a sudoers or polkit rule, or the copied Bun binary. It then moved that cut-off file into place and reported the step as done. A short write is now continued until every byte is on disk. If the disk is really full, the step fails, the old file stays where it was and no temporary file is left behind.

    Cursor keys pressed at a guided-install prompt no longer leave stray characters such as [D in the answer.

  • An archive restore refuses an ontology node another writer created between the plan and the write

    A restore plans its ontology definitions first and writes them later. A definition another writer created in between used to be overwritten silently, as if it were new. The plan is now checked again at the moment of writing: an identical definition is left alone, and a different one is refused unless the restore was asked to overwrite conflicts. The restore's report now counts what was actually written.

  • An archive restore no longer plants ontology definitions that the next ontology rebuild would delete

    An archive carries the ontology definitions of its sections, but not the ontology records they are built from. Restoring it into an installation whose ontology already has records for the same TLD used to add those definitions anyway: the next ontology update (or a rebuild from the ontology parser tool) removed them again, and the restored records were left without their definitions. The restore now refuses this case before writing anything, and names the TLD and the definitions involved. Install or update that ontology so that it defines them, then run the restore again: the definitions are then found unchanged, or the conflict is named. A TLD for which this installation has no ontology records restores as before. The restore also waits for any ontology rebuild of the same TLD that is in progress, instead of writing at the same time. See The archive door.

  • A backup's verification proof now survives a power cut, and a backup is reported done only once its name is safely on disk.

    When a backup is verified, Dédalo writes a small proof file next to it so the archive is not read in full again. That proof could be lost in a power cut right after the verification, or left half-written by an interrupted write, and the next check then re-read the whole archive. It is now written through the same durable path as every other engine file: complete, synced, and its name synced too. If the disk refuses to make a finished backup's name durable (an I/O error), the backup job now says it failed instead of reporting it done; the backup file itself is kept.

  • Code updates of a git-checkout install no longer refuse over .claude and CLAUDE.md

    An install made with git clone holds two files that no release ships, .claude and CLAUDE.md. The code updater saw them as unknown files and refused every update with "Unknown entries at the code-tree root". The updater now reads the files a release leaves out from the release's own .gitattributes. Those files move into the backup with the old code, and the update proceeds.

    One manual step for checkout installs on an earlier release: the refusal is checked by the code that is already running, so the next update still refuses once. Before updating, delete the two links from the install directory: rm .claude CLAUDE.md. They are tracked by git, so git checkout -- .claude CLAUDE.md restores them.

  • An update or a move transform that committed after a lost connection says what did not run

    If the database connection fails while a data update or a move transform file is being committed, Dédalo asks the database whether the work committed and reports it as applied when it did. Some follow-up actions only run after a commit Dédalo saw succeed, such as updating dependent fields and ending the sessions of users whose access changed. In this case they were skipped without a word. The report now says that they did not run and what to do: run the observer_mirrors reconcile (an update starts one itself), and end the sessions of any account whose access the change affected.

  • An export's record selection now respects the database statement ceiling.

    With DB_STATEMENT_TIMEOUT_MS set, computing an export's record selection could still run far past it: PostgreSQL lifts the statement ceiling at the moment it builds the selection. Now the selection runs under the same ceiling as every other request: an export whose selection takes longer to compute is stopped at the ceiling with db.statement_timeout, before its first line, and its database connection is freed. Installations that keep the default 0 (no ceiling) see no change.

    Wire contract: WC-2026-10-11-export-buffered-cap-and-admission.

  • A lookup in an external catalogue now always ends within its timeout, even when the name server or the answer stalls.

    The timeout of an external record service (for example Zenon) started only when the connection was opened. Resolving the service's name, and reading an answer that stopped arriving, were not covered, so a stalled name server or a stalled answer could keep the lookup — and one of the service's few parallel request slots — waiting indefinitely. The whole request, from the name lookup to the last byte of the answer, is now covered by the service's configured timeout, and such a request is reported as timed out.

  • Exporting a thesaurus never leaves or certifies a partial dump

    The hierarchy export panel used to stream each thesaurus straight to its final file and judge the export by "the file exists". A dump cut short (a server error, a dropped connection, a full disk) still left a valid-looking file, and hierarchy.json then certified it with a checksum, so an installation importing it received a partial thesaurus. Each dump is now written beside its final name and moved into place only when psql succeeded, the file reads to its end, and it holds exactly the rows psql reported; otherwise the panel names the reason and the previous good file is kept. Building hierarchy.json also refuses, keeping the previous manifest, when a data file it would list is torn or changed while it was built. A complete but short dump left by an export made before this fix cannot be told apart: export those thesauri again.

  • Installer and container fixes — --abort removals survive a power cut, a half-imported thesaurus is no longer reported installed, the published client never points at a missing copy

    • --abort survives a power cut. When the installer takes back what it wrote — the .env it created, its backups and staged copies of the secrets, and its own run record — the removals are now forced to disk before it reports aborted. Before, a power cut right after could bring those files back: the database password on disk again, or a run record that --resume took for a live install.
    • A thesaurus whose models never landed is no longer "already installed". When the terms of a thesaurus are in the database but a section its release ships data for (typically the models) is empty, installing it again used to answer already installed — skipped, and the missing section never arrived. It is now refused with a line naming the empty section and Reset to seed (Maintenance → Install hierarchies), which imports the whole thesaurus; nothing is written by the refusal.
    • Container stacks: the client copy is replaced without a gap. When the engine republishes a damaged copy of the client at start-up, the new copy now gets its own directory and the web server is switched to it in one step. Before, there was a short window — and, after a power cut there, a lasting one until the next start — in which the web server pointed at a copy that was not there.
    • The installer's ontology step says what it checked. Downloaded ontology files are checked for their format, over a verified connection to the configured ontology server; the master server does not publish digests to check them against. The step now reports them as shape-checked, not verified.

    Wire contract: WC-2026-10-21-hierarchy-import-partial-refused.

  • The installer's configuration and the published client now survive a power cut

    Three install and start-up writes were safe against a crashed process but not against a power cut or a kernel crash.

    • The .env the installer writes (the only copy of the generated salt and the database password) is now forced to disk before the installer records that step as done. So is the pre-install backup that --abort restores, and so is the restored file itself. Before, a power cut could bring the .env back empty while the installer's own record said it had been written. A failed write no longer leaves a staged copy of the secrets next to it either.
    • The client a container publishes for nginx is now written to disk in full before the switch to it. At every start the engine also re-checks the published files against the client it runs, so a copy damaged by a power cut is republished instead of being served until the next update. A republish no longer empties the live copy first, which used to cause a short burst of 404s.
    • The seed restore no longer passes the database dump through a predictable file name in the shared temporary directory. Before, another local user on a multi-user Linux host could plant that file and add SQL to what the installer ran. The dump now goes to a fresh private directory.
  • The temporary audio copy made for on-premise transcription is deleted as soon as it has been sent.

    Automatic transcription with the institution's own recognition server makes a temporary speech-quality copy of the interview and sends its bytes to that server. The copy used to be deleted only when the background check of the transcription finished, so it stayed on disk when the submission failed, when the check never ran (the job was stopped while waiting, or the server restarted) or when the check could not be scheduled. It is now deleted the moment the submission ends, whatever its outcome.

  • Interrupted exports and transcriptions no longer leave temporary files behind

    A thesaurus export stopped mid-dump (the server killed or restarted) left a <tipo>.copy.gz.tmp-… or hierarchy.json.tmp-… file in install/import/hierarchy for good. The next export now removes such leftovers once they are more than six hours old; a newer one, which may belong to an export still running, is kept. Building hierarchy.json also checks each data file in one streamed pass instead of reading whole dumps into memory. A background transcription that failed with an unexpected error kept its temporary copy of the recording on disk; the copy is now removed however the job ends.

  • Deleting or restoring an ontology record now removes or restores its runtime node.

    Deleting an ontology record (or emptying its data) left its node in the runtime ontology, still served everywhere; restoring a deleted record from the Time Machine brought the record back without its node. Both now move the node with the record, in the same transaction. A local override follows the same rule: deleting or restoring it re-applies the node it overrides. Writing a record's content into the runtime ontology stays a developer action: when a user who is not a developer restores a record, or deletes or restores an override, the record changes but the node does not, and the ontology inspection lists it as drift for a developer to apply. The same holds when no user is identified at all (for example a write through the stdio MCP server): only a developer's action writes node content. An ontology rebuild, a registry activation and a record delete or restore on the same TLD no longer race each other, and activating a TLD while it is being deleted no longer leaves its root node behind. See Ontology write pipeline.

    Wire contract: WC-2026-10-21-ontology-record-lifecycle.

  • Building the ontology recovery file no longer stops the server when the disk is full, and the file survives a power cut

    The ontology recovery file (Maintenance → Build database version) is the safety net an ontology update relies on.

    • A full disk or a read-only private directory while the file was being written used to stop the whole server process, or leave the request waiting forever. The build now fails like any other failed build: it reports the error, keeps the previous recovery file as it was, and leaves no partial file behind.
    • A power cut right after a successful build could leave an empty or partial recovery file under its final name. The file is now forced to disk before it replaces the previous one, and so is the replacement itself.
  • The ontology status panel reports a TLD shown under the wrong typology, and the hierarchy typology groups are filed where the master ontology files them

    In the ontology tree, each TLD hangs under the typology set in its registry record. A TLD placed under a different typology used to be reported as in sync. The ontology status panel now reports it as drift, and Rebuild puts it back under the right typology. Separately, the first time a thesaurus is created, Dédalo registers the hierarchytype and hierarchymtype groups itself, because no ontology update brings them in. It used to file them under Core, with generic names. It now uses the typology and names that the master ontology gives them. Installations that registered these groups before this change keep their current placement, which stays consistent and is not reported as drift.

  • The installer's downloaded ontology files survive a power cut

    The installer fetches the chosen ontology files before it touches the database, and records that step as done. The files were not forced to disk, so a power cut right after that step could lose them, and resuming the installation then refused with a "changed outside the installer" error that only recreating the installation would clear. The files and their checklist are now written to disk safely before the step counts as done, and each file appears under its final name only once it has been checked.

  • An ontology update now removes the nodes a new package no longer carries.

    Before, Update ontology only added and refreshed nodes: a node dropped upstream stayed in the runtime ontology for good, and a record whose TLD field named another TLD was silently written into that other TLD. Each updated TLD is now re-derived in one transaction by the same writer the ontology parser uses: dropped nodes go, a misfiled record is skipped and named in the update's warnings, and the nodes that remain keep their internal ids. A model that the update adds or renames now reaches every component that uses it in the same update, including components in another updated TLD. A typology grouper that this installation created (for a thesaurus or TLD of a typology the release does not ship) is kept: the update re-creates its record instead of removing it, so the thesauri under it stay in the ontology tree. See Ontology write pipeline.

    Wire contract: WC-2026-10-21-ontology-update-rebuild.

  • Phantom-language removal and the public-tier reconcile no longer hang on a busy publication target

    Removing phantom publication languages and applying the public-tier reconcile used to wait without limit while a publication run held the target database. They now wait at most 2 minutes in total, then report the busy target and leave it untouched: re-run the action when the publication run has finished.

    The publication-host kit (hostagent:pack) is now written durably, and a failed pack no longer leaves a temporary file beside it.

    Wire contract: WC-2026-09-30-diffusion-target-fence.

  • A publication run or unpublish no longer waits without limit on a lock held outside Dédalo

    Publishing to a MariaDB database waited as long as MariaDB let it (a day, by default) when another session held a lock on the table — for example a long read on the public website while a run added a column. Meanwhile every other publication to that database waited too, and deleted records stayed published. Now a statement waits at most 30 seconds for such a lock: the run fails and releases the database (re-run it), a deleted record's unpublish stays pending and is retried, and the language sweep and the media-index rebuild report the table.

    Wire contract: WC-2026-10-21-target-statement-lock-wait.

  • The Reconcile panel offers Apply only where you can apply, and a refusal no longer replaces the page

    In Maintenance → Reconcile, every row showed an Apply button. Two of them could not work for most people: media_copy applies only for the root user, and publication_apis never applies there at all (Publication API code is pushed from the publication-hosts panel). Clicking either one showed the no-access page in place of the panel.

    Now each row knows who may apply it. Where you cannot, Apply is disabled and its tooltip says why. Check (the dry run) works as before on every row.

    Wire contract: WC-2026-09-03-maintenance-reconcile-status-widget.

  • New installations and imported thesauri store record references in their final form

    Every reference from one record to another names the target record by number. Dédalo stores that number as a number, but a fresh installation was born with thousands of references written as text (the administrator account's profile and project, the languages, the core ontology), and installing or resetting a thesaurus from Maintenance › Install hierarchies copied the text form of the vendored files as it was. Searches matched both forms, so nothing looked wrong, but the text form is what the planned clean-up of old installations removes. Now a new installation and every thesaurus you install or reset store numbers only, and a new installation records that its data is already in the final form. Nothing else in the data changes.

    Wire contract: WC-2026-10-21-seed-and-thesaurus-int-clean.

  • Installing or resetting a thesaurus takes effect at once and no longer needs gigabytes of memory

    After Maintenance › Install hierarchies installed a thesaurus, or Reset to seed replaced its terms, lists and counts built from those terms could keep showing the old terms until the server was restarted: deleted terms were still offered and edited labels still shown. They now refresh as soon as the import finishes.

    The import also no longer loads whole thesaurus files into the server's memory. The largest vendored thesaurus is about one gigabyte once unpacked, and the server needed more than two gigabytes to import it, enough to be stopped on a modest machine with every user's request lost. It now reads the files piece by piece. A file that changes on disk while it is being imported is refused, and nothing of that thesaurus is changed.

    A reset still does not rebuild what other parts of Dédalo derived from the deleted terms. The result message now says so and names what to run afterwards: the observer_mirrors reconcile, and a new publication or semantic-search reconcile if the thesaurus is published or indexed.

  • Time machine restores respect every record's projects and every field's permission; the ontology recovery file is built one at a time

    • Reverting a bulk change never brings back a record outside your projects. When a revert re-links a record that an earlier revert deleted, that record is now checked against your projects like any other. If it is outside them, that change is skipped as out of scope and the record stays deleted.
    • Restoring a field's history needs permission on its dataframe too. Apply value in the time machine also rewrites the dataframe attached to the field, such as roles or ratings. It now needs edit permission on that dataframe as well. A user who can only read the dataframe gets a permission error, and nothing is restored.
    • One recovery-file build or restore at a time. In Maintenance → Build database version, a second build or restore of the ontology recovery file started while one is running is refused with already running. Before, two at once could leave a damaged recovery file behind a success message.
    • An ontology update that has finished is not undone by a late error. If a final report step of Update ontology fails after the new ontology is in place, the update stays in place. The message now says that the update landed and that nothing was rolled back.

    Wire contract: WC-2026-10-21-tm-restore-component-write-doors, WC-2026-10-21-ontology-recovery-single-flight.

  • Restoring a whole record from the time machine needs permission on every field it changes; a failed ontology re-index after an update no longer brings the old ontology back

    • Restoring a whole record checks every field it changes. Apply value on a whole record in the time machine now needs edit permission on each field the restore would change, like restoring a single field already does. A user who manages user accounts can no longer give their own account back an earlier profile or administrator flag this way, and a field you may only read is never rewritten by a restore. A refused restore changes nothing. Undeleting a record works as before.
    • An ontology update whose re-indexing fails keeps the new ontology. If Update ontology has imported the new ontology but rebuilding the ontology tree from it fails, the update now stays in place and the message names the ontologies to rebuild in Maintenance. Before, the old ontology was put back under a tree already partly rebuilt from the new one.
    • A stuck recovery-file build no longer blocks the next one. Building or restoring the ontology recovery file in Maintenance → Build database version now gives up after ten minutes and reports it, instead of waiting forever and refusing every later attempt as already running.

    Wire contract: WC-2026-10-21-tm-section-restore-write-door, WC-2026-10-21-ontology-update-derive-landed, WC-2026-10-21-ontology-recovery-deadline.

  • Unpublished media stays unpublished after a power cut — and is never reported withdrawn while it is still served

    • A power cut can no longer bring back the media of a withdrawn record. When a record is deleted or unpublished, the markers that let the web server serve its media files anonymously are now forced to disk before the record is reported unpublished. Before, a power cut right after could restore them, and the next start-up re-derived public access from them — with nothing left to retry the withdrawal.
    • A withdrawal that could not remove the markers stays pending. If the media markers of an unpublished record cannot be removed (a permission problem on the media store, a full disk), the unpublish is now kept pending and retried, instead of being recorded as unpublished over media that is still public.

    Wire contract: WC-2026-10-21-unpublish-markers-durable.

  • Rebuilding or updating one user's activity statistics never counts a day twice

    Two rebuilds of the same user's activity statistics running at once, or a rebuild and an update, could each save the same day, so that day's totals appeared twice. Every writer of one user's statistics now waits for the other, and a day already saved is never saved again.

  • Docker stacks — ontology update no longer fails with EACCES

    Both Docker stacks now set ONTOLOGY_DATA_IO_DIR=/private/import/ontology. Before this, the engine fell back to the in-tree install/import/ontology, which is root-owned in the image, so Maintenance → Update ontology died with EACCES: permission denied, mkdir '…/install/import/ontology/7.0/.staging'. Existing installs pick this up from the updated compose file: docker compose up -d recreates the engine container, and no manual chown is needed.

  • The installer says what to do when the checkout's dependencies are missing.

    bun run scripts/install.ts in a checkout where bun install had not been run stopped at once with Cannot find package 'zod', which named neither the cause nor the fix. It now refuses with one sentence: run bun install in the checkout first. Nothing is written. sh install.sh is unchanged and still installs the dependencies itself. See Running the installer without install.sh.

  • A code update that is refused says so once, not three times

    When an update could not start — no maintenance mode, no recent backup, a bad checksum — the Update code panel printed the same refusal in three places at once: a generic "Process 2" line, a red banner that floated over the list of update steps, and the panel's own notice underneath. The steps list also stayed on screen with every phase marked "Pending", which read as an update that was stuck rather than one that never started.

    Now the panel states the refusal in one place, in the interface language, and nothing floats over the panel. The progress list is kept only when something actually ran; a run that fails part-way still shows which step failed, once.

For developers

Security

  • The component lock requests refuse a section that is not an ontology tipo.

    dd_utils_api update_lock_components_state and get_lock_status now check that options.section_tipo is a string in the tipo grammar before doing anything, for every action — including the caller's own lock cleanup, which runs no section read check. A missing value still answers 400 request.invalid_options; a present value that is not a tipo (an array, an object, a number, a free string) now answers 400 request.invalid_tipo and touches no lock, where it used to reach the database as-is. The Dédalo client always sends a tipo, so nothing changes for it.

    Wire contract: WC-2026-10-23-lock-section-tipo-grammar.

Changed

  • "Build database version: the install-seed build's answer and panel values are documented"

    Maintenance → Build database version → Build install version compiles the install seed from the repository's own sources (since 7.0.0-beta.6). Its answer (the step readout and file size on success; a maintenance.action_failed error carrying the same readout on a refusal) and the panel's three values (now the seed sources, the scratch database pattern and the seed file, never the installation's database name) are now recorded in the wire contract, for anyone scripting against the maintenance API.

    Wire contract: WC-2026-10-09-build-install-version-owned-mode.

  • A developer install gets its own unix socket, named after its entity, and is refused while another server holds it.

    One socket per checkout. The developer route used to write no SERVER_UNIX_SOCKET, so every checkout on a machine listened on /tmp/dedalo_ts.sock, and a second one could not start. The Development server screen now asks for the socket and always writes it. The default carries the entity: entity test gets /tmp/dedalo_ts_test.sock. Change it there or with --socket. On a re-run, the socket your .env already names is kept.

    Refused before anything is written. When a running server already answers on the chosen socket (another checkout's bun run dev), the Review refuses to install, and so does a headless run, before it writes anything. Before, that only failed at the installer's last step, after the install was sealed. A leftover socket file that nothing answers on is not refused; the server removes it when it starts. A socket path longer than 103 bytes (the operating system's limit) is refused on any route. See the developer quickstart.

  • "page_globals now says what kind of build is running (release_kind), and the harvesting user agent names the release."

    page_globals has a new key, release_kind: stable, prerelease or dev, the same values as /health. Like dedalo_version, it is null for a visitor who is not logged in. The version badge in the menu's debug bar uses it. It shows beta for a prerelease and dev for an untagged build. Before, the badge said beta for every 7.0 version, including the stable release.

    The User-Agent header sent by the harvesting tools now carries the release name, for example dedalo/7.0.0-beta.7 (+https://dedalo.dev; heritage cataloguing). Before, it carried the bare data version (dedalo/7.0.0). The dedalo token that a robots.txt group addresses is unchanged.

    Wire contract: WC-2026-10-15-release-identity-from-tag.

  • A developer install is reported complete only once bun run dev has served, and it ends by saying where to open it.

    Served, then complete. A developer install used to end with "install complete" without anything having served the browser. Its last step, verify_dev_server, now starts bun run dev once with the .env the install wrote, asks the development listener for /health over HTTPS (the local certificate authority as the trust anchor), logs in as root over it (every cookie the login sets must be Secure exactly when the listener is HTTPS), and stops it again. It refuses to run as root in a checkout another user owns. If it fails, the install is not reported complete and the message names the cause: the port taken by another program, a certificate the server cannot read or that does not verify, the database unreachable, a development dependency missing, another Dédalo server on the same unix socket, a cookie flag that does not match the listener, or the login refused. The install is sealed by then, so the message names the key to edit in the written .env (SERVER_TCP_PORT, SERVER_UNIX_SOCKET) rather than an installer flag, and the failure hint no longer offers --abort. Fix it and run the installer with --resume, which runs the check again. An install written to another private directory is started, and shown, as DEDALO_PRIVATE_DIR=<dir> bun run dev.

    Where to open it. The installer then ends with what to do next, built from the keys it wrote: bun run dev, every address the client answers on (https://127.0.0.1:3500/dedalo/core/page/ and https://localhost:3500/… with the defaults, plus this machine's network addresses when it is opened to the network), the root login, and the certificate authority's path, SHA-256 fingerprint and whether this machine trusts it. The full-screen installer leaves these lines on the terminal when it closes. See the developer quickstart.

    bun run dev stops when its CSS watcher dies. The stylesheet watcher never stops on a LESS error, so when it exits something is wrong — usually a missing development dependency (Cannot find package 'less'). bun run dev now stops with it and says so, instead of serving on while every stylesheet edit is lost.

Added

  • A developer install now issues the development listener's HTTPS certificate and makes this machine trust it; bun run tls:renew and bun run tls:untrust manage it afterwards.

    The certificate. With the default --tls local_ca, the developer route issues the development listener's certificate with the same generator the other routes use (deploy/dedalo-tls-rotate.sh), into <private>/tls, as the checkout's owner. Its certificate authority can certify only localhost, 127.0.0.1 and, when the listener is opened to the network, the IPv4 address it binds (or this machine's IPv4 network addresses with 0.0.0.0). IPv6 addresses are not certified: a listener on this machine is opened as https://localhost:…, which the certificate names, and a specific IPv6 --dev-host with local_ca is refused. A re-run keeps the authority and renews its certificate when it has less than 30 days left. An authority made for other addresses is refused before anything is written, and the refusal names the command that replaces it.

    Trust. The installer prints the authority's path and SHA-256 fingerprint and installs it into this machine's trust store. The installer itself stays your user; only this step asks for more. On macOS the authority goes into your login keychain, and macOS asks for your password. On Linux it is copied into the system anchors through sudo for one command. Firefox profiles get it through certutil when it is installed; otherwise the step tells you to set security.enterprise_roots.enabled. If you cancel the prompt, the install still completes and reports not trusted yet with the manual command; bun run dev serves either way. On macOS a cancelled dialog leaves no certificate behind in the keychain. If this machine already trusts the authority, nothing is asked.

    Afterwards. bun run tls:renew (--force to renew now) issues a new certificate from the same authority, so nothing needs to be trusted again. bun run tls:untrust removes the authority from every store the installer put it in. --abort removes what the aborted install added; if that removal is refused, it keeps the record in ../private/tls/ so bun run tls:untrust can finish it.

Fixed

  • The ontology tool's "set records" can no longer write a node into another TLD's namespace.

    A record of <tld>0 now only ever produces the node <tld><id>. A record whose TLD field names another TLD is reported and skipped instead of overwriting that TLD's node; the tool refuses sections that hold no ontology nodes (test3, ontology1, localontology0…) with request.invalid_options; in list mode the search must target exactly the tool's section, and a search with "recursive children" ticked is refused, because it would pick records the search did not select. On a TLD registry record (ontology35), a TLD that still has rows in dd_ontology but whose nodes drifted (for example only its root is left) is now re-derived in full, not just its root; a TLD already in sync only has its root refreshed, and a TLD with no rows stays empty — this action does not re-activate a TLD. See tool_ontology.

    Wire contract: WC-2026-10-21-ontology-projection-guard.

  • A developer install now installs the development dependencies bun run dev needs, and the development listener can serve HTTPS (SERVER_TCP_TLS_CERT_FILE + SERVER_TCP_TLS_KEY_FILE).

    Development dependencies. The installer's bootstrap installs only its own, production dependencies, so after a developer install the first bun run dev crashed with Cannot find package 'less'. The developer route now has an install_dev_dependencies step (bun install --frozen-lockfile --ignore-scripts), run as the checkout's owner, that installs only what is missing; a checkout that already has them is left as it is. Production, Docker and Trial installs keep the production-only tree. If the step cannot reach the npm registry, the install fails and quotes bun's error. Run as root in a checkout another user owns, the step refuses.

    HTTPS on the development listener. Two new keys, SERVER_TCP_TLS_CERT_FILE and SERVER_TCP_TLS_KEY_FILE, give the SERVER_TCP_PORT listener a certificate: it then speaks HTTPS and the boot log prints an https:// address. Set both or neither: a half pair, an unreadable file, a file that is not a certificate or a key of another certificate stops the server at boot (exit 78) with the key and path named. It never falls back to plain HTTP. While the certificate has less than 30 days left, every boot warns and names bun run tls:renew. Without the keys, nothing changes; without SERVER_TCP_PORT the keys are ignored. The test and update-drill servers always serve plain HTTP, whatever the pair in ../private/.env. The warning for a listener opened to the network now says "no TLS" only when that is true.

  • A developer install now writes a development listener, so bun run dev serves the browser on port 3500.

    The listener is written. A developer install used to write no SERVER_TCP_PORT, so bun run dev served only its unix socket and the browser reached nothing until the .env was edited by hand. The developer route now writes a Development only section: SERVER_TCP_PORT (default 3500, --dev-port), SERVER_TCP_HOST (default 127.0.0.1, this machine only; --dev-host 0.0.0.0 opens it to the local network, and the Review and a headless run's summary warn), DEDALO_DEV_MODE=true, DEDALO_DEBUG_API_ERRORS=true, and the listener's certificate. The interactive installer asks these on a new Development server step.

    The certificate. --tls on the developer route is local_ca by default (a local certificate authority kept in <private>/tls), none (plain HTTP) or files (your own pair, with --tls-cert and --tls-key). SESSION_COOKIE_SECURE is written to match: true over HTTPS, false over plain HTTP, where a browser on another machine would otherwise drop the login cookie.

    A port in use is refused. When another program already listens on the port, the Review does not offer install, and a headless run stops before writing anything; the refusal names the next free port. The installer never picks another port for you.

    A re-run keeps your values. The port, the address, a certificate pair and the two modes your .env already has are kept unless you answer them again, and the Review lists them as kept. A kept port or address that is not valid is refused like an answer, so you replace it with --dev-port or --dev-host. --tls none over a kept pair clears it. No other route writes any of these keys.

Wire contract — 29 entries
  • WC-2026-10-09-build-install-version-owned-mode
  • WC-2026-10-15-release-identity-from-tag
  • WC-2026-10-19-design-line-removed
  • WC-2026-10-19-inspector-record-info
  • WC-2026-10-20-element-context-default-lang
  • WC-2026-10-21-error-report-granted
  • WC-2026-10-21-hierarchy-import-partial-refused
  • WC-2026-10-21-ontology-projection-guard
  • WC-2026-10-21-ontology-record-lifecycle
  • WC-2026-10-21-ontology-recovery-deadline
  • WC-2026-10-21-ontology-recovery-single-flight
  • WC-2026-10-21-ontology-update-derive-landed
  • WC-2026-10-21-ontology-update-rebuild
  • WC-2026-10-21-seed-and-thesaurus-int-clean
  • WC-2026-10-21-target-statement-lock-wait
  • WC-2026-10-21-tm-restore-component-write-doors
  • WC-2026-10-21-tm-section-restore-write-door
  • WC-2026-10-21-tool-permissions-description
  • WC-2026-10-21-unpublish-markers-durable
  • WC-2026-10-21-update-cache-locked-resave
  • WC-2026-10-22-filter-project-pick-membership
  • WC-2026-10-22-reconcile-deferred-rearm
  • WC-2026-10-22-related-search-one-scope
  • WC-2026-10-22-tm-scope-whole-request
  • WC-2026-10-23-lock-section-tipo-grammar
  • WC-2026-10-23-own-lock-cleanup-ungated
  • WC-2026-10-24-tm-malformed-relation-cell
  • WC-2026-10-25-rich-text-data-url-raster-only
  • WC-2026-10-25-tag-id-length-cap

7.0.0-beta.6 — 2026-10-15

1110 commits: git log --no-merges v7.0.0-beta.4..v7.0.0-beta.6

Action needed when you update

  • Dédalo now runs on Bun 1.4.3, which fixes a runtime defect that could leave a process waiting forever, at full CPU, on a program it had started.
  • Every AI request now counts against daily limits set per user and per profile, and generated answers need their own permission.
  • The browser installer is gone — install with the terminal installer; a server with no configuration now refuses to start.
  • SELinux lets the web server reach the Dédalo engine only, and no backup or rollback step runs as root on files the service user can change.
  • The terminal installer sets up the whole host: production, Docker and trial install in one run.
  • The site builder's OpenCode driver loads only the configuration the site builder states.
  • A site builder agent turn can no longer leave configuration in its home directory for the next turn to read.
  • Thesauri are offered from one exported manifest, hierarchy.json, and nothing but declared dependencies is pre-selected.
  • The PostgreSQL socket you give the installer is now the one the server connects through (DB_SOCKET).
  • Docker installations now pull a published, signed Dédalo image, or build it locally if you prefer, and record the choice in .dedalo.env.
  • Installations now choose their domain ontologies (Oral history by default) and install what each one declares it depends on; the install database carries only the core ontologies and no test data.
  • "The Publication API v1 on a publication host answers again: its configuration names the release that runs."
  • New installations are connected to the official update server and always have the Languages thesaurus; a server restarted by systemd or Docker must now declare DEDALO_SUPERVISED=true.
  • "A publication host can now be installed with one guided command, provision init, on Debian, Ubuntu 24.04 and 26.04, and RHEL, Rocky or Alma 9 and 10 with SELinux; the Publication API v1 runs in a FastCGI pool of its own."
  • The publication host provisioner now refuses an instance declaration that anyone other than root could change.
  • Publication host provisioning works with Apache on Debian and Ubuntu.
  • Several publication hosts on one server are checked for isolation.
  • The site builder's Claude Code agent no longer loads configuration from the site's own files, and refuses a Claude Code that cannot be told not to.
  • The site builder's agent can no longer choose its own agent program, read another site's activity, or stall a turn with a planted brief.
  • A site's AI turn no longer runs commands planted in the site's git settings, and the site builder says why it refuses a run.
  • The site builder runs each site's AI agent as that site's own system user, from units that root installs. systemd 248 is now enough, and provision apply must run before the updated daemon starts.
  • Only profiles granted the assistant tool can use the assistant.
  • The site builder can no longer start its agent units through polkit
  • Outbound fetches now refuse every IPv6 route to an internal address.

For users

Security

  • Logging back in as another user no longer saves the previous user's pending edit.

    When a session expires while you are working, Dédalo shows a login box over the page so you can sign in again without losing your work. Before, if a different user signed in on that box, the edit that was waiting to be saved was sent anyway — under the new user's name, so the Time Machine and the "modified by" stamp named the wrong person. Now a pending save, or a page section waiting to load, is resumed only when the same user signs back in. If someone else signs in, nothing is sent: the page reloads, and until it does nothing can be saved, deleted, moved or uploaded from it — only signing in again is allowed. Your unsaved edit stays marked as unsaved. The decision of who signed in is made in one place only, and the login box just passes the answer on, so the check cannot be weakened by changing one copy of it.

    The same now holds when the other user signs in somewhere else — in another tab or window of the same browser. Before, the next save from your page was quietly retried with the other user's session and recorded under their name. Now every answer from the server says whose session it is, your page keeps working only with your own session, and an answer from someone else's makes the page reload instead of sending anything. A lock you held on a field is no longer released on the other user's behalf when such a page closes, either.

    Wire contract: WC-2026-10-11-session-user-beside-csrf-token.

  • Rich text is cleaned by a real HTML parser when it is saved.

    Text fields with formatting are cleaned of anything that could run a script when they are saved. That cleaning used to pattern-match the text, which missed some disguised forms (for example an encoded javascript: link or an event handler written without a space) and sometimes changed ordinary text that merely mentioned such words. It now reads the text the way a browser does: disguised forms are caught, and text and formatting that are not dangerous come back exactly as written. Embedded SVG, MathML, frames, noscript, title and textarea blocks are now removed together with their content, HTML comments are removed, an attribute written twice on one tag is kept once with its first value (the one a browser uses), and a tag left unfinished at the very end of a value is dropped, as a browser would drop it. The cleaning is repeated on its own result until nothing changes, so removing one dangerous piece can never join the text around it into a new one. Dropping it takes the same short time however long the unfinished part is; in the rare value where the end cannot be found that way, the whole value is kept as plain visible text rather than lost. Values saved before this update are not changed until they are saved again.

    Wire contract: WC-2026-10-11-rich-text-sanitizer-tokenizer.

  • Searching or sorting by a field the user may not see no longer reveals its values.

    A search on a field of the section itself (not of a linked record) did not check the user's permission on that field, so repeated searches like "starts with A", "starts with B" could reveal a hidden field's value, and sorting by it revealed its order. A filter or a sort on a field the user's profile hides now matches nothing and sorts nothing, and the request carries the usual "some content was not shown" notice. Fields shown to the user through a portal or an autocomplete they are allowed to use remain searchable there — and only there: the server checks that the portal really belongs to the section the request names and that the user's own profile grants it, so a request cannot borrow that allowance by naming some other portal, and the record information fields every user may search (created and modified date and user) stay searchable for everyone. Which section's permissions apply is decided by the records being searched, never by what the request says about them. A sort over several sections at once is applied only when the field is visible to the user in every one of them.

    Wire contract: WC-2026-09-30-search-root-step-acl.

  • A search whose conditions are joined by OR at the top level no longer returns records outside the user's projects.

    When every condition of a search was joined by OR (for example "title contains X or title contains Y"), one of the conditions was checked without the user's project restrictions and the other without the section being searched. A user limited to some projects could therefore see records of projects they do not hold. The conditions are now always kept inside those restrictions. Administrators without project restrictions see no difference.

    Wire contract: WC-2026-09-29-search-where-parts-parenthesized.

Removed

  • Activity and Time-machine sections no longer offer a section toolbar

    The Activity and Time machine sections are read-only system logs: every record is written by the engine and never edited by hand. They nevertheless used to show a section toolbar (export, import, print, update cache, …) whose buttons all act on records the engine refuses to modify.

    Both sections now ship an empty section toolbar, so the dead buttons are gone. The Time machine list also stops showing the collapse/expand buttons toggle that Activity had already dropped — the two now render the same search-only toolbar. Components inside these sections (for example the time-machine button on a historical value) are unaffected.

    Wire contract: WC-2026-10-06-consultation-only-no-section-tools.

Changed

  • Checkboxes and radio buttons inside a tool now wear the tool's colour, not Dédalo orange

    A ticked box or radio button inside a tool was filled with Dédalo's orange, whatever the tool — so a checkbox in a blue tool disagreed with the toolbar and header around it. The checked widget now takes the tool's own identity colour, the same colour its header edge and its action buttons use; the browser picks a tick that contrasts with the fill. Covers the tool body and the status controls on the tool header, so every tool follows automatically.

  • The user activity chart names actions in your language.

    The Activity by type chart of a user's activity (Login, Save, Edit, Upload complete…) now takes its action names from the application's translated labels, in the language you are using, instead of from ontology terms. Saved statistics from earlier versions are read as before and add up with new ones under the same action. The seven activity-only names (login, logout, upload complete, recover section, recover component, stats, new version) are new labels, translated in every shipped language.

    Wire contract: WC-2026-10-12-user-activity-what-labels.

  • Searching a thesaurus branch with all its descendants costs the same however large the branch.

    Expanding a term to all its descendants (the "with children" search, fixed filters, the tree) used to ask the database once per term of the branch — about 1,650 requests for a branch of 1,200 terms. The whole branch is now read in one request, in one consistent snapshot, with the same result. Sibling terms that share an internal number across two sections are now always listed in the same order (by section) instead of an order that could change between reads.

    Wire contract: WC-2026-10-11-children-recursive-one-statement.

  • The site builder now says why it refused, and which refusals clear by themselves.

    Before, most refusals of the site builder tool read the same: "The site builder rejected the request". A site name already taken, a build still running, a site with no webspace, a server full of running agent turns and a missing confirmation all showed that one sentence, and none of them was marked as something to retry. Now each refusal has its own message: the name is taken, a folder for that name is still on the server, the domain belongs to another site, there is nothing to publish yet, the site has used its disk space, the server has reached its site limit, and so on. A refusal that clears by itself (another task is running on the site, too many agent turns at once) is reported as temporary, so you can try again in a moment. Permission refusals say that the site builder needs developer or administrator permission, or that the agent session belongs to another user.

    For API consumers: the tool's errors are new site_builder.* codes (listed in the wire contract entry), the busy family moves from site_builder.rejected (400) to site_builder.busy (503, retryable), and no site-builder error carries a free-text message any more — the code and its label are the message.

    Wire contract: WC-2026-10-11-site-builder-reason-codes.

  • A failed agent turn in the site builder now says what went wrong, in your language.

    Before, when an agent turn of the site builder failed, the chat showed the site builder service's own internal sentence — sometimes a server path, a user id or a raw program error — in English whatever your language, and nothing said whether trying again could help. Now each failure has its own message: the turn ran out of time, it was stopped (by you, or because the service was shutting down), the server could not prepare it, the agent stopped with an error (its own output is shown apart, as the agent's), the session history could not be read, or one of the refusals the site builder already explains (the site is busy, the server cannot run the agent safely, …). The details an administrator needs stay in the site builder's log.

    For API consumers: an error record of the session_stream event stream is now {type:'error', error:{code, category, message, label_key, retryable}, agent_output?}, and the stream's event: error frames carry the same error body — never free text. New codes: site_builder.turn_timeout, site_builder.turn_interrupted, site_builder.turn_setup_failed, site_builder.agent_failed, site_builder.replay_failed.

    Wire contract: WC-2026-10-11-site-builder-turn-error-frames.

  • The tools of a section list are now grouped in one Tools menu.

    In list mode the header used to show one labelled button per tool (QR, Export, Import, Time machine, Print layout…), and the row wrapped as more tools were registered. New and Delete stay where they were; every tool now sits in a single Tools menu, sorted by name, that can be opened with the mouse or keyboard (arrow keys to move, Enter to open a tool, Esc to close). A tool can keep its own button next to the menu by setting "pinned_in_list": true in its properties.

  • The audio/video viewer window now fits the media: no size jump on play, no black bars.

    Opening a video from a list used to show the poster at one size and then shrink the player to the stream's size when playback started, pinned to the left with black space around it. The viewer now fills its window: poster and playback share the same box, the popup resizes itself to the media's aspect ratio, and the download button no longer covers the player controls.

  • A slow server shows one quiet progress bar instead of a pile of warning bubbles.

    Before, every request that took more than about 2.5 seconds raised its own yellow "Awaiting for busy server" bubble. A page that loads several things at once stacked several identical bubbles, and they stayed on screen after the answer had already arrived. Now a slow answer shows a thin moving bar along the top of the window after 1.5 seconds, adds one short sentence ("The server is taking longer than usual…") only after 8 seconds, and disappears as soon as the last answer arrives. Background calls never trigger it, and neither do long operations that show their own progress (backups, rebuilds, updates). Identical notices in the notification corner now merge into one, with a ×N count. See the slow-server cue.

  • A tool that opens in a dialog appears at once, even on a slow connection.

    Before, clicking a tool button on a component or section (for example "Propagate component data") showed nothing until the tool's program files and styles had downloaded. On a slow network the page looked frozen for several seconds, and a second click could open the tool twice. Now the dialog opens on the click with the tool's name, icon and a loading spinner, and the tool fills in when it is ready. Clicking again while it loads does not open a second copy, and closing the dialog before it finishes loading cancels the tool cleanly.

  • The password field shows its requirements and says clearly whether a password was saved.

    Before, the password field of a user record was an opaque box: a rejected password only turned the border red, without saying why, and nothing confirmed a successful change. Now the field lists the password requirements and ticks each one as you type, asks you to repeat the new password, and saves only when you press Save (or Enter) — then says Password saved, or why it was not saved. A password typed but not saved is marked as such, and leaving the record or closing the tab asks before discarding it — as it now does for any change the automatic save could not store, which was previously dropped without a word.

    The same requirements now apply everywhere a password is set — this field, the login screen's password recovery and the installer's root password — and the server enforces them too: at least 8 (and at most 64) characters, with a lowercase letter, an uppercase letter and a number, no &, no common words such as "password", and no runs like abcd or 1234. Existing passwords keep working; the rules apply when a password is changed. A program that sets passwords through the API receives a validation.password_policy error naming the first rule the password breaks.

    Wire contract: WC-2026-09-30-password-policy-enforced.

  • Tools no longer show a “Developed by” footer; Update cache uses the standard tool action button.

    Update cache, PDF extractor and QR ended their panel with the tool icon and a “Developed by Dédalo team” line — noise below the work area. It is gone; the tool's identity stays in its header.

    Update cache's action button now follows the shared tool style: the tool colour with its contrast-checked label colour, a reload icon, and the label Update. The number of records it will process is shown beside the button (“Records: 8583”) instead of being joined into the label.

  • The history of a field that links records is one timeline, whatever the working language.

    A field that links records — a portal, a select, a check box, a list of informants — holds links, and links have no language, even when the field's definition is marked translatable. Its time machine history is now always one timeline: every change is one entry holding the whole field and its dataframe frames, shown in the history of every language, and restoring it or reverting a batch run puts back exactly that field, whichever language you work in. Before, such a field marked translatable filed each change under the language of the page that saved it. Language entries remain for text fields. Older entries a previous version filed under a language are part of the same timeline and are listed in every language too. The time machine no longer offers a language choice for such a field, and its restore confirmation says the whole field is replaced.

    Wire contract: WC-2026-09-27-bulk-revert-undo-log.

  • Tools work on phones

    On a phone (screens up to 600px wide), 34 of the 37 tools now display and work: the page no longer scrolls sideways, buttons and fields are large enough to tap, titles no longer break mid-word, and tools that open in a dialog fill the whole screen. Wide tables (such as the label translation matrix) scroll inside their own box, and the thesaurus tree has larger tap targets.

    Tools that relied on dragging (cataloguing and coin ordering) gain a touch alternative: tap a record to pick it, then tap the term or slot to place it.

    Also fixed along the way: the site-builder tool could not be opened from its maintenance widget; the subtitles tool crashed on a record with no subtitles yet; the section list became wider than a phone after returning from a record with a pinned semantic search; the AI assistant panel now fills the screen on a phone.

    For tool developers: every tool built from the template could not reach its server (tool_request was missing from the standard wiring); wire_tool now provides it. New tools are entered in the phone check automatically.

  • Tool windows and dialogs: readable buttons and one consistent header

    Every tool keeps its own colour, but that colour no longer makes text hard to read.

    • Buttons painted in a tool's colour now always pair it with a readable label colour (at least 4.5:1, in the light and the dark theme). Before, some tools put dark text on their colour — the Replace and Delete buttons of Propagate component data were barely legible on red.
    • Headers look the same in every tool and every dialog, whether the tool opens in a dialog or in its own window: a light bar with the tool's colour as a thin top edge and a faint tint, the same shadow, and the title in the normal text colour. Plain dialogs (confirmations, record pickers) use the same header instead of a dark bar. Several tools that showed the generic orange edge now show their own colour.
    • The dialog's minimise and close buttons are larger click targets and show a visible outline when reached with the keyboard.

Added

  • The search pane on a phone: tap a field to add it, no drag needed

    Building a search filter used to mean dragging a field from the FIELDS list onto a search group. A touch screen never fires a drag, so on a phone the filter could not be built at all.

    Every field in the list now carries a + button on a touch screen: tap it and the field is added to the search. Opening a group's Add a field slot still shows the FIELDS list, and the tap remembers which group the next field belongs to; the list closes once the field is added so the result is visible.

    Below 1024px — tablet and phone — the pane stops being a three-column desktop: the TYPE / FIELDS / PRESET tabs become a row of chips above the region each controls, every panel and the filter canvas take the full width and stack in one column, and a drilled-in section list replaces the list it came from instead of opening a second column that ran off the screen. A long field name now ellipsizes instead of stretching the section sideways (the list is an auto table that grew past the screen, leaving the label overlapping the add and drill controls). The saved-search list (CONFIG) becomes compact one-line rows — name, apply, edit and delete, with a long name ellipsized — and its empty paginator strip is dropped, instead of the desktop four-column grid that squeezed it into unreadable slivers. A mouse keeps the drag exactly as before.

  • The search group operator (AND / OR) now shows a "Search operator" tooltip on hover.

    When you hover the AND / OR button at the top of a search group, a tooltip now reads Search operator, so the control is identifiable without guessing from its colour alone.

  • A new tool imports journal articles from any OAI-PMH/OJS source directly into Publication records.

    Paste a journal's OAI-PMH URL (or a normal OJS article/journal URL) and the tool lists every publication it found — title, authors, series, year and page range — so you can narrow the list before committing. Kept publications become real records: the series is found or created and linked, each author is found or created and linked by name, the article's abstract is written in each language the source carries that your installation uses (a variant in any other language, or a second one for a language already written, is left out and named in the summary rather than stopping the import), and the PDF is fetched and attached when the source links to one. A publication already imported before is skipped rather than duplicated. For a journal that blocks automated fetching, you can instead save its OAI-PMH response from your browser and upload the file. An article whose metadata cannot be fetched is listed with the reason, and the rest are still shown. A commit running in the background can be stopped partway through — everything imported up to that point is kept, and the summary tells you how much of the batch landed.

    Wire contract: WC-2026-10-07-acquisition-tools-ts-only.

  • A new tool imports coin-auction lots (jesusvico.com, biddr.com, aureo.com, numisbids.com, sixbid.com) directly into Numismatic Data records.

    Paste an auction or single-lot URL from one of five supported houses and the tool fetches the page, shows every lot it found (weight, diameter, grade, images and all), and lets you narrow the list before committing. Kept lots become real records: the auction and coin type are found or created and linked, images are cropped and uploaded, and a lot already imported before is skipped rather than duplicated. For a page the house blocks from automated fetching, you can instead save it from your browser and upload the file. A commit running in the background can be stopped partway through — everything imported up to that point is kept, and the summary tells you how much of the batch landed.

    Wire contract: WC-2026-10-07-acquisition-tools-ts-only.

  • The RDF import fills the record again, and links or creates its related records.

    The RDF import only fetched and showed a resource's RDF; nothing reached the record. It now imports as it did in Dédalo 6: the resource is mapped through the External Ontology an administrator described, and its labels (in every language the source gives), descriptions, dates and places are written into the record. Related authorities (a mint, a material, a person) are linked: an authority some record already carries is linked without being fetched; a new one is fetched from its own site, created and linked. The import only fills: a field that already has a value is never overwritten, and running it twice changes nothing. Each run that changes something is one bulk process, so it can be reverted; a run that changes nothing leaves no bulk process behind. One refused value never costs the rest: a link or a field the engine refuses is listed as skipped, with the reason, and everything else of the resource is still written. A link the External Ontology maps to a section the field does not accept is skipped before anything is fetched or created, and the reason names the ontology node to fix. The result now lists what was created, what was written and what was skipped, and why. An authority that does not fit in the 15-second budget is reported not fetched — run again: the next run completes it. Only web (http/https) addresses from the source are written or linked. An authority is looked for in every project, so one another project already holds is linked, never duplicated. A new authority is also looked for by its equivalent addresses (the same concept on Getty, Wikidata…): when exactly one record already has one, that record is linked and given the authority's address, instead of a duplicate; when several do, nothing is linked and the result names them. The same address under http:// and https:// counts as one. A creator (or any record between this one and an authority) is only created together with its link to that authority, and a source text carrying Dédalo tag syntax is never written. A record is never created without the identifier that finds it again: when that value cannot be written, nothing of the record is created and the line says why — nor of a chain of records leading to it (a creator and the person under it): when one of them cannot be created, none is. An authority the import created stays when its link is refused for another reason than the External Ontology (the field is full, the term is not selectable): it is a complete record, linked by the next run that may. See RDF import.

    Wire contract: WC-2026-10-01-rdf-ontology-import.

  • The CSV import can now add a column's values to what a record already holds, instead of replacing them.

    Until now every column of a CSV import replaced the component's data, and an empty cell cleared it. Each mapped column now has a Mode: Replace (the default, unchanged) or Append. In append mode the file's values are added after the stored ones and nothing stored is changed: related records are added next to the existing links, a geolocation cell becomes a new map layer, a text becomes a new paragraph, and an empty cell leaves the record untouched. Values already present are skipped and counted in the report, so importing the same file twice adds nothing the second time. Components where adding has no meaning — media, single-choice lists, computed values — refuse append before anything is written. An append records the same Time Machine entry per language as a replace import. See Adding instead of replacing.

    Wire contract: WC-2026-09-27-csv-import-append-mode.

Fixed

  • The date picker button works in the search panel on a phone

    In the search panel a date field's calendar button was floated above its input — in the compact search card that put it on the label and under the field's close (✕) control, and it stayed hidden until the field had focus. Clicking it did nothing: on a phone the ✕ sat on top, and the click activated the field (auto-focusing the input), which closed the picker the same click had opened.

    The button now sits at the end of the input row, always visible, clear of the label and the field controls, and its click opens the calendar and keeps it open. A date range gets a single picker button for the row.

  • The date picker opens again on a phone (edit and search).

    On a phone — and in a desktop browser's device-emulation mode — the small calendar icon of a date or time field did nothing: the field's own calendar never appeared, so the value had to be typed by hand. The icon now opens the calendar on phones exactly as it does on a desktop, both in the record edit form and in the list's search panel.

  • The projects picker follows a user's project assignments at once.

    When an administrator changed the projects a user belongs to, that user's Projects picker (and the project names shown in lists) kept offering the old set — a revoked project stayed selectable and a newly granted one stayed hidden — until an unrelated change cleared the server's memory. The picker is now keyed by the user's current projects, so the next read shows exactly the new set.

  • No change can place a thesaurus term below itself any more — not two tree moves made at the same moment, nor an edit, import or restore — and two people changing a term's broader terms at the same time no longer erase each other's change.

    Before, two drag-and-drop moves in the thesaurus tree made at the same moment, each harmless on its own, could together place a term under one of its own narrower terms. The hierarchy then contained a loop: the term showed up under itself and its list of ancestors could not be resolved. Now the second move sees the first and is refused with The move would create a cycle in the tree, and nothing is stored. The same refusal now answers when the tree's add button would create a term that a leftover link already names as a broader term of one of its new ancestors; before, that failed with a generic error. Duplicating a term is refused the same way when such a leftover link would make the copy one of its own broader terms; before, the copy was stored with the loop. The same check now guards every other way a broader term is stored: saving the broader-term field in the edit form (or its narrower-terms field), the API and assistant field writes, CSV import, propagating a value to many records, and restoring a value, a whole record or a deleted record from the history. Each refuses a change that would close a loop with the same message and stores nothing. A bulk revert from the history keeps going when one of its changes would close a loop: that change is left as it is and listed among the skipped ones with the reason tree.cycle; every skipped change that failed now names the reason as a code. An existing loop can still be repaired: removing the link that closes it, or adding a broader term that does not touch it, is accepted.

    Changes made at the same moment no longer get lost either. Before, if two people moved the same term (one with several broader terms), or one moved it while another edited its broader-term field or its position among its siblings, the change saved last could silently undo the first. Now the second change waits for the first and builds on it. See the thesaurus tree.

    Wire contract: WC-2026-10-11-parent-link-cycle-every-door.

  • The PDF extractor shows extracted text exactly as the PDF holds it, and its HTML mode is cleaned on the server.

    In text mode the extractor used to read the extracted text as if it were web markup, so anything between angle brackets disappeared from the preview — an epigraphic edition such as IMP CAES<A>R became IMP CAESR, and that was what "Select text" copied into the record. Now the preview shows the characters exactly as the PDF holds them. In HTML mode the page markers are now added on the server, using the page offset set on the PDF field, and the result is cleaned by the same sanitizer that cleans rich text on save before the preview shows it; a PDF with no page markers no longer produces an empty preview. HTML mode also works again with current versions of the PDF tools on the server: an option they do not accept made every HTML extraction fail.

    Wire contract: WC-2026-10-11-pdf-extractor-render-class.

  • Moving, adding and reordering terms in the thesaurus tree is recorded in each term's history and updates its "modified by" and "modified date".

    Before, a change made in the thesaurus tree itself — dragging a term under another broader term, creating a narrower term with the tree's add button, or dragging terms into a new order — was stored without a trace: the term's history showed nothing, and its modified by and modified date still named the previous edit. Such a change could therefore be neither seen nor undone from the history.

    Now each of these changes is recorded like an edit made in the form. The moved term's history shows the move as one change (its broader term before and after), the new position among its siblings is recorded too, and every term whose position changed shows who changed it and when. See the thesaurus tree.

    Wire contract: WC-2026-10-11-parent-link-cycle-every-door.

  • The Time Machine history of a Hierarchies record opens again instead of failing with an internal error.

    Opening the history of a record whose section has a field that takes its choices from another field's value (in Hierarchies: General term and General term model) answered an internal error instead of the list of changes. The history now lists those columns like any other, offering the same choices as the record itself.

  • Thesaurus: adding a child to a term now shows its expand arrow at once.

    Adding a child to a term that had none opened the new record in its edit window, but the parent term kept looking like a leaf (no expand arrow) until the page was reloaded. The parent now shows the arrow and its open children as soon as the child is created.

  • The error page shows its Raspa background again.

    When a page could not load (An unexpected error occurred, with its request_id and a Home link), the faded Raspa photo meant to fill the background was hidden behind the page's grey background, so the page looked bare. It now covers the whole window again.

  • Confirm-dialog buttons keep the space between icon and label on a phone.

    On narrow screens the accept button of a confirmation dialog (e.g. Continue when creating or duplicating a record) printed its icon almost touching the label. It now keeps the same gap as on desktop.

  • The CSV import's column mapper shows a real value from the file as each column's Sample data, not the column's own name.

    When a CSV file was staged in Import CSV, the Sample data cell of every row in the columns mapper repeated the column's name (the same text as the Name cell), because the preview was taken from the file's header line. It now shows the first non-empty value of that column in the data rows — the second line of the file, or a later line when that cell is empty — so you can check that each column is mapped to the right component before importing. Nothing was imported differently: the preview was the only thing affected.

  • The CSV importer recognises the common record columns again (created and modified by/date, and the rest of the record information).

    A CSV exported from Dédalo carries the record information every section shares — who created and last modified the record and when, and the other fields of the Record information group. On import, those columns came up unchecked and could not be chosen in the column mapper, and the preflight check reported them as "not a component of section", so a re-import could not restore a record's history. They are listed again after the section's own fields, matched automatically by their column name, and imported with the dates and users the file carries (the record's "modified" stamp is not overwritten by the import itself). As before, these columns are offered to global administrators only, can only replace a value (never append to it). Computed fields, which store nothing of their own (the group's inverse references, a thesaurus's children list, indexations, external-service fields), are no longer offered in the column mapper at all; a column map that names one anyway is refused with an explanation. The Activity and Time machine sections, which have no record information, do not list them.

  • The pinned-records chip of a section list tells where the pins come from and disappears when they are cleared.

    Going back to the list after creating a record showed "1 results pinned by meaning search" although no meaning search was run. The chip now reads N records pinned unless a meaning search really pinned the list, and its text stays on one line. Clearing the pins with its ✕ or Show all now removes the chip at once instead of leaving it over the unfiltered list.

  • The "Show buttons" label no longer covers the search buttons on a phone.

    On narrow screens the vertical Show buttons label of a section list (and of the thesaurus/ontology toolbar) printed over Search and Show all. Below phone width only the arrow toggle is shown; its name is still read on hover and by screen readers.

  • The MARC21 and Zotero imports refuse a computed field, and an AI agent can remove one child of a thesaurus term.

    A field whose value is computed and never stored (a term's children, an inverse or index list, an external record) can no longer be the target of a MARC21 or Zotero import map. Before, an import mapped onto a term's children could quietly move records under another parent in the thesaurus. Now the whole import is refused before anything is written, and the message names the field and tells you what to import instead (for children: the parent, on the child records). The CSV import already worked this way.

    An AI agent using the save tool can now remove one child from a term by naming the child record. Each removal is checked against the agent user's permissions and recorded in the Time Machine. On every other field, a remove still has to name the item id.

    Wire contract: WC-2026-10-02-relation-children-write-through, WC-2026-08-30-remove-requires-item-id.

  • Error messages now show their details instead of placeholders like {section_tipo}.

    About twenty error messages showed their placeholders literally, for example The link into '{section_tipo}' was refused ({constraint}) or Your daily AI budget is used up ({budget_kind}: {limit}). They now show the actual values: the section, the limit, the file size, the action that was still running. The affected messages include link refusals, the AI budget, export limits and quotas, duplicate-request notices, image and file size limits, and unknown API actions.

  • Adding or removing a term's children from its Children field now saves.

    A Children field lists the records that name this one as their parent. Linking a record there, removing one, or emptying the field looked accepted but changed nothing: the field showed the old children again and no record was re-parented. Each change now updates the Parent field of the child records themselves, as the thesaurus tree does — a new child gets this record as its parent (and its place at the end of the siblings), a removed child loses it. Each child's change is checked against your permissions on that child, recorded in its history, and undone by Revert the bulk process when it was part of a batch run. A link that would make a record its own ancestor is refused with an error, and the children cannot be reordered by dragging in this field (each child keeps its own order, as in the tree).

    Three batch tools no longer touch a Children field: Update cache skips it (it holds nothing to regenerate), Propagate component data refuses it, and a CSV import refuses a column mapped to it — import or propagate the Parent field on the child records instead. Administrators can remove the leftover bytes the old behaviour stored with bun scripts/relation_children_orphan_sweep.ts (a dry run that lists them; add --apply to remove them).

    Wire contract: WC-2026-10-02-relation-children-write-through.

  • A language field whose stored language is no longer a project language shows it again.

    When a record stores a language that was later removed from the project languages (for example an "Original language" saved before the list changed), the edit form's language picker showed no selection, as if the field were empty. The picker now lists that language as an extra option marked with an asterisk ("French *") and keeps it selected, also right after a save. The name is shown in the interface language, the same as in list view.

    Wire contract: WC-2026-10-02-select-lang-missing-entry.

  • The subtitles tool opens again instead of failing with an error.

    Opening the subtitles tool from an audiovisual transcription showed an error instead of the tool. It now opens, with its play/pause, auto-rewind and tag-insert key settings working.

  • Browsers that ran Dédalo v6 no longer load stale v6 scripts after the upgrade to v7.

    A browser that had used Dédalo v6 kept v6's file-caching service worker after the installation moved to v7. That worker went on answering the browser's requests for Dédalo's scripts with the old v6 copies, so the v7 interface loaded a mix of v6 and v7 code and misbehaved — and the browser could not replace the worker on its own.

    Now the first visit to v7 removes the v6 worker and its cache, and reloads the page once with the v7 code. Nobody has to clear the browser cache by hand, and saved preferences stay where they are.

  • Autocomplete searches work again in pickers with a related-record field, and the field inputs search as you type

    In an autocomplete whose search fields include a related-record field (for example the ontology "Sobrescritura" picker, with its "Modelo" field), typing in the main search box answered "No se ha podido completar la búsqueda". Such a field is now searched through the values it shows, each with its own input under the search box ("Modelo › Término", "Modelo › Código"): "section" there finds the terms whose model is section. A related-record field that shows nothing searchable (only an image, for example) no longer gets a search input.

    The per-field inputs under the search box (Término, Código, tld…) also search on their own a moment after you stop typing; before, the search waited until you moved to another input.

    Wire contract: WC-2026-10-01-relation-search-display-paths.

  • Result lists spanning several sections show every row again

    In a result list whose records come from several sections — the ontology "Sobrescritura" picker searching across all ontology sections, for example — records of different sections that share the same number (Andorra 1, Portugal 1, Costa Rica 1…) rendered as empty rows (", , ,"), with only the last of them showing its values. Every row now shows its own values.

  • RDF import now works with linked-data servers that redirect, and respects their robots.txt.

    Linked-data servers usually answer an IRI by redirecting to the document that describes it (or from http to https). The RDF import refused every redirect, so on those servers each import failed. It now asks the IRI itself for RDF/XML and follows the redirects, checking every step. Servers that only answer at the IRI with .rdf appended still work: the tool tries that form when the first answer is not RDF/XML. The tool now also reads each site's robots.txt and spaces its requests to the same site a few seconds apart. A site that does not allow automated access gets a per-IRI message saying so, and nothing is fetched from it. The tool's IRI list was empty, so nothing could be imported at all; it now lists the record's IRIs again. The tool now always shows why an IRI could not be imported; before, a failed IRI only showed Empty results. When a remote server does not answer within 15 seconds (or drops the connection, or reports an error of its own), the result says that server is out of service and to contact its maintainer, instead of waiting until the request times out. One run fetches at most three IRIs. See RDF import.

    Wire contract: WC-2026-10-01-rdf-harvest-door.

  • Small editing forms no longer show tool buttons they turn off.

    Some forms turn off the tool buttons on their fields, but the buttons were still shown. Now they are hidden where the form asks for it:

    • the dialogs for editing a saved Export preset or a saved search preset;
    • the fields of the User administration tool (the user image keeps its buttons, so a picture can still be uploaded).

    A field whose own configuration sets these options keeps its configured settings.

  • A duplicated record never points at the original record's image or document files.

    Duplicating a record copies its image, audio, video and document files to the new record. If a copy failed, the new record could keep pointing at the ORIGINAL record's files, with no message anywhere — and deleting either record later moved files the other still showed. The duplicate is now saved with no file list of its own, the files are copied (into the record's named folder when the media field stores its files by a folder name taken from another field), and the new record's file list is then built from the files it really has. A copy that did not complete is reported to the administrator (the duplicate_media_incomplete counter and a media.operation_failed line in the server log); the duplicate itself is still created.

    Wire contract: WC-2026-09-30-media-key-locked-transform.

  • Export — a marker shows where a dragged column will land.

    In Export, dropping a field just below the Active elements list put it above the last column instead of at the end. Now, while you drag a field or a column over the list, a line shows exactly where it will be placed: before or after the row under the pointer, depending on which half of the row you are over, and at the end when you are below the list. The rows no longer shift while you drag.

    Dragging a field that is already in the list highlights the existing column instead, and the drop is refused. An empty list shows a "Drag a field here" area.

  • Export preset editor — the name field no longer offers a second value.

    In the dialog for editing a saved Export preset, the name field showed an "add value" button, as if a preset could have several names, and its floating button bar overlapped the dialog title. A preset has one name: the button is gone, so nothing covers the title any more.

  • Export presets panel — one-line rows, lighter design, click a row to apply.

    In Export, each saved preset showed on two lines, with its delete button wrapped below and blank cells in between. Each preset now fits on one line, and the name gets the full width: the edit and delete buttons appear when the pointer is over the row (they stay visible on touch screens). The same cause (the list received every configured column instead of only the name) is fixed for the search presets list too.

    The panel is lighter, so Export stays the one prominent button: a neutral header with the tool colour as text, an outlined Save changes button, and a tinted row with a filled ring for the applied preset. Header, button and rows share one left margin. Click anywhere on a row to apply the preset. Deleting a preset now asks in an application dialog that names the preset.

  • Dialogs with an edit form leave room for the field's button bar.

    When a dialog shows a record to edit (editing a saved preset, or opening a linked record from a portal, a select list or a dataframe), the floating button bar of the active field went under the dialog title when that field was the first one. The form now starts a little lower, so the bar is always fully visible.

  • A tall dialog title no longer covers the dialog's content.

    When a dialog's title took several lines (a long tool name and description, or a small screen), the title bar kept a fixed height and the extra lines covered the top of the dialog's content. The title bar now grows with its text, also after resizing the dialog by hand, and the content always starts below it. Tool titles also use the space better: the description sits beside the tool name when there is room and moves below it when there is not, instead of squeezing both into narrow columns.

  • The Ontology parser button in the Ontology area is readable again

    In the Ontology area, the Ontology parser button was painted solid green with dark grey text and icon, which made it hard to read. It now looks like the other toolbar buttons next to it (Search, Show all): outlined at rest and highlighted on hover.

  • Restoring or undeleting a record now updates every list and search that shows who references it.

    Some fields are filled in automatically from other records — for example a thesaurus term that lists every object indexed with it, or a broader-term search that finds an object indexed with a narrower term. Restoring a record from the Time Machine, undeleting it (from the Time Machine or by reverting a bulk operation) and recalculating such an automatic list did not always bring these up to date: an undeleted object could stay missing from the term that indexes it, an undeleted term could come back listing objects that no longer point at it, and a broader-term search could miss an object until someone saved it again. Every way of writing a record now brings them up to date, right after the change is saved. During a CSV import, the automatic lists are updated after each row is committed; a failure there is reported to the administrator (the observers_propagation_failed counter and the server log) and repaired by the observer reconcile, and it no longer undoes the imported row. A duplicated record no longer matches a broader-term search for terms only its original is listed under. Reverting a bulk revert that brought back a term together with the objects indexed with it now deletes them again, instead of keeping them as records someone else changed. Restoring an automatic list from its Time Machine history (or reverting a bulk operation that changed one) no longer brings back objects that have stopped pointing at the record since: the restored list is recalculated right after the restore, and the objects still listed keep their extra data. When such a list is itself shown in another record's automatic list, an object that stops pointing at a record now also leaves that second list. Extra data attached to the entries of such an automatic list (for example a rating on each object a term lists) now stays with the right object when the record is undeleted, restored or duplicated: it used to be reattached to a different object after an undelete, kept for objects no longer listed after a restore, and copied onto the duplicate. For a virtual section that has a field of its own, deleting a record's data now also empties its automatic lists, and restoring it recalculates them.

    Wire contract: WC-2026-09-30-record-write-obligation-ledger.

  • Tool action buttons show a spinning ring while working, not a solid disc.

    The main action button of several tools (Update cache, Diffusion, Hierarchy, Import RDF, Ontology, Ontology parser, Propagate component data) showed a still, solid disc while its process ran. It now shows the rotating ring used by every other button.

    Update cache: pressing Update now scrolls to the progress panel as soon as it appears, instead of leaving it below the component list, and the button label is white in the light theme.

  • The Update cache tool scrolls to its result

    When an Update cache run finished, its summary (or a refusal) appeared below the components list, out of sight on a long selection. The tool now scrolls to it, with the same mechanism the Update code and Update ontology panels use.

  • Reverting the same bulk run a second time no longer reports records as "not reverted" when nothing changed.

    Reverting a bulk revert, or a run whose dataframe removal had emptied a record, and then repeating that revert used to report some records as cascade_delete_not_reverted, as if someone had edited them. It happened when the record carried values the run itself had written, even though nothing had changed. A repeat now reports them unchanged.

    A value someone really did edit after the revert is still reported as before: at the record, and also at the field when the edit touched the very value the run wrote.

    Wire contract: WC-2026-09-27-bulk-revert-undo-log.

  • Removing a dataframe frame no longer deletes the frame's target record.

    Since the late-September update, removing a frame (a valuation rating, say) from the frame window, or removing the value it qualified, deleted the frame's target record whenever the ontology slot carried the old hard_delete: true flag. That flag was retired on purpose in v6, because the Time Machine needs the target to show past states. It is ignored again: a removed frame is only unlinked, and its target record stays.

    Targets deleted during that window can be recovered from the Time Machine. Ontology authors who want frame-private targets emptied on unlink can set "dataframe": {"delete_policy": "delete_target"} on the slot. The data is cleared and the record is kept.

    Wire contract: WC-2026-09-29-dataframe-hard-delete-retired.

  • Choosing an option in a radio-button field no longer makes the field flicker.

    Clicking an option used to dim the whole field while the value saved and, for a moment, show both the previous and the new option as selected. The new option is now highlighted at once and the previous one cleared. Any field that saves on a click (radio buttons, check boxes…) only dims when a save takes noticeably long.

  • Applying a Time Machine value no longer breaks a record's rated portal.

    A portal whose items carry a rating (the coloured chip of a dataframe, such as the certainty of an attribution) could stop rendering right after the Time Machine tool applied an earlier value to it: the refresh failed and the portal stayed broken until the page was reloaded. The server now sends the rating's list of options with every copy of the rating it returns, so the chip always finds its colour, and the portal refreshes normally after an apply. The client no longer depends on it either: it picks the copy of the rating that carries the options, keeps new items in the order the server sent them, and paints the default colour instead of failing when a rating has no options.

    Wire contract: WC-2026-09-29-select-family-mode-datalist.

  • Searches through a related section now answer negations and combined conditions correctly.

    A search condition that looks inside a related record (for example Movements → Municipality) now means what it says:

    • Negations mean "none". "Does not contain X", "is empty" and "different from X" now return the records where no related record matches. Before, a record linked to one matching and one non-matching record was returned too, so the result was silently too large (on one installation, 38,749 records instead of 18,635).
    • Two conditions on the same field joined with AND can now be met by different related records: "Municipality = Madrid AND Municipality = Valencia" finds people with one movement to each. Before it always found nobody.
    • Conditions on different fields joined with AND still describe the same related record: "Municipality = Madrid AND Year = 1939" finds a movement to Madrid in 1939, not one to Madrid and another in 1939.
    • A number search "different from n" now returns the records whose value is not n. Before, it returned the records whose value is zero.

    Wire contract: WC-2026-09-29-search-deep-leaf-mixed-rule, WC-2026-09-29-number-not-equal.

  • The time machine shows a field's frames and their values as they were at the chosen change.

    In the time machine, the preview of a field with a dataframe could show today's frames instead of the ones of the chosen entry, and a frame's own values — a role, a rating and its colour — were always shown as they are now. The preview and the history list now show the frames and their values as they were at that change: an entry from before a frame was added shows no frame, a rating edited later shows its earlier value, and a frame record emptied since shows what it held. Switching between entries, or clicking the same entry again, never shows the previous entry's frames, and a save made elsewhere no longer changes an open preview. The history list's frame column now matches the preview for every entry, including the entries of one language of a translatable field, which showed no frames before, and each entry's frame button shows the rating colour of that entry, not the newest one. In a record's whole history (the entries of a deleted or recovered record), each field's frames show their values as they were at that entry: a deleted record shows them as they were when it was deleted, not the emptied values its dataframe policy left after the deletion. Recovering a deleted record now adds its own entry to the record's history, so the deleted record's entry keeps showing those values after the recovery. In the time machine a frame's button is now read-only: it shows the frame's label and colour, but offers no + and opens nothing — before, clicking it opened the record as it is now, editable, from a view of the past. The same holds for a user without permission to edit the dataframe.

    Wire contract: WC-2026-09-29-tm-preview-frame-children-as-of.

  • A section that fails to load says why, and can be reloaded

    When a section or thesaurus element could not be loaded, the red banner always suggested a permissions problem, even when the real cause was a failed request (server restarting, timeout, network). The banner now shows the actual error, keeps the permissions hint only when the server answered with nothing to show, and offers a Reload button for temporary failures that rebuilds just that element.

    It also no longer appears after logging back in: when a session expired and the user re-logged, the page loaded but the "permissions" banner was painted over it anyway. The request is now re-sent once after re-login and the page builds normally.

  • Deleting a record's data now also empties every dataframe of its fields.

    Delete data empties every field of a record, and now also removes the frames of every dataframe those fields had — whatever the field is: a portal, a text or number field, or a link (IRI) with its labels — even a field that held no value of its own when the frames were saved first. Before, a dataframe that was not itself a field of the section (an IRI's labels, a dataframe named only in a field's configuration) kept its frames after the wipe, attached to nothing and impossible to remove from the edit view. Restoring an older entry from the Time machine likewise removes the frames of the values it takes out, in every dataframe of the field. The history entries Delete data writes are filed in each language's own history (and the frames in the lg-nolan entry), so every language's time machine lists the wipe.

  • A field's history now keeps its dataframe with it, and restoring an entry restores both.

    A field with a dataframe — informants with their role, a value with its certainty — stores the two apart, but they mean one thing. Until now the history recorded them apart as well: the field's entries held no frames, so restoring an entry left the frames as they were today. The field's history now holds both, in two kinds of entry: an entry of a language holds that language's value, and an entry marked lg-nolan holds the value that has no language (a field that is not translatable, or the base form of a name with transliterations — Augustus, beside Αύγουστος in Greek) and all the frames. A change to a frame adds ONE lg-nolan entry to the field's own history (the dataframe has none), never a copy per language, and the history of a language lists its own entries and the lg-nolan entries together. It does not matter whether the value or the frame was saved first: the preview of any entry shows the whole state at that moment — the language's value and the frames as they were — and restoring it returns that state: a language entry puts its language back with the frames of that moment, an lg-nolan entry puts its frames back. The field's other languages, and another field sharing the same dataframe, keep theirs; a frame of an item deleted since is never put back. This holds for every kind of field a dataframe can hang from — a list of linked records, a text in several languages, a transliterable name, a number, a date, a web address with its label. Translations and duplicated records record their history the same way. Reverting a batch run does the same for every field it changed, language by language. Entries recorded by Dédalo v6 are read the same way: an entry that carries frames gives its language value and the frames of that moment, and a dataframe such an entry is silent about was empty then; a v6 language entry with no frames at all takes the frames recorded before it. Removing an item of any such field — a text, a number, a date, a web address, not only a list of linked records — now removes its frames too. See Time machine.

    Wire contract: WC-2026-09-27-bulk-revert-undo-log.

  • A field's history now shows the dataframe it had at that moment.

    In the Time machine preview, an entry of a field with a dataframe now shows the frames the field had at that moment — the role of each informant, the certainty of each value — instead of today's, together with the field's value as it stood then, and the field's own values no longer list the frames among them. Entries recorded by Dédalo v6 show their frames too: in an entry that carries frames, a dataframe it holds none for shows empty — which is what a restore leaves there; a v6 language entry with no frames at all shows the frames recorded before it.

    Wire contract: WC-2026-09-27-bulk-revert-undo-log.

  • A transliteration saved in its language no longer replaces the base form.

    A field that keeps a base form and per-language versions — a person's name such as Augustus with its Greek form Αύγουστος (component_input_text with with_lang_versions) — now stores a version saved in a language beside the base form. Before, editing the field in Greek, importing a CSV cell with both forms, or running Propagate component data or Update cache on it replaced the base form with the version, and Update cache could even create a base form that never existed. Each form now also has its own entry in the Time machine history, listed while you work in that form's language; its preview shows the base form, and restoring it puts the form back.

  • Reverting a batch run now restores exactly what the run replaced, and says what it could not.

    Revert the bulk process used to guess each value's state before the run from the history just older than it. Where that history was missing — values stored before the time machine recorded them, written with it off, or appended to by a CSV import — the revert emptied the field instead of restoring it; where a later edit had been made with the time machine off, it rolled that edit back. Every batch run (CSV import, bulk component edit, update cache, MARC21 and Zotero imports, and a revert itself) now records, with each change, the exact value it replaced, so the revert puts that value back. A field someone edited after the run is left alone and reported, never overwritten; records the run created are removed only when nothing else refers to them, and records the run deleted come back together with every link that pointed at them. Reverting an old import never touches a record that a later import created again at the same id. When the revert finishes, a summary lists what was reverted, what was skipped and why, and the id of the revert, which can itself be reverted; reverting the same run a second time changes nothing and says so. Re-importing an unchanged file, or repeating the same bulk replace, records nothing, portal links included, and the dataframe frames of those links stay attached. Runs made before this update are still reverted the old way, with its known fixes, and the summary marks those values as inferred; a frame edited after such a run is left alone and reported, and the dataframe frames of an old run that saved several languages of a field are restored together instead of being refused as changed. The CSV import no longer has a Save time machine history on import switch: every import can be reverted. A revert, a single-field restore or a record recovery of a hierarchical term field now also updates its broader-term search index, so a search for a broader term finds the restored value (and no longer the value it replaced) without waiting for a later save. See Time machine.

    Wire contract: WC-2026-09-27-bulk-revert-undo-log.

  • A CSV import that carries the modification date and user keeps them on records whose cells embed dataframe frames.

    When a CSV row carried the record's modified date / modified by columns and also a cell with embedded dataframe frames (a {"data":…,"dataframe":…} value from a raw export), saving those frames re-stamped the record as modified "now, by the importer", overwriting the imported values. The frames are now saved without touching the stamp, so the record keeps the date and user from the file — as it already did for every other column.

  • Searching through a related record (e.g. a coin's type → its mint → the mint's name) is much faster.

    A filter on a field of a linked record used to scan every record of the section being searched, whatever the filter selected. On a 184,000-coin collection, searching coins by the name of their type's mint took about 3 seconds per paint (up to 9 with a cold cache). The search now starts from the few linked records that match and walks the links back, so the same search answers in a few tens of milliseconds.

    The results are the same: the faster route is used only where it provably returns exactly the same records, and every other search (for example "is empty") keeps the previous route.

  • Saving a language selector is instant again and keeps showing only the project languages.

    Choosing a value in a language selector (for example a record's original language) used to answer with every language the thesaurus knows — tens of thousands of entries and several megabytes — so the save could take seconds on a busy server, and the selector then offered all of those languages instead of the project's own. The save now answers with the same short list of project languages the form shows when it opens. The same list is now used everywhere a language selector's options are offered, including list filters and the state widget.

For administrators

Security

  • Every AI request now counts against daily limits set per user and per profile, and generated answers need their own permission. (action needed)

    Until now any logged-in user could run the assistant, ask for generated answers and run semantic searches without any limit, so one session could exhaust an installation's model budget (or its local GPU). Every request that calls a model is now checked against the user's daily limits before the model is called: assistant runs, model tokens, semantic-search queries, and vision calls of the identification tool. When a limit is used up the request is refused with a message saying when it resets (midnight UTC). Nobody is exempt, administrators and root included.

    The limits are set in the AI usage group of each profile (Administration → Profiles) and, to override the profile for one person, of each user (Administration → Users). Each of the four limits is resolved on its own: the user's value when it is filled in, otherwise the profile's, otherwise 0. A limit of 0 refuses every request of that kind; there is no "unlimited" value. A user cannot raise their own limit: on their own user record the AI limits are read-only, whatever their profile allows (administrators with full access excepted). The day's usage of every user is listed in the AI usage section under Administration. See AI spend limits.

    Action needed: a fresh database is required for this beta: databases created by an earlier beta hold engine-written ontology rows that this version no longer uses, and they are not migrated. On the new installation, set the AI limits on the profiles (or users) that should use AI features, since all limits start empty and every AI request is refused until then. Generated answers over the collection also require the Generated answers tool permission (tool_rag): run Register tools after the update, then grant it in the profile editor to the profiles that should use generated answers. Semantic search does not need it.

    Wire contract: WC-2026-10-01-ai-spend-budget, WC-2026-10-12-ddengine-removed.

  • SELinux lets the web server reach the Dédalo engine only, and no backup or rollback step runs as root on files the service user can change. (action needed)

    On RHEL-family hosts the installer's SELinux module now gives the engine a domain of its own (dedalo_engine_t, entered when systemd starts the pinned runtime, which is labelled dedalo_engine_exec_t). The web server may connect to that domain only. Before, the rule named the generic service domain, so the web server could also connect to the sockets of every other service started from /opt or /usr/local. The engine keeps the rights it had. The installer relabels the runtime after loading the module, checks the label, and refuses a first boot whose engine runs in any other domain.

    The reference units in deploy/ for a host set up by hand no longer run anything as root that the service user can change, and they now describe the host by role rather than by fixed paths. The service user owns an instance directory of your choosing, for example /home/my_domain.org/dedalo or /opt/dedalo/app. It holds the code, the private directory and the backups, and the directory above it can stay root-owned. A root-owned bin directory outside it, such as /opt/dedalo/bin or /usr/local/lib/dedalo/my_domain.org/bin, holds the pinned runtime and every script a root-started unit runs. The units name the two directories and the service user through the placeholders INSTANCE_DIR, ROOT_BIN_DIR and DEDALO_USER, so one set of files serves any layout. The site-builder store (store 5) has moved out of dedalo-backup.service into its own root unit, dedalo-backup-site-builder.service, which reads no .env file and writes to /var/backups/dedalo-site-builder. The backup failure alarm now runs as the service user. The code-update rollback unit moves the code trees as the service user and runs only its restart as root, and it and the watchdog name the code-update backup root (--backup-root INSTANCE_DIR/backups/code) instead of reading it from .env.

    In-app code updates work on such a host: the update renames the code only inside the instance directory. When the service user cannot write that directory, the update is now refused before anything is downloaded, with a message naming the directory. Before, it downloaded and tested the release and then failed when it tried to move the code. A host set up by hand with the earlier layout, whose code sits directly in a root-owned /opt/dedalo, must move its code, private directory and backups into an instance directory and render the units again (Upgrading › A host set up by hand).

  • The site builder's OpenCode driver loads only the configuration the site builder states. (action needed)

    The OpenCode agent used to read configuration from the site's working folder and from its home directory — project settings, plugins (which are code it runs), agent definitions, commands and skills — files a previous turn or a site build could have written. Now every OpenCode turn runs with those sources switched off and with the site builder's own configuration (its connection to the Dédalo publication server, its permission rules, and the site's brief as the one instruction file) passed directly; the site builder no longer writes an opencode.json file into the site's folder. Before it runs any turn, the site builder checks that the installed OpenCode honours these switches — and that it can see each of those sources when they are not switched off, so the check proves something; an OpenCode that fails it is reported as unavailable and its turns are refused. OpenCode 1.18.32 passes. When the check could not be completed at that moment (a question timed out), the turn is refused as "busy, try again" rather than as a defect of OpenCode, and the check runs again on the next attempt.

    Action needed: OPENCODE_ENV may now carry provider credentials only — OPENCODE_API_KEY, OPENCODE_AUTH_CONTENT, or a key named like ANTHROPIC_API_KEY, …_AUTH_TOKEN, …_ACCESS_KEY_ID, …_SECRET_ACCESS_KEY or …_SESSION_TOKEN. Any other key (a switch, a proxy setting, HOME, XDG_*, PATH, a configuration path, a permission override) stops the site builder at start, naming the key: remove it. OpenCode remains unavailable on hosts that confine agent turns (AGENT_CONFINEMENT=systemd_scope). The site builder also states OpenCode's credentials itself for every turn, so a login saved with opencode auth login is no longer read: put the credential in OPENCODE_ENV instead. OPENCODE_AUTH_CONTENT may hold api and oauth entries only — a wellknown entry makes OpenCode download and apply a remote configuration on every turn, so it stops the site builder at start. For the same reason an OpenCode console login is refused: while OpenCode's database for the site builder's turns holds a console account (opencode console login), OpenCode applies that organisation's remote configuration on every turn, and no setting turns this off, so every OpenCode turn is refused, naming the opencode console logout command to run. The site builder now also turns off OpenCode's automatic updates, model catalogue downloads, language-server downloads and session sharing on every turn.

  • A site builder agent turn can no longer leave configuration in its home directory for the next turn to read. (action needed)

    Each site's agent turns used to run with a home directory the agent's own account could write, kept from one turn to the next. Programs a turn runs read their settings from there (git, Bun, the agent command-line tools), so one turn could leave a setting that every later turn of that site would read — including one that reopens the site's repository to the agent. The turn's home directory now belongs to root: only its state/ folder is the agent's, and the agent's settings and caches are pointed there or at a read-only, empty folder.

    Action needed: run provision apply on each site builder host after updating. It stops the site builder once, rebuilds every site's turn home directory, carries the agent's own session state (its conversation history, account record and login — nothing else) into the new state/ folder, and moves everything else that stood there, including the agent's own settings, skills and hooks, into the retired/ folder of the agent state directory (closed to root; nothing is deleted). A later provision apply repairs, the same way, any turn home directory the site builder refuses. Until provision apply has run, every agent turn is refused (confinement_unavailable, naming the home directory that is not in its new shape). On a development host running the site builder unconfined (AGENT_CONFINEMENT=none), AGENT_STATE_ROOT is now required: set it to an absolute path in the instance's .env before updating, or every agent turn is refused (confinement_unavailable; the site builder says so when it starts). Each turn there now gets a fresh home directory, and the agent's state lives in <AGENT_STATE_ROOT>/unconfined/turn/state: the site builder moves the Claude Code login, account record and conversation history there once, the first time it starts after the update (it never overwrites what is already there).

  • Root's writes under a directory another account controls now land only in the exact directory expected.

    Two places that provision apply and provision init write to sit under a directory that root does not fully control. The first is a site's web log directory: on Ubuntu, /var/log is writable by the syslog group. The second is the API configuration files in the state tree's shared/ directories, whose parent belongs to the agent. These writes were already pinned to the parent directory, so that a rename cannot redirect them. Any root-owned directory closed to others was accepted as that parent, though. Now the parent must be exactly the directory expected. For shared/ that means its exact owner, group and mode. For the web server's log directory it means the owner, group, mode and identity that provision check observed. The parent must also be on the same filesystem as its own parent, so a filesystem mounted over the name is refused. A directory swapped in after the write began receives nothing. Init's sudo check now also reads included policy files the way sudo does. A file that is not owned by root, or that other accounts can write, is not followed, and neither is an include whose name contains %h. Each one is listed in the host.sudo item. An API configuration file with the wrong owner or mode is now fixed in place, where before the fix was refused.

  • The publication host provisioner now refuses an instance declaration that anyone other than root could change. (action needed)

    provision check and provision apply build the agent's sudo and polkit rules and its services from the instance declaration, so whoever could edit or replace that file could choose which account the next apply grants them to. Both commands now refuse, before reading it, a declaration that is not a regular file owned by root and writable by no one else, or that sits under a directory that is not owned by root or is writable by others. The other declarations in /etc/dedalo_publication_host/, which the check between instances reads, and that directory itself follow the same rule. Action needed: before the next check or apply, run chown root:root and chmod go-w on each declaration and keep it in /etc/dedalo_publication_host/. provision render is unchanged and still works on a draft anywhere. See Publication host agent.

  • Master media folders are protected whatever their letter case

    The list of media folders that may be served publicly now refuses an archival master or retouched work copy written in any letter case. Before, an entry such as image/ORIGINAL or image/Original in the public qualities setting was not recognised as a master; on storage that ignores case (macOS disks, Windows/SMB network shares) that folder is the same as image/original, so the full-size master files could be served to anonymous visitors. The same applies to a renamed master tier configured by your installation. If you set public qualities by hand, the refused entry is now logged at startup, and this installation's media rules are rewritten automatically at the next restart or login. If you run a separate publication server with rules generated by bun run media:publication-host-rules, generate them again and reload that server's Apache or nginx: those rules do not update themselves.

  • Uploaded script files under the media folder answer "not found" on Apache

    A server script file (.py, .sh, .cgi, …) placed under the media folder was already never executed nor served, but Apache refused it with "forbidden", which confirms to anyone probing that the file exists. Apache now answers "not found", like nginx already did, in every protection mode and on a separate publication host; without mod_rewrite it still refuses the file. The .htaccess regenerates on its own after the update; re-render a publication host's rule files with scripts/media_publication_host_rules.ts.

  • Working files under the media folder are never served, on Apache or nginx

    Soft-deleted, temporary, import and CSV working files kept next to the media (.deleted, .temp, .tmp, .import, .csv) are now refused with "not found" by both web servers, in every protection mode and on a separate publication host. Before, the generated nginx rules did not refuse them at all: a working file of a published record whose name followed the media naming pattern was served to anonymous visitors. Apache already refused them, but answered "forbidden" (confirming the file exists) and only in lower case, so on storage that ignores case (macOS disks, Windows/SMB shares) .TMP was served. The media rule files regenerate on their own after the update; on nginx, reload it afterwards (nginx -t && nginx -s reload), and re-render a publication host's rule files with scripts/media_publication_host_rules.ts.

  • The assistant no longer proposes changes its own apply step would refuse.

    In write mode the assistant proposes a change plan that a person confirms before it runs. The check made before the plan was shown was weaker than the one made when it runs: some plans that named a record outside the user's projects, a field the user may not edit, or a read-only section were shown as valid and then failed when applied. Plans are now checked by the same permission rules that apply when they run, so what the person confirms is what the user is allowed to do.

    Wire contract: WC-2026-10-01-change-plan-write-door.

  • Removing a linked record from a portal now checks the portal field and the record's projects.

    Removing a linked record from a portal (the unlink button, and the "delete index" of the indexation tool) used to check only the section's permission. A profile that could edit the section but only read the portal field could still unlink from it, a record outside the user's projects could be changed, and a user manager could unlink their own profile, active or administrator flag. Removing a link now requires write access to that portal field and to the record itself (its projects), checked before anything is read or locked; a user manager can no longer unlink their own profile, active or administrator flag; and a record id of 0 or below is refused for every user, administrators included.

    Wire contract: WC-2026-10-01-delete-locator-write-door.

  • Asking a vision model for proposals, or identifying a photograph with an external encoder, now requires the identification tool permission.

    Proposals from a vision model and image identification through an external service call a paid model and may send the object's photograph off the server. Any user who could read the section could start them. They now require the user's profile to include the identification tool; without it the request is refused before any model is called. Matching by record, proposals voted by similar records and a locally run image encoder cost nothing and are unchanged. Grant the identification tool to the profiles that should use the vision source.

    Wire contract: WC-2026-10-01-identify-vision-grant.

  • Searches through a related field only reach the sections that field links to, and that the user may read

    A search that follows a related-record field (a portal or autocomplete column) checked the user's permission on the section named in the search, then read whichever section each stored link pointed to. Between sections that share a table and a field — a section and its virtual twin — a user with access to one could match values stored in the other.

    A search now follows a related field only into the sections that field is configured to link to, and only into those the user may read, each under its own record restrictions. Links that point outside the field's configured sections — left over from an earlier configuration or an import — no longer match in searches, for administrators too; they already showed nothing on screen. The data is untouched: adding the section back to the field's configuration makes them searchable again.

    Wire contract: WC-2026-10-01-search-hop-configured-targets.

  • The site builder's Claude Code agent no longer loads configuration from the site's own files, and refuses a Claude Code that cannot be told not to. (action needed)

    Claude Code reads hooks, MCP servers, skills and settings from the project it works in and from its home directory. In a site builder workspace both are written by the agent itself (and by the site's build scripts), so a file planted in one turn ran as a shell command in the next, although the agent is denied a shell. Each Claude Code turn now loads its settings only from the site builder (no user, project or local source; only the site builder's own MCP server), and the site brief (AGENTS.md) is handed to the agent by the site builder instead of being read from the workspace. Action needed: the installed Claude Code must list --setting-sources, --settings and --strict-mcp-config in claude --help (verified on 2.1.286). The site builder checks this at start and before every turn; an older Claude Code is reported in the start log and every turn is refused until it is upgraded. In the site builder tool these refusals now read "cannot run its agent safely on this server" (an administrator must act) or "busy with this site" (try again in a moment) instead of a generic error.

    Wire contract: WC-2026-10-01-site-builder-confinement-codes.

  • The site builder's agent can no longer choose its own agent program, read another site's activity, or stall a turn with a planted brief. (action needed)

    Five gaps around the site builder's confined agent are closed.

    • The agent program is chosen by the site builder. A site's agent program (Claude Code, OpenCode, …) was read from the site's own site.json, which the agent can edit. An agent could switch its next session to another program and plant a plugin that program loads. That bypassed every restriction placed on Claude Code. The choice made when the site is created is now kept in the site builder's private state, outside every site's folder, where no agent run can rename or remove it. A site created before this update gets its record when the site builder starts, from the program its site.json named at that moment. From then on site.json decides nothing. A site with no record is refused ("no driver record") and never falls back to the instance's default program (agent.driver): restart the site builder, or start the session naming its program.
    • One site's agent can no longer watch another's. Each agent run already hid other sites' processes. It could still read the system's process-accounting files (/sys/fs/cgroup), which show another site's runs, when they started and how much they used. Every agent unit now hides that directory. Action needed: run provision apply after updating. Until the units are re-rendered, the site builder finds them different from what it expects and refuses every agent run.
    • A planted brief no longer stalls the site. The site brief (AGENTS.md) is in a folder the agent writes. Replacing it with a pipe, a huge file or a file containing a NUL byte could hang every later turn or make every turn fail. The brief is now read without blocking, only up to its size limit, and refused with a clear message when it is not an ordinary text file. The same holds for every file the site builder reads or writes in a site's folder: a pipe planted at site.json or at the agent's connection settings is refused at once instead of stalling it. A turn's setup now also counts against the turn's time limit, and stopping a session reaches it.
    • An unusable Claude Code is reported, not hidden. A Claude Code binary the site builder cannot run (missing, not executable, or under /home, which the site builder's service cannot see) was a generic, retryable error. It is now refused with "cannot run its agent safely on this server", naming the binary. A Claude Code check that fails once on a busy server, for example by timing out, is asked again at the next turn. It no longer refuses every turn until the site builder restarts.
    • Unsaved agent work is no longer lost to a failed save. After each turn the site builder saves the agent's work as a restore point. When that save failed for any reason other than the site builder shutting down, or when the restart's own recovery save failed or had to wait, the work was left unsaved and never retried. The next turn then saved it under its own name. Any save that did not happen is now retried at every start of the site builder until it succeeds.
  • A site's AI turn no longer runs commands planted in the site's git settings, and the site builder says why it refuses a run. (action needed)

    Eight gaps around the site builder's confined agent are closed.

    • A turn no longer runs commands planted in the site's git settings. Claude Code runs git itself when a turn starts. It switches off git's hooks for that, but not its content filters. A build script, a git hook or the agent itself could add a filter to the site's .git/config, and the next turn then ran that filter's command as the site's user, with the AI provider's key and the museum connection. Setting git's own environment variables does not help: Claude Code removes them. Each turn now runs with the site's .git folder hidden, so no git the turn starts finds a repository. Action needed: run provision apply after updating. Until the units are re-rendered, the site builder finds them different from what it expects and refuses every turn. A site whose folder has no .git is now refused a turn, with a message that names the missing folder.
    • The site builder reads systemd's answers correctly on Ubuntu 24.04 and Debian 12. systemd prints some settings, such as TemporaryFileSystem=, as one line per entry. The site builder kept only the last line, so on those systems it would have refused every agent run as "not what this daemon expects". It now reads every line.
    • "systemd cannot say" never frees a site. When systemd does not answer a question about a run (a timeout, a D-Bus or polkit error, an answer with a value missing), the site stays held until systemd answers. It is never treated as finished or idle.
    • A refused stop is named. When systemd refuses to stop a run, for example because the polkit rule is missing or polkitd is not running, the refusal and the site's "unavailable" message now quote systemd's answer and point at the polkit rule.
    • A host that cannot confine runs says so at start. The site builder now checks the host when it starts: the systemd version, each site's user and groups, and the files a run starts from. If something is wrong, it writes the reason to its log at start, instead of failing the first request. It still starts, and refuses each run until the host is fixed.
    • Session records and site folders are checked before a run. A session's saved settings are used only if they name that same session and site, and only from folders the site builder created itself. A site folder that has been replaced by a link is refused before any run starts.
    • A broken turn setup is reported at start, not at the first turn. The git settings file root renders for every turn, and the systemd units each site's runs use, are now checked when the site builder starts and before a session is accepted. A missing or altered settings file, or an extra drop-in on a unit, is written to the log at start and refused before any work is reserved. A site whose .git is a link or not a folder is refused a turn, with a message naming it.
    • One site's damaged session folder no longer stops the others. When one site's session folder was replaced by a link, the start-up sweep stopped for every site: interrupted sessions were not marked and their work was not committed. The sweep now skips that site, logs why, and goes on with the others.
  • The site builder runs each site's AI agent as that site's own system user, from units that root installs. systemd 248 is now enough, and provision apply must run before the updated daemon starts. (action needed)

    Until now, every site of a museum ran its AI turns, builds and git commands as one agent user, so one site's run could read or change another site's agent state. The daemon also asked systemd to start those runs itself, which the previous release had to stop allowing (see the entry on the narrowed polkit rule).

    Now each declared site has its own system user, dedalo-a-<instance>_<n>, with a private group that only that user and the service user belong to. provision apply creates them and never reuses a number. For each site and each kind of run (turn, build, git), root installs a socket and a service template. The daemon only connects to that socket, and systemd starts the run as the site's user, which the daemon cannot choose. The polkit rule now lets the service user stop or kill those runs and nothing else. What systemd enforces:

    • A site never has two runs at once, and runs of different sites run as different users.
    • Each run gets only its own site's workspace and its own HOME. A build cannot read the turn's ~/.claude, and git gets no HOME.
    • A turn or build reaches the network through its site's egress directory under /run/dedalo-sites-agents/<instance>/egress/. Root creates this directory at every boot from a rendered /etc/tmpfiles.d/ file, and the run sees it read-only.
    • A run of a site whose units differ from what the daemon expects (for example, a hand-added drop-in) is refused, naming the setting. This includes a drop-in that stops a run without killing its last processes, or one that makes systemd open or mount a file for the run as root.
    • A run counts as finished only when systemd reports none of its processes left, so the site's next run never starts beside a survivor of the last one.
    • Stopping or restarting the daemon also stops that museum's run sockets, which then accept no new run. Starting the daemon starts them again.

    Other changes:

    • The daemon starts no new run once it is shutting down. A turn whose final commit was refused that way is committed when the daemon next starts.
    • The oldest supported systemd is now 248. polkit must be 0.106 or newer, because the stop rule is a JavaScript rules file, and provision apply refuses an older one. Supported hosts are Ubuntu 24.04 or newer, Debian 12 or newer, and RHEL 9 or newer. Ubuntu 22.04 is not supported: its polkit 0.105 ignores the rule. Server and minimal installs often have no polkit at all; install it first (apt install polkitd on Debian and Ubuntu, dnf install polkit on RHEL). On systemd 257 or newer each run also gets its own process namespace.
    • A site's user may belong only to the instance group and its own private group. provision apply refuses a site user that any other group lists as a member, and the daemon refuses every run of a site user that has any other group, because a run gets every group of its user.
    • The opencode agent is refused on a host where runs are confined (AGENT_CONFINEMENT=systemd_scope, every provisioned host): it loads configuration and plugins from files a run can write, so a planted plugin would run as the site's user. Use claude_code there.
    • When the daemon is not running, provision apply clears its failed state before starting it. A daemon that the updated code stopped in a restart loop can then be started.
    • If the daemon crashes on systemd older than 254, systemd does not stop its runs. The daemon stops them, or keeps their site unavailable, when it starts again.
    • provision apply refuses a site user whose uid, or a private group whose gid, belongs to any other account or group on the host. It also refuses any other account whose primary group is a site's private group, and it refuses when the system id range in /etc/login.defs has no room left. The daemon checks the same things before every run.
    • Creating a site is refused (503) before anything is written when the daemon cannot run that site's git yet, for example after provision apply added the site but before the daemon restarted.
    • An agent run that was left running (for example, after the daemon was killed) is stopped before its site runs again. If it will not stop, that site stays unavailable until it does.
    • A turn's own git never treats the workspace itself as a repository. Root installs <state dir>/agents/turn.gitconfig (safe.bareRepository = explicit, nothing else), and every turn sees it as /etc/gitconfig, read-only. A turn is refused, naming the file, when that file is missing or says anything else, and when the workspace root contains a HEAD entry. Remove a stray HEAD from the workspace root to run the site again.
    • The daemon checks that it may stop its sites' runs at start and before each run, by asking systemd to stop a run that does not exist. Without the polkit rule, or without a running polkitd, every run is refused with a message that names the rule, instead of failing at the first interruption.
    • A run is refused when its site's user cannot reach the runtime, the shim or the Claude Code binary (CLAUDE_CODE_BIN), for example because a directory on the path is closed to it. The message names the directory.
    • The daemon's .builder/ directory in each workspace is now 0710, so a turn, which runs as the site's user, can open the MCP configuration it is given there. The site's user still cannot list or change anything in it. An existing .builder/ is changed at the site's next turn.

    Action needed: as root, run provision apply for every instance before the updated daemon starts, or in the same maintenance window as the code update. This includes an update installed from within the application.

    • The daemon no longer starts while its environment still sets AGENT_USER, AGENT_HOME or SYSTEMD_RUN_BIN. provision apply removes those keys and writes AGENT_IDENTITIES, AGENT_SOCKET_DIR, AGENT_STATE_ROOT and AGENT_IDENTITY_EPOCH. It writes SYSTEMCTL_BIN only when the declaration names agent.systemctl_bin. Otherwise the daemon uses /usr/bin/systemctl, so declare it only on a host where systemctl is somewhere else. Do not add the key to the rendered environment file by hand: the next provision apply removes it.
    • The first run of provision apply stops the daemon, gives each site's files that the old agent user wrote to the site's new user, installs the units, and starts the daemon again.
    • It also opens to the instance group the files and directories in each existing workspace that are owned by the service user, except .builder/. Sites created before 2026-09-05 have these: back then turns and git ran as the service user and left .git closed to the group, so the site's new user could not commit. The service user keeps owning them.
    • It locks the old agent user without deleting it.
    • It moves the old shared agent HOME aside, next to itself, as <home>.retired-<date>, owned by root and closed to everyone else (0700). Nothing from it is copied to the new users.
    • Once, after the update, a conversation cannot resume its earlier context: its next turn starts a new agent session.
    • Adding a site later also needs provision apply, which restarts the daemon.

    See the site builder internals.

  • The thesaurus term picker grants link mode only from a profile, never from a blanket rule.

    The term picker opens a thesaurus in link mode only for a user who may edit the field that asked for it. That edit right was read without asking where it came from, so a field under the editing-preset section — which every user may edit through a built-in rule, bounded only to their own presets — counted as a link-mode grant for every user. Link mode now requires edit permission granted by the user's profile (or the root account); otherwise the thesaurus opens in ordinary browse mode.

  • Translation, imports, cache rebuilds, uploads and bulk reverts now check permissions on every record and field they write.

    Several tools checked a user's permission on a section and field but not on the specific record they then wrote, so the rule that keeps a user from changing parts of their own account (for example their own profile) did not apply there. Automatic translation, the poster-frame tool, the cache rebuild, file and CSV/MARC21/Zotero imports, the fields an upload fills in automatically, and the bulk revert of a process now check each record and field exactly as the edit form does. A field or row the user may not change is reported and left untouched; CSV imports now need permission on every imported column, including the creation and modification metadata columns.

    Wire contract: WC-2026-10-01-write-door-delegations.

  • The assistant can no longer search or count records of a section the user may not read.

    The assistant's search, count and find-or-create tools applied the user's projects but not the section permission, so a user whose profile did not grant a section could still list and count its records through the assistant. They now refuse such a section, exactly as the record list does.

    Wire contract: WC-2026-09-30-mcp-search-section-grant.

  • Only profiles granted the assistant tool can use the assistant. (action needed)

    With the assistant enabled on the server (DEDALO_AGENT_HTTP_ENABLED), any logged-in user could run it, even if their profile did not include the assistant tool. The assistant now requires the profile to grant tool_assistant, for global administrators too; only the root account holds every tool. Action needed: in the profile editor, grant the assistant tool to the profiles whose users should keep using it.

    Wire contract: WC-2026-09-30-agent-tool-grant.

  • Translation, transcription and RDF-import fetches now connect to the address the SSRF guard vetted (DNS rebinding closed); network failures report a typed reason.

    The guard used to check the server's address and then let the connection look the name up again, so a hostile DNS server could answer "public" to the check and "this machine" or "the internal network" to the connection. The connection now goes to the address that was checked, with the real name kept for the certificate and the Host header, for the translation and transcription services and for every RDF URI a cataloguer imports. Failures are typed instead of carrying the runtime's own error text: the RDF import reports the fixed sentence "The outbound request could not be completed" for each URI that failed (see the RDF import tool reference), while translation and transcription report a short message naming the reason, such as hop connect failed (timeout) or redirect refused (HTTP 302), never an address. A translation or transcription request (a POST) that may already have reached the server is never re-sent to the server's other address, so a failed transcription request cannot start a second job; an RDF-import fetch (a GET, safe to repeat) may be retried on the next address.

    Wire contract: WC-2026-09-30-guarded-text-pinned-typed-transport.

  • Posterframes, audio streams and clip downloads now respect the component's own permission and the user's projects.

    The audiovisual and 3D media actions (create or delete a posterframe, attach a 3D snapshot, read an audiovisual file's streams, cut and download a clip) used to check only the section's permission. A profile that was explicitly denied the audiovisual component could still use them, and any record id could be reached even outside the user's projects. They now check the section, the component itself and the record's project, in that order, before they look at the file. A user who can see a record's video in the player can still download its clips as before.

    Wire contract: WC-2026-09-30-media-pair-scope.

  • Ontology identifiers are checked on every read and write, and the database now refuses malformed ones.

    An ontology node's identifiers (its tipo, its parent, its model, its TLD, and the target of a component alias) are used by the search engine to build its queries. Until now a malformed value stored in the ontology table (for example an alias pointing at a tipo that contains quotes or spaces) could reach a search query unchecked. Now every identifier must be letters followed by digits (a TLD: two or more lowercase letters), no longer than its database column: an alias with a malformed target is refused as an invalid ontology node, the search engine checks the alias target again before using it, the ontology write doors refuse a malformed node, an archive restore refuses one before writing anything, and the ontology recovery file leaves such rows out and names them. The update adds six checks to the dd_ontology table that refuse any malformed identifier from then on. An installation that already holds malformed rows still updates normally: the checks start in a "not yet validated" state, and the reconcile ontology_identifiers (maintenance area, reconcile status, or bun run scripts/reconcile.ts) lists those rows and what it would do with each. Applying it rebuilds each row from its ontology source where one exists, deletes the rows that cannot be rebuilt (every deleted row is listed in full in the report and in the server log, even if the final validation fails), and then validates the checks. Until it is applied, reordering a malformed node is refused with a message that names the check and the reconcile to run. When an ontology source record holds a malformed reference (a parent, model or related-term pointer), the rebuild now drops that reference from the node and names the record in its message, instead of storing the malformed value or failing the whole TLD.

    Wire contract: WC-2026-09-30-ontology-identifier-grammar.

  • Stored password hashes are no longer sent to the browser.

    Until now, anyone able to open a user record received the stored password hash of that account (and, for accounts not yet migrated from v6, the reversible legacy value) — material an attacker could try to crack offline. Every screen and API answer now shows a fixed mask (****************) instead: it says only that a password is set. Logins, password changes and imports are unaffected.

    Wire contract: WC-2026-09-30-password-hash-never-served.

  • Site builder agent turns and builds run in a private network namespace and reach the outside only by hostname, through the daemon's egress gate; the Publication API key no longer reaches the agent, and AGENT_EGRESS_ALLOW is refused.

    A confined agent turn used to be allowed "any" address with loopback and the private ranges denied. systemd's address filter lets the allow list win over the deny list, so that turn could in fact reach the database, the engine, the local network and a cloud host's metadata service. Every confined run (a turn, a build step, a git command) now runs in its own private network namespace with /run hidden. Loopback, the LAN, the metadata service and the host's own sockets do not exist inside it. A turn or a build reaches the outside only through its site's socket directory, served by the site-builder daemon: an HTTPS proxy that connects only to the hostnames that run may use, on port 443, and refuses any name that resolves to a non-public address. It forwards nothing until the connection's TLS handshake names that same hostname, so a hostname behind a shared CDN is not a way to other sites on that CDN. A git command gets no network at all. The database socket directories some distributions keep outside /run (RHEL's MariaDB uses /var/lib/mysql/mysql.sock) are hidden from every run too, and each run gets its own /dev/shm instead of the host's shared one. A run cannot reach another site's socket directory: only its own site's is mounted, and runs of different sites run as different users, so a concurrent run cannot be reached through /proc either. Each blocked destination is written as one line in the session or build log. A run on a host that silently ignores the namespace setting is refused before anything starts.

    The hostnames are:

    • Claude Code turns: api.anthropic.com.
    • opencode/pi turns: the hosts named in AGENT_PROVIDER_HOSTS. With none named, such a turn is refused, naming the key.
    • Builds: the hosts in BUILD_REGISTRY_HOSTS (default registry.npmjs.org).

    On a provisioned host these come from the declaration's agent.provider_hosts / agent.registry_hosts. The Publication API key now stays with the daemon, which adds it on its side of the agent's MCP connection; it is no longer written into the site workspace.

    What changes for an operator:

    • A non-empty AGENT_EGRESS_ALLOW stops the daemon at boot, with a message naming its replacements.
    • A model served on loopback or the museum's LAN can no longer be used by a turn.
    • Builds can no longer reach anything on loopback or the LAN.
    • A command-line tool that ignores the standard proxy environment variables has no network.
    • A tool that tunnels anything but TLS naming the host it asked for (plain HTTP over port 443, Encrypted Client Hello, a handshake naming two hosts) is disconnected, with a line in the log.
    • A run may hold at most 128 connections through the daemon at once. One more is refused, with a line in the log.
    • An opencode turn installs its provider's package from registry.npmjs.org on first use. Name that host in agent.provider_hosts too.
    • The daemon refuses to start any confined run (503, naming the cause) when:
    • the host's systemd is older than 248, or its version cannot be read;
    • its socket (LISTEN_SOCKET) or the agent socket directory is not under /run;
    • it cannot read its own network namespace;
    • the site has no agent user of its own on the host (see the entry on per-site agent users);
    • the site builder or its bun can be changed by any site's agent user, or cannot be read or run by it (a directory above them that such a user owns counts as one it can change, whatever its permissions);
    • the site builder or its bun lives under /home, /root, /run, /tmp or /var/tmp.
    • A site builder and bun owned by the engine's own user, as the documented install lays them out, are accepted.

    See the site builder internals.

  • The site builder can no longer start its agent units through polkit (action needed)

    The polkit rule the site-builder provisioner installs (/etc/polkit-1/rules.d/49-dedalo-site-<instance>-agent.rules) allowed the site builder's service user to start any systemd unit whose name began with the instance's agent prefix. polkit is told the unit's name, but not which user the unit will run as. On systemd 257 or newer that meant the service user could start a unit with that name as root, so the site builder's daemon was effectively root on the host.

    The rule now allows only stop and kill, and only on the runs of the museum's declared sites. The daemon no longer starts any unit. A run is started by systemd from units that root installs for each site, as that site's own user (see the entry on per-site agent users, in this same release). No per-run file is written for systemd to read as root.

    Action needed: run the site-builder provisioner (provision apply) on every host. It installs the narrowed rule together with the per-site units.

  • Transcription actions check the audiovisual component and the user's projects before they touch a recording.

    Building the audio file for transcription, sending a recording to the transcription server, checking its status and building subtitles checked only the section's permission, and two of them skipped the check when a field was missing from the request. They now check the section, the audiovisual component (or the transcription field they write) and the record's project first, before anything is read, sent or written; every transcription path asks the same thing of the recording — permission to consult it — and write access only to the transcription field it fills, so a transcriber gets the same answer whichever engine they choose, and a user with no access to the recording gets none of them. A transcription that finishes after the user lost access to the record, or after their account was deactivated or deleted, is no longer saved. Checking a server transcription's progress now reaches only the checking user's own job on that recording, and answers its progress alone: it no longer accepts a guessed job number, and it never returns the finished text, which the server saves into the record itself.

    Wire contract: WC-2026-09-30-transcription-record-tipo.

  • Every write through a tool, the assistant or the record doors now asks the same four questions, in the same order.

    A write names a section, often a component and a record. Every door that performs one — the record save, duplicate and delete, the tools, the tag delete of a transcription, the assistant's write tools — now answers through one shared rule: is the record id a real id, does the profile grant the section, does it grant that component of that record (with the rule that a user cannot raise their own profile, developer flag or username), and is the record inside the user's projects. Before, some tools asked only part of it: a global administrator with write access to user passwords could reach the root account's record through a tool, a record id like 1.5 or abc was accepted, and a user could change their own profile assignment through a tool that the record editor refused. These requests are now refused. A section that is read-only by design (Activity, the Time Machine) is also refused to every tool, importer and record door that would create, overwrite or delete its records, for administrators too. A CSV import row that names a record id still free when the file was read, but taken by another user's new record while the import ran, no longer writes into that record as if it were the import's own: the row is now checked as a change to that existing record (its projects, and the rule on a user's own account) and reported as updated, not created. The same holds for a file import whose file name names a record id: if the id was still free when the import started but another user's new record took it during the import, the file is now checked as a write to that existing record (its projects) instead of being treated as the import's own new record, and is refused when that record is outside the user's projects. When the assistant is asked to find a record or create it, it now checks every field it would fill before creating anything, and a refused fill no longer leaves an empty record behind. The one exception kept is the record editor's save, and the deletion of a tag in a text field, which is an edit of that same field: as before, they ask the field's grant only, so a user can still edit a linked record's fields through a portal they may edit; your review of that exception is invited in the wire-contract entry.

    Wire contract: WC-2026-09-30-write-door.

  • Outbound fetches now refuse every IPv6 route to an internal address. (action needed)

    When the server fetches a URL on a user's behalf (an RDF import, an external catalogue lookup, a translation or transcription service, a harvest), it first checks that the address is on the public internet, so a user cannot point it at your internal network or at the cloud metadata endpoint. Until now several IPv6 forms passed that check although they lead to an internal address: the NAT64 prefix 64:ff9b::/96 (on an IPv6-only host this reaches 169.254.169.254), 6to4 2002::/16, the IPv4-compatible and IPv4-translated forms, the old site-local range, multicast, Teredo, and any address carrying a zone (%eth0). The documentation ranges (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 2001:db8::/32) were also accepted.

    Now all of them are refused. An IPv6 address is accepted only inside the global unicast space. One that carries an IPv4 address through the IPv4-mapped form or NAT64 is judged by that IPv4 address, so on an IPv6-only install public sites stay reachable. 6to4 and Teredo are tunnels and are refused whole.

    Action needed on an IPv6-only server behind a NAT64 translator that uses its own prefix (a network-specific one, or one taken from the local-use block 64:ff9b:1::/48 or a unique-local range): IPv4 sites are unreachable from it until you declare the translator's prefix, exactly as it is configured, in the new setting DEDALO_NAT64_PREFIXES (for example 2001:db8:64::/96 or 64:ff9b:1::/96). Addresses inside it are then judged by the IPv4 address they reach. The server also asks the network for its NAT64 prefix on its own, but uses the answer only to refuse more, never to allow more. Nothing to configure anywhere else.

    With DEDALO_TRANSCRIBER_ALLOW_PRIVATE_HOSTS on, an on-premise transcription server may still not sit on IPv4 link-local (169.254.0.0/16) or on another cloud's metadata address (100.100.100.200, 192.0.0.192, and the IPv6 metadata servers of AWS, fd00:ec2::254, and Google Compute Engine, fd20:ce::254), in any spelling — through a local-use NAT64 address (64:ff9b:1::/48) included.

    Action needed if the address allowlist (DEDALO_ERROR_REPORT_ALLOWED_IPS) has an IPv4 part with a leading zero (127.0.0.01, 010.0.0.1): such an entry is no longer read as a number and matches no client, because some systems read it as octal — so the error report refuses that client until you rewrite the entry without leading zeros. An entry now matches its address in every spelling: 2001:db8:0::1 matches a client reported as 2001:db8::1, 127.0.0.1 (or the loopback token) matches ::ffff:7f00:1, the form a dual-stack listener may report, and a block written in the IPv4-mapped form (::ffff:10.0.0.0/104 or ::ffff:a00:0/104) is the IPv4 block it spells (10.0.0.0/8).

Removed

  • The browser installer is gone — install with the terminal installer; a server with no configuration now refuses to start. (action needed)

    Every installation route — production, Docker, trial, developer — now runs the terminal installer (./install.sh, or bun run dedalo:install). The browser wizard, its unauthenticated install API and the DEDALO_INSTALL_ALLOWED_IPS and DEDALO_INSTALL_NO_RESTART settings are removed; delete those lines from your .env and compose overrides. A server started without configuration no longer serves a setup page: it writes one line to its log ("not installed: … run the installer", or "restore .env from backup" on an installed instance whose .env is gone) and exits with code 78, which the shipped systemd units do not restart. An interrupted install, or an interrupted re-run on a live instance, keeps the server from starting until it is finished with the installer's --resume. Hosts installed with an earlier dedalo-ts@.service template keep it until it is replaced: update the template from the release so the unit stops on code 78. Adding thesauri after install is unchanged (Maintenance › Import hierarchies). See Installer reference.

    Wire contract: WC-2026-10-12-install-surface-removed, WC-2026-10-08-install-ip-denied-names-address.

Changed

  • The debug info bar is one thin line on a tablet or phone

    On a narrow screen the developer info strip under the menu (server code version, database, PostgreSQL and runtime versions, memory, sapi…) wrapped to three lines, taking a large slice of the screen before any content.

    Below 1024px it is now one thin line whose values scroll sideways, so every value stays reachable and nothing is hidden; the menu reserves far less height for it. On a desktop it is unchanged.

  • The diffusion publication ledger drops the indexes it never used.

    The publication ledger (matrix_activity_diffusion, the dd1758 section) records one row per record per publication run, and it had accumulated indexes the engine never reads:

    • four on columns no component of the section ever writes (geo, iri, media, `relation_search) — the section's fields are id, date, relations, number, text and a select, so those columns are always empty;
    • one on misc, whose only use is the retry stamp, read as an expression a GIN index cannot serve;
    • four that merely duplicate an index already kept, on a table whose section_tipo holds a single value (section_id, section_tipo, section_id DESC, and a second id index alongside the primary key).

    Every one of them was maintained on every published record — a permanent tax on publishing, paid for indexes that were never chosen. On update the useless GINs are dropped at boot and the redundant btrees removed. The indexes that matter stay: the one that answers the pending-unpublish checks, the one that answers the ledger's own record lookups, and the one the retention window uses. Publishing gets slightly cheaper and the database slightly smaller; no search behaviour changes. If you have already run Maintenance → Database info → Optimize tables on an older version, these drops are a no-op.

  • A publication job now says when it has moved on to the linked records.

    A publication job first publishes the records you selected, then every record they link to (for coin types: their coins, mints, designs…). That second part can be most of the work, but the progress used to stay at "100% (estimated)" the whole time, as if the job were finished. Now the job's message says Publishing linked records: 12345 (coins).... In Maintenance → Diffusion server control the bar switches to a moving "Selection done · linked records: 12,345", and the Live activity band says how many jobs are in that phase. The linked part has no estimate, so no percentage or time remaining is shown for it.

  • Large exports no longer load the whole selection into the server's memory.

    An export now reads its records in pages from one consistent snapshot of the selection, instead of loading every record id before writing the first line, so a very large export uses the same memory as a small one. Two limits are now visible: the in-page (non-streamed) export answer is refused with export.format_limit above DEDALO_SEARCH_CLIENT_MAX_LIMIT records — the streamed and background exports serve the same selection — and each server process runs at most a quarter of DB_POOL_MAX exports at once (at least one), answering export.busy (retry shortly) to an export asked for directly beyond that, while a background export waits for its turn instead of failing. A server configured with DB_POOL_MAX=1 cannot run exports; give it at least 2. A streamed export whose reader stops reading for 15 minutes is closed with export.consumer_idle, which frees its database connection; an export that keeps being read is never cut, however slow.

    Wire contract: WC-2026-10-11-export-buffered-cap-and-admission.

  • Thesauri are offered from one exported manifest, hierarchy.json, and nothing but declared dependencies is pre-selected. (action needed)

    The thesauri a release offers at install time, and in Maintenance › Install hierarchies, are now listed in one file, install/import/hierarchy/hierarchy.json. It is exported from a master installation with the new Export hierarchy.json button of Maintenance › Export hierarchy. It replaces the three hand-written files hierarchies.json, hierarchies_typologies.json and hierarchies_to_install.json, which are deleted. Each entry describes the thesaurus completely (name, typology, default language, real section, scope note, declared dependencies) and lists its data files with their sha256 checksums.

    • Every listed thesaurus is offered, with or without data files. One without data files is installed as an empty thesaurus, ready for your editors.
    • Data files are verified before anything is written. A missing file, or one whose checksum does not match, refuses that thesaurus, and the install reports it. A malformed hierarchy.json is refused as a whole rather than offering a shorter list.
    • An entry must fit this installation before its terms are copied. If its language or typology record does not exist here, or its real section is not a section here, the thesaurus is refused and nothing is imported. An install that was interrupted after the import is finished by the next run. If a thesaurus was installed empty and a later release ships its terms, the install says to use Reset to seed; it does not skip it silently.
    • A thesaurus's own dependencies are suggested with it. Ticking a thesaurus ticks the thesauri its entry declares; you can untick any of them. A thesaurus dependency never stops an install: one you leave out, or one hierarchy.json has no entry for, gives a warning that it is strongly recommended and can be installed later. Only a declared mandatory ontology this installation lacks stops the import, and the installer says why.
    • Only declared dependencies are pre-selected. The installers no longer pre-tick Spain or any other thesaurus. Every declared thesaurus is ticked — a mandatory one marked strongly recommended, never locked — and nothing else is selected. Both installers suggest importing your own country's toponymy (for example --hierarchies np for an installation in Nepal); --list-hierarchies prints the codes.
    • A missing data file is no longer an error, because the thesaurus is created empty.
    • The data export takes an explicit list only. The * (every active hierarchy) and all (the whole table) forms are gone. Each tipo must be the thesaurus or model section (<tld>1 or <tld>2) of an active hierarchy. Languages (lg1, lg2) is refused, because its terms ship in the database seed. Export hierarchy.json skips a malformed registry record and names it, and still writes the file for every other thesaurus.
    • Thesaurus names in the installers appear in your interface language.
    • The install seed now carries only the Languages registry record. Every other thesaurus's record is written from its hierarchy.json entry the first time it is activated.

    Action needed if you added your own hierarchies to install/import/hierarchy/ and listed them in the old hierarchies.json: add their entries to hierarchy.json, copied from the source installation's exported hierarchy.json. See Install new hierarchies.

    Wire contract: WC-2026-10-10-hierarchy-json-manifest.

  • Ontology dependencies are declared in the master's Dependencies field, say whether an ontology or a thesaurus is needed, and can be optional.

    An ontology master now declares what each ontology needs in the Dependencies field (hierarchy60) of its Ontologies main record. This replaces the Required ontologies field from the previous build. That field is removed automatically from every installation at its next start. Its values are not converted, so a master that filled it must declare them again in Dependencies.

    Each dependency names a TLD and says whether its ontology or its thesaurus is needed, and whether it is mandatory.

    • Installing. The installer always installs mandatory ontologies. It offers every other dependency already ticked, and you may untick it, on its screen or with --decline-dependencies. A thesaurus never blocks an install: a mandatory thesaurus is a strong recommendation, marked strongly recommended on the screen. Leaving it out, or a release that does not ship it, gives a warning that names it and says it can be installed later from Maintenance › Install hierarchies. You can also keep a thesaurus of your own under another name.
    • Updating. An update reports each missing mandatory dependency under Import warnings, together with how to provide it. It never installs one on its own.

    The ontology registry also keeps local decisions through imports and rebuilds. A domain ontology you switched off stays off, and its project filter is kept. Each ontology is filed under the installation's own structure language, not a fixed one.

    See Declaring what an ontology requires.

    Wire contract: WC-2026-10-10-ontology-dependencies-hierarchy60.

  • Installations now choose their domain ontologies (Oral history by default) and install what each one declares it depends on; the install database carries only the core ontologies and no test data. (action needed)

    Until now every installation got the same ontologies, whatever it was for: the install database carried Oral history (oh) and the developers' test ontology, and nothing asked which heritage domains the institution catalogues. The installer now asks (installer reference).

    • The install database is core-only. It carries the core ontologies (dd, rsc, ontology, ontologytype, hierarchy, lg) and nothing else. The developers' test ontology and the test3 playground records are no longer installed; they exist only in the developers' test database.
    • At least one domain ontology is installed. The default is Oral history (oh). It is built into the release, so it installs without a network. Any other ontology the update server offers can be chosen by its code. For example, tch (Tangible cultural heritage) is the general inventory model for objects and collections. The interactive installer has an Ontologies screen. The command line takes --ontologies oh,tch and lists the server's catalog with --list-ontologies. install.sh asks one more question.
    • Dependencies come with the choice. An ontology server now publishes, for each ontology, the ontologies it needs. The installer installs them too, before the ontology that needs them, and says so ("tch also installs: …"). An older server that does not publish them gets a clear warning: the chosen ontology is installed alone, and nothing is guessed. After the import, the installer checks that every node of the installed ontologies can find the nodes it refers to. Anything missing is reported as a warning that names the ontology to add. On an ontology master, an editor declares the dependencies in the Dependencies field of each ontology's record (Ontology › Ontologies main), and they are published with the next export (updating the ontology); see the separate note on that field for mandatory and optional dependencies and required thesauri. Importing an ontology (at install time or with the update panel) keeps its declaration in that field, so a master that imports its ontologies from another master publishes them too.
    • Fully offline installs. --ontology-source <dir|archive> installs from a directory (or a .tar/.tar.gz/.tgz) laid out like an ontology server's export. An air-gapped install (--no-update-servers) is offered only the built-in oh.
    • Nothing is half-installed. The ontology files are downloaded and checked before the database is touched. If an import fails, the install stops and is not sealed. Recreate the database and run the installer again.
    • ACTIVE_ONTOLOGY_TLDS is written by the installer (the core, the chosen ontologies and their dependencies), so the ontology update panel refreshes exactly what the installation carries. When the key is unset, the engine's fallback is now the core list alone: utoponymy and nexus are no longer in it. Action needed: if your /private/.env has no ACTIVE_ONTOLOGY_TLDS and your installation uses utoponymy or nexus, add the key with them (for example ACTIVE_ONTOLOGY_TLDS=["dd","rsc","ontology","ontologytype","hierarchy","lg","oh","utoponymy","nexus"]), or the update panel stops refreshing them.
    • On a development server, the maintenance Unit test area's Truncate test table and Create new empty test record now refuses on a database that has no test ontology (maintenance.action_refused). That is any installation's database. The message names the developers' test database as the place where the playground lives.

    Wire contract: WC-2026-10-09-ontology-manifest-dependencies, WC-2026-10-09-install-domain-ontologies, WC-2026-10-01-unit-test-widget-dev-gate.

  • "On a publication host with fapolicyd, the trust of the agent, its Bun and every Publication API release it installs is now kept by the host itself: no trust lines to run by hand, and a rollback is trusted too."

    Before, the guided install stopped at host.fapolicyd until you ran printed fapolicyd-cli lines for the site's Bun and the agent's code, again after every code update, and the API releases the agent installed later were not covered at all. Now, wherever fapolicyd is installed, provision apply writes one trust file per instance (/etc/fapolicyd/trust.d/dedalo_<instance>) and installs a root service, dedalo-pubhost-trust-<instance>, that the agent starts after every release install and before every rollback. The service works out what to trust from the instance's declaration (the Bun, the agent's code, the current and the previous release of each API it serves); the agent cannot name a file. A release it cannot verify is refused with trust_failed and the previous release keeps serving. Two new init items warn about fapolicyd's own settings and print the lines that change them: host.fapolicyd_integrity when integrity would let a file changed after it was trusted still run (integrity = sha256), and host.fapolicyd_mounts when allow_filesystem_mark = 0, fapolicyd's default, under which it never sees what the agent and the API services open at all. install.sh still stops, with the one line to run, when fapolicyd denies the Bun it starts. A dedalo trust file left by the old hand-run lines is no longer needed. See Publication host agent.

  • "On RHEL, Rocky and Alma, the v2 API of a site installed in its home now uses the publication host's own SELinux type, as in the system layout."

    With SELinux, the v2 API tree of a site installed in its home (/home/<domain>) was labelled data_home_t, a home type the web server can read when httpd_read_user_content is on. It is now labelled dedalo_publication_v2_t, the one type of the publication host's own policy module dedalo_publication_host, in both layouts: systemd may only read it and the web server may not read it under any boolean. provision apply (or init) installs the module on every SELinux host. On a site already installed in its home, the next run re-types its own rule in place and relabels the tree, before v2 starts again; nothing is to be done by hand. A rule of yours on the same path is refused, never changed. Run provision apply (or init) again on such a site. See Publication host agent.

  • The guided publication-host install now proposes the v1 database connection it finds on the host.

    provision init asks how the v1 Publication API reaches MariaDB: through its unix socket or over TCP. Until now the proposed answer was always the socket, even on a host without a local MariaDB. Init now looks for a local MariaDB socket (/run/mysqld/mysqld.sock on Debian and Ubuntu, /var/lib/mysql/mysql.sock on RHEL, Rocky and Alma). It proposes that socket when one exists, and otherwise TCP to 127.0.0.1:3306, saying whether anything listens there. The question is still yours to answer. For TCP the proposed host is 127.0.0.1, never localhost, because v1's database driver reads localhost as "use the socket" whatever the port. See Publication host agent.

  • "install.sh now asks you to trust its Bun in the instance's own fapolicyd file, and says when to remove it."

    On a first install with fapolicyd running, install.sh stops until the Bun it starts is trusted. The line it printed wrote a shared dedalo trust file, which the guide elsewhere tells you to remove, and kept a stale entry for every instance installed that way. It now writes /etc/fapolicyd/trust.d/dedalo_init_<instance>, and prints the line that removes it once init converged; the installed Bun is then trusted by the instance's own trust file.

  • The publication host's guided install is now proven on Ubuntu 26.04, including the agent's one root permission under its new sudo.

    The guided install of a publication host (install.sh, provision init) now passes its full drill on a real Ubuntu 26.04 machine with AppArmor enforcing: Apache and nginx, the Publication API v1 runtime, systemd 259 and polkit 127. Ubuntu 26.04 replaces sudo with sudo-rs; the drill now also checks, on every system, that the agent's single root permission (testing the web server configuration before a reload) works through the machine's own sudo, from the agent's own service, and allows nothing else. Ubuntu 26.04's sandboxed apache2 service serves the site with its logs in /var/log/apache2/<domain>. No AppArmor profile confines the web servers, the v1 runtime pool, polkit or the agents, so no AppArmor change is needed. See the publication host install guide.

  • A publication host on its own machine is now proven on Ubuntu 26.04, from the panel's kit to the first publication.

    A work system and a separate publication host, both on real Ubuntu 26.04 machines with AppArmor enforcing, went through the whole two-machine path with no change needed: the kit built in the Publication hosts panel, the guided install from it, pairing from the command line and from the panel, a token rotation and a new pairing, then the media rules, the media probe, pushing the Publication APIs (v2 alone, and v1 with v2 on a second site), rolling them back and pushing again. The firewall rule from the guide, Ubuntu 26.04's sudo-rs and its sandboxed apache2 service on both machines, and the Publication API v1 runtime on version 8.5 all work as they are. See the publication host install guide.

  • New installations are connected to the official update server and always have the Languages thesaurus; a server restarted by systemd or Docker must now declare DEDALO_SUPERVISED=true. (action needed)

    Every way of answering the installer — its screens, its flags, an answers file — now works from one install plan, so the same answers give the same configuration whichever one you use (installer reference).

    • Update servers are written by default. Installs used to leave ONTOLOGY_SERVERS and CODE_SERVERS out of /private/.env, which meant no ontology or code updates were ever offered. The installers now write the official Dédalo server (v7.master.dedalo.dev) into both. For an air-gapped installation, answer no to the update-server question, or pass --no-update-servers to the command-line installer. Both keys are then written as [], and you can add them later. Re-running the installer does not overwrite update servers you already set. An earlier air-gapped [] is not kept, though: answering yes on a re-run switches the installation back to the official server.
    • Languages is always active. The Languages thesaurus (lg) is switched on together with the database on every install. It is no longer a choice and is never imported, because its terms already ship with the installation database.
    • Default working languages: English and Spanish. The installer defaults to lg-eng and lg-spa; the other languages are still offered, but they are optional and you tick them yourself.
    • One default set of optional thesauri. Every way of answering starts from the same default: today that is Spain (es). The command-line installer used to install no thesaurus by default and now installs this set; use --hierarchies none to skip it. A thesaurus that fails to install now stops the install before anything is sealed. Unknown command-line flags (including the old --yes) are refused, and the database host defaults to localhost instead of /tmp.
    • Supervision must be declared (action needed). The code update panel only replaces the code tree when something will restart the server afterwards. It used to guess this from systemd's own variables. Those variables are also inherited by terminal shells in desktop sessions, so an unsupervised bun run start could look supervised: the update would install the new code, stop the server and leave it stopped. The guess is gone. Now the code update runs only when the process manager declares DEDALO_SUPERVISED=true, and the shipped systemd units, compose stacks and the dev / dev:server / start:supervised scripts all do. If your systemd unit or compose file was written before this release, add Environment=DEDALO_SUPERVISED=true to the unit's [Service] section, or DEDALO_SUPERVISED: "true" to the dedalo service's environment:. Setting it in /private/.env has no effect, because every launch method reads that file, including the unsupervised bun run start (updating the code).

    Wire contract: WC-2026-10-08-install-plan-update-servers-core-lg.

  • "provision check now refuses a publication host where a service could not read or run its own code, and an engine_group that is one of the instance's own groups."

    Until now, a home directory left at Ubuntu's 0750, an agent copy that only root could read, or a site Bun the v2 account could not execute passed check and apply, and the service failed later with Permission denied. check now judges every path a service runs from with that service's own user and groups: the agent's code and every directory above it, the site's Bun for the agent and the v2 API, the binary that checks v1 releases, and the way down to the state root for the agent, v2 and v1. Each refusal names the account, the path and the chmod that fixes it. On one machine it also refuses an engine_group that is the agent's own group, or the group of the v1 or v2 accounts: the agent's socket would then be closed to the work system. Whether the work system's user is in the declared group is still proved by the socket request of step 7. check reads only the mode bits, so a host that grants access through an ACL alone is now refused: give the access with the mode instead. See Publication host agent.

  • The Ontology area can now be opened by global administrators whose profile grants it, not only by root.

    Until now only the root account could see the Ontology area in the menu or open it. It now also opens for a global administrator whose profile grants the Ontology area. Both are needed: a global administrator without that permission does not see it, and neither does a user who has the permission but is not a global administrator. Inside the area, the profile decides what the administrator sees: each ontology (for example dd, rsc or a local one) appears only if the profile grants read access to it, and its records and fields follow the usual read and edit permissions. Root still sees and edits everything. To give someone access, grant the Ontology area and the ontologies they should work on in their profile.

    Wire contract: WC-2026-10-07-ontology-area-admin-grant.

  • Preparing the publication host agent now installs exactly the locked dependency versions, and only the ones it runs with.

    bun run hostagent:install now runs a frozen, production-only install: it uses the committed lock file exactly, stops if the lock and the package list disagree, and leaves out the development tools, so they no longer reach the publication server, and provision check refuses an agent copy whose node_modules/ holds one. Contributors who run the agent's test suite prepare the tree with bun run hostagent:install:dev instead. See Publication host agent.

  • "provision check no longer reads as an error when it lists the changes to make."

    A first provision check on a new publication host lists the changes apply will make. It used to exit with code 1, so bun run printed error: script "provision" exited with code 1 under a perfectly normal report. It now exits 0 and ends with run 'apply' to make them. Scripts that need to know whether changes are pending pass --exit-code, which keeps the old code 1. Refusals (3) and failures (4) are unchanged. See Publication host agent.

  • The in-browser AI runtime is updated to transformers.js 4.3.0, running on the exact ONNX Runtime build it was made for.

    Browser-side transcription, translation and background removal run on transformers.js, which is now 4.3.0 (WebGPU on Safari 26 and later, plus fixes to Whisper's progress reporting). Its ONNX Runtime is now exactly the build that release was made and tested with (onnxruntime-web 1.31.0-dev.20260914). Before, the installed ONNX Runtime was a different version from the one the bundle was built against. That combination worked, but nobody had tested it. The two versions are now checked against each other on every build and always update together. Nothing to do on update. Models you already downloaded keep working.

  • The code-update panel now leads with its verdict and the update button; the reference facts fold away.

    The Update code maintenance panel used to list every fact at once: installation details, all eleven readiness checks, the last update and every restore point. It now shows the readiness verdict and only the checks that need attention (refusals, warnings, a pending backup waiver), followed by the server picker and the update button. This installation, All checks, Last code update and Restore points are folded on a one-line summary (version and build type, per-state counts, the last update's outcome, the number and date of restore points) and open with a click; each fold remembers whether you left it open in this browser. A last update that is still pending or was rolled back opens by itself. The developer-builds warning is shown once that option is ticked.

  • The maintenance Unit test area only offers the JS test runner and the test-table reset where they can work.

    Open JS unit test is shown only on a development server (DEDALO_DEV_MODE) whose dev dependencies are installed. A production install (the default Docker image, or one kept current by the code updater) does not have the browser test libraries, so the runner page used to open and fail with unreadable MIME-type errors; the panel now says which libraries are missing instead. Truncate test table and Create new empty test record is shown only on a development server, and the server refuses it elsewhere (maintenance.dev_mode_required). Run long process stays available everywhere.

    Wire contract: WC-2026-10-01-unit-test-widget-dev-gate.

  • Hitting a database limit now shows a clear "try again" error, and maintenance runs on its own database connections.

    When a database statement runs past DB_STATEMENT_TIMEOUT_MS, or a request waits past DB_POOL_ACQUIRE_TIMEOUT_MS for a free database connection, the user now sees a "took longer than the server allows" or "the server is busy, try again" message instead of a generic server error. Maintenance no longer runs under the statement limit: the long maintenance-area actions (rebuilds, VACUUM and REINDEX, bulk transforms, imports) and data updates use a separate set of database connections without it, sized by the new DB_MAINTENANCE_POOL_MAX (default 2 per process). Count it TWICE in your connection budget, plus 2: each engine process may hold DB_POOL_MAX + 2 × DB_MAINTENANCE_POOL_MAX + 2 connections (16 with the defaults), because the index-rebuild lane keeps its own idle connections and a stop or an update verdict opens up to 2 short-lived ones of their own (see the database settings). The other maintenance-area actions keep the limit and never wait behind a long one, and the dataframe integrity scan now stops a batch that runs past its time budget instead of running on. When the server stops, it cancels only its own running maintenance statements: another installation sharing the same PostgreSQL server is never touched. The defaults of the three limits are unchanged (0, off); 60000, 30000 and 5000 are the recommended production values, and the database settings page lists the long operations to measure on a large installation before you set the statement limit. A concurrent index rebuild, plain VACUUM or ANALYZE that is still running when the server stops is left to finish on the database instead of being cancelled, because a cancelled concurrent rebuild leaves a broken index behind (the blocking forms — the REINDEX TABLE and VACUUM FULL of "Re-create db assets" — wait at most 5 seconds for their table like any other maintenance step, are retried a few times, and are cancelled at shutdown, which undoes them cleanly; before, such a statement queued behind a long backup kept every later reader of the table waiting for the whole backup); and "Optimize tables" now first removes any such broken index an earlier interrupted rebuild left on the tables it optimizes. A long maintenance action still gives up after 5 seconds of waiting for a table another operation is using, so it never holds up the users reading that table: the step it was on is undone, steps it had already finished are kept, and the action reports "try again in a moment" (or, for the search-store rebuild, an error line for that store). The bulk transforms (move TLD, move locator, move to portal, move to table, move language) now apply each definition file all-or-nothing: a file that waits too long is retried a few times and, if it still cannot finish, is left completely unapplied and named in the report. Before, a file could stop part-way with some tables already changed, leaving a section's records split between the old and the new tipo. Rebuilding the database constraints, triggers or indexes is now all-or-nothing for each table: if the new constraint, trigger or index cannot be created (including after that 5-second wait), the old one is kept instead of being left removed, and the action lists the failure by constraint, trigger or index, table and database error code (the full database message is in the server log). An index is now built next to the old one, which keeps serving searches until the new one takes its place, so the table stays readable during the rebuild.

    Wire contract: WC-2026-09-30-db-typed-503.

  • The official update server for v7 installations is v7.master.dedalo.dev.

    Code and ontology updates are now split by version. v7 installations update from https://v7.master.dedalo.dev/dedalo/core/api/v1/json/ — the address the configuration examples for CODE_SERVERS and ONTOLOGY_SERVERS now show. v6 installations keep updating from their own server exactly as before; a v7 update server answers only v7 installations.

    For an installation that serves updates to others, the suggested layout keeps the release archives in /srv/dedalo/code (DEDALO_CODE_FILES_DIR) and the ontology files in /srv/dedalo/ontology (ONTOLOGY_DATA_IO_DIR), next to the media in /srv/dedalo/media. Nothing changes for an installation that leaves these settings unset.

  • A published code release is now built from a release tag (vX.Y.Z); developer builds come from master.

    On a code server, Serve Code's Build release button used to archive the tip of the master branch, and Build developer release whatever other branch the server had checked out. Now a published release is always a tagged version: Build release archives the newest vX.Y.Z tag of this engine in the build checkout (prerelease tags such as betas are not releases, earlier-engine v6 tags are never candidates, and a tag whose version file disagrees with its name is refused), and Build developer release archives the tip of master — the latest integrated code, before its release. The branch the server has checked out no longer matters.

    What to do on a code server: tag each release commit (git tag v7.0.1, pushed to the remote) and fetch tags into the build checkout (git fetch --tags) before building; until a tag exists the panel reports that nothing can be published. While v7 is in beta this is the expected state: installations receive v7 code only as developer builds. See Updating code.

    Wire contract: WC-2026-09-29-code-release-channel-refs.

  • Building and serving code releases now has its own maintenance panel, Serve Code.

    The Update code panel used to hold two jobs: installing a new release on this installation, and — on a code server — building releases from git and serving them to others. The second job is now its own panel, Serve Code, shown only on a code server (IS_A_CODE_SERVER=true) or the development installation. Update code keeps installing, restoring and deleting restore points.

    For scripts that build releases through the API: send the build request to serve_code instead of update_code (same action name, build_version_from_git_master, same options).

    Wire contract: WC-2026-09-28-maintenance-serve-code-widget.

  • Serving your ontology to other installations now has its own maintenance panel, Serve Ontology.

    The Update Ontology panel used to do two jobs: download an ontology from a master server, and — folded away at the bottom — report whether this installation can serve its own ontology to others. The two are now separate panels. Update Ontology only downloads; the new Serve Ontology panel shows the three ../private/.env settings that decide serving (IS_AN_ONTOLOGY_SERVER, ONTOLOGY_SERVER_CODE, DEDALO_CORS_ALLOWED_ORIGINS), the lines to add, and the address other installations must register.

    Wire contract: WC-2026-09-28-maintenance-serve-ontology-widget.

Added

  • The diffusion job queue shows who started each publication.

    The Job queue table of the Diffusion engine & queue maintenance panel has a new User column with the name of the user who launched each publication (or the user id when the account has no name), so an administrator can tell whose job is running or waiting before cancelling it.

  • The publication job queue shows how long each finished job took.

    In Maintenance → Diffusion server control, a finished job's progress line now ends with its total time (for example "42,137 / 42,137 · 12 min 8 sec"), measured from the first start to the finish, retries included. It is the same figure the publication tool shows when the job ends.

  • The publication server panel shows an estimated time remaining for the running jobs.

    In Maintenance → Diffusion server control, the Live activity band now shows next to the progress bar roughly how long it will be until the running publication jobs finish (for example "~12 min remaining (estimated)"). It is measured in your browser from how fast each running job has moved over the last few minutes. It appears a few seconds after live updates start, grows if a job stalls, and leaves out queued jobs and jobs queued without a record estimate.

  • The publication tool names the target database of an SQL publication.

    Each SQL panel of the publication tool now shows a Database row: the MariaDB database the records are published into, which is also the database the Connection status refers to. The panel's Name row reads Table name, because for SQL it is the target table. Panels for other types (RDF, XML…) are unchanged.

    Wire contract: WC-2026-10-14-diffusion-info-node-database.

  • A Production instance can be installed by hand again, step by step, exactly as the installer does it.

    Install by hand is the Production route without the installer: one section per host step, in the installer's order, with the commands that step runs and the full text of every file it writes (the systemd drop-ins, the nginx or Apache site, the media map, the host registry entry). The engine part — the database seed, the ontologies, the thesauri, the root password — runs through the installer itself, as the service user, with --engine-only. A host built this way has the same layout, units, site and backups as one the installer built, and later installer runs on the same host see it. Each section of Production install links its hand version and back. A host set up by hand now points to this page; hosts already running the single deploy/dedalo-ts.service unit set keep their instructions in its last section.

  • A production install can live in any folder and run as an existing account.

    The Production route's Identity screen now asks, after the instance name, for the install folder (default /home/ded_<instance>, any absolute path — for example /home/my_domain.org/dedalo), the service user (default ded_<instance>) and the service group (default: the user's own). The same answers are the flags --install-dir, --service-user, --service-group and the answers-file keys install_dir, service_user, service_group (production only). A new account is created; an existing account — a hosting account — is used as it is and is never created, changed or deleted by the installer, --abort included. The folder's parent is never changed. The database role is the service user (peer authentication), and the backups default to <install folder>/backups. The System check refuses another instance's user, group or folder (and an account in another instance's group), root and system accounts, the packages' accounts (postgres, the web server's), privileged groups, a folder the installer may not use (an OS directory or another account's is never taken over), and a backup dir that already exists; the Review shows what will be CREATED and what is USED AS IS. The systemd templates now name no user or path: each instance's drop-ins carry them, and a template without its drop-in runs nothing. See The install folder and the service account.

  • The terminal installer sets up the whole host: production, Docker and trial install in one run. (action needed)

    A production install used to be a fifteen-step manual procedure. Now sudo sh install.sh --route production --instance <name> (or the interactive installer, choosing Production) does it as resumable steps: packages on the newest Ubuntu, Debian and RHEL-family releases (an older one is refused with the supported list), the instance user, PostgreSQL 18 with a password-less peer role, ONE site file for the instance (nginx or Apache — never another site or the global configuration), Let's Encrypt or your own certificate (checked against the address and expiry), the systemd units, SELinux on RHEL, a first boot, the health watchdog (armed only after the first boot) and a first backup verified store by store. Every command is shown on the Review before it runs; --resume continues after a failure or an interruption, --abort undoes only what the install created. The firewall and the web server's modules are checked and told, never changed.

    The Docker and trial routes now install from the terminal too — on Linux, or on a Mac with Docker Desktop: the image is verified (cosign, and its version label), .dedalo.env and compose.override.yaml are written, the engine steps run inside the container with their secrets on standard input, and the stack is started and probed.

    nginx serves a root-owned, validated COPY of the media access rules the engine generates (never the instance-writable file itself). After changing the media access mode, refresh it with the command the installer prints at the end of the install. --abort asks for --confirm-database before it undoes anything that touches the database.

    install.sh is now only the bootstrap — it verifies the release and the pinned Bun, then starts the installer with your arguments. It no longer installs Docker, asks questions or runs compose itself, and --wizard is retired. See Production host steps.

  • The installer asks its questions in the terminal, checks each answer as you give it, and shows the whole plan before it installs anything.

    Before, installing from a terminal meant writing every answer as a flag (or an answers file) and finding out at the end which one was wrong; the step-by-step guidance, the connection checks and the plan preview lived only in the browser wizard.

    Now bun run scripts/install.ts with no answer flags asks the questions itself, one step at a time: the kind of installation (production with a named instance, Docker, a Docker trial without media protection, or a developer checkout), a system check, the database, the institution, the ontologies and thesauri (what they declare pre-ticked — ticking one ticks what it declares — untick to decline), the languages, publication, mail and the administrator password. Every default and every refusal is the same one a flag-driven run uses. The database must answer before you go on; the MariaDB and SMTP connections are checked (the mail check can send a real test message), and you can keep a failed one by typing yes. Passwords are never shown and are typed twice; the root password is checked rule by rule. The Review lists what will be installed and the equivalent command line (without passwords), then you type install, go back with edit <n>, or save the answers with write <path> for a later unattended run. --plain (also chosen automatically for TERM=dumb, a redirected output or a small terminal) gives the same questions one per line, with no colour or screen drawing, for screen readers and serial consoles.

    Other changes for administrators: the new --route and --instance answers change a few defaults (on a production route the database and role are dedalo_<instance>, the media root /srv/dedalo/<instance>/media); the publication database now defaults to web_dedalo (web_<instance> on production) instead of being required; a run at a terminal without --execute asks for a missing root password instead of failing; --write-answers <file> saves the answers of any run; --list-langs prints the language offer and --list-hierarchies now includes what each thesaurus declares. A run whose standard input is not a terminal and that carries no answers is refused and points to --answers. A typed quit exits 0, Ctrl-C exits 130. A check whose answers change later (a new instance name, another route) runs again before install is offered. On the Docker routes the terminal installer writes the answers but does not install from the host yet; the production web server, TLS and systemd steps are still done by hand. Details: Installer reference.

  • The terminal installer draws full-screen at a capable terminal, and can abort an interrupted install

    Run at a terminal of at least 80×24, bun run scripts/install.ts now shows the installer full-screen: a rail of the route's steps, filtered lists for the ontologies, thesauri and languages, passwords drawn as dots with the root password's rules checked as you type, checks shown in place, the Review with the equivalent command line, the install's steps with their times, and a Done screen that shows the generated salt once. The questions, defaults, rules and the install are exactly the plain mode's; --plain, a small or dumb terminal, or a machine without the terminal library's native build get the plain mode (one line says why). Whatever way it ends, the terminal is restored. When an interrupted install is found, both modes now offer abort next to resume and quit (the database name typed back). See the full-screen installer.

  • The command-line installer checks every answer before it writes anything, and an interrupted install can be resumed.

    Before, the command-line installer (scripts/install.ts, which install.sh drives) found a bad answer only at the step that first used it: a root password that broke the password policy was refused after the .env had been written, the database restored and the ontologies imported, and an entity such as 1x or a port such as 70000 was not refused at all. An install that stopped part-way left a half-built database, and the only way on was to drop it and start again.

    Now every answer is checked first, and nothing is written until all of them pass and the target database is confirmed to exist, be empty and be owned by the database user the install connects as (no other install can be running against it). That covers the entity name, ports, time zone, locale, paths, SMTP encryption, media access mode, the diffusion database and user, and the root password against the policy. The answers can also come from a file (--answers answers.json, the plan's answer names in a dedalo.install.answers/1 envelope). Passwords should come from the DEDALO_INSTALL_* environment variables or --secrets-stdin, never from the command line: the --*-password flags still work but now warn, because ps shows them to every user. An answers file that contains a password is refused. No password is written to the installer's record or printed, and the generated DEDALO_SALT_STRING is printed once, at the end.

    Every run is now recorded in <private>/install/ (a plan file and a state file, kept as the install's record). If an install stops, whether from a failure, Ctrl-C or a killed process, run bun run scripts/install.ts --resume. It checks each step that already completed, does not repeat finished work, and asks again only for the passwords a remaining step still needs. To take the install back, run --abort --confirm-database <db>. That empties the database (the database itself is kept) and restores the previous .env, but a sealed install is never undone. The database restore is now a single transaction that also marks the database as this install's, so an interrupted restore leaves the database empty rather than half-filled, and neither --resume nor --abort ever adopts or empties a database the install did not fill (for example, a backup restored into it in the meantime). Backups of .env that existed before the install are never removed. Running the installer again over an existing record is refused and points you to --resume or --abort. Details: Installer reference.

  • Docker installations now pull a published, signed Dédalo image, or build it locally if you prefer, and record the choice in .dedalo.env. (action needed)

    Until now every Docker installation built its own image from the checkout. Dédalo now publishes the engine image, signed, to its own registry and to mirrors — GitHub Container Registry and Docker Hub — and you choose where your installation takes it from (where the image comes from):

    • One of Dédalo's registries. The registry table says which ones publish today; the registry Dédalo runs itself is the primary, the other two are mirrors of the same images. A registry that is not yet available is never offered. Images are published per release, so until the first stable release is published, the installer builds locally.
    • A registry of your own, holding copies of the published images (how to mirror them).
    • A local build from the checkout, with -f deploy/compose.build.yml (a local build).

    ./install.sh asks the question, offers the registries that publish your checkout's version first, and builds locally when none does. With cosign installed it checks the image's signature (checking the signature). The answer is written to .dedalo.env as five keys — DEDALO_COMPOSE_FILE, DEDALO_IMAGE, DEDALO_VERSION, DEDALO_IMAGE_MODE, DEDALO_IMAGE_VERIFY (the keys) — and every later update takes the image from the same place. The compose files no longer build: they run DEDALO_IMAGE:DEDALO_VERSION. Updating is now one command, ./deploy/dedalo-image-update.sh --version <version>, which takes a database backup, gets the image, re-pins DEDALO_VERSION and rolls back on its own if the new version does not come up healthy within the health timeout (15 minutes by default, so long boot migrations are waited for) (upgrading); with signature checking on, it also refuses a signed image that is not the version it asked for. Going back after an update that succeeded is a short manual procedure (rolling back by hand). Its old --mode, --image and --tag flags are gone.

    Action for an existing Docker installation. Add these lines to .dedalo.env (on the full stack, create the file with your three POSTGRES_* values as well), with the version your checkout declares:

    DEDALO_COMPOSE_FILE=docker-compose.simple.yml
    DEDALO_IMAGE=localhost/dedalo
    DEDALO_VERSION=7.0.1
    DEDALO_IMAGE_MODE=build
    DEDALO_IMAGE_VERIFY=none
    

    Then build once under the new name and recreate the stack:

    docker compose -f docker-compose.simple.yml -f deploy/compose.build.yml --env-file .dedalo.env build dedalo
    docker compose -f docker-compose.simple.yml --env-file .dedalo.env up -d
    

    (on the full stack, docker-compose.yml instead of docker-compose.simple.yml, in the file and in the commands). The first update of such an installation needs --skip-version-check. See an installation from before the image pin.

  • A publication host on another machine installs from one kit file built on the work host.

    Installing a publication host on a second machine no longer means copying five entries of the work checkout by hand. On the work host, bun run hostagent:pack -- --draft <draft.json> builds one archive — the agent's code with its production dependencies (installed for the kit, without the tests), the draft and the installer, with a list of every file's checksum — and prints its sha256. The same checkout and draft always give the same file. The kit carries no password and no token: those are typed or created on the publication host. There, sh install.sh <instance> --kit <file> --kit-sha256 <sha256> refuses a kit whose sha256 is not the one the work host printed, then refuses an altered, extra or missing file before any of its code runs. A draft the guided install would refuse is refused when the kit is built. Installing from a checkout (--source) is unchanged. See the kit.

  • After an install from a kit converges, provision init offers to remove the kit file it was given.

    install.sh --kit now tells init which kit file it was given and the sha256 it verified. When the install converged, init asks whether to remove that file (the default is no); without a terminal, -- --yes removes it. Only that file is removed, and only while it still has the verified sha256: a replaced file or a link is left in place and named. Re-runs need no kit, and the work host can always build it again. See Publication host agent.

  • Two-machine publication hosts pair from one sealed, passphrase-protected file.

    At the end of a guided install on a publication host reached over TLS, provision init now writes one encrypted pairing package — the engine fragment, the agent's token and the engine's TLS bundle — readable by root only, and shows its one-time passphrase once on the terminal (it is stored nowhere, and without a terminal no package is written). On the work host, dedalo:pair-publication-host add <name> --package <file> asks for the passphrase without echo (or reads it with --passphrase-stdin), opens the package in memory and runs the same checks and the same live proof as before. A wrong passphrase or an altered file is refused before anything is contacted. Delete both copies of the package afterwards; --decide pair.package=again writes a new one. Pairing with the three loose files is unchanged, and a host on the same machine still pairs directly. See Pairing.

  • "provision init reports the sealed pairing package as stale once the work host has used it, and removes it on request."

    The sealed pairing package init writes on a two-machine install holds the agent's token and the work system's TLS key. Once the publication host's agent has recorded a command from the work host after the package was written, every later init run (and --dry-run) reports the package as stale and offers to remove it — the default on a terminal, --decide pair.package=remove without one, which also removes it earlier. Init removes only the file it wrote and records the removal in its journal; a later run reports the package as gone. See Publication host agent.

  • A new publication host can be drafted, kitted and paired from the Publication hosts panel.

    As the Dédalo root user, Maintenance → Publication hosts → New publication host now makes the draft for a new publication host: type the site's domain, choose one machine or two, and the panel proposes the instance, the accounts, the service and free ports, which you can change before saving. The draft is checked with the publication agent's own rules and against the other instances on the same machine, and a refusal names the field. For two machines the panel builds the install kit from the release the code updater verified, shows its sha256 and lets you download it; then, once the publication host is installed, you upload its sealed pairing package and type the passphrase there. The package must belong to that draft — its instance and the agent address inside it — and the agent must prove it live before anything is stored; the passphrase and the package are never kept. The command-line paths are unchanged. See New publication host.

    Wire contract: WC-2026-10-09-publication-host-panel-setup.

  • A publication host whose declaration drops the Publication API v1 now has its v1 pool, log rotation and v1 files removed by provision apply.

    Re-declaring an existing v1+v2 publication host as v2-only used to leave its v1 FPM pool, the pool's log rotation, the v1 API releases and configuration (with its database credentials) and the pool's own directory behind. provision apply now keeps a record of what it provisioned (provisioned.json in the instance's configuration directory) and removes what the declaration no longer has: the pool through the FPM configtest and a reload (a pool that was the install's only one is put back and apply stops), the rotation, and both v1 trees, after the agent restarted. provision check lists each removal first. Only files that still carry the provisioner's own stamp for that instance, and trees exactly as it left them, are removed; anything edited, unstamped, foreign or with another owner stops the run and is named. The v1 SELinux rules go with them; the v1 account stays. A pool that moves to another FPM version is retired the same way. See Publication host agent.

  • A publication host can now serve the Publication API v2 only, without the Publication API v1.

    The Publication API v1 is legacy: it is needed only by websites built for Dédalo v6. A publication host's declaration without a v1 block now installs a v2-only site, the recommended shape for a new site: no v1 account, no v1 configuration file and no v1 runtime on the host, and the guided install (provision init) neither looks for nor asks about any of them. The draft chooses with "apis": "v2_only" or "v1_and_v2", or by the presence of its own v1 block, and a v2-only site names its distribution family in site.os_family. The work system never pushes a v1 release to such a host, and its Publication hosts panel shows the v1 row as Not served, which is not a fault. Existing hosts that declare v1 are unchanged. See the publication host install.

    Wire contract: WC-2026-10-09-publication-host-v2-only-site.

  • On Docker installations, the Update code panel now shows how to install each release, and can hand the update to an optional host updater.

    On a Docker installation the code lives inside the image, so the panel cannot replace it in place. Until now it only said Update blocked. It now shows an Image updates block: where this installation's image comes from (pulled from a registry, and whether that registry is one of Dédalo's, or built on the host), and, for the release you select, the exact command to run on the Docker host, ./deploy/dedalo-image-update.sh --version <version>. The in-container readiness checks are still listed, folded, because they describe the in-place update this installation does not use.

    You can also install the optional host updater on the Docker host (sudo ./deploy/dedalo-image-updater.sh install-units). It is off by default. With it installed, the panel offers Request this update: the host updater picks the request up within about a minute, runs the same command with its backup, health check and rollback, and the panel shows the outcome. A request needs the superuser and maintenance mode, and can only name a release on the normal upgrade path. The registry, pull or build, and the signature check are decided on the host, from .dedalo.env. The engine is never given control of Docker. See the host updater.

    Wire contract: WC-2026-10-09-update-code-image-channel.

  • The Update code panel shows each release's notes before you install it.

    When you check for available updates, each published release now lists what it changes: its Action needed items and one line per change, the same ones this change log publishes for that release. You can read what an update will do before you choose it, on every installation. Developer builds and releases cut before this change have no notes. See updating the code.

    Wire contract: WC-2026-10-09-code-manifest-release-notes.

  • The backup panel now says when backups are manual.

    Dédalo never makes database backups on its own: a nightly job installed with the operating system does (see Backups). When no scheduled backup of the database has ever reached the backup directory, the Make backup panel in Maintenance now shows No scheduler, backups are manual., with a link to the backup documentation. Before, it showed only the age of the newest backup, which looked like automatic backups that had stopped.

    Wire contract: WC-2026-10-08-make-backup-scheduled-evidence.

  • On an nginx publication host, the media map is now pushed by Apply media rules, and shared safely by every instance on the host.

    nginx's media rules need a small map in the server's http {} block (it checks the media login cookie and decides how SVG files are served). That map may be defined only once per server, but a publication host can carry several Dédalo instances, and until now it had to be placed and kept current by hand. On a publication host whose declaration sets web.nginx_map: conf_d, Apply media rules now sends this work system's map to the agent before the media rules. The agent never writes the file nginx loads: it hands its part to a small service that runs as root on the host, which checks every instance's part, merges them into one file, tests the configuration and reloads nginx. When every instance runs the same Dédalo version and media folder, the file is exactly the map Dédalo writes for a work host. The publication hosts panel shows the map's state for each host: the map expected and the map applied, the shared host map and how many instances it serves; a host whose map is out of date counts as drift. The host refuses, and the panel says why: a map that is not in the expected form, an instance whose media folders changed since its map was first accepted (the earlier map stays until provision apply is run for that instance), or another instance with a newer map than the host's renderer understands (the shared map is left as it is until provision apply is run for that newer instance). provision init sets web.nginx_map: conf_d when nginx includes conf.d inside http {} (the Debian and RHEL default), and moves a map placed earlier by hand into the shared one after you confirm. A host whose map is placed by hand (web.nginx_map: none, the default of a hand-written declaration) is not touched, and an agent too old to know the shared map is refused with a request to update it. Every configuration test and reload on a publication host, by the agent or by root, now waits for the others, so two instances can no longer reload the same web server at the same time; if the shared lock is missing, run provision apply for the instance.

  • "A publication host can now be installed with one guided command, provision init, on Debian, Ubuntu 24.04 and 26.04, and RHEL, Rocky or Alma 9 and 10 with SELinux; the Publication API v1 runs in a FastCGI pool of its own." (action needed)

    sh …/publication/host_agent/deploy/install.sh <instance> --source <checkout> --draft <file> looks at the publication host, compares it with what the instance needs, prints what is already right, what it will change (with the exact commands and diffs) and what needs your decision, and changes nothing until you confirm. It creates the missing accounts, gives the site's home to root, downloads and verifies the site's Bun against a hash table taken from Bun's signed checksums, installs the agent's code, writes the declaration, provisions, adds two lines to the site's virtual host (with a backup, a configuration test and an automatic roll back), asks for the API database password on the terminal only, checks that the agent answers and pairs it with the work system on one machine (on two machines it prints the pairing commands to run on the work host). It never installs a package, never edits a FastCGI pool of yours, never sets an SELinux boolean or edits one of your files without a typed answer, and never runs on Ubuntu 22.04 (its polkit cannot read the agent's rules) or on RHEL, Rocky or Alma 8 (systemd 239 and kernel 4.18 are below what the units and Bun need: install.sh refuses EL 8 before it downloads Bun). Whoever can write the source it is given can become root through it: give it only a checkout you trust.

    On RHEL, Rocky and Alma with SELinux, provision apply now labels the instance's own paths and the v2 port, and the guided install asks before widening any SELinux boolean. A shared media directory is labelled for the web server only with your consent, kept in the declaration as media.selinux_label: true (the guided install writes it when you answer act). A typed secret (the database passwords, the v1 web user code) may not contain a space, ', \ or $. The units need systemd 247 or newer: provision apply refuses an older one.

    The site's web server logs stay outside its home: in the home layout provision apply creates /var/log/apache2/<domain>/ (RHEL /var/log/httpd/<domain>/, nginx /var/log/nginx/<domain>/), owned by root, and the rotation file /etc/logrotate.d/dedalo_<instance>_web (the distribution's own rotation does not reach a directory per site); point the virtual host's logs there. On Ubuntu 26.04 a log under /home stops Apache from starting, and the guided install says so. The guided install reads the sudo policy the installed sudo uses (/etc/sudoers-rs under sudo-rs, when it exists), accepts a polkit that the system bus starts on demand, and asks for e2fsprogs when chattr is missing.

    The Publication API v1 now runs in its own FastCGI process pool, dedalo_<instance>_v1, under its own account, which the provisioner writes when the declaration names the new site block, with the site's web include beside it. A hand-run provision apply now waits for a running provision init of the same instance, and provision check answers exit 5 (busy) instead of drift while one runs.

    Action needed on an existing publication host:

    • create the host group every agent's service now runs with, once per host: groupadd --system dedalo_pubhost. Until then provision apply refuses and prints that line;
    • a declaration whose v1.user is www-data, apache, nginx, www or nobody (the old page allowed the web server's user with the web server's own module) is now refused: create an account for v1 alone, give it the v1 configuration file (chown <account>, chmod 0400), declare it and the site block, and run apply. See Publication host agent and its manual steps 2, 3 and 9.
  • The Publication hosts panel now shows in red a publication server whose Bun differs from the version the work system pins.

    Each host in Maintenance › Publication hosts has a new Bun version row, with the expected version (this work system's .bun-version) and the version the host reports side by side. It is green only when they are exactly equal, red on any difference (for example 1.4.2 != 1.4.3), and unknown when the host cannot be reached or proved. The install guide now gives each site on a publication server its own Bun, installed by root in /home/<site>/.bun/ at the pinned version, so each site can be upgraded on its own, and suggests naming each instance after its site's domain (my-hosts.org → my_hosts_org). See Publication host agent.

    Wire contract: WC-2026-10-03-publication-hosts-widget.

  • Several publication hosts on one server are checked for isolation. (action needed)

    provision check and provision apply now read the other publication-host declarations on the server. They refuse an instance that shares a user (agent, v1 or v2), the v2 group, unit or port, a listening port, or a directory with another. Before, a shared user let one instance change the other's media rules and API releases, and a shared port only failed when the service started. When a runtime path (Bun, the v1 API's runtime) or the agent's directory is a link, the refusal now prints the real path to declare. The new section Several instances on one server lists what each instance needs of its own.

    The installation page now declares the instance before creating its accounts, and gives the exact commands. provision check names each missing account with its declaration field and the command, in the order to run them. The new section Lay out each site in its home directory puts each site's state root beside its document root (/home/<site>/dedalo, with the home owned by root) and shows the virtual host that maps the APIs into the site.

    The examples' v2.health_url was …/dedalo/publication/server_api/v2/health, which answers 404 under the v2 API's default BASE_PATH, so every v2 release would fail its health check. It is now http://127.0.0.1:<port>/health, which answers whatever prefix the API is published under; use that form in your declaration.

    Action needed: the declaration's web.group is replaced by v1.user, the user the Publication API v1 runs as: with one process pool per site, that site's pool user (the pools may share the web server's group). Write "web": {"server": …, "unit": …} and "v1": {"user": …}. The v1 configuration file in shared/ must now be owned by that user and readable by it alone (chmod 0400): installing a v1 release is refused with shared_config_exposed otherwise. The agent, v1 and v2 users must be three different accounts.

  • A paired publication server now gets the Publication APIs matching the work system's version, can keep a verified copy of the published media, and is checked from the public side.

    After every confirmed code update or code restore (a rollback to an earlier version), the work system sends the matching Publication API releases to each paired publication server, exactly as the update verified them. A database restore does not change the code and sends nothing; the panel shows any lag and Push API releases realigns it. A file changed on disk since then stops the push and is named in the panel. In copy mode the publication server keeps its own copy of the published media, only public qualities of published records. Unpublishing makes the files answer "not found" as soon as the publication server accepts the unmark (until it does, for example while it cannot be reached, the panel shows the deletion as pending and the files stay public), then deletes them, and the deletion counts as done only once the server's file list confirms it. The maintenance panel can also check each publication server through its public address: a published file must load and an unpublished one must not, after every rules change and on a schedule. On a copy server, where the unpublished file is normally absent, a "not found" proves nothing and the check says unknown, never a pass. See Publication host agent.

  • The publication hosts panel shows each copy-mode host's media copy, and root can bring one host back in sync on demand.

    For a publication server that keeps its own copy of the published media, the publication hosts panel now has a Media copy row. It reads OK when the copy matches what the work system publishes. It reads pending while files are still being copied or removed, and red when an unpublished file has not been confirmed deleted from the public server after one check period (ten minutes), or when the server stopped being a copy server while it still held files. The engine checks and repairs every copy server by itself every ten minutes, and right after a data restore. The root user can run it for one server at once with Reconcile media copy. Applying it by hand, here or from the Reconcile panel, is reserved to root. A server that cannot be reached is reported as a failure, never as done, and its pending deletions are completed on the next check. A copy server is now protected by the same media rules as a server that mounts the shared media (Apply media rules works for it too), so an unpublished record stops being served before its files are deleted.

    Wire contract: WC-2026-10-03-publication-hosts-widget.

  • A separate publication server can now run a small agent that the work system pairs with, to install the Publication APIs, apply media rules and report status, without root access.

    Institutions whose public website runs on its own server, or on its own hostname on the same server, can install the publication host agent there. It accepts a fixed list of requests from the work system and nothing else: report status, check the media mount, apply the web server's media rules (keeping the previous rules if the new ones fail the configuration test), and install or roll back a Publication API release (the previous release keeps serving when a new one is not healthy). The work system connects over mutual TLS, or over a local socket on a single server, and proves the pairing with a fingerprint. The agent runs as its own user with two narrow grants (the web server's configuration test; and reloading the web server, restarting the v2 API, and starting or stopping a scratch copy of the v2 API on a local port to test a new release), and the publication server never needs to download packages. See Publication host agent.

  • A separate publication server can now serve published media from shared storage, with its own access rules.

    Institutions whose public website runs on its own server, reading the same media storage as the work system (read-only), can now generate media rules for that server: bun run media:publication-host-rules --root <mount> (Apache, or nginx with --server nginx). The publication server then serves only the files of published records, in the public quality folders, and never accepts the work system's login cookie. Originals are never served. See A separate publication server with shared media storage in the media protection manual page.

  • The maintenance panel now shows each paired publication server, and can apply its media rules or roll back a Publication API release.

    A new Publication hosts panel in Maintenance, in the Publication group, lists every publication server the work system is paired with. For each one it shows whether the server answers, whether the pairing still matches, its media mode and mount, whether its installed media rules are the ones the work system would generate now, and which Publication API releases are current. The root user can apply the media rules, check the media mount, roll back a Publication API release, edit a server's public address, quality folders and probe files, or remove a server; other administrators see the panel read-only, without the servers' network addresses. Servers are added only on the work server's command line (bun run dedalo:pair-publication-host), run as the user that runs Dédalo, from the files the publication server's provisioner writes; the token is given in the fragment, a private file or standard input, never as an argument. The pairing is proved before anything is saved, and the work system never sends its token to a server whose pairing fingerprint does not match. The Media access control panel links to it. See Publication host agent.

    Wire contract: WC-2026-10-03-publication-hosts-widget.

Fixed

  • The Docker and Trial installers check, before you type yes, that the official image of your version is published — a beta has none, so choose Build.

    Official Dédalo images are published for stable releases only: a prerelease (a beta) and a development checkout have no official image. Until now the Docker and Trial routes defaulted to the official image anyway, so on a beta an install that kept the defaults failed at stack_image, after yes, with every answer lost. The Review now asks the registry first (docker manifest inspect, then docker buildx imagetools inspect — nothing is pulled). When the image of your version is not published, the Review says so and does not offer to install; your answers are kept, so go back to the Image screen and choose Build (--image-source build) or a registry of your own. When the registry cannot be reached, the Review says it could not be checked and does not block the install. That check waits at most 15 seconds, shows checking the official image … while it waits, runs once per Review, and says what happened in plain words (the registry did not answer, or could not be reached) — Docker's own error goes to the install log. A local build or a registry of your own is never checked this way. See official images are stable releases only.

  • Dédalo now runs on Bun 1.4.3, which fixes a runtime defect that could leave a process waiting forever, at full CPU, on a program it had started. (action needed)

    On Bun 1.4.2, a process that had already run many synchronous calls to external programs could, now and then, wait forever on one that had already finished, at 100% CPU (Bun issue 34069). The server makes such calls for a few checks (the code update panel's git checks, the versions of the PostgreSQL tools, ImageMagick, ffmpeg and curl); the command-line tools and the test suite make many, which is where it was seen. Bun 1.4.3 fixes it, so the pinned runtime (.bun-version) moves from 1.4.2 to 1.4.3, and the update panel refuses this release until the server runs it.

    Action needed, before you apply this update:

    • Host installation: as root, install the verified 1.4.3 runtime at /opt/dedalo/bun/1.4.3/bun exactly as the host_runtime step does (with PIN=1.4.3, checked against this release's .bun-sha256), point the instance's ExecStart at it (the host_units step), run systemctl daemon-reload, restart the instance, then apply the update.
    • Container installation: nothing extra; the new image carries Bun 1.4.3 (Upgrading).
    • Publication hosts: each site's Bun must move to 1.4.3 as well, or its Bun version row turns red (Upgrading a site's Bun).
  • Publication progress no longer runs past its estimate while a job is still working.

    When a section links to other records of the same section (for example types related to other types), the publication job counted those linked records as part of the selection. The progress then passed its estimate ("Processing records 42206 of 42137…", "estimate exceeded") while the job was still running. Progress now counts only the records you selected; linked records are still published as before.

  • The installer refuses an installed instance's name up front, and a refused Review checks again on install.

    • Production, an instance name already installed on the host: the System check, right after the name, now says instance <i> is already installed on this host (<registry file or drop-in>) and asks for another name. The Identity check no longer calls that instance's account "an earlier install's leftover" to remove with userdel, and a typed service user that belongs to another instance gets that instance's clash, not the leftover advice. A fresh headless run over a complete install of the name says the same instead of "resume it or undo it".
    • A refused Review: pressing Enter (or typing install in --plain) measures the plan again; once the cause is fixed (a CA trusted, a port freed) it goes on to the yes prompt with every answer kept. The key bar says enter check again instead of enter install, and the untrusted-chain refusal says "check again" instead of "run this again".
    • The first instance on a fresh host: when the run installs nginx, the closing To-do now includes the note about nginx's own default site on port 80 (/etc/nginx/sites-enabled/default).
    • Resume after an interrupted Docker install: the released-lock line says whether the earlier run was in a container or on a host.
    • Docs: production.md lists the update-server question before the domain ontologies, in the order the screens ask them.
  • The installer's System check edit, Review re-check and default-site note, salt hint and Docker abort messages now match what happens.

    • A failed System check: the check asks no questions, so edit (e on the full screen, edit in --plain) now goes back to Install type, the screen whose answers it checked. Before, it only ran the check again. The prompt, the button and the key bar all say edit (back to Install type).
    • A refused Review checked again: the checking again… line disappears when the check ends. Before, it stayed frozen under the yes prompt or next to the result. When nothing is refused any more, the installer says checked again — nothing is refused now: install is offered.
    • The salt after a failure or a Ctrl-C: once the engine steps have generated DEDALO_SALT_STRING, every ✖ block names the command that reads it back. The value itself is never printed there. Ctrl-C leaves the full screen, which was the only place that showed the salt. On docker and trial the command is now docker compose … run --rm --no-deps dedalo grep ^DEDALO_SALT_STRING= /private/.env, which works whether or not the stack is running. docker.md, quickstart.md, production.md and the installer reference describe this.
    • Docker / trial --abort: the undo names only the containers that exist, measured with compose ps --all. An install stopped before the engine started now says no container of dedalo, nginx, backup was ever created. The kept-images hint and its docker image rm command list only images present on the machine, checked with docker image inspect. Before, it named nginx:alpine even when that image was never pulled, and the command failed.
    • Production, nginx installed by the run (Debian/Ubuntu): the Review's To do now says that the package's default site keeps port 80 (port 80: the default site stays yours (/etc/nginx/sites-enabled/default, default_server) …). Before, this note appeared only after the run, on the Done card.
  • The installer names a killed engine and shows what the engine said while it stopped.

    • When the engine steps' process or container is killed (for example out of memory, or docker kill), the installer's ✖ line now says so, e.g. "the engine container was killed by SIGKILL — out of memory, or a kill", instead of quoting a Docker Compose progress line such as "Container dedalo-install-engine Created".
    • After a Ctrl-C, the lines the engine prints while it stops (such as "ended the seed restore's database session") now appear inside the ✖ block, on both the full screen and the plain output. On the Docker routes they were lost before.
    • On a production install that uses the institution's own certificate, the packages step no longer lists certbot, which it does not install.
  • Installer: one interruption block on Ctrl-C, a resume that re-checks and tells everything, the salt on a terminal resume, and clearer screens.

    • Ctrl-C during a step that runs a child (the engine steps, a build) ends as one install interrupted block with exit code 130 and the run recorded interrupted — never also an install failed block, or exit 1.
    • A --resume that completes the install shows the generated DEDALO_SALT_STRING when it runs on a terminal (the docs always said so; it printed only the command that reads it), and the closing block reminds you to keep it with your backups. Its closing To do on this host lists the notes of every step, the ones done in an earlier run included.
    • A --resume after earlier steps ran again (after an --abort of a served instance) probes the public address again instead of trusting the old result; every resumed step says whether it was verified again or had nothing to re-check, and a resume of a complete install says install already complete — every done step that has a check was verified again.
    • A refusal is worded refused:, never install failed — for example --abort of a sealed install. An --abort that keeps a served instance now closes its run record.
    • The ontology's RETIRED-property notices (for ontology authors) no longer reach the operator's terminal during an install.
    • Screens: the Install type starts on Production, the recommended route; an unsupported system ends with This system cannot run this route — quit, or go back to choose another route; a failed check is marked ✗ on the step rail; the System check's warnings stay in view under the next screen, drawn with ! and ✗; back re-asks the question you left and runs the check again; a re-check before the Review names the answer that changed it; yes/no switches need Enter; the Review's w says it writes the answers and quits. In plain mode the bootstrap's dependency install draws no progress bar (--no-progress).
  • "Production and by-hand installs: one backup directory from the first run, another web server refused, and an abort that tells a started instance from one that never started."

    • --engine-only --backup-dir (the by-hand host procedure) now checks and creates the backup directory it writes to .env (DEDALO_BACKUP_DIR=<backup dir>/db) from the first run. It used to create <private>/backups/db instead, and a --resume after an interruption then stopped at check_directories. When that check fails, it now names the directory.
    • Another web server is refused before any change. The Production System check refuses a run whose ports (80 and 443, or yours) another program holds, such as another instance's nginx when Apache is chosen, and a run that would install a second web server beside the installed one. The Review's To do list names the program before you type yes.
    • An abort after an interruption inside host_first_boot, before the unit was started, now removes the instance completely. It used to count the instance as served and keep its user, database and code for removal by hand.
    • An abort interrupted during host_packages names only the packages and repositories the host really has as kept, never a package the run did not get to install.
    • A failed or interrupted --engine-only run says to run the same command again with --resume or --abort, instead of printing a bun run scripts/install.ts command that would run as root with other answers.
    • A resume says what it did with each step already done: checked again (verified again: …) or nothing to check, instead of "skipped".
    • The Review marks a directory that already exists on its own mkdir line (exists: not created).
    • Install by hand (manual_install.md): the host check reads /etc/os-release in a subshell, so it no longer overwrites the page's $NAME (the public address) with the distribution's name. The host record's nginx_map_owner is rendered from what host_web_site did. The site file's header names both the installer and the page. Both chain-trust failures that openssl verify prints are named. Its One web server check now says what the installer refuses: installing one web server beside the other, and any other program on the ports; a host with both already installed goes ahead.
    • Removing an instance: take the backup before any --abort (a direct pg_dump is given for after one), clear the failed units, and remove the instance's web logs and timer stamps. The steps an abort already did are marked.
    • Production: the host_engine Verify enters the 0750 install folder as the service user, so it no longer fails with Permission denied.
  • An install interrupted during the database restore can be resumed or undone at once, and the host shows the engine's own refusal.

    • Ctrl-C during the seed restore (Docker route above all): the restore's database session could outlive the removed engine container on the PostgreSQL server for about two hours, and until then --resume and --abort refused with a seed restore into '<db>' is still running. Now a Ctrl-C ends the restore's session before the installer exits (on Docker the engine container is given time to stop itself before it is removed). A --resume or --abort that holds the database's install lock ends a restore this install left behind, says so (⚠ ended the seed restore an interrupted run left on '<db>' …) and goes on. A restore that belongs to another install is still refused and left alone. Every installer database session also asks the server to drop it within about a minute if its client disappears.
    • The engine's reason in the host's ✖ line: on the docker, trial and production routes, a refused or failed engine child is now quoted (the engine steps failed: <its reason> — …, the engine child refused its --abort: <its reason> — nothing before it was undone). Before, the reason was lost, and the abort said "see above" with nothing above.
    • Docs: troubleshooting lists both refusals; the installer reference explains how an installer's own leftover restore is ended.
  • Docker and trial installs: the local image build reports the release version, nginx always publishes its ports, and a resume re-checks the site.

    • A local image build at install time now goes through deploy/dedalo-image-build.sh, the builder the image updater already used: a clean snapshot of the checkout's last commit, so uncommitted and untracked files never reach the image, /health reports the release version (never X.Y.Z.dev), and the image carries its own version and commit labels instead of the Bun base image's. The Review shows that command.
    • nginx is recreated on every start of the stack and must publish every port it declares. After a start that failed because another program held port 80 or 443, a --resume used to reuse a container that came up with no port and report the install complete; it now recreates it, or names the port it cannot publish.
    • A --resume probes /health again (on the engine's socket and through the published address), even when an earlier run recorded the probe done, so a stack started again is never reported complete without a check.
    • An institution certificate whose chain this machine does not trust is listed on the Review under TO DO, before you type yes, instead of being refused only after every answer was given.
    • Removing an install: a served --abort names the images it keeps and points to the route's own removal section — the new Removing the stack on the Docker route — and both removal sections now say how to reclaim the images, the build cache and the installer's Bun caches. The build-time estimate is one figure everywhere (about 2 to 6 minutes on 2 CPUs).
  • Every installer front end ends with the same To do on this host list, the salt's read command in it.

    The full-screen installer's Done screen had no To do on this host list: the firewall command, the outside check and the other notes flashed by during the run and were gone. The Done screen now lists them (and leaves them on your normal screen when it closes), as the line mode and --resume runs do. On Production, Docker and Trial the list also names the command that reads the generated DEDALO_SALT_STRING back, on every run that completes the install — also a --resume after an earlier screen had already shown it.

  • Docker and Trial installs probe the media gate again on resume, and name their images and locks correctly.

    A --resume that starts the stack again (after an --abort, or a stopped stack) now runs every stack_health probe again, the media gate included, instead of /health only. A locally built image carries Dédalo's own source, url, description and licenses labels instead of empty ones. On a completed install, --abort no longer suggests docker image rm postgres:18, because PostgreSQL keeps running on that image. A resume after an interrupted run says in plain words that the earlier run's lock was released, with no warning sign. An untrusted certificate chain is refused on the Review, before yes. The Docker and Trial pages now give the exact command that reads the salt back, say the salt is not shown again by a later --resume, and include node_modules in the removal steps. The installer reference says --image-version also applies to a local build.

  • The full-screen installer's closing lines start on a clean line, and the manual removal list covers Bun and a gpasswd after an abort.

    When the full-screen installer ends (done, failed or interrupted), the closing ✔ or ✖ lines now start at column 0 and nothing of the screen is left beside or under them, also on terminals that do not support the alternate screen (screen without altscreen, a serial console, a session recording). The manual list for removing an instance (Removing an instance) now removes the instance's Bun runtime under /opt/dedalo/bun/<pin>/ when no other instance uses that pin, and says that gpasswd exiting 3 after an --abort is harmless.

  • Installer re-checks, interrupts and messages say things once and at the right moment.

    A check made stale by a later answer (the Identity check, once the web server is chosen) runs again right after that step, not at the end. back from a failed System check and forward again runs it again, its rows printed once. Ctrl-C during the engine step prints one interruption block. The System check's ok rows say their result once (ok Bun runtime 1.4.3), the short Docker and Trial check stays visible on the next screen, a typed quit in line mode prints the closing line, an unknown word at a failed check gets a plain sentence, the Review's local-build line says how long the build takes, and the Trial's Review points at the Trial page's firewall warning.

  • A Production install interrupted before its server ever ran is removed whole by --abort, and the production pages match what the installer does.

    On Production, an instance now counts as served only when its unit has actually run or the engine answered. Before, an interruption a few milliseconds into the first start, before the unit ever ran, was treated as served, and --abort kept its user, database and code to be removed by hand. Now --abort checks systemd and the socket first and removes such an install completely. An --abort that removes the service user first removes what that user left at the top of /tmp and /var/tmp (Bun's bun-node-<hash> directory and .<hash>-0.hm marker), so no file of a removed account is left behind; the dependency install also keeps its temporary files inside the instance's tree. When the firewall already allows the ports, the closing To do on this host list no longer mentions it; the System check line still reports it. The Production page lists the questions in the order they are asked, adds sudo to the Verify commands that read inside the install folder and the socket directory, and points to the salt's read command. Removing an instance now says where the backup is written before step 7 deletes it, warns that copies made with cp -a keep the service user as owner, and gives the gpasswd, userdel and groupdel commands. Install by hand defines render where it is first needed, and its unit loop stops instead of writing empty files when render is missing. It also shows the full --status --json answer and says that a tree built from a checkout reports a .dev version.

  • The installer says what its system check would refuse on the Review, before yes, and a refusal never loses your answers.

    An untrusted certificate chain, Apache beside a running nginx, an image version other than the installer's, or a port already in use were refused only after you typed yes — and every answer had to be typed again. The Review now measures the same system check the run makes and lists what it refuses as errors; install is offered once they are fixed. The Image version field refuses a tag other than the installer's own (or its -dev tag) at once, and the Docker and Trial check refuses a published port something else already listens on. If the check still refuses at yes, the session stays on the Review with your answers; if the run is refused after it started, before it changed anything, your answers are written to an answers file and the command that runs them again is printed. See Installer reference › Interactive mode.

  • A Docker installation that builds its image here now updates from a clean snapshot of the release, like the install

    On an installation whose image is built on the host (DEDALO_IMAGE_MODE=build), deploy/dedalo-image-update.sh used to build the new image straight from the checkout. Every untracked file in it went into the image, and the engine then reported its version as X.Y.Z.dev even though the image was tagged X.Y.Z. An update now builds the way the installer does, with the same builder (deploy/dedalo-image-build.sh). It takes a snapshot of the release commit with git archive, builds the image from that snapshot, then deletes the snapshot. The image holds only committed files and reports the release version and its commit date. Changes to tracked files are still refused before the update starts. A tree without git is now refused with a sentence that says why, instead of failing halfway through. If you build an image by hand, use ./deploy/dedalo-image-build.sh "$PWD" localhost/dedalo:<version>. See a local build.

  • The official Dédalo image carries Dédalo's own url, description and licence labels.

    The published image set only its title, source, revision, version and creation date, so docker inspect showed the base image's url, description and licence, or none. It now carries the same org.opencontainers.image.source, url, description and licenses values as an image built locally by the installer, read from one shared definition.

  • On the Docker and trial routes, --abort removes an install interrupted before the engine container ever started, database included.

    The installer counted a Docker or trial instance as served — a live instance that --abort never takes apart — as soon as stack_up began, before the dedalo container had started. An install stopped at that moment (a Ctrl-C, a failed up -d dedalo) was then kept by --abort as if it had served. Whether it served is now measured, as on the production route: the container started, or the engine answers /health; when that cannot be read, the instance is kept. An instance that never served is removed whole by --abort --confirm-database <db>. See Resume and abort.

  • The publication tool shows every publication of a section, including two that share a database definition.

    When two publications reach the same table through their own database alias pointing at one shared database definition (for example a production and a pre-production web), the publication tool showed only the first one, so the second could not be published from the tool. Each publication now has its own panel, with its own database, connection status and Publish button.

  • An interrupted or stopped install now resumes, an abort says exactly what it undid, and an instance that never served can be removed completely.

    • A resume restarts what stopped. When PostgreSQL, the Docker stack or the instance's unit stopped after the installer started it (a reboot, docker compose stop, an abort of a live trial), --resume starts it again. Before, it refused with "changed outside the installer; --abort and start again", even though nothing had been changed.
    • "Start again" now says what --abort costs. Every message that suggests aborting says that --abort deletes the database the install created, unless the instance has already served.
    • An instance that never served is removed completely. Until host_first_boot starts the instance's unit, no request has reached it, and --abort now takes it apart whole: its engine install, its database, its user, its folders and its record. The engine undoes its own install only after two checks pass: the installer states that the instance never started, and the database still holds exactly the rows it had when the install sealed it. Once the instance has served, --abort still undoes only the host steps after the engine. The failure hint and the closing sentence say which case applies, and Removing an instance (several instances page) lists what to remove by hand.
    • An abort cleans up after itself. It removes the empty media tree the install created. It keeps the service user while that user still owns a file under the instance's paths, because a removed uid is given to the next account created on the host. On the Developer route it removes the state file and the session store the install created.
    • The closing sentence of an abort is built from what it undid. "Nothing was undone" is no longer followed by "removed: its site, units, user…". On Docker and the trial, the abort uses the stack's own words and lists the images it keeps; the engine's own "the database itself is kept" no longer appears before the host removes the database volume.
    • The generated salt is shown once by the run that completes the install, including a --resume that finishes an engine run that went on alone after its parent stopped. When no terminal can show it, the run prints the command that reads it back. It is never written to a log.
    • The engine child stops with the installer. It no longer goes on alone when the installer is killed, and a --resume refuses while a child of an earlier run is still working. On Docker, an interrupted run removes the engine's container.
    • --resume asks only for the passwords a remaining step needs. An engine run that already set the root password does not ask for it again.
    • A refused System check leaves nothing behind. An unsupported system or an instance folder that already exists stops the run before any record is written, and no resume or abort is offered. --plan --route production runs the same checks and reports them in errors.
    • Quieter, truer installs. The System check no longer creates ../private (ts_state.json, the session store). The engine checks the backup directory the instance's units write to, not a second one under private/. Tools that never call the server no longer print "no server module". The Docker image creates the media root with mode 0750, so the mode_differs warning is gone. An --engine-only run ends with the systemd pointer, and an interactive install records "execute": true.
  • The Docker and Trial installs now start a working nginx, publish only what they serve, and say why a check failed.

    • Docker route: the generated nginx site includes the media rule files where the engine writes them (/srv/dedalo/media/dedalo_media_protection*.nginx.conf). Before, it named files that did not exist in the container, and nginx restarted forever.
    • stack_up waits for nginx to stay up. It checks that nginx is running, is not restarting, and accepts its configuration. If not, the step fails and quotes nginx's [emerg] line, instead of reporting "the stack is up".
    • --abort of a completed install keeps PostgreSQL running. It removes only the dedalo, nginx and backup containers (and certbot), so --resume can start them again. The abort also names the images it keeps.
    • An untrusted certificate is refused early. On the Docker route, an institution certificate whose chain this host does not trust is refused at the System check, which names its issuer and how to install the CA. A failed stack_health probe says why: for example, the certificate chain is not trusted by this host.
    • The trial publishes plain HTTP only: no port 443 and no certificate volumes. This needs Docker Compose 2.24.4 or newer.
    • A local image build shows each build step as it runs.
    • The backup container stops cleanly on docker compose stop instead of being killed (exit 137).
    • The Docker and Trial pages now cover installing Docker per distribution (Podman is not supported), the docker group, the bootstrap tools (curl, unzip, git), the firewall bypass of published ports, and how to read DEDALO_SALT_STRING from the volume. The dc helper is now ddc.
  • The Production install now completes on RHEL 10 with SELinux, says before the install what you must do yourself, and keeps an unknown address away from every instance.

    • RHEL 10 with SELinux enforcing: the socket's file context is written in the spelling the host's policy accepts (/run/dedalo-<i>(/.*)?). Before, semanage refused the /var/run spelling and the step could never pass. --abort now removes only the contexts the host really defines.
    • The firewall is checked, and the fix printed. The System check, the Review and the closing To do on this host list say whether firewalld's default zone or ufw lets the instance's ports in. When they do not, they print the command that opens them (for example firewall-cmd --permanent --add-service=http --add-service=https && firewall-cmd --reload). The installer still never changes the firewall.
    • The Review starts with a To do list of what the installer checks but never does: the firewall, the Apache modules (a2enmod … && systemctl restart apache2, now also when the installer itself installs Apache on Debian or Ubuntu), and an institution certificate that expires within 30 days or that this server does not trust. Directories that already exist are marked exists, used as is.
    • An institution certificate this server does not trust is refused at the System check, naming its issuer and how to add the authority to the system trust store. Before, the install failed at its last step after 90 seconds with "got no answer". A failed public probe now says why (for example, the certificate chain is not trusted by this host).
    • An address no site declares no longer reaches an instance. On nginx, the first instance on ports 80 and 443 writes dedalo-00-default.conf, a site that serves nothing and comes first; a default_server of yours stays the default, and the last instance's --abort removes it. On Apache, each instance's site answers any other name with 421 Misdirected Request.
    • Each instance's database is its role's alone: CONNECT and TEMPORARY are revoked from PUBLIC, so another instance's role can no longer connect to it.
    • Installing from a git clone works under sudo. install.sh installs its dependencies as the clone's owner (root writes nothing in your clone, and nothing in /root/.bun), and host_runtime archives the checkout without changing any git setting. --route production on an unsupported system is refused before anything is downloaded.
    • A second instance on the same Bun pin no longer reinstalls the runtime the first one is running. The release tree stays 0750 after extraction.
    • Clearer refusals: a clash with another instance is said once, naming every file that records it, instead of once per file. An unsupported system shows one sentence, the same as the troubleshooting page. On ARM servers the processor row no longer warns. On the Developer route, a psql older than the database server is flagged.
    • The Production, manual-install, several-instances and web-server pages cover the certificate trust requirement, the firewall commands, installing from a checkout, the explicit --media-access-mode for a hand install, sudo on the Verify commands, the files-mode certificate checks, and ocrmypdf being unavailable for aarch64 in EPEL 10.
  • A tch install no longer asks for crm, a local image build reports its real version, and a production instance keeps its backups in one place.

    • No false "install it too" warnings after the ontology import. References from an ontology's external-ontology mapping (tch maps itself to CIDOC CRM, crm) are no longer reported: only the RDF import reads that mapping. References into a thesaurus that the same install creates (for example dc), and references from a branch that no tree reaches, are not reported either. A warning that remains now says that the nodes using the missing ontology do not work until it is installed, and to add it only if you use them.
    • A local image build reports the version it is tagged with. The Docker and Trial routes build from the checkout's last commit (git archive), as the production route installs it. /health and the page footer show 7.0.0 and the commit date, not 7.0.0.dev. Changes you have not committed are not in the image.
    • One backup directory per production instance. The installer writes DEDALO_BACKUP_DIR=<backup dir>/db to the instance's .env, the directory the backup units write. The server's own backups and its directory check use it too, instead of a second <dir>/private/backups/db. Install by hand: pass --backup-dir "$BACKUP_DIR" to the engine step.
    • Install by hand, Apache: the name guard now escapes the dots of the address, as the installer does (@@SERVER_NAME_RE@@).
    • Docs: the socket health checks in Upgrading and Migrating from v6 use sudo, because the socket directory is the service group's. The Docker page says that SERVER_TCP_PORT listens on the container's loopback unless SERVER_TCP_HOST is set.
    • Tests: the suite declares its own languages (the same as CI) instead of the ones in your .env, so a fresh install that declares two languages no longer turns about 25 language gates red.
  • The installer's screens now say what each route does, take a typed yes everywhere, and write --write-answers at the Review.

    • The trial asks no HTTPS port, and its HTTP port is described as plain HTTP. Its acknowledgement now says what the documentation says: for an evaluation only, never real records.
    • An acknowledgement takes a typed yes on the full screen too, as in plain mode; Space still ticks it. The screen says how, in its own words.
    • A pre-filled answer is replaced by the first key you type, so the entity name no longer reads museoMuseo de prueba. Enter keeps the default.
    • --write-answers works on an interactive run. The file is written at every Review whose plan is valid, so quitting there keeps it. A session that ends before a valid Review says the file was not written.
    • The Review speaks for the route. Its "same as" line and the next step after write <path> give the route's own command (sudo sh install.sh on production, sh install.sh on Docker and Trial). "Install now" says whether the image is pulled and verified, pulled from your registry, or built here. The Docker and Trial Review no longer shows the production text about packages. The engine is handed only the passwords the plan uses.
    • The Ontologies screen is in English and shows each heading once. Group names come from the installer's language, every typology is one group, and the typology groupers (hierarchytype, hierarchymtype) are no longer offered.
    • The System check says each row's state once (warn: Processor unknown, not warn: warn). An unsupported operating system offers only quit, or back to another route, never "Retry". A check that passes moves on with its rows headed by its name.
    • One step count. Plain mode's Step n of N and the full screen's rail count the same steps. A created production database asks only its name: the role is the service user, with no host, port or password.
    • The thesaurus advice of an interactive run points to the Thesauri step and to Maintenance › Install hierarchies, never to command-line flags. The trial's To-do gives this machine's address, not localhost.
    • Smaller fixes: the header of an interrupted install no longer says "nothing installed yet"; the route cursor starts on the --route default; an empty password field breaks no rule until you type; plain mode's bootstrap output is uncoloured; the footer no longer shows null for a development build.
    • The installer reference, the troubleshooting page and the installation hub describe the real screen order, the System check rows per route, the firewall and own-CA cases, restart after a2enmod, the salt on --resume, and what --abort keeps once an instance has served.
  • The installer's System check no longer fails on a host without PostgreSQL

    On a freshly installed server (no PostgreSQL client yet), the System check screen of sh install.sh failed with the system probe failed unexpectedly (Error) and still printed System ready. beside it. Now:

    • Production shows the PostgreSQL client as ok when it is absent — the installer installs PostgreSQL 18 itself — and warns when a client of another major is present.
    • Docker and Trial no longer check a host PostgreSQL at all; they show the Docker, Compose and Disk rows instead, with the same sentences the install's own check uses.
    • Developer shows a failed row with the fix: this route installs into an existing PostgreSQL and needs its client.
    • System ready. appears only when nothing failed. A check that could not run says so, with the error's class, its first line and the file holding the full text, to report.

    See The System check screen.

  • The installer no longer leaves Bun's cache in an existing hosting account's home.

    On the Production route, the commands the installer runs with Bun as the service user (the dependency install, the engine steps, the migration check) now set HOME to the install folder on their command line (runuser … -- /usr/bin/env HOME=<install folder> …). runuser resets HOME to the account's own home, so with an existing hosting account whose home is not the install folder, Bun's caches were written there. The Review screen shows the new form, and Install by hand uses the same commands.

  • The update-ontology warning no longer claims your local ontology will be deleted.

    The Update ontology panel and its confirmation dialog warned that the update would "delete the actual ontology" and that "you will lose all changes made to the local Ontology". That is not what the process does: it replaces the ontology of the TLDs you select (the common/shared ontologies) with the master's version and re-processes them. Your local ontology — custom TLDs and localontology overrides — is a different namespace and is kept, re-applied automatically when the updated nodes are processed. The panel note, the lead text, the submit label and the confirmation dialog now say so, in every interface language (the confirmation dialog was previously an untranslated English string; it is now a translated label). The only way the local ontology is replaced is if you explicitly add localontology to the list of ontologies to update — keep it out.

    Wire contract: WC-2026-10-10-update-ontology-confirm-text.

  • The PostgreSQL socket you give the installer is now the one the server connects through (DB_SOCKET). (action needed)

    The installer's socket answer (--db-socket) was used by the installer's own connection test and then written as DEDALO_SOCKET_CONN, a key the server never read: an installation reachable only through the socket passed the install and then connected to DB_HOST instead. The answer is now written as the new key DB_SOCKET, and one rule decides the route for everything that connects to the database: the server, the installer's test, the nightly backup and the maintenance tools. When DB_SOCKET is set it wins over DB_HOST, and a socket that does not exist fails loudly instead of quietly falling back to localhost. An existing .env that holds DEDALO_SOCKET_CONN keeps working: it is now read as the old spelling of DB_SOCKET, so check that the directory it names is where your PostgreSQL socket really is before updating. A re-run of the installer writes the value once, under the new name. If you use the nightly backup unit, update deploy/dedalo-backup.service from the release so its database backup connects the same way.

  • On Docker, the browser client now always matches the engine that is running.

    The proxy used to serve the browser client straight from the checkout on the host, while the engine ran from its image. When the two came from different versions — a checkout at one release behind an image of another — the client and the engine no longer understood each other, and pages broke in ways that looked like engine bugs. Now the engine publishes the client of its own image into a client volume each time it starts, and the proxy serves it from there. An update or a rollback therefore moves the client together with the engine. No action is needed: the new compose files declare the volume. If you keep your own compose file, give the dedalo service DEDALO_CLIENT_PUBLISH_DIR: /srv/dedalo/client with the client volume mounted there, and mount the same volume read-only in nginx instead of ./client (the files).

  • The installation guides now install the watchdog and rollback scripts, tell existing servers how to declare supervision, and give RHEL a complete systemd unit.

    The non-Docker installation and update pages were brought in line with the installer and the shipped units.

    • Watchdog and rollback scripts. The shipped watchdog and rollback units run two scripts from /opt/dedalo/bin/, outside the code tree. No page said to install them, so a server set up from the manual alone had a watchdog failing with status=203/EXEC every 30 seconds and no automatic rollback of a failed code update. Production step 10 now installs them, and Upgrading refreshes them after each update.
    • Existing servers and supervision. Upgrading has a new step that checks the installed unit for Environment=DEDALO_SUPERVISED=true, SuccessExitStatus=75 and Restart=always, and adds what is missing with a drop-in. Troubleshooting explains the No supervisor declared refusal. The installer's last line now names the supervised ways to start the server instead of bun run start, which cannot take code updates.
    • RHEL. The RHEL-based systems page (since folded into Production install) gives a complete dedalo-ts.service ordered after postgresql-18.service. The shipped unit names postgresql.service, which does not exist there. The page also runs the installer the same way as the production guide, and keeps DEDALO_PG_BIN_PATH in .env.
    • Several instances. The multi-instance template unit now carries KillMode=process, the wider start limit, the rollback hook and the watchdog. The watchdog script takes --restart-unit / --rollback-unit, so a templated instance can point it at its own units. Each site is installed with --socket, --media-path and --media-access-mode instead of appending keys by hand.
    • Corrections. The backup set is five stores (site-builder instances included). Without --media-path the media root defaults to ../private/media. The installer keeps every .env key it does not own, including keys added before the install. An air-gapped installation can update its ontology from local files (updating the ontology). STRUCTURE_FROM_SERVER and DEDALO_SOURCE_VERSION_LOCAL_DIR are documented as having no effect. The four language keys are documented as required, with no default. The configuration pages point at install/sample.env in the code tree for the current list of keys: ../private/sample.env is the installer's copy and a code update does not refresh it. The developer quickstart lists the supervised start scripts and the second-instance script.
    • Key names in .env. The installer now writes every key under the name the configuration reference uses (DB_NAME, DB_HOST, DB_USER, DB_PASSWORD, DB_PORT, ENTITY, PROJECTS_DEFAULT_LANGS, APPLICATION_LANG, DATA_LANG) instead of the older fallback spellings (DEDALO_DATABASE_CONN, DEDALO_ENTITY, …). Before, a DB_HOST you added by hand silently won over the answer you had just given the installer. A re-run over an existing .env replaces the old spelling with the new one, so no value is left under two names. The engine and the backup scripts still accept the old spellings, so an existing .env keeps working unchanged.
  • The install database is compiled from the repository and proven by a fresh install before it ships.

    Maintenance → Build database version → Build install version refused with "not runnable on this engine", pointing at a maintenance dashboard that no longer exists. It now compiles install/db/dedalo_install.pgsql.gz — the database every fresh install starts from — from files in the repository only (the schema and its migrations, the core ontology release packages, the languages and hierarchy registry files, the default accounts), never from an installation's database, so it cannot carry one installation's data or mistakes into every new install. Before the file is replaced the result installs itself in a scratch database and is checked; the same command is bun run seed:build. The seed ships with a manifest that records what it was built from. See the seed.

  • "Probe media on a new copy-mode publication host no longer reports a problem before anything is published."

    On a copy host, the marker directory .publication/pub/ is created by the agent with the first published record. Until then the probe reported it as missing, so the first probe after pairing said found 1 problem(s). An absent marker directory is now zero published records on a copy host; on a shared host it is still a problem.

  • "The guided publication-host install now runs on RHEL 9 with SELinux enforcing and fapolicyd: the polkit rules directory, the SELinux tools and fapolicyd's trust are judged as EL ships them."

    The first run of the EL install drill on a real RHEL 9.8 machine (SELinux enforcing) found three things the guided install got wrong there. provision apply refused every instance because EL's polkit package owns /etc/polkit-1/rules.d by the polkit daemon's account: that directory, as the package ships it, is now accepted (any other owner is still refused). init reported the SELinux tools missing because /usr/sbin/restorecon is a link to setfiles on EL: a link to a program now counts. With fapolicyd active, an untrusted Bun may not even read the code it runs: install.sh now stops before the hand-over with the one line that trusts its Bun, and init's host.fapolicyd prints the lines for the site's Bun and the agent's code (re-runnable: an existing entry is updated). See Publication host agent.

  • "On RHEL, Rocky and Alma with SELinux, the Publication API v2 of a site installed in its home now starts: systemd may read its settings and its release links."

    The first API v2 release pushed in the EL install drill never started. Under the home layout the v2 directory kept the home's own SELinux type, which systemd may not read, so neither the v2 settings file (v2.env) nor the links to the current release and to the release being tested could be followed: every install was refused (scratch_start_failed). provision apply now labels <home>/dedalo/publication_api/v2 dedalo_publication_v2_t, the publication host's own SELinux type, which systemd may only read and the web server may not read. The v2 services also start their entry with bun src/index.ts instead of bun run src/index.ts, which left links in the service's private /tmp that systemd could not remove at every stop (an SELinux denial each time). Run provision apply (or init) again on such a site. See Publication host agent.

  • "The guided install of a publication host now gives a working command for the newer AppStream runtime of the v1 API on RHEL, Rocky and Alma 10."

    On EL 10 the guided install said that no dnf command installs a version of the v1 API's runtime other than 8.3 as the system one, and offered Remi only. RHEL 10.2 ships 8.4 in AppStream as alternative packages with the same files as the default ones. When the declared version is not installed, host.fpm_install now prints the dnf install line for those 8.4 packages (the FastCGI server and the command-line interpreter). When an older system version is already installed, the command has --allowerasing, and the item says that every pool of the system FastCGI server then runs the new version. Remi and the default 8.3 are still offered. The guided install has now been tested on RHEL 10.2 with SELinux enforcing, as well as RHEL 9.8. See Publication host agent.

  • "On a publication host with fapolicyd's allow_filesystem_mark = 1, the agent keeps its right to reload the web server and start its services."

    With allow_filesystem_mark = 1, the setting the guided install recommends, fapolicyd also checks the sandboxed polkit service, and polkit could no longer load the agent's rule: Apply media rules failed with reload_failed and every API push with trust_failed, while the agent logged Interactive authentication required. The instance's trust file now lists the agent's polkit rule, and provision apply restarts polkit after each trust update, so a rule that failed to load earlier is loaded again. The same setting stops the distribution's own polkit rules from loading, for every program on the host: Publication host agent shows how to check for it and the lines that trust those rules. A failed start of the trust service now says that polkit refused it, instead of a bare exited 1.

  • On an nginx publication host, a media map that stops nginx at its reload is now rolled back and nginx restarted, and the panel counts a map that is not loaded as a red check.

    The root service that renders the shared nginx media map (dedalo-pubhost-map) tests every new map before nginx reloads it. On SELinux hosts nginx can still stop on the reload itself, after a test that passed. Until now the service then only reported the failure and left nginx down, with every site on that server. It now watches nginx for five seconds after the reload. If nginx is down, the service puts back the map nginx had loaded (or removes a first one), tests it, restarts nginx and checks that it runs. The push is reported as failed and the panel keeps showing the map that is actually loaded. In Maintenance › Publication hosts the map's state is now a check of its own, Host media map. It is red when the host's agent is too old for the shared map, when the host refused this work system's map, or when this work system's map is not the one nginx serves. It is green when that map is loaded or when the map is placed by hand. Before, a map that was not loaded was painted red, but it was not counted with the other checks. See Publication host agent.

    Wire contract: WC-2026-10-03-publication-hosts-widget.

  • The publication host's guided install now configures the running web server when both Apache and nginx are installed.

    On a machine with both Apache and nginx installed, where only one of them runs, the guided install (install.sh, provision init) proposed Apache whatever ran and then found no site to attach to, so the install stopped asking for a manual step. It now proposes the web server that is running, and when you choose the other one, it looks at that server's sites before going on. Measured on Ubuntu 24.04, which keeps a stopped, disabled Apache listed. See the publication host install guide.

  • "When provision apply is refused and you answer manual, the guided install no longer goes on with the steps that need it."

    Answering manual to a refused provision.apply (for example a copy media root whose parent directory does not exist) let init run the steps that depend on it, which then failed (the API configuration with ENOENT, exit 4, "a change failed"). Those steps are now left out, listed under still to do with what they wait for, and init ends with exit 3 naming them: fix the refusal, then run init again.

  • "The guided install no longer refuses when you keep the default answer of an optional question."

    Answering skip to an optional question (the default for move the vhost logs, asked whenever the site's virtual host logs into the distribution's log directory, as Ubuntu's stock virtual host does; or keeping a stale pairing package) made init stop with exit 3, skipping leaves required items undone: init.keep_ref, before it changed anything. An optional question now holds nothing back: init installs the host and leaves only that item as it is.

  • "Pairing from the sealed package: the guide puts the copy where the Dédalo user can read it, and an unreadable copy says why."

    The guide told you to carry the .pairing file to /root/, which the Dédalo user cannot pass (0550 on RHEL, Rocky and Alma, 0700 on Debian and Ubuntu), so the pairing command answered could not be read (EACCES). The guide now uses /opt/dedalo/pairing/, and the command's refusal for an unreadable file says that every directory above it must be passable too. The guide also shows the firewall rule that lets only the work host reach the agent's port, on firewalld and ufw: Publication host agent.

  • "On RHEL, Rocky and Alma, a publication host in the system layout now starts its v2 API: the provisioner installs its own small SELinux policy module."

    With SELinux enforcing, an instance installed in the system layout (/srv and /opt) left its v2 API tree with /srv's own type, which systemd may not read: the v2 service could not start. provision apply now writes and installs the policy module dedalo_publication_host (one per host, a stamped CIL file, installed with semodule, no compiler needed). It defines one type, dedalo_publication_v2_t, which systemd may read and the web server may not, and labels the v2 tree with it before any service starts. The guided install lists it as selinux.v2_policy. A module of that name that the provisioner did not install is refused, never replaced, and the module is removed when no instance on the host needs it any more. A first system-layout install also creates the shared directory /srv/dedalo_publication_host (it was refused when that directory did not exist). See Publication host agent.

  • "Rotating a publication host's token now restarts its agent."

    After you remove credentials/SERVICE_TOKEN, provision apply (and a re-run of install.sh) mints a new token and now restarts the running agent, which reads its token only when it starts. Before, the agent kept the old token: the guided install failed its own health check (publishes another pairing fingerprint, exit 4) and pairing again answered pairing_mismatch until the agent was restarted by hand. The manual restart step is gone from Rotating the token.

  • "Upgrading a publication host's agent from a new kit no longer leaves the agent stopped on a host with fapolicyd."

    When the new agent code was the only change, the guided install restarted the agent before fapolicyd trusted the new files, and the agent stopped with EPERM reading …/src/index.ts. On a host with fapolicyd, a run that installs new agent code or a new Bun now always runs provision apply first, which trusts them, and only then restarts the agent. provision apply also clears a service that stopped after too many failed starts, so a run that fixed the cause brings it back instead of failing with Start request repeated too quickly.

  • "The Publication API v1 on a publication host answers again: its configuration names the release that runs." (action needed)

    The v1 configuration lives in publication_api/v1/shared/ and is linked into every release, but the sample it is made from found the API's files from its own location. On a publication host every v1 request therefore failed while answering 200, and the v1 error log said Class "manager" not found. The guided install now writes the API_ROOT line that names the release of the script that runs. Action needed on a publication host already serving v1: in the v1 API configuration file in publication_api/v1/shared/ (the one step 5 of the install guide creates), replace the API_ROOT line with define('API_ROOT', dirname(get_included_files()[0], 2));, keeping the file's owner and mode (see Create the API configuration files).

  • The Publication API v1 error log on a publication host is now rotated.

    Each site's v1 API writes its errors to /var/lib/dedalo_publication_host/<instance>/v1/log/error.log. Until now nothing rotated that file, so it grew until the disk was full. provision apply now writes /etc/logrotate.d/dedalo_<instance>_v1 for every site, in either layout. The file rotates daily and keeps 14 compressed copies. The rotation runs as the v1 user, because that account owns the directory and root never renames files in a directory another account can change. Run provision apply (or provision init) once for each site to get it. See Publication host agent.

  • "Publication API v1 releases install on a publication host with fapolicyd's allow_filesystem_mark = 1."

    The agent checks the syntax of every v1 file before a release goes live. It did so on a copy fapolicyd did not trust yet, so with allow_filesystem_mark = 1 every v1 release was refused (Could not open input file). The check now reads the release in its final place, after fapolicyd trusts it; a release that fails it is removed and the previous one keeps serving.

  • Container installs take their nightly backups again, and the full-stack install no longer stops at "directories".

    Since 2026-08-30 the backups volume of both compose stacks came up owned by root while the engine and the backup service run as an unprivileged user. Two things followed. The documented command-line install of the full stack (docker-compose.yml) stopped with install failed: /backups/db. And the nightly backup service wrote nothing at all. Because it could not write its own failure marker either, docker compose ps still showed it as healthy.

    The image now creates that directory with the right owner. Rebuild the image (docker compose build, then docker compose up -d, adding -f docker-compose.simple.yml --env-file .dedalo.env on the simple stack). An existing backups volume is still empty, so it takes the right owner by itself; no manual step is needed. The backup service now reports unhealthy whenever it cannot write to /backups. The simple stack also declares which stores each run must produce, so a store that never ran is reported as a failure, as on the full stack. After updating, check that /backups/LAST_OK appears the morning after. See Docker backups.

  • "./install.sh runs on macOS again (and wherever Docker's storage lives in a VM)."

    On macOS, and on Docker Desktop under WSL or with a remote Docker host, ./install.sh stopped silently right after its banner. Its free-disk check used a Linux-only option. The check is now portable. Where the free space cannot be measured from the host, the check is skipped and the install continues. See Simple install.

  • "The publication-host pairing command now names a command you can run, and says why it cannot read a copied file."

    When the pairing command refused because of the user running it, its message suggested sudo -u <engine user> bun run …. Pasted as is, the shell read <engine user> as a file redirection, and with the user filled in, sudo could not find a bare bun. The message now prints the full command: the checkout to run it from, the user (by its id), and the Bun that ran it, by its full path. A token file or engine bundle copied as root, which the Dédalo user cannot read, used to end in unexpected failure (Error), exit 4. It is now named like the fragment already was, could not be read (EACCES), exit 3. The engine fragment that provision apply writes no longer claims that no release reads its keys: the pairing command reads exactly those keys. The next provision check lists the fragment as a change, and apply rewrites it. See Publication host agent.

  • The auction-URL and journal-URL import tools now declare the sections they belong to.

    The two import tools are registered for their own sections only (numisdata4 for auction URLs; rsc205 and rsc3 for journal URLs), so the restriction no longer depends on their server code loading. Run Register tools after the update to apply it.

  • Ontologies re-installed by an ontology update keep their typology and name instead of moving to "Others".

    After an ontology update, ontologies such as dd or tch appeared in the profile permissions tree under the Others typology with only their bare tld as name (dd), although their registry record says otherwise (for dd: Core, "Dédalo | dd"). An update now keeps the typology and name from the ontology's registry record. To repair an installation already affected, rebuild the ontology's main node from the Ontology tool (the registry records themselves were always correct).

  • A tool whose code fails to load no longer appears in every section.

    When a tool's server code failed to load (for example a missing dependency), its "only in these sections" rule was skipped and the tool appeared in every section. Such a tool is now hidden, and the server log names the load failure.

  • Code updates no longer refuse over a file an older release shipped and a newer one removed.

    An update from the Update code panel refused with "Unknown entries at the code-tree root … .vscode" on installs that had taken an older release: the file was shipped by Dédalo itself, then removed from later releases, and the updater could not tell it from a file the administrator had added — nor could it be cleared without shell access. Each update now records the top-level entries of the release it installs, so the next update moves a retired release file into the backup instead of refusing, and the panel's Code-tree root entries check reports only files nobody shipped. An install updated before this release still refuses once over such a file: remove it by hand that one time (what makes an update refuse).

    Wire contract: WC-2026-10-07-update-code-root-entries-stamp.

  • Publishing no longer fails creating a MariaDB integer column sized by varchar.

    A field_int diffusion node whose properties carried varchar (for example "varchar": 1024) was created as INT(1024), which MariaDB refuses, and the whole publication run stopped with "An unexpected error stopped the diffusion run". The two sizing properties are separate again: varchar sizes text columns only and length sizes integer columns only. The integer column is now created as INT(8) unless length says otherwise.

  • A publication table too wide for MariaDB is refused by name before publishing starts.

    MariaDB limits a table row to 65,535 bytes, and every VARCHAR(n) column reserves 4 × n bytes of it whatever it holds. A table with many wide varchar fields could not be created, and the publication stopped with "An unexpected error stopped the diffusion run", for every table of the element and not just the wide one. The element's plan now refuses such a table before anything is published, naming the table, its width and its widest columns. To fix it, change those fields to field_text (add "index": "BTREE" to keep the same index) or reduce their varchar.

  • Publication host provisioning works with Apache on Debian and Ubuntu. (action needed)

    provision check refused every Debian or Ubuntu Apache host with web.configtest_bin '/usr/sbin/apachectl' is a symlink, because the provisioner used one fixed path for every system. It now picks the configuration-test command found on the host: apache2ctl on Debian and Ubuntu, apachectl on RHEL, nginx for nginx. The agent's settings file now names that command. Action needed for an agent provisioned before this change: after copying the new agent code, run bun run provision apply <instance> again. Until you do, the agent refuses to start because the setting is missing. If the agent restarted repeatedly before you ran it, systemd may have stopped retrying: run systemctl reset-failed dedalo-publication-host-<instance> and apply again. See Publication host agent.

  • The Docker image can now write AVIF, so .avif alternative versions of images work out of the box.

    The Docker image's ImageMagick could read AVIF but not write it: the Debian package it is built on ships the AVIF decoder only. An installation that lists avif in DEDALO_IMAGE_ALTERNATIVE_EXTENSIONS therefore had those alternative versions refused on every upload. The image now includes the AVIF encoder (libheif-plugin-aomenc). Docker installations get it with the next image build; on a host install, add the same package to have AVIF versions written.

  • Interlaced videos are now deinterlaced when their web versions are built.

    Every video quality was meant to be deinterlaced, but the encoder was given the deinterlace filter and the colour-correction filter as two separate options, and ffmpeg keeps only the last one. The deinterlace step was silently skipped, so video recorded interlaced (most analogue and DV tape transfers) got web versions with visible combing on movement. Both encoding passes now receive one combined filter, and interlaced video is deinterlaced. Progressive video is left untouched: only frames marked as interlaced are processed.

    Existing versions are not rebuilt automatically. To fix a video already in the archive, rebuild its qualities with the media versions tool; the original file is never changed.

  • The audio encoder for video and audio derivatives is chosen from what the configured ffmpeg can actually encode, and a failed encode names its error.

    The AAC encoder used for video and audio derivatives (libfdk_aac, then aac) is now read from the configured ffmpeg's own encoder list (ffmpeg -encoders) instead of its build flags, which could name an encoder the binary does not have. The answer is remembered per ffmpeg binary, so a different ffmpeg resolved by the same server is asked again rather than handed the first one's answer — and the same holds for ImageMagick's "can this format be written" check. A probe that cannot run is no longer remembered for the life of the server. A failing second encoding pass or audio extraction now reports ffmpeg's error line instead of the tail of its banner and progress output; the encoded files are byte-identical. See the media engine.

  • The profile permissions tree opens in a fraction of a second instead of several seconds.

    Opening a profile record rebuilt the whole permissions tree (every area, section and field of the ontology — some 13,000 entries) on every visit, with several database queries per entry: 6–7 seconds on a real installation. The tree is now built from one ontology query and kept per interface language and per set of granted areas, so the first opening takes well under a second and later ones are immediate. It is rebuilt automatically after any ontology change and after any change to a profile or a user's profile assignment, so it never shows outdated structure or another user's areas.

    Siblings that share the same ontology order number now always appear in the same order (by creation), where before their relative order could vary between servers.

  • Published files survive a power cut, and deleting a record no longer races a running publication into the same directory.

    Two gaps in file publications (Markdown, XML, RDF, CSV, JSON) are closed:

    • A power cut no longer leaves a "completed" run with broken files. A run recorded each batch, and finally its completed state, in the database while the files it had written could still be only in the operating system's memory. A power cut (or a kernel crash) could then leave empty or missing record files, a deleted record's file back in place, or a truncated CSV/JSON export — behind a job that said completed and could no longer be resumed. Every file, archive and merged document is now forced to disk, and its directory with it, before the run records it.
    • Deleting a record waits its turn on file targets too. When a record is deleted, its published file is removed under the same per-target hold the publication runs use. It could otherwise be removed just before a running batch wrote it again (the deleted record reappeared on the public site), or while a run was building its archive. If a run is writing that directory at the moment of the deletion, the removal stays pending and the retry queue completes it, as it already did for publication databases.

    Wire contract: WC-2026-09-30-diffusion-run-ledger, WC-2026-09-30-diffusion-target-fence.

  • Resetting a hierarchy to its seed can no longer delete it without restoring it.

    "Reset to seed" (Add hierarchy) and the installer's hierarchy step now apply each hierarchy all-or-nothing. The reset used to delete the hierarchy's terms first and load the seed in a separate step: if the seed then failed to load, the hierarchy was left empty — every edit and addition gone and the seed not restored. A models file that failed to load, or a failed update of the record counter, was ignored and the hierarchy reported as imported. Now the delete, the terms, the models and the counter are one database transaction: if any part fails, nothing changes and the hierarchy is reported as failed with the reason.

  • Local ontology overrides now apply to the node they point at, and change only what they state.

    A record in the local ontology (localontology0) overrides a shared node through its Overwrite field. Before, any link in the record counted as an override (so its parent was overridden too), and re-parsing the node could move it to the localontology namespace, erase its other translations, drop its layout CSS and make it translatable. Now only the Overwrite field links an override; the node's TLD, translatable flag, order and model flag stay as shared; the term merges per language, and each property the override fills (CSS included) replaces the shared one whole while the others are kept — set a property to null to remove it. Local records are no longer parsed as nodes of their own. See Overriding shared ontology nodes.

    Wire contract: WC-2026-10-01-ontology-overwrite-scoped.

  • The Publication server API maintenance panel shows its "Open Swagger UI" buttons again.

    The panel (Maintenance → Publication → Publication server API) never showed the buttons that open the interactive documentation of the publication server API v1, because API_WEB_USER_CODE_MULTIPLE was not read. It is a configuration key again: list each publication database and its API code, e.g. API_WEB_USER_CODE_MULTIPLE=[{"db_name":"web_my_entity","code":"my_api_code"}], optionally with api_ui when the API runs on another server — see the configuration reference. A v6 configuration migrated with the config migrator now carries the value across; the empty placeholder entry of a stock v6 configuration is left out, so a migrated install does not report a dropped entry on every start.

  • Creating a site twice at the same moment can no longer overwrite or delete the first site.

    When two requests created a site with the same name at nearly the same time, the second one could pass its checks while the first was still being set up. It then wrote its own settings over the finished site, and if anything later failed, it deleted the whole site folder, including the first site's work. The site folder is now claimed by exactly one request: the second request is refused with "a site with this name already exists", and the first site is left untouched.

    A site folder that exists but has no site.json (left by a create that was interrupted, or by a site whose settings file was removed) is no longer reused. Creating a site with that name is refused with the reason workspace_exists, and nothing in the folder is changed. An administrator must inspect the folder and remove it before the name can be used.

  • Deleting a site while an agent session or a build is running is refused, and no longer blocks the site name.

    A site could be deleted while an agent session was still working on it. The session kept writing its own history, which re-created an empty folder with the site's name. Creating a site with that name was then refused with the reason workspace_exists until an administrator removed the folder by hand. Every restart of the site builder also retried a recovery that could never succeed for that folder.

    Now a delete is refused while a session, a build or a repository operation is running on the site. The response names what is running (for example session_running), and nothing is removed. Stop the session or wait for the build, then delete again. Sessions and builds can no longer re-create a deleted site's folder. At startup, the site builder ignores folders left by the old behaviour, so it no longer retries their recovery. Remove those folders by hand to free the name.

  • Site builder state survives a power cut, and a damaged driver record is reported at startup.

    The site builder's own state (each site's driver record, session and build records, site.json) was written atomically but not forced to disk. After a power cut or kernel crash, a newly written driver record could come back empty. The site then refused every session that did not name its driver, on every restart, and nothing explained why until a session was attempted. These files, and the directories they are created in, are now forced to disk before the write is reported done. A driver record that is present but unreadable is now named in the startup log with the steps to fix it. It is left as found and never rebuilt from site.json, which the agent can edit.

  • A second Site Builder started by hand no longer stops the running service's agent runs.

    When the Site Builder daemon was started a second time for an instance that was already running (for example, by hand as the service user while debugging), the second process stopped the running service's agent turns and marked its sessions interrupted. Only after that did it notice the instance was already served and exit.

    The daemon now checks first. If the instance's socket or port already answers, or (with systemd confinement) systemd says another process is the service's main process, the second start exits with one line saying why. It stops nothing and writes nothing. Start the service with systemctl, not by hand.

  • Administrators' toolbars now show only the tools their profile grants.

    Global administrators (other than root) saw every installed tool in their toolbars, even tools their profile does not grant — and clicking one was then refused. The toolbar and every tool door now follow the same rule: a tool is available when the user's profile grants it (or it is always active); only the root account holds every tool. An administrator who asks for a tool their profile does not grant now gets "not authorized" rather than "unknown tool".

    Wire contract: WC-2026-10-01-tool-grant-one-decision.

  • Rebuilding a user's activity statistics can no longer lose them half-way.

    "Rebuild user stats" (Database info) used to delete a user's daily statistics first and then recompute and save them day by day, each step on its own. A failure part-way — a database error, a server restart — left that user's statistics deleted or half rebuilt. Now the activity log is read first and the old statistics are replaced in one transaction: if the rebuild fails, the user's previous statistics are kept and the error names the user (and the users already rebuilt before it). A day whose statistics record could not be created is no longer skipped silently. The rebuild still recomputes only from the activity log that exists, so statistics older than the log are still lost when it succeeds.

  • A database backup counts only once it has been read back completely.

    The code updater requires a recent database backup, and the update panel shows whether there is one. Both used a quick check that reads only the start of a dump, so a dump that had stopped part way through still counted as a backup, and a code update could go ahead with no usable way back. Now a backup counts only after PostgreSQL has read it back from beginning to end; a dump that is cut short is named in the refusal ("did not verify (truncated)"), and the next older complete backup is used if there is one. The read happens once per backup file and never makes the server unresponsive: while it is running the panel shows the backup as "verifying" instead of guessing. A read that does not finish in time proves nothing, so that backup does not count either; on slow backup storage raise the new setting DEDALO_BACKUP_VERIFY_SECONDS_PER_GB (default 60 seconds per gigabyte), which the refusal names. A file named like a Dédalo backup (.custom.backup) that does not even start like a PostgreSQL dump — for example one left full of zeros by a crash — no longer counts as a backup either. Only a problem in the file itself marks a backup as broken: if the read is interrupted, the backup disk reports an error, or the server's PostgreSQL tools are older than the dump, the backup does not count for now but is read again next time instead of being written off — and a backup that an earlier version wrote off for one of those reasons is read again once. This holds whatever language the server's system runs in: PostgreSQL's messages were misread when they came out translated (for example on a server set to Spanish), which could write off a good backup. Stopping a code update while its backup is being checked, or before it replaces the code, now really stops it: nothing is installed and the server is not restarted. The backup line of the update panel is checked only for the superuser, the only account that can run an update. If the backup cannot be checked at all (for example the server's PostgreSQL tools cannot be started), a data update now warns that the backup could not be checked instead of refusing to run.

    Wire contract: WC-2026-09-30-backup-freshness-deep-async.

  • A database backup that is still being written no longer appears as a backup.

    Before, a dump started from the maintenance area was written straight under its final backup name, so for the whole time it ran the backup list showed an unfinished file, and pressing the button twice in the same second could throw away the dump that was already running. Now a dump is written under a temporary .part name and receives its backup name only after it has finished successfully and — where the server can check it — has been read back completely (a finished dump that could not be checked — no time, a disk error, the check interrupted — is named but reported as "not verified", never thrown away); the list shows only finished backups, and a second press while one is running is simply skipped. A dump that fails is kept as .failed for you to inspect (a later failure under the same name becomes .failed.1, and so on — nothing is overwritten), and never looks like a backup. While the finished dump is being read back, the progress panel keeps showing it as running (it used to report a successful backup as interrupted), and you can stop a running backup from the panel. A .part file left behind by a server that restarted mid-dump is never deleted: after 24 hours the next backup reads it back and, if it is complete, gives it its backup name; if the file itself is broken it is kept as .orphaned for you to inspect, and if it simply could not be read this time it is left where it is and read again next time (the nightly backup job follows the same rule). The progress of a backup now belongs to the user who started it. An empty .part is left alone, because it may belong to a dump that is still waiting to start.

    Wire contract: WC-2026-09-30-backup-part-promotion.

  • A publication run that crashes, is cancelled or meets another run now publishes exactly what it should — and large archives no longer exhaust memory.

    Four defects of the publication (diffusion) runs are closed:

    • Resume after a crash. A run that was interrupted and resumed used to lose the records its primary records link to, and rebuilt the downloadable archive (diffusion_md.zip, the merged RDF/XML document and its zip) from only the part it did after the restart. Each run now keeps a ledger of what it has queued and published, committed with every batch: a resumed run publishes byte for byte what an uninterrupted run would, and its report still says Partial success for problems met before the crash. A run left by a version before this one restarts from the beginning. A cancelled run no longer rewrites the published archive from its partial work. A linked record that was unpublished while a run was stopped is not published again when the run is resumed: the run checks each linked record's publication state when it reaches it.
    • One writer per target. Two runs, a record deletion and the maintenance repairs could write the same publication database or directory at the same moment, and a run whose job had been taken over could keep writing. Each target is now written by one batch at a time — a waiting run shows Waiting for the publication target (busy)… — and a record deleted while its batch waited is removed from the public site instead of published. A long step (adding a column to a large published table, a language sweep) keeps its hold on the target for as long as it runs, and a batch that takes longer than about 20 seconds no longer makes a healthy run look stopped (it used to be restarted, and could end failed after its retries). Deleting records while a run holds their target no longer waits: the unpublish is queued and retried. Deletions never hold each other up: two users deleting records published in the same database both have them removed from the public site at once. DB_POOL_MAX must be at least 2 for a publication run to start. The media-file allowlist is covered too: Rebuild media index and the startup repair wait for a run that is publishing, so the media of a record that was just published no longer disappears from the public site until the next repair. They wait for at most two minutes and hold nothing while they wait: a run or a deletion on one publication database is never held up because a run is busy on another. When the wait runs out, Rebuild media index names the database it could not repair, and the startup repair is reported as not applied.
    • Another user's publication. Pressing Publish on an element and section another user is already publishing used to show you that user's run as if it were yours. You now get a clear "the publication target is busy" message; pressing Publish again on your own running request still reconnects to it.
    • Archives in bounded memory. Zip archives and merged documents are now built from disk one file at a time, with unchanged bytes, instead of loading the whole publication into memory at the last step. Two files with the same name in one archive are refused instead of one silently replacing the other. A file removed while its archive or merged document is being built (a record unpublished at that moment) is left out and named in the run's report, instead of the whole run failing — including when every file of a Markdown run is gone, which now just produces no archive. A file removed after the archive started reading it is archived whole.

    Wire contract: WC-2026-09-30-diffusion-run-ledger, WC-2026-09-30-diffusion-target-fence, WC-2026-09-30-diffusion-attach-scope, WC-2026-09-30-diffusion-zip-streamed.

  • Duplicating a record now files the history of a transliterable or IRI field in the language it was saved in.

    When a record was duplicated, a field that keeps per-language versions beside a base value (a transliterable field) or an IRI field got its history row in the language-neutral lane, next to an empty extra row, while a normal save of the same field files it in the working language. The Time Machine of the copy therefore listed the change under the wrong language. The copy's history now lands in the working language, exactly where a save puts it, and the empty extra row is gone. The rule that decides which language a history row belongs to is now one rule shared by every door that writes history.

    Wire contract: WC-2026-09-27-bulk-revert-undo-log.

  • Regenerating the media cache no longer undoes an upload made while it runs.

    The "Update cache" tool (media components) and the media files repair (scripts/media_repair_files_info.ts, and the files_info entry of the reconcile tools) rebuild files and then record which files a record has. They used to record that from what they had read at the start, so a file a curator uploaded to the same record while they ran — and its original file name — was silently undone. They now record it from the record as it stands at that moment, so the curator's upload is kept. "Update cache" also reports records deleted while it ran (and rows that stayed locked) instead of counting them as regenerated. The repair now also fixes a record whose media list names another record's files (what a failed duplicate could leave), and reports records it could not write instead of counting them as repaired. It judges each media item on its own: an item whose files are not on this server keeps its record of them (unless you allow shrinking), even when another item of the same field is repaired, and a file named some other way (for example by an image id) is never mistaken for another record's. Both tools now give up on a record another user is holding after a few seconds, report it, and go on with the next one, instead of waiting indefinitely.

    Wire contract: WC-2026-09-30-media-key-locked-transform.

  • A move_* data transform can be stopped, and only one runs at a time.

    Running a move transform for real (Move TLD, Move locator, Move to portal, Move to table, Move lang with dry_run: false) used to happen inside the web request. Nothing could stop it except restarting the server. It kept every record it had changed locked until the end of each definition file. If it was sent again it waited behind itself and then reported a failure while the first run carried on unseen. Now the transform runs as a background process that answers at once, reports its progress in the maintenance panel and has no time limit. Stopping it cancels the definition file it is working on and undoes that file completely; the files after it are reported as not run. A second transform started while one is running is refused ("Another move_* transform is running") instead of waiting. The maintenance panel can now actually run a transform: until now the five move widgets only ever sent a dry run, so no button reached the real run. The submit button now runs a preview (dry run); when the preview ends without errors an Execute button appears, which asks for confirmation and runs exactly the files that were previewed (change the selection and it asks you to preview again). The preview also runs in the background, with the same progress panel and Stop button. A run that fails — a definition file undone, a file refused because another transform is running, an unknown outcome — now ends as an error in the panel, with the reason; before, the panel said "Process completed" and the failure was visible only in the raw report. If a definition file fails at the moment its changes are being saved (a lost database connection, a server shutdown), the report now says what the database actually did with it: applied, undone, or, when that cannot be read back, an unknown outcome that stops the run and asks you to check the data before running that file again. Before, every failed file was reported as undone, and running a Move locator file again after it had in fact been applied moved its locators twice.

    Wire contract: WC-2026-09-30-move-transform-execute-job.

  • A failed ontology update now says why, inside the Update ontology panel

    When fetching the master's file list or importing the ontology failed, the panel showed nothing — the error only reached the browser console. The panel now shows the failure in place: the error, the server's explanation (for example which address was refused) and the request id to find it in the server log.

  • The Update ontology panel scrolls to its result

    The result of an ontology update — success, warnings or a failure — appeared below the submit button, out of sight unless the admin scrolled down, so the panel looked idle. It now scrolls to the version change while the import runs, and then to the outcome, as the Update code panel already does.

  • Updating the ontology works from every configured master, not only the first

    When ONTOLOGY_SERVERS listed several masters sharing the same access code, choosing any of them except the first failed every file with Download failed … (origin mismatch: <chosen> != <first>). The engine now identifies the chosen master by its address, so each listed server updates from itself. A server address that is not in ONTOLOGY_SERVERS is still refused before anything is downloaded.

  • A site builder turn whose egress gate fails to close now still ends, instead of leaving the session running forever.

    When the daemon could not remove a turn's egress sockets (for example, the host refused the unlink), the turn's remaining cleanup was skipped: the driver's MCP configuration stayed in the workspace and the session never left the running state. Each cleanup step now runs on its own. The turn ends normally, and the failure is written as an [egress] line in the session log. The same holds for a build or git step: its gate failing to close no longer replaces the step's own result, and the [egress] line goes to the build log. A run refused after its gate opened (for example, an environment value with a control character) now reports that refusal, not the error from closing the gate.

  • A data update now applies completely or not at all, and two updates can no longer run at once.

    Before, each step of a data update was saved as soon as it ran: a failing step, a stopped update or a server restart left the earlier steps applied while the installation still reported the old data version, and running the update again applied them twice. Now the whole update — every step and the new version number — is saved in one piece. If anything goes wrong, or the update is stopped, nothing of it is kept and the report ends with "Rolled back: no statement of this run persisted"; after a restart, simply run the update again. A second update started while one is running, or an update that was already applied from another window, is refused. Stopping the update job now also stops the statement it was running, even while the server is busy. A statement stopped by a server shutdown is reported as an interruption, not as an error in the update's SQL, and the update log marks every run that was saved with a COMMITTED line. The installed data version shown in the panel is the final word: if the update log cannot be written (a full disk, say), the update still completes and reports its real outcome. Every required step must stay checked: the update refuses a selection that leaves one out, because the new version number would otherwise claim work that was never done. If the connection to the database is lost at the very end, the report says whether the update was saved after all — as the database itself records it, not as the version number happens to read — or asks you to reload the panel when that cannot be read; it never claims a rollback it cannot confirm. Long updates are no longer cut short by the database statement limit or by the background-job time limit, and a step waiting for a busy table waits only briefly and retries, so ordinary work on that table is not held up behind it. An update step can no longer end the update's own database transaction half-way (a COMMIT inside a step is refused before it reaches the database); should that ever happen anyway, the report says the update was partially applied and was not recorded as done, instead of claiming nothing was kept.

    Wire contract: WC-2026-09-30-update-engine-atomic.

  • Error reports now include logged client errors

    The "Report a problem" tool now attaches errors the client caught and logged (console.error), not only uncaught ones, so a report of real breakage no longer says "0 errors". Only a short message and stack are kept; repeats are counted, not duplicated.

  • An update server with no public host now says so, instead of sending download links that point at localhost.

    When an ontology or code update server had no DEDALO_HOST set (or set it to localhost), it still answered other installations, but every download link in its answer pointed at http://localhost. The installation being updated rightly refused them, with an "origin mismatch" error that seemed to blame its own setup.

    The server now refuses those requests itself, and its message names the setting to fix: set DEDALO_HOST (and DEDALO_PROTOCOL) on the update server. Requests from the same machine are still served, so local development setups keep working.

    Wire contract: WC-2026-09-30-update-manifest-local-origin-refusal.

  • Time machine restore no longer fails on installs whose outbound host allowlist is empty.

    Before, restoring any value from the time machine was refused when DEDALO_EXTERNAL_ALLOWED_HOSTS was empty — which is the default. The error named a section you had not touched (on a standard install, test3) and its external catalogue host (Zenon), because the engine checked the allowlist while merely reading an external service binding, even though a restore never contacts that service.

    Now restores work with no change to your .env. The allowlist still guards every request the server sends out: a request to a host that is not listed is refused before any connection is opened. Where a host is not allowed, what you see changes in three places: an external search notice now names the real service and says the host is blocked, and an external value in a record and an export's degradation report name the real service, all instead of reporting an unknown, misconfigured source.

    Wire contract: WC-2026-09-27-external-allowlist-at-door-only.

  • The multi-instance Apache example now sets an empty DocumentRoot and the entry redirects.

    The Apache virtual host in Multiple instances had no DocumentRoot, so a vhost copied from it inherited the server-wide one (often /var/www/html) and served its contents on any path it did not route. It now points at an empty directory, like the single-instance reference, and carries the 302 redirects from /, /dedalo/ and /dedalo/core/ to the login page. Check each existing Dédalo vhost for a DocumentRoot line.

  • The developer information bar is shown to developers and root again, on any server.

    The information strip at the top of the interface — engine version, build, database and runtime — is a developer surface. It had become tied to DEDALO_DEV_MODE, so on an installation that did not set that key it was hidden even from a logged-in developer, and setting it in private/.env did nothing because the container environment takes precedence. That is what a Docker installation saw: no developer bar, whatever the .env said.

    The bar now follows the logged-in user, as the application itself did before the TypeScript rewrite: a user flagged as a developer in their record sees it, and root (superuser) always counts as a developer. DEDALO_DEV_MODE keeps its own meaning as the server posture — it selects the no-cache boot, the readable client libraries and the dev-only libraries the browser test harness needs — but it no longer decides who sees developer surfaces.

    Debug-only surfaces (SHOW_DEBUG) are now shown to root alone, as before the rewrite; other developers keep the developer surfaces but not the debug ones, and non-developers see neither, even on a development server.

    The main navigation bar is unchanged; only the extra information strip and the developer-only shortcuts are affected.

For developers

Security

  • The RDF import no longer shows the internal address a refused link resolved to.

    When an RDF link was refused because its host leads into the institution's own network, the per-link error list of get_rdf_data repeated the server's log text, which names the internal address the host resolved to. Each failed link is now reported as {uri, error}, where error is the same error body a failed request carries: a fixed, public sentence and a code (security.ssrf_blocked), never the address. The import screen shows one line per failed link, as before, now in the user's language when that error has a translated label.

    Wire contract: WC-2026-09-29-rdf-per-uri-error-wire-body.

Changed

  • Related-record lists answer one bounded page, and say so when they cut it.

    The Referencias list (read with source.action get_relation_list), the tools' related-records listing (related_search) and the thesaurus indexations grid (get_indexation_grid) used to read an absent limit or limit: 0 as "every record", and answered a malformed limit or offset ('all', 1e21) with a server error. Every client window is now bounded by DEDALO_SEARCH_CLIENT_MAX_LIMIT: an absent, zero or out-of-range limit becomes the ceiling, an offset past the end returns an empty page, and the answer carries a search.page_clamped notice naming the window asked for and the one served — for an absent limit, only when a record the caller may see lies past the page (records outside the caller's sections or projects never decide it). An indexation cell embedded in a record read is bounded the same way. The same bound applies to every API request at the parse door. A non-administrator's page may hold fewer rows than the limit while more pages exist (rows outside the caller's projects are dropped after paging): page by offset until an empty page.

    Wire contract: WC-2026-10-11-relation-reads-client-page.

  • The test suite now runs its diffusion gates against its own MariaDB server, never an installation's.

    Before, the gates that publish to MariaDB used whatever server the machine's configuration named. On a developer machine they created and dropped tables in a real publication database. On a machine without that server they skipped silently and reported green.

    Now bun test starts a MariaDB server of its own for each test lane, under ../private/test_mariadb/<suite database>. It listens on a private unix socket and never on the network. The gates check a marker on that server before they write, and they fail loudly rather than skip when it is missing. The gates never write to an installation's databases. CI runs each of these gates on its own and checks that the rows that gate is declared to write really changed on the suite's server during its run. It also runs every other unit and parity test that can reach the MariaDB connection code, directly or through other modules, with the suite's server up. It fails if any of them connects to the server without first passing the suite's check, or if any of them never really ran. These runs come last in CI's database tier, after the parity tests, so they cannot change the data the earlier stages measure. A check fails if they are ever moved earlier. The tier stops the server when it exits. One gap remains until the engine itself is fixed: while a lane's server is not running, a test that opens a MariaDB connection without going through the suite's own check can still make a login attempt against the machine's default MariaDB server. It cannot write there, since the attempt uses the suite's own user.

    Removing a lane's server is safe against a run that is still using it. The lane's directory is either fully there, with its marker, or gone, both when it is created and when it is removed. A test waiting to use a removed lane is told so instead of carrying on. A removal that was killed halfway is finished by the next one. A MariaDB command that hangs is killed after a deadline, so it cannot block the lane for everyone else, and a test waiting for the lane waits long enough to report the real problem instead of "lane busy". A server that does not start in time is stopped, not left running. A server that answers with an error is reported and never restarted. Stopping or sweeping a lane also stops any stray MariaDB server or installer still running on that lane's data directory, for example one left behind by a killed run, and waits until it has really exited; if a process survives, the command fails and names it. The shard runner (bun run test:shard, and its bun run test:shard:sweep) removes the servers of its shard lanes too, and reports a lane it could not remove with the real error, refusing to run or exiting with an error for it, instead of claiming the lane was not the suite's.

    What this means for you: running the full suite on a development machine now needs the MariaDB server binaries (mariadbd, mariadb-install-db and mariadb; on macOS, brew install mariadb). Without them the MariaDB gates fail and name what is missing. Stop a lane's server with bun run scripts/ci/suite_mariadb.ts stop. To remove it together with its data, use bun run scripts/ci/suite_mariadb.ts sweep.

Added

  • Each release is published as a signed multi-architecture image, with the same digest on every Dédalo registry.

    A release tag vX.Y.Z now also produces the engine image X.Y.Z, for linux/amd64 and linux/arm64. It is built once from the same git archive as the code server's release zip, signed keylessly with Sigstore by the release workflow, and copied with its signature to every registry that engineering/image_registries.json lists as provisioned. Each copy is checked to carry the same digest. A published release tag is never rebuilt or overwritten. Developer images (X.Y.Z-dev) are built only on demand, from master. There is no latest tag. To check that an image is genuine, verify it against the release workflow's identity, as shown in checking the signature. A registry that is not provisioned, or whose credentials are missing, is skipped by name, never silently.

  • Tool authors can read other sites through harvestFetch, a harvesting door that obeys robots.txt and paces its requests.

    A tool that imports from another institution's site (an auction catalogue, a journal's OAI endpoint, a publisher's PDF) can now call harvestFetch instead of building its own fetch layer. For every hop of a redirect chain it re-checks the address and your host policy, refuses a switch from https to http, and asks the site's robots.txt — for images, PDFs and POSTs too. It sends one request at a time per origin, for the whole installation, at least three seconds apart, or at the site's Crawl-delay or Retry-After capped at one minute. It connects to the address it checked, bounds each hop by a total and an idle timeout and a byte ceiling, and can refuse an unexpected media type before downloading it. Its refusals carry their own codes (harvest.refused, harvest.robots_disallowed, harvest.robots_unavailable, harvest.too_large, harvest.unexpected_type), which name the site and the reason, so a cataloguer learns why a URL was not fetched. See Fetching from other sites.

Fixed

  • The developer install and its test suite work over a PostgreSQL socket, never prompt for a password, and keep the dev server on this machine.

    • The test suite connects the way .env says. bun test and bun run test:db:setup use DB_SOCKET when it is set, as the engine does. Before, they used DB_HOST only: on a socket install the suite reported that an existing test database did not exist, and test:db:setup failed with password authentication failed.
    • No pg client prompts. Every psql, pg_dump and pg_restore that the engine or the suite starts runs with -w and a connection timeout. A missing password now fails at once with no password supplied, instead of waiting at Password for user …:.
    • test:db:setup refuses a server without pgvector before it creates anything, and names the package to install. If a vector build fails after that, it removes its own database, so the next run starts clean without --force.
    • The development listener binds 127.0.0.1. A new key, SERVER_TCP_HOST, sets another address on purpose (for example 0.0.0.0). Before, SERVER_TCP_PORT listened on every address, with full error details on.
    • The time zone and locale defaults are this machine's (TZ, LC_ALL, LANG), falling back to UTC and en-US. Before, they were Europe/Madrid and es-ES everywhere.
    • A production install writes DEDALO_MEDIA_ACCESS_MODE=publication, through the host steps or --engine-only, so the first boot no longer reports an unconfigured media mode.
    • Quieter output. A retired ontology property is reported once per property, naming the first node and the command that lists every node, instead of once per node. A development server no longer prints the nginx reload notice, and .env no longer carries a comment about the retired engine.
    • A test run leaves nothing in the temp directory. The session store's scratch directory and the scratch directories of two test files are removed.
    • The developer quickstart now covers Linux: creating your PostgreSQL role, the socket directory per system, a RHEL, Rocky and Alma column, git, curl and unzip, pgvector, MariaDB and an AVIF-capable ImageMagick for the suite, which parity gates are expected red, and the GeoIP download at first boot.
  • A save whose value is not shaped like the field's data is now refused instead of stored.

    The save API accepted a value in any shape and answered success: a bare text sent to a translatable text field was stored beside the other languages, and the next save in another language silently removed it; a number sent as text, an empty null item or a non-list replacement were stored or emptied the field the same way. Such a save is now refused with request.invalid_data, before anything is written. Each item must be an object — {value: "…"} for a text field, {value: 12} for a number, {start: {…}} for a date, {iri: "…", title: "…"} for a link — and a replacement (set_data) must be a list of them. The application's own editors already send these shapes. The CSV/JSON importer now converts numbers written as text in a number column ("55") and numbers in a text column, and refuses a number cell it cannot read instead of storing it. Emptying a date field in the record editor now removes the stored date; before, it left an empty placeholder in the record. Records that still hold numbers stored as text by earlier versions keep working with the cache-update and propagate-data tools: those tools convert such numbers when they save the record again. The RDF importer converts a value mapped to a number field (a Nomisma weight, diameter or axis) into a number, and reports one that is not a number (number_unparsed) instead of sending it.

    Wire contract: WC-2026-10-03-save-refuses-malformed-value-shape.

  • The client-library version table in the manual is generated from the pins, so it can no longer go stale.

    Client library versions listed outdated versions for five npm-tracked libraries (three, geoman, turf, highlight.js, mocha). Its npm table is now generated from package.json and the client-library registry (bun run libs:gen) and checked byte for byte by a gate, so a version bump that leaves the page behind fails the build. The vendored table stays hand-written and is checked against vendor/vendor_manifest.json, as before.

  • A refused save no longer leaves an empty record behind.

    A save to a record that does not exist yet creates the record first. When the change itself was then refused (for example, removing a value the field does not hold), the answer was a failure, but the new empty record stayed, the section's id counter had moved to its id, and the activity log recorded its creation. A refused save now leaves nothing behind: no record, no counter move, no history or activity entry. The answer to the request is unchanged. A save run inside a larger operation (an import row, for example) leaves that choice to the operation, which rolls the row back as before.

  • A relation search with an unreadable value now fails with an error instead of quietly matching every record.

    Relation fields (selects, portals, autocompletes…) take their operator — is empty, has a value, is different — in q_operator, and their value as a record link. When a request put something else in the value, such as the operator itself ("q":"!*") or plain text, the condition was silently dropped, and the search and its count covered the whole section: records with no author counted every record. Such a request is now refused with a request.invalid error that names q_operator. The searches the interface builds were never affected.

    Wire contract: WC-2026-09-23-relation-q-is-a-locator.

Wire contract — 126 entries
  • WC-2026-08-24-install-ip-gate-fail-closed
  • WC-2026-08-24-media-auth-session-scoped
  • WC-2026-08-24-media-protection-default-closed
  • WC-2026-08-24-media-svg-response-headers
  • WC-2026-08-24-proxy-trust-and-password-cost
  • WC-2026-08-25-update-code-restore-point
  • WC-2026-08-25-update-code-waive-backup
  • WC-2026-08-27-csv-ingest-refusals
  • WC-2026-08-27-duplicate-reminted-dataframe-targets
  • WC-2026-08-27-tm-lang-slice-restore-merge
  • WC-2026-08-27-write-language-provenance
  • WC-2026-08-28-export-gate-b-refuses-unresolvable-segment
  • WC-2026-08-28-idempotency-key
  • WC-2026-08-28-login-active-account
  • WC-2026-08-28-root-self-service-write
  • WC-2026-08-28-search-path-acl-every-hop
  • WC-2026-08-29-tm-apply-value-confirm-label
  • WC-2026-08-30-remove-requires-item-id
  • WC-2026-08-30-section-id-counter-is-a-high-water-mark
  • WC-2026-08-31-client-reads-three-fields-the-engine-never-emits
  • WC-2026-08-31-import-code-lookup-refuses-a-truncated-candidate-set
  • WC-2026-08-31-time-machine-history-belongs-to-a-generation
  • WC-2026-09-01-children-recursive-search
  • WC-2026-09-01-ts-object-svg-url
  • WC-2026-09-03-bulk-revert-skipped-typed-entries
  • WC-2026-09-03-duplicate-record-new-activity-row
  • WC-2026-09-03-info-stored-value-never-served
  • WC-2026-09-03-maintenance-reconcile-status-widget
  • WC-2026-09-03-monovalue-insert-replaces
  • WC-2026-09-03-read-door-component-acl
  • WC-2026-09-04-client-limit-bound
  • WC-2026-09-04-committed-third-party-bundles-leave-the-client-tree
  • WC-2026-09-04-context-render-class
  • WC-2026-09-05-children-order-one-rule
  • WC-2026-09-05-diffusion-lease-epoch-fence
  • WC-2026-09-05-multi-locator-order-is-deterministic
  • WC-2026-09-05-preauth-intake-bounds
  • WC-2026-09-05-search-terms-are-literal
  • WC-2026-09-06-dataframe-delete-policy-on-slot
  • WC-2026-09-23-relation-q-is-a-locator
  • WC-2026-09-24-external-foreign-target-and-verbatim-id
  • WC-2026-09-24-external-record-4xx-is-not-found
  • WC-2026-09-24-external-record-field-set
  • WC-2026-09-24-multi-section-search-identity-dedup
  • WC-2026-09-24-tool-export-server-built-artifacts
  • WC-2026-09-27-bulk-revert-undo-log
  • WC-2026-09-27-csv-import-append-mode
  • WC-2026-09-27-external-allowlist-at-door-only
  • WC-2026-09-28-maintenance-serve-code-widget
  • WC-2026-09-28-maintenance-serve-ontology-widget
  • WC-2026-09-29-code-release-channel-refs
  • WC-2026-09-29-dataframe-hard-delete-retired
  • WC-2026-09-29-number-not-equal
  • WC-2026-09-29-rdf-per-uri-error-wire-body
  • WC-2026-09-29-search-deep-leaf-mixed-rule
  • WC-2026-09-29-search-where-parts-parenthesized
  • WC-2026-09-29-select-family-mode-datalist
  • WC-2026-09-29-tm-preview-frame-children-as-of
  • WC-2026-09-30-agent-tool-grant
  • WC-2026-09-30-backup-freshness-deep-async
  • WC-2026-09-30-backup-part-promotion
  • WC-2026-09-30-db-typed-503
  • WC-2026-09-30-diffusion-attach-scope
  • WC-2026-09-30-diffusion-run-ledger
  • WC-2026-09-30-diffusion-target-fence
  • WC-2026-09-30-diffusion-zip-streamed
  • WC-2026-09-30-guarded-text-pinned-typed-transport
  • WC-2026-09-30-mcp-search-section-grant
  • WC-2026-09-30-media-key-locked-transform
  • WC-2026-09-30-media-pair-scope
  • WC-2026-09-30-move-transform-execute-job
  • WC-2026-09-30-ontology-identifier-grammar
  • WC-2026-09-30-password-hash-never-served
  • WC-2026-09-30-password-policy-enforced
  • WC-2026-09-30-record-write-obligation-ledger
  • WC-2026-09-30-search-root-step-acl
  • WC-2026-09-30-transcription-record-tipo
  • WC-2026-09-30-update-engine-atomic
  • WC-2026-09-30-update-manifest-local-origin-refusal
  • WC-2026-09-30-write-door
  • WC-2026-10-01-ai-spend-budget
  • WC-2026-10-01-change-plan-write-door
  • WC-2026-10-01-delete-locator-write-door
  • WC-2026-10-01-identify-vision-grant
  • WC-2026-10-01-ontology-overwrite-scoped
  • WC-2026-10-01-rdf-harvest-door
  • WC-2026-10-01-rdf-ontology-import
  • WC-2026-10-01-relation-search-display-paths
  • WC-2026-10-01-search-hop-configured-targets
  • WC-2026-10-01-site-builder-confinement-codes
  • WC-2026-10-01-tool-grant-one-decision
  • WC-2026-10-01-unit-test-widget-dev-gate
  • WC-2026-10-01-write-door-delegations
  • WC-2026-10-02-relation-children-write-through
  • WC-2026-10-02-select-lang-missing-entry
  • WC-2026-10-03-publication-hosts-widget
  • WC-2026-10-03-save-refuses-malformed-value-shape
  • WC-2026-10-06-consultation-only-no-section-tools
  • WC-2026-10-07-acquisition-tools-ts-only
  • WC-2026-10-07-ontology-area-admin-grant
  • WC-2026-10-07-update-code-root-entries-stamp
  • WC-2026-10-08-install-ip-denied-names-address
  • WC-2026-10-08-install-plan-update-servers-core-lg
  • WC-2026-10-08-make-backup-scheduled-evidence
  • WC-2026-10-09-code-manifest-release-notes
  • WC-2026-10-09-install-domain-ontologies
  • WC-2026-10-09-ontology-manifest-dependencies
  • WC-2026-10-09-publication-host-panel-setup
  • WC-2026-10-09-publication-host-v2-only-site
  • WC-2026-10-09-update-code-image-channel
  • WC-2026-10-10-hierarchy-json-manifest
  • WC-2026-10-10-ontology-dependencies-hierarchy60
  • WC-2026-10-10-update-ontology-confirm-text
  • WC-2026-10-11-children-recursive-one-statement
  • WC-2026-10-11-export-buffered-cap-and-admission
  • WC-2026-10-11-parent-link-cycle-every-door
  • WC-2026-10-11-pdf-extractor-render-class
  • WC-2026-10-11-relation-reads-client-page
  • WC-2026-10-11-rich-text-sanitizer-tokenizer
  • WC-2026-10-11-session-user-beside-csrf-token
  • WC-2026-10-11-site-builder-reason-codes
  • WC-2026-10-11-site-builder-turn-error-frames
  • WC-2026-10-12-ddengine-removed
  • WC-2026-10-12-install-surface-removed
  • WC-2026-10-12-user-activity-what-labels
  • WC-2026-10-14-diffusion-info-node-database

7.0.0-beta.4 — 2026-08-24

534 commits: git log --no-merges v7.0.0-beta.3..v7.0.0-beta.4

For users

Fixed

  • A blocked pop-up now says so instead of breaking the action that opened it.

    Opening a tool, a record, an ontology page or a related-records list in a new window failed silently when the browser refused the pop-up: no window appeared and the action behind it stopped half way, with nothing on screen to explain why. The refusal is now reported to the user, and open related records no longer claims success for a list nobody saw.

  • Editing one field and then correcting a typo in another no longer loses the first edit.

    A long text field commits its change half a second after you stop typing. If, inside that window, you typed a character in any other field and deleted it again, the page concluded that nothing was unsaved: closing the tab or moving to another record then discarded the text field's edit with no warning, no save and nothing in the console.

    The page tracked unsaved work as a single yes/no answer for the whole screen, and every field was allowed to write it. A field returning to its stored value answered no on behalf of every other field, including ones still holding work. Each field now reports only for itself, and the page answers yes while any of them still has something unsaved — so the save-before-leaving sweep and the confirmation prompt both run when they should. A test now holds that a field may only ever retire its own answer.

  • A missing language list no longer disables language selection for the rest of the session.

    The list of languages is fetched once and kept for the whole page. A failed fetch was kept in exactly the same way: the failure itself was stored as if it were the list, every later request for languages was answered with it, and nothing ever tried again. A map field would then fail outright and the rich-text editor would silently load in the wrong language. Only a usable list is kept now; anything else leaves nothing behind, so the next request for languages asks the server again.

  • A field whose save comes back without its record keeps its value instead of emptying itself.

    When the answer to a save did not carry the record back — the save itself having succeeded — the field threw away everything it knew about what it was editing: the value on screen, and the copy it keeps to decide whether anything has changed since. From that point the field looked perfectly normal but had nothing left to save, every later edit was reported as a change whether or not it was one, and the only notice of it was a console line that appeared solely with developer mode on.

    The field now keeps what it was editing, records that it is out of step with the server, and says so unconditionally. While it is in that state it also stops trusting its own record of what the server holds: its next save is always sent, rather than being skipped as nothing has changed — so the edit the server never confirmed is retried instead of being quietly dropped. Tests hold all of this, including that the retry really goes out.

  • A request to a slow server can no longer wait forever.

    While a request is in flight the page asks the server whether it is still alive, so that a slow answer is not mistaken for a dead server. That check removed the request's time limit instead of extending it: a server that answered the check and then stopped responding left the page waiting indefinitely — the awaiting for busy server notice dismissed itself, the panel stayed empty, and no error was ever raised. A busy server now buys the request more time, and the notice stays visible for as long as the wait it describes.

    Long operations keep their own limit. The extra time is added to whatever is left of the original allowance rather than replacing it, so a database backup still gets its full hour.

  • Text you have typed but not yet confirmed is now protected when you reload or close the tab.

    Typing into a field and reloading the page dropped the typing with no warning: no prompt, no save, nothing in the console. It happened in most field types — plain text, dates, numbers, e-mail, passwords, coordinates — while long text fields were unaffected, which is what made the loss look arbitrary.

    The page only counted a field as unsaved once the field had confirmed its value, and most fields confirm on leaving the field. Reloading or closing a tab never leaves the field, so the page believed there was nothing to protect. Unconfirmed typing now counts from the first keystroke, and leaving the page forces the field you are in to confirm first — so a real edit raises the browser's leave-page warning and can still be saved, while typing you have already undone raises nothing.

    Only fields that hold record data in edit mode are watched: search boxes, list cells and the lookup box inside a related-records field are not your unsaved work and never trigger the warning. A test now types into every kind of field, for every component, and fails if any of them can be reloaded away unnoticed.

  • Logging in no longer asks for the credentials twice.

    The login screen rendered the login panel twice — the form itself, dimmed, with a second one overlaid on top of it — so entering the user and password once appeared to do nothing and the operator had to type them again.

    The second panel was the re-login overlay, which exists to recover a session that died under a working page. The activity tray mounts with the page, and it mounted on the login screen too: its first act is to read the caller's own background work, which for a caller who has not logged in yet is correctly refused with not_logged — and any not_logged raised the overlay. The tray now mounts only for a logged-in page and asks nothing while anonymous, and the overlay refuses to open on a page that never had a session: it is a re-login, never a second login. A session that expires mid-use still gets the modal, which is the case it was built for.

For administrators

Security

  • The activity panel of the area dashboard renders user names as text.

    That panel lists the most active users by the name recorded in the users section. The name was inserted into the page as markup, so a name containing HTML was interpreted by the browser of everyone who opened the dashboard instead of being displayed. Names — and the counts beside them — are now written as text, as the chart legend on the same panel already did.

Removed

  • The ontology parser's Reconcile button.

    The tool offered two writes onto the runtime ontology — an incremental Reconcile that applied only what had drifted, and Regenerate, which wipes the hierarchy's runtime nodes and re-derives them. Reconcile's whole justification was that it never left the ontology momentarily empty, and that stopped being true of Regenerate the day the rebuild moved inside a single transaction: readers keep seeing the current ontology until the new one commits, and a failure rolls back. What was left was a strictly weaker write offered beside the stronger one, with nothing to tell an operator which to press — and a second code path to keep in step with the ontology parser forever.

    Regenerate is now the one write onto the runtime tree, and Refresh status still answers "what drifted" without changing anything. Ordering a node no longer prompts the question the button existed to ask: reordering genuinely changes every sibling that moved, so "only what drifted" was the whole hierarchy anyway.

Changed

  • Rebuilding an ontology no longer refreshes the export files, and is ~20× faster for it.

    Regenerate used to rebuild the LLM map for the whole installation afterwards, whichever single hierarchy you had ticked. That map is one of the files other installations download, and a rebuild refreshes none of its companions — so keeping it alone current never made the published set coherent, while costing 21 of the 22 seconds an operator spent waiting. The files are refreshed by Export, which publishes all of them together.

    If you rebuilt in order to publish

    Press Export afterwards. That was already true of the ontology definition files; it is now true of the LLM map too.

    This is also where the dropped sqo target … lines in the server log during a rebuild came from — the whole-install map walk, reporting components that point at ontologies this installation has registered but never imported. They still appear on an export, and in normal editing of those components, which is where they are actionable.

Fixed

  • The Export hierarchy maintenance panel opens again.

    Selecting it in the maintenance area refused with "The 'export_hierarchy' panel is not available on this engine" and rendered nothing — which also put its working Sync hierarchy active status action out of reach, since the whole card dies with the failed value load. The panel now reports that hierarchy EXPORTING is not offered by this engine (it wrote install dump files) and renders the sync form, which is the operation it actually has.

  • The five migration panels (Move TLD, Move locator, Move to portal, Move to table, Move lang) show their definition files again.

    Each opened with no explanation text and an EMPTY file list, so there was nothing to select and the transform could not be run at all. The engine had been serving the body and the file list all along — the panel simply never asked for it.

    Both bugs are the same invariant broken from opposite sides: a panel's value load is a PAIR (the widget serves a value, the panel asks for it) and either half alone is a broken panel. A gate now checks the pairing in both directions for every maintenance widget, so a half-wired panel fails the test suite instead of an operator's browser.

  • Any component offering "every ontology" as a search target took 42 ms to configure — on every single request.

    Resolving such a component's search configuration enriches each target section it offers, and asked the database twice per section whether it has a new record and a delete record button — four times over for a section with neither. On an installation carrying 205 ontologies that is ~800 queries to draw one autocomplete, repeated on every page that shows it, and nothing about the answers changes between requests.

    The lookup is now remembered alongside the other ontology reads, so it costs 0.64 ms. Authoring a new button still shows up immediately — the memory is dropped whenever the ontology is written, like every other ontology cache. Building the LLM map, which resolves 9 408 such fields, went 21.7 s → 1.2 s on the same installation, producing a byte-for-byte identical file.

  • The active-tools registry was re-queried once per rendered row.

    The lookup takes no arguments — it is the same query every time — and a media-icon widget runs it per row, so a ten-row page ran it ten times for 43 ms. It is now read once and cleared whenever the tools registry (dd1324), the install config or a user profile is written, through the existing tool-cache invalidation.

    Out-of-band registry edits need a cache clear

    Activating or deactivating a tool through the interface invalidates the cache immediately. Editing matrix_tools directly in the database does not — use the maintenance area's cache-clear action, or restart.

  • A list page re-read the same record once per widget path.

    The info widgets ask for one component at a time, and each ask fetched a WHOLE matrix row — so a component_state declaring eight paths over the same related record fetched that record eight times, and a media-icon widget on the same page fetched it again. On a ten-row page of an oral-history section that was 145 record reads for 27 distinct records — 81% pure repetition, and 328 ms of the 371 ms the request spent in the database. The waste grew with both the page size and the archive, which is why the page kept getting slower without anything changing.

    A record is now fetched once per read, and the page-level batch loaders hand their results to the same store, so a component asking for a record the page already loaded costs no round trip. Same page: 3 record reads, and the count no longer grows with the page size. The list read went 421 ms → 66 ms, and an edit read of the same section 107 ms → 56 ms. Responses are byte-for-byte identical — this is purely the number of round trips.

Wire contract — 114 entries
  • WC-2026-08-03-chunked-upload-identity
  • WC-2026-08-03-cors-allowed-origins
  • WC-2026-08-03-service-dropzone-folded-into-service-upload
  • WC-2026-08-04-import-derivative-failure-counts-as-imported
  • WC-2026-08-04-media-versions-panel-wire
  • WC-2026-08-05-external-api-config-publication
  • WC-2026-08-05-external-derived-emission
  • WC-2026-08-05-external-entry-normalisation
  • WC-2026-08-05-external-first-record-reduce
  • WC-2026-08-05-external-request-field-union
  • WC-2026-08-05-external-source-status
  • WC-2026-08-05-multi-engine-ddo-expansion
  • WC-2026-08-06-external-client-render
  • WC-2026-08-06-external-search-request
  • WC-2026-08-06-external-write-refusal
  • WC-2026-08-06-observer-degraded-seed-refuses-shrink
  • WC-2026-08-06-observer-equivalence-class-targets
  • WC-2026-08-06-observer-grow-only-failsafe-retired
  • WC-2026-08-06-observer-removal-propagation
  • WC-2026-08-07-breakdown-total-order
  • WC-2026-08-07-media-alternate-version-builder
  • WC-2026-08-07-retouched-tier-is-a-master
  • WC-2026-08-08-csp-connect-src-update-masters
  • WC-2026-08-08-geolocation-emptiness-explicit
  • WC-2026-08-08-media-thumb-handler
  • WC-2026-08-09-autocomplete-hi-ancestor-search
  • WC-2026-08-09-av-fragment-failure-reason
  • WC-2026-08-09-changes-in-locators-integrity
  • WC-2026-08-09-code-manifest-target-dedupe
  • WC-2026-08-09-dataframe-import-id-key-identity
  • WC-2026-08-09-date-search-uncovered-mode-throws
  • WC-2026-08-09-ddo-map-per-entry-sanitization
  • WC-2026-08-09-diffusion-degradation-and-loud-ddo-fns
  • WC-2026-08-09-export-raw-dataframe-own-column
  • WC-2026-08-09-image-svg-envelope-preserved
  • WC-2026-08-09-indexation-grid-read-grant-scope
  • WC-2026-08-09-info-legacy-stored-value-fallthrough
  • WC-2026-08-09-ingest-companions-and-human-filename
  • WC-2026-08-09-media-index-target-selection
  • WC-2026-08-09-media-restore-no-overwrite
  • WC-2026-08-09-media-spawn-integrity-and-liveness
  • WC-2026-08-09-media-tree-boot-provisioning
  • WC-2026-08-09-media-tree-configurable-folders-and-boot-budget
  • WC-2026-08-09-move-lang-collision-refused
  • WC-2026-08-09-move-to-table-self-move-refused
  • WC-2026-08-09-ontology-tld-derived
  • WC-2026-08-09-ontology-update-partial-restore
  • WC-2026-08-09-portalize-portal-merge
  • WC-2026-08-09-provider-message-truncation-boundary
  • WC-2026-08-09-record-birth-defaults
  • WC-2026-08-09-simple-schema-changes-pre-import
  • WC-2026-08-09-time-machine-restore-replays-paired-dataframe-frames
  • WC-2026-08-09-tm-date-search-yearless-q-drops
  • WC-2026-08-09-users-section-record-scope
  • WC-2026-08-10-maintenance-catalog-fail-soft
  • WC-2026-08-10-section-id-int-canonical
  • WC-2026-08-11-diffusion-computed-inverse-hops
  • WC-2026-08-11-diffusion-uninstalled-package-skip
  • WC-2026-08-11-regenerate-drops-llm-map-post-step
  • WC-2026-08-11-vector-tag-badges
  • WC-2026-08-12-authorization-denial-token
  • WC-2026-08-12-filter-records-enforced
  • WC-2026-08-12-indexation-tool-buttons
  • WC-2026-08-12-media-job-visibility
  • WC-2026-08-12-user-activity-full-history
  • WC-2026-08-13-maintenance-ai-models-widget
  • WC-2026-08-13-transcription-status-panel
  • WC-2026-08-14-llm-map-target-sections-plural
  • WC-2026-08-14-model-section-registry-branch
  • WC-2026-08-14-non-section-target-refused
  • WC-2026-08-14-relation-insert-accepts-batch
  • WC-2026-08-14-relation-insert-target-validation
  • WC-2026-08-14-relation-model-target-in-sqo
  • WC-2026-08-14-target-mode-retired
  • WC-2026-08-14-thesaurus-picker-caller-declared
  • WC-2026-08-14-tm-cells-obey-list-emit-policy
  • WC-2026-08-14-tm-ddo-mode-retired
  • WC-2026-08-14-tm-permission-floor
  • WC-2026-08-14-tm-scope-server-owned
  • WC-2026-08-15-code-manifest-sha256-and-snake-keys
  • WC-2026-08-15-diffusion-connection-status-ok-message
  • WC-2026-08-15-diffusion-job-result-record
  • WC-2026-08-15-error-envelope-v2
  • WC-2026-08-15-error-status-is-a-channel
  • WC-2026-08-15-external-degradation-is-a-notice
  • WC-2026-08-15-http-layer-error-envelope
  • WC-2026-08-15-identify-decline-codes
  • WC-2026-08-15-install-refusals-typed
  • WC-2026-08-15-mcp-error-code-alignment
  • WC-2026-08-15-stream-error-frames
  • WC-2026-08-15-tool-response-envelope-v2
  • WC-2026-08-16-error-envelope-compat-removal
  • WC-2026-08-16-sqo-session-not-persisted-on-session-save-false
  • WC-2026-08-16-tm-tool-note-value
  • WC-2026-08-17-list-row-is-indexable
  • WC-2026-08-17-list-row-selectability-declared
  • WC-2026-08-18-dashboard-total-non-record-store
  • WC-2026-08-19-rqo-body-csrf-token
  • WC-2026-08-19-test-tld-replay
  • WC-2026-08-19-test-tld-replay-search-group
  • WC-2026-08-19-tool-lang-translator-engine-type-and-browser-transformer
  • WC-2026-08-19-tools-gates-test-tld
  • WC-2026-08-21-files-cache-finish-message
  • WC-2026-08-22-preauth-language-cookie
  • WC-2026-08-23-build-provenance
  • WC-2026-08-23-dedalo-files-post-harvest-census
  • WC-2026-08-23-diffusion-langs-widgets-derived
  • WC-2026-08-23-maintenance-widget-label-case
  • WC-2026-08-23-projects-default-langs-derived
  • WC-2026-08-23-test6228-clone-recut
  • WC-2026-08-23-tool-export-register-labels
  • WC-2026-08-23-update-mode-clean-only
  • WC-2026-08-23-user-tools-post-harvest-census
  • WC-2026-08-24-update-code-dev-channel

7.0.0-beta.3 — 2026-08-03

440 commits: git log --no-merges v7.0.0-beta.2..v7.0.0-beta.3

Action needed when you update

  • Media URLs honor DEDALO_MEDIA_WEB_BASE

For users

Changed

  • The state widget's edit view was restyled.

    It now follows the maintenance widget-kit language: card surface, uppercase header, hairline row rules, tabular numerals, and a meter per percentage (full at 100%, muted empty track at 0%). Every colour goes through a theme token, so the breakdown panel is no longer a white slab in dark mode. Percentages are keyboard reachable, and the panel stays open while the pointer travels into it.

  • Append-only logs restrict sort and search dimensions

    Arbitrary component sorts on the append-only log are disallowed: every dd542 column is sortable:false except When (dd547), whose order maps to the direct section_id column (append-only ⇒ insertion order). The edit-mode search FIELDS panel now omits the shared section-info group (dd196) for Activity (dd542) and Time Machine (dd15), where that editorial metadata is meaningless as a search dimension.

    Wire contract: WC-044, WC-045.

Added

  • Pre-expiry warning.

    The client warns before a session dies, so unsaved work can be committed instead of the next click failing. Lead time is SESSION_WARNING_SECONDS (default 300; 0 disables it). The boot payload ships only the absolute deadline — the idle window restarts on every request, so the client re-arms a local timer from its own activity beat.

  • Assistant grounding tools

    The AI assistant's loop gains section/facet scoping on dedalo_semantic_search and the new dedalo_retrieve_passages (exact chunk-level passages for cited answers, deduplicated across languages). The external-model egress gate now also classifies every deep-resolution CONTRIBUTOR section of a hit — text a group document pulled from a forbidden section can never reach an external model through a public host record.

  • Semantic search in the client

    A "Search by meaning" quick input in the section-list toolbar and a semantic block in the search panel (composes AND with the structured filter). Ranked hits pin the list via filter_by_locators plus the new {"mode":"locator_position"} SQO order entry, so relevance order survives pagination, counts and exports; an SQO-derived pinned chip makes the state visible and clearable. Sections with several embed groups get a facet selector (dd_rag_api embed_groups). Search presets store the LIVE natural-language query and re-run it on Apply.

    Wire contract: WC-047.

Fixed

  • A row with no data collapsed the layout.

    The cells of an empty situation/state column were not rendered at all, so the next row's label was auto-placed into the hole and the labels marched across the columns. Empty cells are now drawn, and read —.

  • The state breakdown panel contradicted its own cell.

    A source record that saved nothing contributes 0 to the average but emits no detail item, so a record with two linked resources and one value showed a hover panel reading total : 50% beside a cell reading 25% — two numbers, both labelled total. The panel now lists one row per source record (the empty ones included, as explicit 0%) under a single ruled-off total that always equals the cell. It also dropped every row after the first on a non-translatable leaf, and clipped long option names to one letter per line.

  • An expired session raises the re-login modal instead of a network error and blank widgets.

    The client's whole recovery path keys on the error code not_logged, which the server never emitted (it put the human message in errors[]), and the client's retry wrapper treated the 401 as a transport failure and threw before the response could be read. Expiry now raises the re-login modal in place, and pending saves replay on login_successful.

  • Images, video, PDFs and 3D files no longer 404 for anyone logged in longer than a day.

    Until this fix they did, while the application itself looked completely healthy. The media cookie was minted only at login with a fixed 24-hour Max-Age while the session refreshed on every request; since the web server enforces media access, losing the cookie removed every media file with no other symptom. The reverse leak is closed too — a cookie minted just before logout stayed a valid media credential for up to 48 hours with no session behind it.

  • Search preset leaked fields from the wrong section

    The search panel restores your in-progress filter from a per-user temp editing preset (dd655) plus named presets (dd623). These were keyed by the search's caller section (self.section_tipo) instead of the section actually being searched (self.target_section_tipo, the same source the field list uses). The two diverge whenever the searched section differs from the host section — an ontology/thesaurus browser, or a relation/portal/autocomplete picker opened from inside another section — so a filter built from one section's fields was stored under a different section's key and later surfaced there (e.g. an ontologytype0 filter appearing in the Activity dd542 search panel). Presets are now scoped by the searched section (client/dedalo/core/search/js/preset_scope.js preset_scope_tipo), and the section-key match is exact (q_operator: '==') so a tipo that is a substring of another can no longer bleed. Guarded by test/unit/search_preset_scope.test.ts.

For administrators

Changed

  • The media access cookie now lives exactly as long as the session.

    dedalo_media_auth is re-issued on any authenticated request whose value is missing or stale, with Max-Age = the session idle window. Media protection · login.

  • Session expiry retuned.

    The idle timeout drops from 12 h to 1 h (SESSION_TTL_SECONDS) and the absolute cap from 30 days to 12 h (SESSION_ABSOLUTE_TTL_SECONDS). An unattended browser is the threat the idle window exists for, and because the client polls in the background the idle clock alone never expires anything — the cap is what actually ends a working day. Both clocks were already enforced on read; only the defaults moved. Long-running work is unaffected: background imports keep their requesting user on the job record and diffusion re-derives the enqueuing principal at run time, so a publication run or a massive import survives its owner's logout and reattaches after re-login.

  • Activity (dd542) list survives 30M+ rows

    On a 32.9M-row / 85 GB matrix_activity a header sort or a deep page used to full-scan the whole table (>60 s at every page). Three wire-identical internal rewrites (same rows, same order, same paginated envelope): the structural sort key (section_id / id) is emitted index-aligned with no NULLS LAST, so the default newest-first list is index-served (>60 s → ~11 ms); deep and last pages use a late-row-lookup + order-flip on the flattened unique-key path (>5 s → ~64 ms); and the bare-browse total is served from a save-event-invalidated cache. Ordered-search SQL is flattened to an inline ORDER BY … LIMIT whenever the target table carries the full unique (section_id, section_tipo) key.

    Wire contract: WC-044, WC-046.

  • Media URLs honor DEDALO_MEDIA_WEB_BASE (action needed)

    Every client media URL is now built on a configurable base (config.media.webBase), so an install serving media from a different origin than the app can emit correct absolute URLs; unset means the previous same-origin relative default. Distinct from DEDALO_MEDIA_EXPORT_BASE, which continues to root export cells only (unset means unresolved, never guessed). That key is the rename of DEDALO_MEDIA_BASE_URL: the two names were indistinguishable while the values differ only in audience, so the pair now says who each one serves — the old spelling is retired and refuses the boot until the line is renamed. Its value is normalized like webBase (trailing slash stripped), and it takes the media directory too (https://host/dedalo/media), because both bases are prefixed to the same media-root relative path. See config reference.

    Wire contract: WC-042.

  • Optimize tables prunes redundant indexes on the two logs.

    The Optimize tables action of the Database-info maintenance widget prunes dead/redundant indexes on the two logs by a single-source-of-truth policy (never a constraint or a proven-used index); ~7.9 GB reclaimed on the reference install.

  • tool_update_cache now requires an explicit scope

    The silent whole-section fallback is removed: a missing or malformed sqo fails closed with invalid_request, and the client sends a deep clone of the caller list's live sqo, so a run's scope is exactly the scope the list displays. The confirm dialog carries the record and component counts; the media regenerate path rebuilds only files that are missing (instead of re-encoding everything), mints a dd800 bulk-process record per run, and suppresses Time Machine for the run's re-saves.

    Wire contract: WC-043.

Added

  • The full 16-code activity WHAT map is emitted

    Eight event emitters were added (LOG IN / LOG OUT / NEW / UPLOAD COMPLETE / DELETE FILE / RECOVER SECTION / RECOVER COMPONENT / NEW VERSION), so the user_activity charts are no longer limited to the four previously-instrumented events. Login failures are recorded too (throttle lockouts and maintenance refusals included). Known gap, ledgered: the projects dimension (relation.dd550) is not written by the engine, so activity filtered by project shows nothing from the current era.

    Wire contract: WC-040.

  • Agent-built public websites

    A wholly TS-native subsystem lets users build public sites over the published data by talking to a coding agent: a standalone daemon (publication/site_builder/, isolated like the publication API), the proxy tool tool_sitebuilder (a three-pane workspace), and a site_builder_status maintenance widget that probes the daemon and hosts the launcher. No counterpart existed in the previous engine. Docs: Site builder · internals.

    Wire contract: WC-035.

  • RAG embed-groups descriptor

    What a section vectorizes is now AUTHORED in its section_map node: properties.rag.embed is an array of named groups, each an exact request_config ddo_map. A group is one vector document per (record, data language) — the facet unit (a person's profession vs filiation; a transcription with its own chunking) — stored under rag:<group>. Resolution reuses the section-read machinery (emitDdoData), so DEEP relation resolution works: a coin type's card can embed its mint's name, resolved through the relation. Virtual sections select their own maps (the section_map read is virtual-aware) — the earlier per-component boolean opt-in, which could not differentiate virtual siblings and indexed no text at all for virtual sections, is retired. Documentation: RAG & semantic search · cookbook R1.

  • RAG ops

    The drain cron + embedding-sidecar runbook (engineering/PRODUCTION.md §11); Ollama's native /api/embed matches the sidecar contract directly.

Fixed

  • Expired sessions are garbage-collected when they hit the absolute cap.

    The sweeper matched only the idle clock, so a session kept warm by a polling client past the cap was refused on every request and its row kept forever.

  • Long text fields are embedded in full, not as their list preview.

    Text-area values embedded through list-mode ddos were truncated to the 130-character list preview (a 2.1 MB transcription embedded as 154 chars). Ddo mode in rag.embed maps is now honored verbatim when explicit and defaults to full-value resolution for literals when absent.

For developers

Changed

  • sanitizeClientSqo caps filter_by_locators at 1000 pins.

    sanitizeClientSqo clamps filter_by_locators to 1000 pins (loud log).

  • Semantic indexing no longer depends on who saved the record.

    Index-time resolution always runs under a system scope with explicit data languages — a record's vectors never depend on which user's save triggered the re-index (guarded by rag_index_scope_tripwire).

  • Scoped semantic searches can no longer be starved by a dominant section.

    Retrieval scope is pushed down into the vector-store legs (a dominant section can no longer starve scoped searches into empty results).

Added

  • items on the state widget's total items

    The source-record count the total is averaged over, i.e. its own divisor. TS-only and additive; wire contract WC-2026-08-03-state-widget-total-source-count. Shape and rationale.

  • A generic Stop wire for background jobs

    dd_utils_api::stop_process is registered (the copied client's Stop button always posted it but no handler existed), owner-gated and job-scoped; the abort reaches handlers as a per-job AbortSignal, and update_cache checks it per record and returns a partial summary.

    Wire contract: WC-043.

Wire contract — 50 entries
  • WC-035
  • WC-040
  • WC-041
  • WC-042
  • WC-043
  • WC-044
  • WC-045
  • WC-046
  • WC-047
  • WC-048
  • WC-049
  • WC-050
  • WC-051
  • WC-052
  • WC-053
  • WC-054
  • WC-055
  • WC-056
  • WC-057
  • WC-058
  • WC-059
  • WC-060
  • WC-061
  • WC-062
  • WC-063
  • WC-064
  • WC-065
  • WC-066
  • WC-067
  • WC-068
  • WC-069
  • WC-071
  • WC-072
  • WC-073
  • WC-074
  • WC-075
  • WC-076
  • WC-077
  • WC-078
  • WC-079
  • WC-080
  • WC-081
  • WC-2026-08-02-index-tag-legibility
  • WC-2026-08-02-observer-cascade-bounded-flag
  • WC-2026-08-02-observer-references-limit-not-honoured
  • WC-2026-08-02-observer-relay-writes-nothing
  • WC-2026-08-02-observer-subscription-registry-activation
  • WC-2026-08-02-register-tools-registry-state
  • WC-2026-08-02-tool-string-lang-fallback
  • WC-2026-08-03-state-widget-total-source-count

7.0.0-beta.2 — 2026-07-18

274 commits: git log --no-merges v7.0.0-beta.2

For users

Added

  • Login forgot password flow is live

    The recovery actions request_password_reset / confirm_password_reset are implemented natively (src/core/security/password_reset.ts + a nodemailer SMTP mailer), with pending codes in the session store. request always returns {result:true, reset_id} (anti-enumeration); a successful reset evicts the user's existing sessions. TLS peer verification is never disableable (pin a private CA via NODE_EXTRA_CA_CERTS). Config: catalog domain mailer (DEDALO_SMTP_*, DEDALO_PWRESET_*). User guide: Password recovery.

    Wire contract: WC-039.

Fixed

  • Special-table component search restored

    component_json gained a search builder (Activity's Data dd551 and every JSON component were previously unsearchable), and the Time Machine table (dd15), which stores each component in a flat physical column, gained a component conformer so its clauses are honored instead of returning all rows. Text matching on these paths is accent- and case-insensitive (a safe superset that never hides a match).

    Wire contract: WC-037.

  • Time-machine date search now filters

    The When (dd547) search in Activity (dd542) sent a structured object that the builder had been stringifying to "[object Object]" and dropping, so every date search ran unfiltered; object-q is now normalized. Directional operators are implemented for the special-table date path: each typed value defines a precision-sized half-open period and the operator picks the boundary (>2026 → strictly after 2026), where the frozen engine left them all falling through to a range equality.

    Wire contract: WC-036.

For administrators

Changed

  • IP→country resolution moved server-side and offline

    The ip_api descriptor is removed from page_globals; the client no longer has each browser fetch a third-party geolocation service. Resolution is now the native GeoIP subsystem (src/core/geoip/, DB-IP Country Lite) behind the same-origin get_ip_country action. Config keys: DEDALO_GEOIP_* (the old IP_API key is dropped).

    Wire contract: WC-038.

Added

  • Staged cutover levers

    DEDALO_DIFFUSION_NATIVE (flips the byte-identical client from the old engine to the main API) and DEDALO_DIFFUSION_NATIVE_ELEMENTS (per-element routing); native in-process MariaDB delete propagation with the old engine socket kept as transition fallback.

  • Native diffusion engine

    The publication pipeline (publish, not just delete/status) now runs natively in the TS work server: dd1190 → PublicationPlan compiler, streaming resolver (recursive ddo-chain walk, publication gate, linked-record frontier), the 33-fn parser registry (23 runtime + 10 compile-absorbed), 5-rung language projection, format writers for SQL/Socrata/CSV/JSON/Markdown/RDF/XML, a durable Postgres job queue (dedalo_ts_diffusion_jobs) with spawned runner processes, checkpointed crash-resume (byte-equivalent), and the complete dd_diffusion_api client action set — the copied tool_diffusion client works with zero edits. 228 tests across 16 suites; oracle spot-check against old-engine-published rows. Documentation: diffusion/native_engine.md; spec engineering/DIFFUSION_SPEC.md.

For developers

Changed

  • Label catalog cleanup

    The master key census went 686 → 413: 28 renames to English keys, 240 proven-unused removals, and 21 single-tool keys migrated into their tools' own register.json labels (edited with tool_dd_label). The get_label wire shape is unchanged; only the key set.

    Wire contract: WC-034.

  • Program strings are now repo-owned label catalogs

    The application's buttons, menus, dialogs and error text are served by committed files (src/core/labels/master.json = the complete key set with source strings; src/core/labels/catalog/lg-<code>.json = per-language translations) merged into the get_label dictionary by src/core/labels/catalog.ts. Labels ride code deploys, not ontology updates: a key ships in the same commit as the code that references it. The served dictionary always carries the full master key set (previously a lang file missing a key served undefined). The prior model — dd_ontology model='label' (dd383) rows rebuilt into generated JS lang files — is retired: those rows are inert and the generated lang files are deleted. New invariant gate: labels_tripwire. See Internationalization → Program strings.

    Wire contract: WC-033.

  • The diffusion documentation describes only the native engine.

    The legacy pre-rewrite + external-engine diffusion documentation was removed: the TS server is a new version built from scratch, and its docs describe only the native engine. Deleted pages: diffusion/dd_diffusion_api_and_bun.md, diffusion/engine_internals.md, diffusion/diffusion_config_properties.md, diffusion/diffusion_multiple_databases.md, api/diffusion_api_documentation.md and the whole api/diffusion/ directory (README.md, architecture.md, data_model.md, endpoints.md).

    core/system/diffusion.md rewritten as the lean conceptual overview of native diffusion; diffusion/diffusion_markdown.md rewritten for the native Markdown writer; diffusion/diffusion_data_flow.md cleansed of old-architecture wording and re-anchored to diffusion/native_engine.md, now titled The diffusion engine — the single technical reference.

Wire contract — 39 entries
  • WC-001
  • WC-002
  • WC-003
  • WC-004
  • WC-005
  • WC-006
  • WC-007
  • WC-008
  • WC-009
  • WC-010
  • WC-011
  • WC-012
  • WC-013
  • WC-014
  • WC-015
  • WC-016
  • WC-017
  • WC-018
  • WC-019
  • WC-020
  • WC-021
  • WC-022
  • WC-023
  • WC-024
  • WC-025
  • WC-026
  • WC-027
  • WC-028
  • WC-029
  • WC-030
  • WC-031
  • WC-032
  • WC-033
  • WC-034
  • WC-036
  • WC-037
  • WC-038
  • WC-039
  • WC-2026-07-09-installer-diagnostics-grid