Skip to content

feat(time): the planned time entry is a satellite, not a third timeEntry - #575

Merged
rubenvdlinde merged 1 commit into
developmentfrom
feat/planned-time-entry-satellite
Sep 5, 2026
Merged

feat(time): the planned time entry is a satellite, not a third timeEntry#575
rubenvdlinde merged 1 commit into
developmentfrom
feat/planned-time-entry-satellite

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Why

Three apps declared a timeEntry. A schema slug is global per organisation and SchemaMapper::find() matches LOWER(slug), so whichever row was reached first answered for all three: humaniq's HR booking, pipelinq's billing record, and this app's project booking.

humaniq is the agreed owner of the hours. This app's record is the planning side of one — the task it was booked against, the contractor, the hourly rate — so it is named for what it is: plannedTimeEntry, carrying a timeEntry property naming the humaniq booking.

That reference is a plain uuid, not a $ref — humaniq's register is a different register, and ADR-062 rule 7 gives a cross-register target no $ref.

The migration

RenameTimeEntrySchemaSlug renames the row before InitializeSettings, modelled on pipelinq's step for the same collision (ConductionNL/pipelinq#1805). Without it the descriptor change renames nothing: the import matches by (application, slug) and its not-found branch creates a second schema, orphaning every existing entry silently while the app reads an empty list.

Registered in both <post-migration> and <install> — the install block is the one that actually runs here, for the reason info.xml already documents about the planixplanninq app-id change.

Eighteen sites move together

The register's schema list, its key and slug, six seedData "schema" references across the register and the mock, four manifest widget sources plus a schemaSlug, two stores, and the portal contribution provider.

Six existing tests asserted the old slug and now assert the new one — they are what caught the manifest and portal sites.

What this does not do yet, deliberately

date, duration, user and description stay here rather than moving onto the humaniq booking, because four dashboard widgets read duration, user and date straight off this schema. Moving them would make all four render nothing on an instance without humaniq — blank, with no explanation.

The fleet already has the pattern: a requiredApp widget, which gate-55 supports and which hides rather than rendering empty when the backing app is absent — and pipelinq already reads this app's project that way. Applying it here is the next change, recorded in #572, and it is what makes the hours live once.

The collision is cleared either way, which is what this change is for.

Testing

  • 186 tests green: 4 new, 6 updated.
  • phpcs, phpmd, psalm and phpstan clean via composer check:strict.
  • All 73 applicable hydra gates pass — after composer update conduction/hydra-gates (v1.14.0 → v1.15.0). The stale vendored copy invented two manifest failures; worth updating before diagnosing one.

🤖 Generated with Claude Code

Three apps declared a `timeEntry`. A schema slug is global per organisation and
`SchemaMapper::find()` matches `LOWER(slug)`, so whichever row was reached first
answered for all three: humaniq's HR booking, pipelinq's billing record, and
this app's project booking.

humaniq is the agreed owner of the hours. This app's record is the PLANNING side
of one — the task it was booked against, the contractor, the hourly rate — so it
is named for what it is: `plannedTimeEntry`, carrying a `timeEntry` property
naming the humaniq booking.

That reference is a plain uuid and not a `$ref`. humaniq's register is a
different register, and ADR-062 rule 7 gives a cross-register target no `$ref`.

`RenameTimeEntrySchemaSlug` renames the row before `InitializeSettings`,
modelled on pipelinq's step for the same collision (pipelinq#1805). Without it
the descriptor change renames nothing: the import matches by
(application, slug) and its not-found branch CREATES a second schema, orphaning
every existing entry silently while the app reads an empty list. Registered in
BOTH `<post-migration>` and `<install>` — the install block is the one that
actually runs here, for the reason the file already documents about the
`planix` -> `planninq` app-id change.

Eighteen sites move together: the register's schema list, its key AND `slug`,
six seedData `"schema"` references across the register and the mock, four
manifest widget sources plus a `schemaSlug`, two stores and the portal
contribution provider. Six existing tests asserted the old slug and now assert
the new one — they are what caught the manifest and portal sites.

WHAT THIS DOES NOT DO YET, and it is deliberate. `date`, `duration`, `user` and
`description` stay here rather than moving onto the humaniq booking, because
four dashboard widgets read `duration`, `user` and `date` straight off this
schema, and moving them would make all four render nothing on an instance
without humaniq — blank, with no explanation.

The fleet already has the pattern: a `requiredApp` widget, which gate-55
supports and which HIDES rather than rendering empty when the backing app is
absent — and pipelinq already reads THIS app's `project` that way. Applying it
here is the next change, recorded in #572, and it is what makes the hours live
once.

The collision is cleared either way, which is what this change is for.

186 tests green (4 new, 6 updated). phpcs, phpmd, psalm and phpstan clean, and
all 73 applicable hydra gates pass — after `composer update conduction/hydra-gates`
(v1.14.0 -> v1.15.0); the stale vendored copy invented two manifest failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rubenvdlinde
rubenvdlinde merged commit 90ef3ed into development Sep 5, 2026
27 of 28 checks passed
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/planninq @ 9862245

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-l10n-js
check-schema-l10n
composer ✅ 104/104
npm ✅ 536/536
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-05 13:00 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde added a commit that referenced this pull request Sep 5, 2026
#575 renamed `timeEntry` to `plannedTimeEntry` — three apps declared a
`timeEntry` and a schema slug is global per organisation. The seed's
required-schema list still asked for the old name.

That is the ninth hiding place a slug rename has, and the one that costs
the most: the seed exits 1 before Playwright starts, so the whole suite
is reported as NOT RUN rather than as failing. On development:

    ##[error]Planninq schemas missing after import: [timeEntry]
    ##[error]The e2e suite cannot seed a project, columns, tasks or labels
    ##[error]Seed command failed with exit 1

...while the line above it listed `plannedTimeEntry` among the 28 schemas
that had in fact imported.

Verified against `lib/Settings/planninq_register.json` on development:
every one of the six required slugs is declared.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
rubenvdlinde added a commit that referenced this pull request Sep 5, 2026
…580)

`Portfolio renders capacity` clicked a nav entry titled "Portfolio".
There is none: the report moved onto the Reports page as a card labelled
"Capacity" — ADR-112, a report is a card OR an entry, never both — so
the click waited out its 60s timeout on a locator that can never
resolve, which reads as a broken page.

This is the first failure this suite has ever reported. It never ran
before: the seed asked for the `timeEntry` slug that #575 renamed, exited
1, and took Playwright with it. 1 failed, 34 passed is the first real
tally.

`openReportCard()` sits beside `navigateTo()` and addresses the card by
its own testid and title span, NOT by the link's accessible name:
CnReportsPage wraps title, description and category in one anchor, so an
exact match on the label finds nothing. A liveness control goes first, so
a card that does not match reads as a missing card rather than a page
that never mounted.

Verified: the "Capacity" card names page Portfolio, which routes to
/portfolio — the URL the test still asserts.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Claude Opus 5 (1M context) <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