Skip to content

feat: Alpine.js + Harmonia runtime UI template (list + manage)#6078

Open
delchev wants to merge 51 commits into
masterfrom
feat/harmonia-runtime-template
Open

feat: Alpine.js + Harmonia runtime UI template (list + manage)#6078
delchev wants to merge 51 commits into
masterfrom
feat/harmonia-runtime-template

Conversation

@delchev

@delchev delchev commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

What

Adds template-application-ui-harmonia-java — a parallel runtime UI stack that generates a self-contained Alpine.js + Harmonia SPA (client-routed via Pinecone Router, no iframes/postMessage hubs) from the same .model as the existing Angular template, reusing the Java REST/DAO backend unchanged. The AngularJS + BlimpKit IDE is untouched.

This is the first slice of the initiative captured in HARMONIA_RUNTIME_PLAN.md (research + phasing). Both stacks coexist by URL; the choice of UI template is explicit (Angular remains the default everywhere).

What's included

  • New generation template registered on platform-templates — appears in the EDM Generate picker as "Application - UI (Harmonia) - Java". Composes the reused template-application-rest-java sources with a Harmonia UI source set.
  • View types: read-only list, and manage (CRUD list + a shared create/edit form on a baseFormPage with 422 ValidationError → per-field mapping, relationship dropdowns, client-side validation, delete-confirm dialog). Remaining view types (master-detail, report, setting, forms/BPM task forms) are stubbed as a parity checklist.
  • Reusable shell adopted from codbex-athena-app: x-h-split layout, sidebar, route-derived breadcrumb, responsive sidebar↔drawer, a fetch entity client, and a localizable error/i18n catalog.
  • Phase 1 embedding (no CDN): Alpine 3.15.11 + Harmonia 1.24.1 + Lucide 1.8.0 served as webjars via application-core (its harmonia.version bumped 1.4.2 → 1.24.1 — it bundles the webjar but has no Harmonia content); Pinecone Router (no published webjar) vendored under application-core/.../vendor/ (license-excluded in the root pom). The generated index.html references only local /webjars/... and /services/web/application-core/vendor/... URLs.

Verification

Verified end-to-end against a live app from a real model (DependsOnIT/edm.model, Orders MANAGE entity with Country/City dropdowns):

  • Generation via the live service-generate endpoint succeeds; the SPA is served at /services/web/<project>/gen/<model>/index.html.
  • The generated REST path contract matches the controllers (/services/java/<project>/gen/<model>/api/<perspective>/<Entity>Controller); a live CRUD round-trip (create/list/count) + the relationship dropdown sources return 200.
  • All embedded assets resolve locally (Harmonia 1.24.1 confirmed to contain x-h-split/gutterless/breakpoint listener; Lucide UMD exposes createIcons).

Notes / not in scope

  • In-browser render not automated here; all HTTP layers (generation, file correctness, REST contract, CRUD) are verified.
  • Forms/BPM task forms, master-detail, reports, parity Selenide ITs, and an optional harmonia-view platform-links category remain follow-ups (tracked in the module README + plan doc).

🤖 Generated with Claude Code

delchev added a commit that referenced this pull request Jun 24, 2026
Follow-up to the glue handlers switching from class-level @Listener/@scheduled
to self-describing @component beans (destination()/kind()/cron()). Update the
IntentEngineIT assertions accordingly: notification/integration/rollup triggers
check @component + the destination() return value; the job checks @component +
implements JobHandler + the cron() return value. Fixes the PR #6078 CI failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
delchev and others added 21 commits June 25, 2026 02:18
Adds `template-application-ui-harmonia-java`, a parallel runtime UI stack that
generates a self-contained Alpine.js + Harmonia SPA (client-routed via Pinecone,
no iframes/hubs) from the same `.model` as the Angular template, reusing the
Java REST/DAO backend unchanged.

- New generation template registered on `platform-templates` (appears in the EDM
  Generate picker as "Application - UI (Harmonia) - Java"). Composes the reused
  `template-application-rest-java` sources with a Harmonia UI source set.
- View types: read-only `list`, and `manage` (CRUD list + shared create/edit form
  on a `baseFormPage` with 422 ValidationError -> per-field mapping, relationship
  dropdowns, client validation, delete-confirm). Other view types stubbed.
- Reusable shell adopted from codbex-athena-app: x-h-split layout, sidebar,
  breadcrumb, responsive drawer, fetch entity client, error/i18n catalog.
- Phase 1 embedding: Alpine 3.15.11 + Harmonia 1.24.1 + Lucide served as webjars
  via application-core (harmonia.version bumped 1.4.2 -> 1.24.1); Pinecone Router
  vendored (no webjar) under application-core/vendor (license-excluded). The
  generated index.html references only local assets, no CDN.
- HARMONIA_RUNTIME_PLAN.md: research + phasing. IntentSettings: comment noting the
  intent model recipe names the template explicitly (Angular default today).

Verified end-to-end against a live app from a real model (DependsOnIT/edm.model):
generation succeeds, the SPA is served at /services/web/<project>/, and a CRUD
round-trip + relationship dropdowns hit the generated controllers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add `setting` view type: SETTING entities (e.g. Country/City) reuse the manage
  CRUD templates against the uiSettingModels collection, grouped under a "Settings"
  sidebar section with their own routes; their derived controller path
  (<restBase>/settings/<Entity>Controller) matches the generated backend.
