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.
- Published
buildiq-*app repositories (app-repo-format-v2) now carry an application's bound OpenRegister flows and the agents that point at it —flows/<uuid>.jsonandagents/<uuid>.json, alongside the existing data-registers/connectors/automations/skills channels. Export reusesFlowAndAgentExportBundler(buildiq-exporter) unmodified via a new adapter,FlowAgentChannelCollector. Import creates each flow throughFlowService::save()and rebinds it onto the local application'sflows[]with the published uuid tracked assourceUuid(a new OPTIONAL schema property, declared before it is written), and writes each agent at its published uuid withapplicationSlugalways overwritten to the local application's own slug. Both channels skip-if-already-applied on a repeat pull, matching every other v2 channel.
GitHubCatalogService::fetchChannelFiles()'s fetch-side channel-prefix allowlist was never extended when the flows/agents channels were added toAppRepoParser, sogithub/pull(and the shop-install path, which shares this method) silently never downloaded either channel from a published repository — the exact "parser can read it, fetch never downloaded it" defect this method's own docblock already warned about once, for data-registers/connectors/automations/skills. Found live during round-trip verification: a repository publishing both channels pulled back with both declared0until the two missing prefixes were added.- Every app Buildiq 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 Buildiq-only fixes (including "the generated app could not be built at all", #39) and swap Buildiq'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 Buildiq-owned applier.
src/composables/useAppTheme.js— Buildiq's own scoped-CSS:root-rewriter and injector;CnAppRoot's ownuseScopedThemewatcher now owns runtime theme application end-to-end, with zero Buildiq-side wiring inBuilderHost.vueorPageDesignerHost.vue.src/services/manifestValidation/theme.js— Buildiq'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 eightBuildiqToolProvidertools,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 Buildiq'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 Buildiq). - 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
buildiq-{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 (buildiq-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 (buildiq.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, nobuildiqdependency 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.buildiq-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 /
buildiq-page-designerREQ-OBPD spec, tasks 6.1/6.2).
- Page designer save path now targets the active
ApplicationVersion.manifest(PUT /api/objects/buildiq/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
buildiq.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.