All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Every app OpenBuild has ever generated was born declaring the wrong licence.
The embedded template snapshot's
appinfo/info.xmlhardcoded<licence>agpl</licence>while the very same file's description read "Free and open source under the EUPL-1.2 license". It now reads<licence>{{license}}</licence>, so thelicensevalue the export already carried end to end (ExportJobService→RunExportJob→PlaceholderResolver, defaulting toEUPL-1.2at all three layers) finally reaches the file that declares it, and a caller who picks a different licence gets the licence they picked. Verified against the Nextcloud appstore schema (https://apps.nextcloud.com/schema/apps/info.xsd): thelicenceenumeration does includeEUPL-1.2. It does not includeeupl.
ExporterEndToEndTest::testGeneratedAppDeclaresTheRequestedLicence()— asserts a really-exported app'sappinfo/info.xmldeclaresEUPL-1.2and does not declareagpl, and thatsrc/manifest.jsonandcomposer.jsonagree. Shown to fail against the pre-fix snapshot before it was made to pass. Nothing in the suite covered the generated app's licence declaration until now: the existing unresolved-placeholder assertion matches/\{\{[a-zA-Z]+\}\}/, and a hardcoded wrong value contains no placeholder.
lib/Resources/template/.snapshot-meta.jsonanddocs/releasing.mdnow record that the embedded template is a fork, not a snapshot, and that the documentedrsync -a --deleterefresh is unsafe to run as written — it would revert OpenBuild-only fixes (including "the generated app could not be built at all", #39) and swap OpenBuild's{{token}}placeholder dialect for upstream's{APP_NAME}dialect, whichPlaceholderResolverdoes not resolve and no test would catch.docs/releasing.mdalso now records that the "CI drift check" it describes does not exist.lib/Resources/template/.path-manifest.txtno longer lists.snapshot-meta.json; the regeneration command indocs/releasing.mdexcludes it, so the checked-in manifest disagreed with its own generator.
- Theme picker now consumes nldesign's published catalogue
(theme-picker-consumes-nldesign) — bumps
@conduction/nextcloud-vueto^1.0.0-beta.221, which shipsuseScopedTheme()and wiresCnAppRootto self-applymanifest.runtime.theme.ThemePickerDialog.vuecollapses its old three-tier admin/probe/free-text catalogue fallback to a singleuseScopedTheme().listTokenSets()call against nldesign's real non-adminGET /api/token-setsendpoint, and adds a warn-only WCAG contrast preview viaevaluateContrast()that never blocks Save. Live theme preview now retargets the page-designer's sandboxed live-preview-paneCnAppRootinstance instead of a separate OpenBuild-owned applier.
src/composables/useAppTheme.js— OpenBuild's own scoped-CSS:root-rewriter and injector;CnAppRoot's ownuseScopedThemewatcher now owns runtime theme application end-to-end, with zero OpenBuild-side wiring inBuilderHost.vueorPageDesignerHost.vue.src/services/manifestValidation/theme.js— OpenBuild's ownruntime.themeshape validator;@conduction/nextcloud-vue'svalidateManifest()(schema 2.21.0,$defs/runtimeTheme) is now the single source for this validation.
-
Runtime group-scoped access (runtime-group-scoped-access) — a manifest
menu[]/pages[]entry may declare apermission: "group:<gid>"; the runtime resolves the caller's Nextcloud group context server-side andManifestResolverService::filterManifestForCaller()strips any entry the caller does not hold the permission for from the manifest response BEFORE it leaves the server — the authoritative gate, not client-side hiding. Admins and callers with an owner/editor role on the Application see the manifest unfiltered. A group-scoped dashboard page is promoted to the landing position for members who satisfy it, falling back to the default dashboard otherwise.PermissionGroupField.vueadds a group picker to the menu-item and page editors. Client-sideCnAppNavfiltering mirrors the server decision as defense in depth, not the only defense. Documented boundary: this hides navigation only — object-level access for the underlying data remains OpenRegister schemaauthorization's job. -
Agent workspace (agent-workspace) — named, tool-scoped AI agents layered on the existing
ai-copilotplan/execute engine (ADR-022 consume-not-rebuild): anAgent(instructions, an explicit subset of the eightOpenBuildToolProvidertools,maxActionsPerRun) is never a wider capability surface than the bare copilot — enforced server-side as a narrowed intersection of the existing eight-tool catalogue on every plan/execute request, never trusted from the client. -
Transparent per-run log (
AgentRun): every plan+execute/discard turn persists the prompt, plan, every tool call's arguments and result, and the outcome (applied/rolled-back/discarded/plan-rejected) — the Retool tool-chip transparency pattern, addressing the market-wide "trust gap" evidence directly. -
AgentsPage.vue(CRUD list),AgentEditDialog.vue, and a run-history view (AgentRunHistory.vue) restricted server-side to owners/editors of the agent's parent Application;CopilotPanel.vuegains optionalagentId/name/instructions/enabledToolsprops, fully backwards-compatible with the existing bare-copilot surfaces. -
No autonomous/automation-triggered agent runs in v1 — an agent acts only inside a human-initiated chat turn.
-
Component blocks (component-blocks) — capture a configured widget, or a selected multi-widget page section, from the page designer into a named, reusable
ComponentBlock(newcomponentBlockOR schema,lib/Settings/register.d/60-component-blocks.json). -
Block-library panel (
NcAppSidebar) in the page designer listing every org-wide block, filterable by category, with insert support. -
Insert deep-copies the fragment and mints fresh widget ids, so repeated insertions never collide and editing the source block never affects an already-inserted copy.
-
Schema-dependency remap prompt (
BlockRemapDialog.vue) on a cross-app insert whose schemas don't exact-match — never a silent guess, never a silently dropped binding. -
Blocks export/import as standalone JSON.
-
Template-catalogue gallery gains a "Blocks" filter alongside full-app templates.
- Document-generation automation action (automation-document-action) — a
new
generateDocumentaction kind onobject-created/object-updated/object-deleted/lifecycle-transitiontriggers, compiling to no compile-time artifact (Docudesk'scorrespondence/generateroute is stateless) and dispatching at trigger-fire time through a newDocumentGenerationListener→DocumentGenerationService. DocumentGenerationServicecalls Docudesk's existing, Newman-pinnedPOST /apps/docudesk/api/correspondence/generateroute — never aOCA\DocuDesk\*PHP class import — impersonating the owning Application's owner (via the existingJobOwnerImpersonator) for the duration of exactly one internal call, authenticated with a short-lived Nextcloud login token minted throughOC\Authentication\Token\IProviderand invalidated immediately after use.- Three output modes:
attach(writes the generated document to Nextcloud Files and sets a{ "ref": "<fileId>" }reference on the triggering object'sgeneratedDocumentfield),download-link(a short-lived, ~24h signed URL served by the newGeneratedDocumentControllerfrom OpenBuild's own app-private storage — never the user's Files tree), andnotify(reuses the existingRuleActionDispatchersend-notification path; must be paired withattachand/ordownload-link). AutomationEditDialoggains thegenerateDocumentaction editor: a Docudesk template picker (via the new shareduseDocudeskTemplates.jscomposable, also adopted byDocumentTemplateAttachmentDialogso the template-list fetch has exactly one implementation) and an output-mode multi-select, disabled with a missing-app hint when Docudesk is absent.- Compile-time validation (
AutomationCompilerService):templateIdrequired,outputa known non-empty set,notifynever alone, and a fail-closedUnsupportedAutomationCombinationExceptionnaming the missingdocudeskdependency when Docudesk is not installed.
- Approval automation action (automation-approval-steps) — a new
approvalaction kind onobject-created/object-updated/object-deleted/lifecycle-transitiontriggers, group-only assignee, compiling to an OpenRegisterApprovalChaininstantiated against the trigger object (consume-not-rebuild, ADR-022 — no new approval engine in OpenBuild). - On-approve/on-reject follow-up actions, composed from the same typed-action
vocabulary (send-notification/object-op/webhook), dispatched by a typed
listener on OR's
ApprovalStepApprovedEvent/ApprovalStepRejectedEvent. - "My approvals" runtime widget — lists the viewer's pending approval
steps (filtered client-side by NC group membership) with approve/reject
actions calling OpenRegister's
/api/approval-stepsendpoints directly. AutomationsController::status()and the dry-run test panel now reportapprovalState: none|pending|approved|rejectedfor automations carrying anapprovalaction.
- Version lifecycle + switcher (version-lifecycle-and-switcher): draft versions, release-to-production, and a version switcher UI.
- New draft action — clones the production manifest and SHARES production's data register (manifest-only versioning; the create endpoint inherits the production register when none is supplied).
- Release action (owner-only, no admin bypass) — set-as-production + publish
- demote the previous production, enforcing exactly one production version via
the single-valued
productionVersionpointer (a draft previous-production is demoted by the pointer move alone).
- demote the previous production, enforcing exactly one production version via
the single-valued
- Open-app split button — primary opens production; a chevron lists versions to view/use and edit (production marked, archived hidden).
- Click-to-open a version (
?_version=) and per-row Edit (designer) in the version history; production/active markers; EN + NL i18n.
- Version history list was always empty — it queried a non-working OpenRegister
objects endpoint and filtered on a non-existent
applicationUuidfield; it now uses/api/applications/{slug}/versionswith the real fields. - App-detail Register widget (and KPI register links) showed a phantom
openbuild-{slug}-{versionSlug}register for shared-register versions; they now use the active version's realregisterfield.
- Delete guard: never drop an OpenRegister register that is shared with the
production version (a
delete-nowon a production-shared draft is downgraded to keep-register so production data is never destroyed).
- Remote template store (openbuild-remote-template-store): search + install
virtual-app templates from a remote OpenRegister-backed catalogue. Admin
registry config (URL/register/token, token write-only), a server-side
SSRF-guarded proxy (
RemoteTemplateStoreService),StoreControllersearch/install endpoints, and a store-aware Templates gallery (store primary when a registry is configured, built-in templates fallback otherwise). Install clones via the sharedinstallFromTemplateArrayseam. Consume-only this cut. - DocuDesk-style dashboard: a self-contained
DashboardIndexview (oneCnDashboardPage) with a 4-KPI row (Apps / Hybrid apps / Templates / Published versions), a Recent apps table, and a Quick start panel.
SeedApplicationTemplates+PopulateApplicationPermissionsrepair steps now write in system context (OR RBAC/multitenancy bypassed) so they no longer fail as the Anonymous user — the Templates KPI count is now accurate.- Dashboard Templates KPI queried the wrong schema slug (
applicationTemplate→application-template).
- Unified app model (unify-apps-with-app-type): every app now carries an
appTypediscriminator (virtual|hybrid). Hybrid apps — customizations layered over an installed Nextcloud fleet app — are first-classApplicationrecords with a delta-onlyApplicationVersion, replacing the standaloneAppOverrideschema. appType+baseRefon theApplicationschema andmanifestDelta+baseRefonApplicationVersion.- Virtual/Hybrid badge on app cards + the app detail header; an all/virtual/hybrid
filter on the Apps list persisted in the
?filter=URL query param. - App-creation wizard gains a Virtual/Hybrid branch (hybrid = pick an installed app).
- Idempotent migration converting existing
AppOverriderows into hybrid Applications (system-context writes; schema dropped only when every row migrates successfully).
- "Virtual apps" renamed to "Apps" across the UI (menu, titles, copy); route paths unchanged so deep-links survive.
GET/PUT/DELETE /api/app-overrides/{appId}are now compatibility shims backed by the hybrid Application's version (HTTP contract preserved).
- The standalone
AppOverrideschema (folded into the unified hybrid-app model).
- Hybrid metadata-lock: a hybrid app's
slug/nameare read-only (mirror the installed app), enforced by a pre-save guard (openbuild.hybrid_metadata.locked).
- Exporter GitHub delivery target:
GitHubPushServicenow performs a real create-repo → blob/tree/commit → bootstrap-branch → pull-request sequence against the GitHub REST + Git Data API via Nextcloud'sIClientService(replacing the Phase-1 stub). Fails fast when the target repo already exists (REQ-OBEX-007), scrubs PAT-shaped tokens out of error messages, and keeps the PAT method-scoped (never stored on the instance, never logged). - Exporter end-to-end integration test (
tests/Integration/ExporterEndToEndTest.php) asserting the resolved tree carries no unresolved{{placeholder}}tokens, noopenbuilddependency reference (REQ-OBEX-010), and is byte-equivalent across re-exports (REQ-OBEX-008). CleanupExpiredExportsTestunit test (expired-ZIP purge + fresh-ZIP retention + idempotency).docs/export-pipeline.md(ZIP + GitHub flows, PAT contract, OQ-2/OQ-3 heuristics) anddocs/releasing.md(embedded-template resnapshot procedure)..github/workflows/exporter-e2e.ymlrunning the exporter integration + unit tests on every PR, parallel to the main Code Quality job.openbuild-exportercapability registered inopenspec/app-config.json.
ExportService::scratchTreeDir()split out as a pure path resolver so the GitHub push target can read the generated tree;prepareScratchDir()owns the wipe + create.
- Full Dutch + English translations for the visual page designer (170 strings, en↔nl parity) — the designer UI was previously untranslated (ADR-007 /
openbuild-page-designerREQ-OBPD spec, tasks 6.1/6.2).
- Page designer save path now targets the active
ApplicationVersion.manifest(PUT /api/objects/openbuild/applicationVersion/{uuid}) per ADR-002 / Decision 6 / REQ-OBPD-009, surgical-merging the UI-controlledmanifestfield for round-trip safety; falls back to theApplicationobject for apps that predate the versioned model.
- Removed two designer strings that leaked the internal
openbuild.page-designer.*dotted-key prefix into the user-facing UI (live-preview unavailable note and the menu nesting-depth error).
- Schema-declared notifications:
x-openregister-notificationsrules on theexportJobschema (export-succeeded/export-failed) and theApplicationVersionschema (version-published/version-archived), routed to manage-ACL holders via the OpenRegister notification engine with bilingual (nl/en) subjects. - Unit test pinning that every
transition-trigger notification rule'strigger.actionmatches a declared lifecycle transition name (ApplicationVersionLifecycleSchemaTest::testNotificationActionsMatchLifecycleTransitionNames).
- Aligned notification rule action keys with the actual OpenRegister lifecycle transition names (
succeed/fail/publish/archive) instead of destination-state names (succeeded/failed/published/archived). The engine matches the transition action name, not the state, so the previous keys would never have fired — theexportJobrules now dispatch end-to-end via the export pipeline'sTransitionEnginecalls.