- Fix: the shell index.html header comment contained a bare `$models` Velocity
  reference, which rendered the entire entity object graph into a comment in every
  generated app. Reworded to prose.

Verified live: generating DependsOnIT/edm.model now produces Country/City CRUD UI
(list + form pages + views), a Settings sidebar group, and /Country|/City routes;
the header no longer dumps the model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… paths

Master-detail (MANAGE_MASTER/LIST_MASTER + MANAGE_DETAILS/LIST_DETAILS):
- Master entities get a master page at /<Master> — an x-h-split with the master
  record list (left) and, for the selected record, its detail panels (right).
  Masters reuse the manage form for their own routed create/edit.
- Details are decoupled via a runtime registry: each detail emits a registration
  (App.registerDetail(<master>, {...})) so a master renders one generic detailPanel
  per detail without enumerating them at generation time. The panel lists rows
  filtered to the master via the controller's ?<masterEntityId>=<id> query (built
  into the reused rest-java controller), with delete + routed create/edit (FK + a
  returnTo preset; baseFormPage now prefills any form field from a matching query
  param).
- New: detailPanel.js (shell), masterDetail.js collector, master-page/-view +
  detail-register templates; App.details registry; index.html wires master nav,
  routes and detail registration/form scripts.

Fix (surfaced by the hyphenated `sales-order` model): the generated SPA built
its REST base from the raw genFolderName/lowercased perspective, but the Java
backend lives under the *sanitised* package (sales-order -> sales_order). config.js
restBase now uses ${javaGenFolderName} and every page's apiPath uses
${javaPerspectiveName} — matching the DAO/REST templates. Phase 0/1 worked only
because `edm` needed no sanitising.

Verified live against sales-order.model: master Customer + two CustomerPayment
details created through the controllers, and the detail panel's filtered fetch
(?Customer=1) returns exactly that master's payments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… equivalent)

Surfaces a process-aware record's BPM user tasks inline, the Alpine/Harmonia
counterpart of the dashboard ProcessTasks module:
- New `processTasks` Alpine store: fetches the current user's inbox tasks
  (/services/inbox/tasks ?type=assignee + ?type=groups), buckets by
  processInstanceId, claims candidate tasks (POST /services/inbox/tasks/{id}
  {action:CLAIM}), and opens the task's formKey in an app-wide dialog (iframe),
  re-fetching on close so completed tasks drop off.
- Generated list / manage / master rows render an inline inbox popover (task
  count + menu) gated on $hasProcess, matching the record via
  entity.ProcessId === task.processInstanceId.
- Shell wires the store script + the task-form dialog into index.html.

