inspector
The client-side side panel of the section edit view — a context-aware dashboard that shows record/component metadata, tools, projects, relations, time-machine history and save activity for the section currently being edited.
See also: Sections · Components · dd_object · Events
This page is the subsystem reference for the inspector. The inspector is a
pure client (JavaScript + LESS) subsystem — it has no server-side
counterpart. It lives in client/dedalo/core/inspector/ and is driven
entirely by the data its caller section already holds (context, data,
tools) plus a handful of small API calls. Everything it displays is
resolved server-side by the
section and tool
machinery; the inspector only renders and reacts.
Role
The inspector is the right-hand panel of the section edit view (mode==='edit').
It is created by the section instance and rendered into #inspector_container
by view_default_edit_section.js. Conceptually it is the section's "developer +
curator console": for the active record it surfaces identity (tipo, model,
matrix table, section_id), creation/modification/publication metadata, the
record's project assignment, its inbound relations, recent change history and a
live activity feed; for the currently focused component it swaps that record
view for component-level info (tipo, model, translatable flag, current value,
per-component history).
It is a plain constructor function (inspector) whose prototype is patched
with a few methods borrowed from the shared client common (render,
refresh) plus its own edit/init/build/destroy/get_raw_record*. It
sits next to these neighbouring client subsystems:
| subsystem | relationship |
|---|---|
section (client/dedalo/core/section/js/section.js) |
The inspector's caller. The section constructs the inspector in mode==='edit' with permissions, owns its lifecycle, and is the source of all context/data/tools/rqo the inspector reads. |
view_default_edit_section (client/dedalo/core/section/js/view_default_edit_section.js) |
Builds #inspector_container, renders the inspector wrapper into it, attaches the resize handle, applies the persisted rail/width state, and moves the section paginator into the inspector. |
Time Machine list (a dd15 section instance, sqo.mode 'tm') |
Instanced on demand for the section "Latest changes" list and the per-component history, 10 rows per page with the compact paginator (1-10 of 47). A tm read is a page view, never a single-record view: its one locator names the record (+ component) whose history is listed. Paging an embedded list never writes browser history or the address bar — only the page's own section does. A page turn refreshes the mounted rows in place (the block dims only if the turn is slow), exactly like the Relations card. |
| open_tool | Every tool button in the inspector calls open_tool(); the inspector only decides which tools to show (show_in_inspector) and renders their buttons. |
| relation_list | Instanced on demand to render the "Relations" block, paginated with the same compact strip (1-10 of 47) as the Time Machine panels; a page turn refreshes the mounted rows in place instead of rebuilding the block. |
No server counterpart
The inspector itself has no server-side object. The data it shows is
produced by ordinary section/component reads, the tools registry
(including the show_in_inspector flag — see below), and the Time
Machine read path. The client creates the inspector whenever a section
renders in mode==='edit' with sufficient permissions — there is no
separate server-side toggle for it.
Responsibilities
- Own the panel lifecycle —
init(wire events),build(no-op, kept for lifecycle parity),edit/render(build the DOM),destroy(unsubscribe). - Render the record dashboard — the paginator, the top button bar (search / new / duplicate / delete / open-relationships / graph / diffusion), the inspector tool buttons, selection info, element-info block, project block, relation list, time-machine list, component history, activity feed and the bottom "view record data" / register-download buttons.
- React to the active component — on the
activate_componentevent swap the element-info block from section info to component info, load that component's history, and (in developer mode) re-point an open ontology window. - React to saves — on the
saveevent refresh the selected component's history, refresh the section time-machine list, and push an activity bubble. - Manage panel chrome — rail collapse/expand, live drag-resize, and persist
both states (rail + width) in the IndexedDB
statustable as global keys. - Fetch the raw record — build a
read_rawRQO and request the full database record for the "view record data" link and thedd1340register download. - Resolve ontology documentation links — build docu / local-ontology / master-ontology URLs per tipo and open them in a recycled window.
- Worker/instance hygiene — keep its event tokens and on-demand service
instances so
destroycan tear them down cleanly.
Key concepts
The caller is the single source of truth
The inspector holds self.caller — the section instance. Almost everything
it renders is read straight off the caller, not re-fetched:
self.caller.context.tools— filtered byshow_in_inspectorto decide which tool buttons appear.self.caller.context.buttons— to detectbutton_new/button_delete.self.caller.context.config.relation_list_tipo— present ⇒ render the Relations block.self.caller.tools— to detecttool_diffusion(the dedicated diffusion button) and to open tools viaopen_tool.self.caller.section_tipo/section_id/model/label/mode/view/rqo— identity, paginator and theread_rawRQO.
The inspector issues its own API calls for three things: the raw-record
fetch (get_raw_record), the on-demand service_time_machine /
relation_list instances, and the record's created / modified / published
metadata (load_record_info, below).
Record info: created, modified, published
The section read does not carry the record's creation, modification and publication metadata — every read (lists included) would pay for a panel that shows one record. The inspector fetches it with its own request, sent after the section has rendered:
{ "dd_api": "dd_utils_api", "action": "get_record_info",
"options": { "section_tipo": "test3", "section_id": 1 } }
The answer carries the owned top-level key record_info (null when the record
does not exist):
{ "record_info": {
"created": { "date": "2017-11-19 17:41:43", "user_id": null, "user_name": null },
"modified": { "date": "2026-07-10 13:25:05", "user_id": -1, "user_name": "root" },
"publication": { "first": null, "last": null, "state": null } } }
- created / modified come off the record's audit components:
dd199(created date),dd200(created by),dd201(modified date),dd197(modified by). Whendd199/dd200are absent, the created pair falls back to the record header (data.created_date/data.created_by_user_id) — the case of thetest3record above. A user id is any non-zero integer — root is the user record-1. - publication comes off the publication ledger (
dd1758, tablematrix_activity_diffusion), not off the record:first/lastare the first and last publish rows,stateis the latest action of any kind —published,unpublished,pending(a withdrawal still owed to a public target) ornull(never in the ledger). - Dates are
YYYY-MM-DD HH:MM:SSin the installation's time zone; the panel showsDD-MM-YYYY HH:MM:SS. - The server answers only with the section read grant and the record in the user's project scope; each audit value also needs its read grant (the metadata components are granted to everyone who can search the section).
On the client, render_section_info renders the three rows and
load_record_info fills them in an idle callback. The result is cached per
record on the inspector (cleared on save and on every section render), so
leaving a field re-renders the panel from the cache with no request. To keep the
panel from jumping, a row waiting for its values keeps the height of the
previous render, or — on first paint — reserves two lines (date + user; one for
Published). Values that arrive after the panel is on screen fade in (200 ms,
none under reduced motion); values from the cache show at once. A response for a record the inspector has already left is
dropped. User names are written as text, never as markup.
Publication history retention
With DEDALO_DIFFUSION_LEDGER_RETENTION_DAYS above 0, settled ledger rows
are pruned, and first becomes the oldest publication still kept, not the
first ever. See the configuration reference.
Section view vs component view
The element-info block (the "Info" card) is dual-mode and shares one body
node, self.element_info_container:
- Section view —
render_section_info()fills it with the record's tipo, docu links, model, matrix table, section_id, view+mode, created, modified and published metadata. Shown when no component is active. - Component view —
render_component_info()fills it with the focused component's tipo, docu links, model, translatable flag, view+mode and a collapsible JSON dump of the component's currentdata.entries(with a copy button). It subscribes toupdate_value_<component.id_base>so the value re-renders live as the user edits, and unsubscribes the previous token each time the selection changes.
The switch is event-driven: activate_component → component view;
deactivate_component (debounced 250 ms, only if no component is active) and
render_<section.id> → section view.
Lazy, collapsible blocks
Each card is a header + body wired through ui.collapse_toggle_track, with the
collapsed/expanded state persisted under a stable collapsed_id
(inspector_main_block, inspector_element_info_block, inspector_project_block,
inspector_relation_list, inspector_time_machine_list,
inspector_component_history_block, inspector_activity_info,
inspector_component_value). The heavy blocks (relation list, time-machine
list, component history) default to closed and only fetch their data in the
expose callback (lazy load), guarding every async load with a
"still open / not destroyed" check.
Persisted panel state (rail + width)
Two global IndexedDB status keys persist across sections and reloads:
| key | meaning | default |
|---|---|---|
inspector_rail_state |
true ⇒ collapsed to the icon rail |
not collapsed |
inspector_width |
user-chosen panel width (e.g. '24rem') |
'19rem' (DEFAULT_WIDTH) |
Width is applied by writing the CSS custom property --inspector_width on
:root; the section content and the panel position are calc() consumers of
that var, so the layout reflows automatically. Rail mode and resize are inert
below the narrow breakpoint (max-width: 1024px), where the panel is inline and
full-width.
Files & structure
client/dedalo/core/inspector/
├── js/
│ ├── inspector.js # the instance: lifecycle, events, RQO helpers, ontology URLs
│ └── render_inspector.js # the DOM: edit() builder + every block renderer/loader
└── css/
├── inspector.less # panel + card styling (included from main.less)
└── inspector.css.map
inspector.jsis the entry: it exports theinspectorconstructor and patches its prototype. It ownsinit(event wiring),build,destroy,get_raw_record/get_raw_record_rqo, the module-levelupdate_section_info()helper, and the exportedget_ontology_url().inspector.prototype.editis borrowed fromrender_inspector.prototype.edit.render_inspector.jsis the view layer:edit()assembles the wrapper andget_content_data()builds every card. It also exports the chrome helpers (toggle_inspector_rail,apply_inspector_state,init_inspector_resize) and the per-block render/load functions consumed byinspector.js's event handlers.
DOM shape
#inspector_container (built by view_default_edit_section.js)
├── .inspector_resize_handle (left-edge drag handle)
└── #inspector .inspector (the wrapper returned by edit())
├── .label .icon_arrow (collapsible header + rail toggle)
└── .content_data.inspector_content_data
├── .paginator_container (section paginator moved in here + .section_id)
├── .buttons_container.top (search / new / duplicate / delete / open-rel / graph / diffusion)
├── .tools_container.top (inspector tool buttons, when >1)
├── .selection_info (current element label + List button)
├── .element_info_wrap (the dual-mode "Info" card → .element_info)
├── .project_wrap (component_filter, when relation_list config present)
├── .relation_list_container (when context.config.relation_list_tipo)
├── .time_machine_list ("Latest changes", section-level)
├── .component_history (per active component)
├── .activity_info (save/error bubbles)
└── .buttons_container.bottom (view record data + dd1340 register download)
Instantiation & lifecycle
The inspector is not built through the get_instance() factory. The section
instantiates it directly with new inspector() when it renders in edit mode and
the user has permissions (client/dedalo/core/section/js/section.js):
// in section build, mode 'edit' only
if (self.inspector==null && self.mode==='edit' && self.permissions) {
const current_inspector = new inspector()
current_inspector.init({
section_tipo : self.section_tipo,
section_id : self.section_id,
caller : self // the section instance
})
self.inspector = current_inspector
}
init(options) sets self.caller (the section), self.id =
'inspector_' + section_tipo, self.mode = 'edit', the placeholder node
pointers (paginator_container, element_info_container,
component_history_container, …) and subscribes the event handlers (below). It
has a double-init guard (is_init) and tracks self.status
(initializing → initialized → building → built → destroyed).
Then the view builds and renders it (client/dedalo/core/section/js/view_default_edit_section.js):
const inspector_container = ui.create_dom_element({ id:'inspector_container', /* … */ })
await self.inspector.build() // no-op, lifecycle parity
const inspector_wrapper = await self.inspector.render() // → edit()
inspector_container.appendChild(inspector_wrapper)
apply_inspector_state(self.inspector, inspector_container) // rail/width before paint (no flicker)
// + resize handle and the section paginator moved into inspector.paginator_container
Setting inspector: false suppresses the panel
The section accepts an inspector option. A section built with
inspector: false (e.g. notes/modal cases) skips the panel entirely; the
view then adds the no_inspector class to the section wrapper. When the
option is null/undefined the section creates the inspector itself under the
edit-mode + permissions guard above.
Events the inspector subscribes to (in init)
| event | handler effect |
|---|---|
render_<caller.id> |
Clear the record-info cache, then update_section_info() — reset active component, re-render section info, load the record info, reload the time-machine list, clear component history, sync the paginator section_id. |
activate_component |
Store self.actived_component, update selection label, render_component_info(), load that component's history; in developer mode re-point an open ontology window. |
save |
Clear the record-info cache (the Modified stamp moved). If the saved instance is the active component, reload its history; always reload the section time-machine list and push an activity_info bubble. |
deactivate_component |
Ignored for a component of another section (a modal, e.g. the Time Machine note editor in rsc832), so closing a note keeps the history it was opened from. Otherwise, after a 250 ms debounce, if no component is active, update_section_info() (back to the section view). |
render_component_filter_<section_tipo> |
Capture the rendered component_filter node (published by render_edit_section_record) and refresh the project block body. |
Teardown
destroy(delete_self=true, delete_dependencies=false, remove_dom=false)
overrides common.destroy: it unsubscribes the live
update_value_* token stored on element_info_container, then delegates to
common.prototype.destroy. (Event tokens collected in self.events_tokens and
the on-demand service_time_machine instance are cleaned up through the common
destroy / service destroy paths.)
Key methods
Grouped by concern. The inspector spans two files; file marks where each lives.
Functions exported from render_inspector.js are consumed by inspector.js's
event handlers and by the section view.
Lifecycle & instance (inspector.js)
| method | static? | purpose |
|---|---|---|
inspector.prototype.init(options) |
Wire identity, node pointers and all event subscriptions; double-init guarded. | |
inspector.prototype.build() |
No-op kept for lifecycle parity (status → built). |
|
inspector.prototype.edit(options) |
(borrowed from render_inspector) Build the wrapper: label + rail toggle + content_data. render_level:'content' returns just the content node. |
|
inspector.prototype.render / refresh |
Borrowed from the client common prototype. |
|
inspector.prototype.destroy(delete_self, delete_dependencies, remove_dom) |
Unsubscribe the live value-update token, then call common.destroy. |
Raw record & RQO (inspector.js)
| method | static? | purpose |
|---|---|---|
inspector.prototype.get_raw_record() |
Build the RQO, call data_manager.request, return the first result (or false). |
|
inspector.prototype.get_raw_record_rqo() |
Build a read_raw RQO filtered by the caller's section_tipo/section_id (limit 1, filter_by_locators). Used by both the "view record data" link and the dd1340 register download. |
|
get_ontology_url(tipo, target) |
export | Resolve a documentation/ontology URL for a tipo. target ∈ docu_link | local_ontology | local_ontology_search | master_ontology. |
Panel chrome (render_inspector.js)
| function | export? | purpose |
|---|---|---|
toggle_inspector_rail(self) |
export | Collapse to / expand from the icon rail, reflow via --inspector_width, persist inspector_rail_state. Inert on narrow viewports. |
apply_inspector_state(inspector, container) |
export | Apply the persisted rail/width to the fresh container before attach (no flicker). |
init_inspector_resize(handle, inspector) |
export | Wire the left-edge drag handle: rAF-coalesced live resize, clamped (14rem … min(60vw, 40rem)), persist inspector_width on release. Disabled while railed/narrow. |
decorate_block_header(header, icon_class, label_html) |
Add the leading mask icon + text-label wrapper to a card header (icons: info|gear|link|history|note|activity|panel). |
DOM builders (render_inspector.js)
| function | export? | purpose |
|---|---|---|
edit(options) |
export (prototype) | Top-level builder: label, rail toggle, collapse tracking, content_data. |
get_content_data(self) |
Build the whole content: paginator, top button bar, inspector tool buttons, and append every card block. | |
render_selection_info(self) |
The orange "current element" label card (+ List nav button via update_label). |
|
render_element_info(self) |
The collapsible "Info" card shell; sets self.element_info_container. |
|
render_section_info(self) |
export | Fill Info with section metadata (tipo, model, matrix_table, docu links) and the three record-info rows — filled at once from the cache, else reserved for load_record_info. |
render_component_info(self, component) |
export | Fill Info with the active component (tipo, model, translatable, live JSON value + copy); subscribes update_value_<id_base>. |
render_project_block(self) |
The "Project" card wrapping the section's component_filter node. |
|
update_project_container_body(self) |
export | Swap the project card body to the current filter node. |
render_relation_list(self) |
The "Relations" card (lazy); built only when context.config.relation_list_tipo exists. |
|
render_time_machine_list(self) |
export | The section-level "Latest changes" card shell (lazy, default closed). |
render_component_history(self) |
The per-component "Component history" card shell (lazy, default closed). | |
render_activity_info(self) |
The "Activity" card shell (save/error bubbles). | |
render_docu_links(self, tipo) |
Build the docu-link buttons; the developer-only local/tree/master ontology buttons appear when SHOW_DEVELOPER===true. |
On-demand data loaders (render_inspector.js)
| function | export? | purpose |
|---|---|---|
load_time_machine_list(self) |
export | Serializing entry point. Chains each load onto the previous one through self.tm_list_loading, then delegates to load_time_machine_list_run. |
load_time_machine_list_run(self) |
The actual load: instance a dd15 section (mode list, tm_view mini) for the current record and render it into the Latest-changes body; skips when collapsed/destroyed, destroys the previous tm_list. |
|
load_component_history(self, component) |
export | Instance a service_time_machine (view history) for the active component + its notes (rsc329/rsc832) and render it; clears when component is null. |
load_record_info(self) |
export | Fetch dd_utils_api::get_record_info for the current record (edit mode, saved record only), cache it, fill the rows; drops a stale response. |
fill_record_info_nodes(nodes, record_info) |
export | Write a record_info payload into the created / modified / published rows (text only) and release their reserved height. |
format_record_info_date(date) |
export | YYYY-MM-DD HH:MM:SS → DD-MM-YYYY HH:MM:SS. |
load_activity_info(self, options) |
export | Render one notification bubble (render_node_info) and prepend it to the activity body. |
open_ontology_window(self, url, docu_type, focus=false) |
export | Open/recycle the shared window.docu_window at the given ontology URL. |
load_time_machine_list must stay serialized
Three independent triggers call it — the block's expose_callback, the save
handler, and update_section_info — and they can fire in the same frame.
Run in parallel, the second call's self.tm_list.destroy(true, true) nullifies
caller on every section_record the first call is still rendering (see
common.js do_delete_self), and the row render throws
Cannot read properties of null (reading 'model'), leaving the panel stuck on
loading. Never call load_time_machine_list_run directly, and never move the
destroy up into the entry point.
Gate: test/unit/client_tm_list_destroy_race.test.ts.
Verified surface only
The tables above list the real functions present in client/dedalo/core/inspector/js/.
Module-internal helpers (set_inspector_width, is_narrow_viewport,
update_section_info) are not exported and are listed only where they shape
behaviour. No method names here are invented.
How it fits with the rest of Dédalo
The inspector is a thin client renderer over data other subsystems resolve. The load-bearing contracts:
show_in_inspector — how tools opt into the panel
The inspector's tool buttons are not hardcoded. get_content_data() filters the
caller's tool contexts:
const inspector_tools = self.caller.context.tools.filter( el => el.show_in_inspector )
That show_in_inspector flag is a server-resolved property of every tool's
context: a plain boolean field on the resolved tool context
(src/core/tools/types.ts), read off the tool's registration
(SHOW_IN_INSPECTOR in src/core/tools/ontology_map.ts, resolved by
src/core/tools/registry.ts / register.ts). A tool authored with
show_in_inspector: true in its register.json therefore appears as an
inspector button with no client change.
The companion flag show_in_component governs whether the same tool renders in a
component's own toolbar instead. See the tool context example in
Components → Datum/context and the
tools subsystem.
Two diffusion paths
The dedicated Diffusion top button is rendered only when the caller has a
tool_diffusion in self.caller.tools; other tools reach the panel through
the generic show_in_inspector filter. Both ultimately call
open_tool({ tool_context, caller }) from tool_common.
Time Machine
The "Latest changes" and "Component history" cards are rendered by instancing
service_time_machine (model service_time_machine) on demand and calling its
build() / render(). The inspector never queries dd15 history directly. The
component-history loader also pairs the selected component (mode tm,
fixed_mode) with the annotation note component (rsc329 in section rsc832),
so each historical change can carry a note.
Relations & projects
- The Relations card builds a
relation_listinstance keyed bycontext.config.relation_list_tipo(set server-side incommonwhen the section declares a relation-list trigger). It renders the inbound "who references me" view — see component_relation_index and relation_list. Its paginator is apaginatorinstance inminimode, so it reads exactly like the "Latest changes" strip. - The Project card surfaces the section's component_filter
node, captured from the
render_component_filter_<section_tipo>event, letting the curator change the record's project assignment from the panel.
Open relationships
The Open relationships button in the top bar answers "which records do these records point at?" and opens the answer in a new window.
- The dialog asks for the scope: the Current record, or All found records of the list (the whole found set, not only the page on screen).
- It then lists the sections those records point at, each with its number of records. The numbers are exact: they are what the opened window will show, with your permissions already applied. Sections you cannot read, and sections with nothing left to show, are not listed. When nothing is related, the dialog says so (No related sections available) instead of opening an empty window. Changing the scope refreshes the list.
- Open opens a window on the chosen section, filtered to the related records. The window is an ordinary list: it counts, paginates and searches normally, however many records are related, and it keeps the filter on every page.
Opening relationships again from that window follows the chain one step further ("relationships of relationships"), up to four steps.
A portal's List from component data button does the same for one field: it counts only the records that field points at, and opens one window per target section.
Under the hood the dialog calls the relation_targets action and the window's
search carries a referenced_by filter leaf built from the same source set,
so the counts and the windows cannot disagree. See
SQO — relationship filter leaves
and rqo — relation_targets.
The related records are read from what the found records store, not from the
relation index, so the answer does not depend on that index being up to date.
Locators with a non-integer id (an external record's remote id) are not
followed. A very large found set can take long to count: after 30 seconds the
server stops and the dialog shows a timeout error instead of the list.
Section coupling
The section orchestrates the client inspector's whole lifecycle: it
constructs the inspector whenever it renders in mode==='edit' with
sufficient permissions, feeds it context/data/tools, and the section
view moves the paginator into inspector.paginator_container. There is no
separate server-side flag gating it — the mode + permission check is the
whole gate. See section for the section instance
the inspector treats as its source of truth, and dd_object
for the context/tools/show_in_inspector shape it consumes.
Examples
A tool opting into the inspector (server registration)
A tool whose context arrives with show_in_inspector: true (resolved by
tool_common) becomes an inspector button automatically. The context the client
filters on looks like:
{
"name" : "tool_propagate_component_data",
"model" : "tool_propagate_component_data",
"label" : "Propagates component data",
"section_tipo" : "dd1324",
"mode" : "edit",
"icon" : "/dedalo/tools/tool_propagate_component_data/img/icon.svg",
"show_in_inspector" : true,
"show_in_component" : true,
"type" : "tool"
}
The inspector then renders its button and, on mousedown, opens it:
// render_inspector.js → get_content_data()
const inspector_tools = self.caller.context.tools.filter( el => el.show_in_inspector )
// for each tool_context: build a .light.blank button and …
open_tool({
tool_context : tool_context,
caller : self.caller // the section instance
})
Reading the raw record behind the panel
The bottom "view record data" button and the read_raw RQO the inspector builds:
// inspector.prototype.get_raw_record_rqo()
const rqo = {
action : 'read_raw',
options : {
type : 'section',
section_tipo : self.caller.section_tipo,
tipo : self.caller.section_tipo,
model : self.caller.model
},
sqo : {
section_tipo : [ self.caller.section_tipo ],
limit : 1,
filter_by_locators : [{
section_tipo : self.caller.section_tipo,
section_id : self.caller.section_id
}]
},
pretty_print : true
}
Toggling the rail and persisting width
// collapse/expand to the icon rail; reflows the layout via --inspector_width
toggle_inspector_rail(self) // persists { id:'inspector_rail_state', value:true } in the 'status' table
Related
- section — the caller; owns the inspector's lifecycle
and supplies its
context/data/tools/ paginator. - Sections concept — the record model the panel reflects.
- Components — components, the
context.toolstoolbar, and theshow_in_inspector/show_in_componentflags. - dd_object (ddo) — the context object carrying
show_in_inspector,tools,config.relation_list_tipo. - component_filter — the node shown in the Project card.
- component_relation_index — the inbound relations surfaced by the Relations card.
- Events — the
activate_component/deactivate_component/save/render_<id>events the inspector subscribes to.