New features
Connection cleanup is now a single on-event mechanism instead of two parallel ones. When a page closes on the client a lottery (
cleanup_threshold, default 5%) gates an attempt to spawn a background cleanup pass; the daemonclaim_cleanupprimitive ensures only one pass runs percleanup_interval_minutes(default 240). The pass walksdata/_connections/once, drops stale pages and connections from the in-memory register, andrmtrees their filesystem folders. The legacySiteRegister.cleanup()(which ran on every request and only expired the in-memory register, leaving folders behind) is removed. The legacy--noclean/--counterCLI flags and the init-time folder purge are removed too — folders are kept across restarts and cleaned by the on-event pass. (#874)Config keys (under
<cleanup>in siteconfig):cleanup.threshold— lottery percentage [0..100], default 5cleanup.interval_minutes— claim min gap, default 240cleanup.connection_max_age— default raised from 600s to 7200scleanup.page_max_age— unchangedcleanup.guest_connection_max_age— unchangedcleanup.interval(legacy 120s gate) — removed
ApiEngine — programmatic DB facade - A new application-level class,
gnr.app.api_engine.ApiEngine, provides a unified, JSON-safe interface for introspecting and querying a GenroPy database without going through the web layer:package_names()/table_names()— enumerate packages and tables.table_schema()— relation-tree introspection with configurable depth.table_columns()— flat per-column typing (dtype, python_type, openapi_type).openapi_schema()— synthesise an OpenAPI 3.0 schema for any table or package.run_query()— JSON-safe selection with paging, partitioning, counting and a cap-based truncation flag (probe-limit pattern avoids false positives).
All row values are coerced to JSON-safe types (
Decimal→ string, dates → ISO 8601,Bag→toJson(), etc.) before being returned.Bearer authentication — A new
adm.api_tokentable enables stateless Bearer token authentication for REST clients. Tokens are generated viasecrets.token_urlsafe(48)(384 bits of entropy) and stored as a SHA-256 hex digest; the plain-text value is shown exactly once on creation and never persisted.The admin form has been reworked with a lifecycle bar and status badge:
Status
formulaColumndisplayed as a coloured-dot cell in the view.Editable formlet on top; passive info strip with a token hint below.
Actions — Activate, Revoke, Reactivate — exposed as toolbar buttons with server-side guards; an
activated_tscolumn is set on first activation.
Multidb dashboard - A new superadmin dashboard at
/multidb/multidomain_dashboardenables pushing content from the root store to any configured secondary dbstore:Tables tab — pick any propagable table (
multidb='*', startup-data or whitelist); drag rows with copyRows or bulk-push with Propagate to all stores.User Objects tab — same UX, fixed on
adm.userobject, with extra filters by table, type and user.
Row colouring marks
toprop(primary only),localonly(secondary only) anddiff(present on both sides, mismatched).propagateToAllStoresusesraw_insert/raw_updateso derived unique columns (e.g.adm.userobject.identifier) are written byte-for-byte without recomputation.groupletGridimprovements * fillParent — new opt-infillParent=Truekwarg. The gridclaims 100 % of its dijit ancestor's height via plain CSS and scrolls internally. Works with
tabs,vtabsandcardslayouts.Themeable tab palette — the tab/vtab colour scheme is now exposed as
--gg-tab-*CSS custom properties on.grouplet_grid--tabs, so consumers can recolour chips and the active panel by overriding three or four variables on a scoped ancestor — no!important, no selector duplication. Defaults reproduce the historical chrome exactly.Loading dimmer —
_setLoading()toggles the canonical.dimmedclass on the body while a row template resolves via RPC.
ComboBox— validation pipeline andaddMissingValue* The Genro validation pipeline (validate_case,validate_regex,validate_call, …) is now wired intognr.widgets.ComboBox, which previously silently ignored all validators.New opt-in
addMissingValueflag: when a ComboBox is backed by a localstorepath, any value typed by the user (or arriving via a datapath binding) is consolidated into the underlyingGnrBagstore. Auto-added nodes are tagged__autoadded=Trueto allow selective purge. Particularly useful when several comboboxes share the same store.
Enhancements
Print resources now emit a baseline
body { font-family: sans-serif; }rule fromBagToHtml.defineStandardStyles(coveringTableScriptToHtml,TableTemplateToHtmland any otherBagToHtmlsubclass) and from theRecordToHtmlPage/RecordToHtmlhierarchy, so HTML/PDF prints default to a neutral sans-serif when nofont_familyis set and no bodyfont-familyis declared by the project. Settingfont_familyon the print class still overrides the baseline as before; project-level styles that declare their own bodyfont-familyare unaffected. Backward compatibility: prints that previously relied on the implicit serif fallback (Times) will now render in sans-serif by default. Projects that want the old serif rendering can declare it explicitly in their own styles. (#881)
Experimental features
PLEASE NOTE: these feature are experimental, YMMV. Use at your own risk.
Column-level encryption - Transparent encryption is now available for any
dtype='T'column by adding anencryptedkwarg to the column definition:encrypted='R'(Reversible) — Fernet AES-128-CBC; suitable for reusable secrets that must be read back in plain text.encrypted='Q'(Queryable) — AES-SIV deterministic encryption; suitable for searchable personal-data fields.whereTranslatorauto-encrypts the search term so existing queries work unchanged.encrypted='X'(one-shot hash) — SHA-256; suitable for issued tokens and other values that never need to be decrypted.
Security
SQL injection blacklist in ApiEngine (
feat/app):run_querynow rejects SQL fragments (columns,where,having,order_by,group_by) that match injection-style patterns — statement injection, DDL/DML keywords, privilege keywords, system catalogs, reconnaissance functions, encoding bypass tricks, sleep/DoS primitives and tautologies. Patterns are word-boundary anchored so legitimate identifiers such asupdated_atorselection_idpass through.sqlparams(bind parameters) are not subject to the blacklist.API token hashing:
adm.api_tokennow stores only the SHA-256 hex digest of the issued token. The plain-text value is returned to the caller exactly once and never written to the database.
Infrastructure
gnr_d20 JS directory removed: the
gnrjs/gnr_d20/tree — a full duplicate ofgnrjs/gnr_d11/kept for a paused future port — has been deleted. It had no runtime consumers but was inflating repository size and full-text search results. The configuration machinery ingnrdeploy.pyand the<gnr_20>nodes in the exampleenvironment.xmlfiles are intentionally preserved so the option to ship a second JS bundle remains open.ep_sourcerer rewritten on ApiEngine: the Sourcerer MCP endpoint now delegates introspection and query execution to
ApiEngine, removing ~62 lines of inline helpers that duplicated framework-level logic.``statement_timeout_ms`` removed from ApiEngine: the parameter was dropped because
gnrsqlhas no native primitive for per-query timeouts and aSET SESSIONworkaround does not survive across the transactional boundary. Per-query timeout support is tracked in issue #919.
Bugfixes
'gnr dev mkthresource' refression fix
groupletGrid — orphan form registrations (
fix/grouplet): tile widgets built from a detached template clone cached a falsythis.formand were never de-registered on teardown, breakingsetLastSavedValueson autosave._reproxyForm(on mount) and_unproxyForm(on teardown) now keep the form register clean.groupletGrid — stale tiles on record load (
fix/grouplet): switching to a record with a different grouplet structure could reuse tiles from the previous record. A full_clearBody()teardown is now performed on every record load and whole-Bag swap.dbstores — instanceconfig store resolution (
fix/dbstores): stores declared in<dbstores>insideinstanceconfigare loaded intoauxstores, butSqlTable.dbImplementationonly looked atdb.dbstores, raisingKeyErroron any query against such a package. Additionally,add_auxstorewas silently dropping theimplementationanddbbranchattributes, causing the resolved connection to fall back to the root database. Both issues are fixed.multidb — identifier collision on cross-store sync:
copyRowsandpropagateToAllStoresnow look up the target row by theidentifierbusiness key (when present) before falling back to pkey, avoidingUniqueViolationwhen the same logical record carries different pkeys across stores.rpc_query — structured kwargs decoding: JSON-string kwargs (
sqlparams,partition_kwargs) arriving over HTTP are now decoded before being forwarded toApiEngine.rpc_query — countOnly envelope: the
totalrowsvalue is now lifted intorowcountso callers read a plain integer regardless of query mode.ApiEngine — truncation flag false positive: switched to the probe-limit-plus-one pattern;
truncatedis only set when the result genuinely exceeds the cap.ApiEngine — unknown / invalid dtype columns: columns with an unrecognised dtype are now silently skipped rather than aborting introspection of the entire table.
ApiEngine — orphan FK on unloaded package:
AttributeErrorfromrelatedColumn()is caught and the FK entry silently omitted; the column itself is still described.PDF watermark service: fix
code=4: source object number out of rangewhenwatermarkedPDFis applied to PDFs saved with multiple incremental xref generations (e.g. macOS Preview annotations, PAdES signatures). The source PDF is now normalized withgarbage=4, clean=True, deflate=Truebefore the watermark is composed, following the official PyMuPDF workaround. No backward-compatibility impact: PDFs that already had a single xref generation are unaffected. (#872)dbSelect: the
notnullparameter is now honored client-side. It was silently overwritten byvalidate_notnullinDynamicBaseCombo.creating(), coupling the two flags and forcing callers who only wanted to suppress the empty option row to also make the field mandatory. With the fix,notnull=Trueon the widget removes the empty row without triggering required-field validation, as documented in the server-side handler. No backward-compatibility impact: when onlyvalidate_notnullis set the behavior is unchanged. (#883)