Verified: the store + dialog are emitted and wired; gating is correct (a
non-process model emits zero task markup); the inbox endpoints the store calls
respond 200. Full surfacing of a live task needs a process-enabled model
(deferred to combined testing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New module — the Alpine.js + Harmonia counterpart of template-form-builder-angularjs.
Given a .form artifact it generates a standalone Harmonia form page
(gen/<genFolder>/forms/<form>/index.html + form.js), for app forms and BPM task
forms (opened by the processTasks store). Registered on platform-templates
(extension "form") so it shows in the Generate picker and can be the intent
recipe's `form` template.

Neutral form-controller contract (the decided replacement for AngularJS $scope/$http):
the .form `code` is the body of formController(ctx), with ctx.{model, params, http,
task:{id,processInstanceId,complete()}, notify, close}. Button `callback`s name
handlers attached to ctx, invoked via the page's run(). BPM task forms complete via
ctx.task.complete() -> POST /services/inbox/tasks/{id}.

v1 renders header/paragraph/line/spacer/link/textfield/textarea/number/checkbox/
date/datetime/time/color/button + one level of container-hbox/vbox; other widgets
fall back to a text input. Reuses the co-generated SPA shell's app/config/api/apiError
(../../js) + the Harmonia/Alpine/Lucide webjars.

Verified live: registers as "Harmonia Generator from Form Model"; generating the
BPMLeaveRequestIT .form produces a Harmonia page (header + textfield + two date
inputs bound to model.*, Approve/Decline buttons with data-variant + run()) and the
formController(ctx) wrapper. Documented follow-ups: migrate existing AngularJS .form
code to the ctx contract and have FormIntentGenerator emit it; feed-driven/complex
widgets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Report entities (type REPORT) run server-side via the generated Java report
controller (reportFileEntity: GET / rows, POST /search, POST /export). New report.js
collector emits, per report:
- REPORT_TABLE (uiReportTableModels): a <Name>ReportPage + table view — data table over
  the report rows with a Refresh and a client-side CSV Export.
- REPORT_BAR/LINE/PIE/DOUGHNUT/POLARAREA/RADAR (uiReportChartModels): a chart page that
  renders the rows with chart.js (already a bundled webjar, /webjars/chart.js/dist/
  chart.umd.js), labelled by the report's primary-key column with one dataset per other
  column — mirroring the Angular report-chart mapping.

Both resolve <restBase>/reports/<Name>Controller (javaPerspectiveName). index.html adds
report routes, a Reports sidebar group, the page scripts and the chart.js tag.

Verified non-breaking: a non-report model generates cleanly (201) with the chart.js
tag present and no Reports group emitted. Full table/chart runtime needs a
report-bearing model (intent-produced) — covered in combined testing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The old dashboard shell's built-in Process Inbox and Documents perspectives, ported
as always-present sections of every generated Harmonia app (their backends — the
platform inbox + CMS APIs — are reused unchanged):

- Process Inbox (/inbox): lists the user's BPM tasks (assignee + candidate groups)
  via the shared processTasks store (extended with a flat `tasks` list), with
  search and claim-&-open into the app-wide task-form dialog.
- Documents (/documents): a CMS folder browser over /services/js/documents/api —
  navigate folders, download files, create folder, multipart upload, delete, with a
  path breadcrumb. Folders vs files via type === 'cmis:folder'.

Wired as static shell assets: a built-in "Inbox"/"Documents" sidebar group, the
/inbox + /documents routes, and the page scripts — emitted for every app.

Verified live: both views + page components serve; index.html routes/nav/scripts
present; the inbox (/services/inbox/tasks) and documents (/services/js/documents/api)
endpoints respond 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ot class-level @Listener/@scheduled

The rollup, notification, integration and schedule glue templates put @Listener /
@scheduled on the class, but those annotations are @target(METHOD) only (the SDK moved
to method-level + self-describing interfaces). javac rejected the generated classes
with "annotation interface not applicable to this kind of declaration", so engine-java
failed to compile gen/events/*.java and the app's client-Java sync broke.

Align them with the working Trigger template's self-describing style:
- Rollup / Notification / Integration: @component implements MessageHandler with
  destination() + kind() instead of class-level @Listener(name=, kind=).
- Job: @component implements JobHandler with cron() instead of class-level
  @scheduled(expression=).
(Webhook's @controller is @target(TYPE) — fine; Resolver is a JavaDelegate — fine.)

Verified live against dirigiblelabs/sample-intent-model: intent generate 200, glue
generate 201, BookLoanCountRollupOnDelete + all gen/events classes now compile clean
(no "annotation interface not applicable"), and the handlers register without
instantiation errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d temporal format

A datetime-local/date/time input value (local, no zone) was POSTed raw, so a
java.time.Instant/Timestamp field failed Jackson binding ("Cannot deserialize
java.time.Instant from String 2026-06-09T16:04") — the form-create 400 the user hit.

form-page now converts both directions (matching the AngularJS stack's new Date(value)):
- toPayload() (create/edit submit): empty date -> null; an HTML date/datetime value ->
  a full ISO instant (…Z) via new Date().toISOString() so Instant/Timestamp parse; a
  bare TIME ("16:04", not a valid Date) passes through so a LocalTime field still parses.
- toDateInput() (edit load): the backend's full ISO value -> the value the HTML widget
  expects (DATE 0..10, DATETIME-LOCAL 0..16, TIME 0..5, MONTH 0..7) so an edited record's
  dates populate the fields.

Both generate per date-type property (uses a per-field temp var to avoid clashes).
Verified at generation against dirigiblelabs/sample-intent-model (Member.JoinedAt):
MemberFormPage emits the correct toPayload + toDateInput.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The detail registration baked an ABSOLUTE apiPath (App.config.restBase + ...), but
detailPanel calls api.get(def.apiPath + q) without a baseUrl override, so the client
prepended restBase again — producing /…/api/services/java/…/api/member/LoanController
(404). Make the detail apiPath relative ('/<perspective>/<Entity>Controller') like the
entity pages, so restBase is prepended exactly once.

Also guard detailPanel.load(): skip the fetch when no master is selected (masterId ==
null) so it no longer fires a wasted ?<fk>=null request before a row is picked.

Verified at generation against sample-intent-model: Loan.detail.js now registers
apiPath '/member/LoanController' (relative) -> resolves to a single
/services/java/lib/gen/library/api/member/LoanController?Member=<id>.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…EADME refresh

Consolidates documentation now that the stack is functional (Loan create/edit,
master-detail, dropdowns, dates all working):

- HARMONIA_RUNTIME_PLAN.md: new "Implementation status (built & verified)" section —
  the two modules, view types + shell sections, the embedding, the runtime contracts
  (REST path sanitisation, {baseUrl:''} semantics, neutral form contract, detail
  registry, processTasks store, date conversion), the fixes surfaced during live
  testing, and follow-ups (next up: intent glue runtime — trigger -> process-start ->
  task -> complete).
- CLAUDE.md: a "Harmonia runtime UI" module section with the gotchas future work must
  keep (Java-sanitised REST paths, {baseUrl:''} = prepend-nothing, two-way date
  conversion, registry-driven master-detail, neutral .form code, self-describing glue
  handlers) + pointers to the module READMEs and the plan doc.
- template README: status -> functional; forms row -> done via template-form-builder-harmonia.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the glue handlers switching from class-level @Listener/@scheduled
to self-describing @component beans (destination()/kind()/cron()). Update the
IntentEngineIT assertions accordingly: notification/integration/rollup triggers
check @component + the destination() return value; the job checks @component +
implements JobHandler + the cron() return value. Fixes the PR #6078 CI failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… just null

A new record's process never started when the form submitted ProcessId as an
empty string: the trigger guard checked `ProcessId != null`, and "" != null, so
it bailed before Process.start. Two complementary fixes:

- Trigger.java.template: treat a blank ProcessId as "not started yet"
  (`ProcessId != null && !ProcessId.isBlank()`), robust to any client.
- Harmonia form-page toPayload(): convert empty-string fields to null (matching
  the AngularJS contract of omitting untouched fields), so a system-managed
  back-reference like ProcessId is sent as null on create and the trigger fires;
  a populated value still round-trips on edit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Starting a process on onCreate writes the new process-instance id back onto the
entity via repository.update(), which republished "<entity>-updated" and
spuriously fired every onUpdate reaction — e.g. the notification emailing the
member the instant the loan was created, before any review happened.

The back-reference is a system write, not a user edit, so it must not emit a
lifecycle event. Generate a no-event update on the DAO (updateWithoutEvent,
persists via super.update without the Producer.sendToTopic) and have the
process trigger use it for the ProcessId write-back. Update IntentEngineIT to
assert the silent update.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… + AngularJS-compat)

Opening a BPM task form in Harmonia hit three bugs:

1. Claim failed with "Unexpected end of JSON input": the claim endpoint returns
   a 200 with an empty body, and api.request() called r.json() unconditionally.
   Read the body as text first and only parse when non-empty (the claim actually
   succeeded — hence it showed claimed after a refresh).

2. The form 404'd on the SPA shell assets (js/app.js, config.js, services/api.js)
   and left "App is not defined": the standalone form page referenced the shell
   via fragile relative paths, but a task form is opened directly (iframe/dialog),
   not inside the shell. Make the form self-contained — it now loads only form.js
   (which carries its own minimal fetch client, harmoniaHttp) plus the
   Harmonia/Alpine/Lucide webjars, and no longer depends on window.App.

3. "no handler for callback onApproveClicked()": intent-generated .form code is
   AngularJS idiom ($scope/$http/NotificationHub/DialogHub); inlined into
   formController(ctx), $scope was undefined so attaching the handler threw and
   was swallowed. Add compatibility shims in formController that map those legacy
   globals onto ctx, so the existing .form code runs unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Inbox: rebuild the built-in /inbox as an Outlook-style master-detail (mirrors the
dashboard redesign #6064/#6068) instead of a flat table — a resizable x-h-split
with the task list on the left and, on the right, the selected task's claim action
and its form rendered inline in an <iframe> (the form is self-contained, so it
embeds cleanly). Adds an auto-refresh toggle, last-updated stamp, and clears the
selection + re-fetches when a form completes (harmonia.form.close postMessage).

Documents: it did not work at all — every operation went through a root listing of
GET <base>?path=/, which the CMS backend rejects with 400 ("null has no such
function getName"). Align with the dashboard js/documents.js contract:
  - list the root with NO ?path= param (folder.path comes back as "/");
  - navigate/download/delete by each child's absolute .path;
  - DELETE sends a JSON body of paths (not ?path=).
So New folder, Upload, Delete, navigation and download now work.

Also wire the harmonia.form.close postMessage into the processTasks store so the
app-wide task-form dialog (list/manage/master row popovers) closes + refreshes on
completion (previously only the X button closed it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scaffold the model and form recipes to the Alpine.js + Harmonia templates
(template-application-ui-harmonia-java + template-form-builder-harmonia) instead
of AngularJS + BlimpKit; glue stays the framework-neutral client-Java template.
The standalone report-file UI stays the AngularJS perspective for now (there is no
Harmonia report-file template yet) — its Java backend is framework-neutral, so the
recipe flips to Harmonia once that template lands (TODO noted in code).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ngs/Reports nav

Theme: Harmonia is light by default (its :root tokens) and dark via a `dark` class
on the document root. Hardcode the default to light with a FOUC-free head bootstrap,
and add an Appearance switcher in the sidebar (the counterpart of the dashboard's
BlimpKit theme menu) that toggles light/dark and persists the choice to localStorage.

Navigation: Settings and Reports are now SINGLE sidebar entries (not one item per
setting/report entity, which flooded the sidebar — there can be tens). Each opens a
generated landing page (_settings.html / _reports.html) listing its entities, which
still have their own CRUD/report routes. Only the most-used PRIMARY entities remain
as direct nav items — matching the dashboard's organization.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e it the default

Adds the Harmonia counterpart of the AngularJS report-file template, so a standalone
.report generates a Harmonia report UI:

- template/template-report-file.js ("Application Report - Table - Harmonia") — clones
  the AngularJS -java entry (same model processing + generateGeneric, same reportModels
  / generateReportModels collections) but points at the Harmonia UI sources.
- template/ui/reportFile.js — reuses the framework-neutral Java backend verbatim
  (reportFileEntity Repository + Controller + default roles + project.json) and swaps the
  AngularJS perspective for a self-contained Harmonia page.
- ui/perspective/report-file/{index.html,report.js} — a standalone Alpine + Harmonia page
  (its own fetch client; light-themed like the shell) under gen/<genFolder>/reports/<name>/:
  queries the Java report controller (GET list with $limit/$offset, GET /count, POST
  /export), renders a table with pagination + CSV export.
- template-report-file.extension registers it on platform-templates.

With the template in place, flip the intent `report` recipe default to it
(IntentSettings.scaffold) — so model, form and report all default to Harmonia; glue stays
the framework-neutral client-Java template.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ark page on a light shell)

A BPM task form opened from the inbox rendered dark on the light shell. The form/report
pages open standalone in an iframe, and Harmonia themes `body` (not `html`); the form's
body is only as tall as its content, so the uncovered `html` area showed the browser
default — dark on a dark-mode OS. Two fixes on the standalone form + report pages:

- theme the `html` element (background/foreground from the Harmonia tokens) so the whole
  page surface follows the theme, and give the body a min-height so it fills the iframe;
- apply the shell's theme bootstrap (light by default, restoring the localStorage choice)
  so the iframe'd page matches the shell — Harmonia is light at :root, dark via a `dark`
  class, and a standalone document must set the class itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…create the tables)

The Harmonia model template merged only the REST-java backend + Harmonia UI, omitting the
schema layer. But nothing else creates the physical tables: the client-Java JavaEntityManager
only *registers* an @entity against an existing table (the log shows "Registered ... -> table",
never "Created table") — TableCreateProcessor creates tables from the schema's .table artefacts.
So a freshly generated Harmonia app had no tables; CRUD and CSVIM seeds failed
("Table metadata was not found for table [LIBRARY_GENRE]"). It only appeared to work when a
prior AngularJS/schema generation had already created the tables (kept in place across restarts).

Merge template-application-schema sources (as the AngularJS full-stack template does) and dedupe
the shared parameters. Now the Harmonia stack generates the .table/.schema artefacts and the
tables are created on sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@delchev delchev force-pushed the feat/harmonia-runtime-template branch from dbb767a to db95e4c Compare June 24, 2026 23:18
delchev and others added 7 commits June 25, 2026 02:28
…ail + File Preview)

Rebuild the built-in /documents view from a basic browser into the full Document Storage
experience of the dashboard's AngularJS documents perspective, as an Outlook-style
master-detail:

- Left: file/folder list — file-type icons, multi-select checkboxes (select-all), row
  overflow menu (copy link, rename, download, delete), drag-and-drop upload, empty state.
- Right: File Preview pane — CSV rendered as a table via PapaParse (added to the shell as a
  webjar), other types in an <iframe> over the /preview endpoint, with a can-preview check
  that excludes model/code artifacts; loading + "cannot be previewed" states.
- Toolbar: back/forward history, breadcrumbs, search-in-folder toggle, new folder, delete
  selected, download folder as zip, upload menu (files / unpack zip contents).
- Dialogs: new folder, rename, delete (single + multi).

All operations go through the CMS REST API exactly as the dashboard does
(/services/js/documents/api/documents.js): root listed with no ?path=, navigate by path,
POST /folder, PUT {path,name} rename, DELETE with a JSON body of paths, multipart upload
(+ /zip for unpack), GET /download + /zip + /preview.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… ?path= (subfolders 400'd)

Drilling into a subfolder (and download/preview/zip/upload within one) failed with HTTP 400
"null has no such function getName". The CMS query layer does not decode an encoded slash —
GET ?path=/__EXPORTS returns 200 but ?path=%2F__EXPORTS returns 400 — and the previous code
ran the whole path through encodeURIComponent, turning every "/" into "%2F".

Add pathParam(): encode each path SEGMENT but keep the slashes literal
(`p.split('/').map(encodeURIComponent).join('/')`), and route all ?path= URLs (list, download,
zip, preview, upload, copy link) through it. Root still lists with no ?path=. Matches the
dashboard, which sends raw paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…theme, ProcessId, form shims

Update the Harmonia section after the recent work:
- .form now runs AngularJS code via compat shims and is self-contained (no migration needed).
- Full-stack model template MUST merge template-application-schema (JavaEntityManager registers
  entities but never creates tables; the schema sync does) — else a fresh app has no tables.
- Process trigger persists ProcessId via updateWithoutEvent() (no spurious onUpdate); guard is
  != null && !isBlank().
- Documents/CMS path contract: root with no ?path=, encode segments not slashes (%2F 400s),
  PUT {path,name} rename, DELETE body of paths.
- Theme: light default, class-based; harmonia.min.js auto-applies OS dark, so standalone iframe
  pages must re-assert; Settings/Reports are single nav entries; intent recipe defaults to Harmonia.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ight switch; Settings in sidebar footer; runtime Reports nav

Aligns with codbex-athena-app and fixes the lingering issues:

Theme — delegate to Harmonia's native colour-scheme API (window.Harmonia.get/setColorScheme,
key codbex.harmonia.colorMode) instead of a custom localStorage key + manual `.dark` toggle. The
switch moves to the top-right of the toolbar (VitePress-style sun/moon). Because every page —
including the standalone form/report iframes that each load harmonia.min.js — reads the same
colour-scheme key, they now all honour the shell's choice; this also fixes the task form / report
rendering dark on a light shell (harmonia.min.js otherwise auto-applies the OS scheme). The head
bootstrap just defaults the scheme to light on first run.

Settings — pinned to the sidebar footer (bottom-left), like athena, instead of a content group.

Reports nav — discovered at RUNTIME: a new `reports` store walks the project's registry tree for
`*/reports/*/index.html` pages, so intent reports (standalone pages in a separate gen folder the
shell can't see at generation time) finally surface. The sidebar Reports entry shows when the
store is non-empty; the Reports landing lists them and opens each report page in a new tab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… footer; runtime reports

Update the theme gotcha: the theme is now driven through window.Harmonia.get/setColorScheme
(key codbex.harmonia.colorMode) — never hand-toggle .dark — so the shell and the standalone
form/report iframes theme consistently (fixes the iframe-dark issue). Settings sits in the
sidebar footer; Reports is discovered at runtime via the reports store.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s empty)

The report controller returns rows keyed by the report's result-set aliases (e.g. "Member Name",
with spaces), but the page looped `$properties` for headers/cells — which is empty for a
standalone .report — so the table generated no columns and showed nothing even though the backend
returned a row. Derive the columns at runtime from the data (Object.keys of the first row) and
render cells via row[col] (bracket access, so aliased keys with spaces work).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ntent, per-entity icons

Three polish items:

- Humanized display names: the Reports nav/landing and the standalone report page show
  "Members With Loans Due" instead of "MembersWithLoansDue" (camelCase -> spaced Title Case),
  via a small humanize in the reports store + report page (display only; URLs/ids unchanged).

- Meaningful app title/caption: EdmIntentGenerator now writes a model-level `title` (the intent's
  humanised `name`, e.g. "Library") + `description` into the .model. The Harmonia template exposes
  them as appTitle/appDescription; the shell uses the title for the document title + sidebar header
  and the description as a subtitle. Falls back to the project name when absent (non-intent apps).

- Per-entity icons: a new optional `icon` on an intent entity (a Lucide icon name, e.g. book/user).
  EdmIntentGenerator stores it as `iconName` (Harmonia sidebar, Lucide) and maps it to a unicons SVG
  URL for `icon`/`perspectiveIcon` (AngularJS perspective); the Harmonia sidebar renders it, falling
  back to "list". Documented in the intent assistant guide. (Both new model keys are ignored by
  tooling that only reads entities/perspectives.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
delchev and others added 23 commits June 25, 2026 12:06
…monia skill references

Edit forms rendered empty controls because the values weren't in the shapes the inputs expect:

- Dates: the Java REST controller serializes java.time via Jackson as ARRAYS ([y,m,d] /
  [y,m,d,h,mi,s,ns]) and Instant as a numeric epoch, not ISO strings — so toDateInput()'s
  String(value).slice() produced garbage and the date/time inputs showed empty. Rewrote it to
  handle arrays + epoch numbers + ISO strings, emitting the exact value each widget needs.

- Relationship combobox: the FK comes back as a number, but an option's data-value (an HTML
  attribute) is a string, so the value never matched an option and the x-h-select rendered empty
  on edit. Stringify dropdown values on load (the pattern codbex-athena-app's edit pages use), and
  align the combobox markup with the Harmonia select contract — data-filter="contains" + an
  in-dropdown x-h-select-search, data-value coerced to String.

Also copy the Harmonia + Alpine.js reference skills from codbex-athena-app into .claude/skills/
(annotated with provenance) and point CLAUDE.md at them, so future Harmonia markup follows the
library's documented contracts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…odule

.claude/* is gitignored, so the skills copied from codbex-athena-app could not live there. Move
them to components/template/template-application-ui-harmonia-java/reference/{harmonia,alpinejs}/
(tracked) and repoint CLAUDE.md, so the codbex-harmonia directive catalog (x-h-select combobox
contract, theming, layout) + Alpine patterns are versioned references for the Harmonia templates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d offset below the frame)

The relationship combobox showed the selected value pushed ~half its height below the field
frame. The only deviation from codbex-athena-app's (visually correct) forms was the
x-h-select-clear button — which athena never uses (it appears only in the standalone SKILL
example). Removing it matches the reference and fixes the layout; the x-h-select-input already
offers clear-on-empty via typing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…chas

Capture the lessons from the edit-form/combobox fixes:
- Java REST serializes java.time as Jackson arrays / Instant as epoch (not ISO), so toDateInput
  must handle arrays/numbers; FK values must be stringified on load to match an option's string
  data-value or x-h-select renders empty on edit.
- Combobox follows the x-h-select contract as used in codbex-athena-app's forms (data-filter +
  x-h-select-search, String(data-value)); do NOT add x-h-select-clear — it offsets the value below
  the field frame. Points at the in-repo Harmonia SKILL reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
IntentSettings.scaffold now defaults model/form/report to the Harmonia templates, but
IntentEngineIT still asserted the AngularJS templateIds — failing
generate_writes_all_model_files_into_the_workspace_project (codeGenerations 'orders.model'
templateId) and the assertSettings recipe check. Update both:
- orders.model -> template-application-ui-harmonia-java/template/template.js
- OrdersByCustomer.report -> template-application-ui-harmonia-java/template/template-report-file.js
- scaffolded .settings contains template-application-ui-harmonia-java

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ariables

The Loan Approval (and any BPM task) form opened empty — its fields bind to model.<X> but nothing
loaded the task context, so LoanedOn/DueOn etc. were blank. The data is available at
GET /services/inbox/tasks/{id}/variables ({"variables": {LoanedOn, DueOn, Member, Book, ...}}).
The Harmonia form runtime now fetches it on init (when a taskId is present) and merges the
variables into the reactive model, so the fields show the current values. Runs after the .form
controller so the live values win. Documented in form.js + the README ctx contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Form layout (template-application-ui-harmonia-java):
- Lay form fields on a responsive 12-column grid; each field's width comes
  from the model's widgetSize, so consecutive narrow fields share a row.
  Wide widgets (textarea/checkbox) span full width.
- Use widgetLabel for labels, mark required with text-negative, add
  :data-invalid per field, and give the form more horizontal padding.

widgetSize is now a plain column count (out of 12) instead of a Fundamental
CSS class string:
- editor-entity: store 3/4/6/12 (Small/Medium/Large/XLarge).
- Harmonia template maps it to grid-column span (12 -> col-span-full).
- All AngularJS UI template stacks (angular, angular-v2, angular-java) now
  emit fd-col-md--<widgetSize> so they keep working.

Reports and Settings open content inline (master-detail), not in a new tab
or the main pane:
- Reports: report list on the left, selected report embedded in an iframe.
- Settings: setting entities on the left, the selected entity's manage-list
  loaded inline via x-html (new settingsPage component).

Split panes use data-size (data-default-size is a no-op in harmonia 1.24.1);
Reports/Settings/Inbox/Documents default to a 20/80 split.

Stop entity-name page headings from truncating to "Bo..."/"Mem..." by adding
shrink-0 (x-h-toolbar-title bakes in overflow-hidden + ellipsis).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Harmonia runtime UI (template-application-ui-harmonia-java):
- List & Manage views are now master-detail: the table shows the major
  columns; selecting a row reveals that record's FULL property list in a
  right-hand pane (an entity may have many properties but only 2-3 columns).
- Master view's right pane now also shows the master's own fields (Details
  card) alongside its detail collections.
- Process user tasks surface as inline buttons (not a dropdown) wherever a
  record appears: list/manage detail pane, master header, and master detail
  rows (e.g. a Loan's "Librarian Review" task).
- Relationship (lookup) FKs render the referenced label instead of the raw id
  across all tables and panes — each page/detail loads the reference rows from
  the relationship controller (the same source the form uses) and resolves
  FK -> label.
- java.time values display readably: Jackson arrays (LocalDate/LocalDateTime)
  and epoch numbers (Instant/Timestamp, via the property's isDateType hint).
- Reports/Settings/Inbox/Documents split panes default to 20/80 (data-size;
  data-default-size is a no-op in harmonia 1.24.1).

IDE editors — auto-close when the backing file is gone (e.g. the workspace was
cleaned by a Maven rebuild), matching the Monaco editor's behavior. On a 404
while loading content the editor now calls layoutHub.closeEditor: intent, bpm,
form-builder, jobs, listeners, websockets, csv, csvim, report, entity, schema,
mapping, and the extensions/security/data-structures sub-editors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sidebar (template-application-ui-harmonia-java):
- Split the sidebar into labeled sections: "Application" (Inbox, Documents),
  "Entities" (the model entities), and "Reports".
- Reports are now listed individually in the sidebar (one entry per report,
  runtime-discovered) instead of a single landing link; selecting one sets
  $store.reports.selected and opens it embedded on /reports. The report's
  active highlight clears when navigating away (gated on isActive('/reports')).
- The /reports page just embeds the selected report (no own toolbar title, so
  the report's own header isn't duplicated).
- Sidebar header shows only the app name; the description moved to a hover
  title tooltip instead of an always-visible second line.

Navigation labels come from the model:
- The sidebar renders each entity's menuLabel (falling back to the name)
  through a navLabel() helper that humanizes PascalCase/camelCase names so
  multi-word names read as words (SalesInvoice -> "Sales Invoice"); it is
  idempotent and does not pluralize.
- The intent EDM generator now emits a humanized + pluralized menuLabel
  (new IntentNaming.pluralize), so generated models carry natural plural nav
  labels (Book -> "Books", SalesInvoice -> "Sales Invoices",
  Category -> "Categories"). Pluralization lives in the model, not the UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…stone

Pinned JDT Language Server milestone tarballs on download.eclipse.org are
pruned over time, so the pinned URL started returning 404 and broke the build.
Switch the download to the rolling 'jdt-language-server-latest.tar.gz' snapshot
(never removed) and drop the sha256 pin (the snapshot's contents change), both
in the root version properties and the ide-java-lsp download-plugin config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In the master, manage-list and list views the right-hand detail pane is now
hidden (0%, gutter included) while nothing is selected — the table fills the
full width — and appears at ~40% once a record is selected, via
:data-hidden bound to the no-selection state (the same mechanism the sidebar
panel uses). Also fixed the master view's list panel to use data-size (it had
the no-op data-default-size).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The top-toolbar breadcrumb (next to the Home icon) was inert — its sections /
routeTitles maps were empty placeholders, so only the Home icon showed. It now
builds a live trail from the current path: Home › <shell section>, Home ›
Reports › <selected report>, or Home › <Entity>[ › Create | › Edit] for entity
routes (earlier crumbs link back). The entity crumb is labeled from a generated
route -> menuLabel map (window.__harmoniaNav in index.html) through the same
navLabel helper, so it reads exactly like the sidebar ("Book" -> "Books").
pageTitle now derives from the last crumb; the dead nav/routeTitles/sections
placeholders are replaced by the model-driven navLabels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Home dashboard (routed at '/' and /dashboard, reached via the Home icon):
- One KPI tile per top-level entity, generated from the model — shows the
  entity's label and its live record count (fetched from the controller's
  GET /count, skeleton while loading), click-through to the entity list.
- One tile per runtime-discovered report (opens it embedded on /reports);
  report count + description is a follow-up (needs the intent generator).

New generated dashboardPage component (bakes the entity widget list from the
model — label/icon/route/count apiPath) + a generic _dashboard.html view that
iterates it; counts load independently so a slow/erroring controller can't
block the rest. KPI-tile style adopted from the codbex dashboards. Wired the
routes, the page-component script tag, and the shell.js manifest entries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Report tiles now embed a compact preview of the report instead of a count:
the report page gains a preview mode (?preview=1) that hides the toolbar
(refresh/export) and pagination and shows only the first 5 rows, and the
dashboard report tile iframes the report in that mode with an "open" affordance
to the full report. Reuses the same report model/page — no separate renderer.

(Settings entities are already excluded from the dashboard: the entity tiles
only include type == PRIMARY.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entities: a new intent `dashboard: false` on an entity excludes its dashboard
tile. EntityIntent.dashboard -> EdmIntentGenerator emits dashboardWidget="false"
on the .model entity -> the Harmonia dashboard skips it. (Setting entities are
already excluded by their type.)

Reports: report tiles are now model-driven. ReportIntent gains `dashboard`
(default shown) and the existing `description`; ReportIntentGenerator writes
both into the .report. The Harmonia reports store enriches each discovered
report from its .report file (label, description, dashboard flag); the dashboard
shows the description on the report tile and omits reports with dashboard:false
(they remain in the sidebar). No separate widget file — the .report is already
valid JSON read directly by the store.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dashboardPage.js is Velocity-generated (entity tiles), so Alpine's $store
collided with Velocity's $: the new reports getter's
$store.reports.items.filter(r => r.dashboard !== false) made Velocity try to
parse that method call and fail ("Encountered ="). Escape $ via
#set($dollar='$') and use ${dollar}store (the same pattern report.js uses for
$limit/$nextTick).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Dashboard" entry at the top of the sidebar's Application group
(navigates to /dashboard, active on the home route), so the home dashboard is
reachable from the sidebar as well as the Home icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The shell header now shows a brand icon before the title, configurable like the
title/description:
- Template parameter `appIcon` (Generate dialog) wins, else the model icon, else
  a default ('blocks'). A value with '/' or '.' is rendered as an <img> (custom
  SVG/PNG/data URI); a bare word is a Lucide icon name.
- Intent-level `icon:` (IntentModel.icon) -> .model -> appIcon.
- .settings `branding` block (title / description / icon): developer-owned and
  preserved across regenerations, so one model (e.g. "Library") can be
  rebranded per deployment (each library) without editing the intent. Scaffolded
  from the model so it's visible/editable; the generator prefers branding over
  the intent's own name/description/icon, which win over defaults.

Verified end-to-end: editing .settings branding (title "Sofia City Library",
icon "book") rebrands the generated shell header.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e toggle

Rework the shell's top-right toolbar (left to right): a visible Lucide sun/moon
theme switch (the previous x-h-icon button rendered blank), a notifications bell
with an unread-count badge and a dropdown list, and a user avatar (Lucide) whose
dropdown shows the logged-in user and Log out.

- New currentUser store: loads the platform user name
  (/services/js/platform-core/services/user-name.js) and logs out via /logout.
- New notifications store: in-app list behind the bell (unread count, add,
  markAllRead, remove); app code/glue pushes entries. Starts empty (no fake
  data); subscribing to a server-side source is a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…notifications

Theme fix: standalone report and form pages (embedded as iframes — dashboard
report tiles, /reports, task forms) only applied the colour scheme at load, so
toggling the shell theme left them stale until a full refresh. They now listen
for the localStorage `storage` event (fired in the iframe when the parent shell
changes the shared codbex.harmonia.colorMode key) and re-apply via Harmonia's
colour-scheme API (guarded against a set/echo loop).

Notifications (Phase 1): the user's actionable BPM tasks now surface in the
top-right bell — processTasks.load() calls notifications.syncTasks(), which adds
a "New task: ..." item per task (preserving read state, dropping completed
ones); clicking it opens the task form (appShell.openNotification). Added the
app-wide Harmonia notification overlay (toast template) so components/forms/glue
can raise transient toasts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Theme as a global store: move the light/dark state from the app component to
  an Alpine store ($store.theme). The toolbar toggle referenced `theme` which
  errored on load ("theme is not defined") from eager x-show evaluation; a store
  resolves from any scope at any time. Removed the now-dead app theme code.
- Bell badge: the unread count is now a small chip tucked into the bell's
  bottom-right corner (absolute, ~15px) instead of an x-h-badge pill that
  covered/off-centred the icon and widened the button into its neighbours.
- Live task refresh: the processTasks store now re-checks the inbox on
  navigation (pinecone:end) and on a 30s poll, so the notification bell AND the
  in-record task buttons update on their own when a task is raised (e.g. a new
  Loan starting a process), not only after a full refresh or an Inbox visit.
- Embedded report/form iframes re-theme correctly on shell theme switch: the
  storage handler can't gate on Harmonia.getColorScheme() (reads the same key,
  already holding the new value) — track the last applied scheme and re-apply.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The detail pane is toggled with x-show, which only hides the element — Alpine
still evaluates the inner x-text bindings. Before a row is selected, `selected`
is null, so displayValue(selected.Email, ...) / lookupText(..., selected.Field)
threw "Cannot read properties of null" on page load. Use optional chaining
(selected?.<field>) in the list / manage / master property panes so the value
resolves to '—' until a row is selected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…AUDE.md

Root CLAUDE.md (Harmonia section): the Velocity-vs-Alpine `$` trap in
velocity-generated shell files (incl. inside HTML comments), put cross-scope
state in Alpine stores not the app component (theme), iframe re-theming via the
storage event (don't gate on getColorScheme), x-show still evaluates inner
bindings (use selected?.), data-size (not data-default-size) for split panes +
:data-hidden to collapse, x-h-toolbar-title truncation needs shrink-0, the
richer shell (dashboard/sections/breadcrumb/user-bell-theme/notifications), and
widgetSize as an integer column count.

engine-intent CLAUDE.md (Done): model-driven dashboard + per-deployment branding
(.settings branding > intent icon/name/desc > defaults; appIcon param) +
dashboard exclude flags (entity dashboardWidget / report dashboard + description)
+ widgetSize integer + IntentNaming.pluralize.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant