CLAUDE.md is now committed project guidance for future Claude Code sessions; CLAUDE.local.md stays gitignored for machine-local notes.
8.9 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
A Nextcloud app (workflow_deck_automation, namespace WorkflowDeckAutomation) targeting Nextcloud Hub 26 Spring (server 34.x). It lets each user configure, from their personal settings, automation "workflows" that move overdue Deck cards from one stack to another, with optional assigned-user/label filters and an email notification. A background TimedJob evaluates all enabled workflows every 5 minutes.
Non-negotiable architectural constraint
No HTTP/OCS calls against Deck, anywhere. Every read (boards, stacks, cards, labels, assigned users) and the card move itself must go through Deck's own internal PHP classes (OCA\Deck\Service\CardService, StackService, BoardService, OCA\Deck\Db\CardMapper, …), resolved in-process via \OCP\Server::get(). This was an explicit, repeated product requirement from the user — do not "fix" it by switching to IClientService/curl against /ocs/..., even though that would be the more conventional cross-app integration approach.
All Deck access is funneled through lib/Service/DeckIntegrationService.php — it is the only class that references OCA\Deck\*. If you need new Deck data, extend that class rather than reaching into Deck internals from elsewhere. Every call in there is wrapped (class_exists() checks, IAppManager::isEnabledForUser('deck', …), try/catch → DeckUnavailableException) because OCA\Deck\* is not a documented/stable public API — it has no @since markers and can change between Deck releases without notice.
Per-user impersonation in the background job
Deck's ACL/permission checks read the live IUserSession, not a value frozen at construction. RunWorkflowsJob (a TimedJob) has no logged-in user by default and must evaluate workflows belonging to many different users within one PHP process. lib/Service/WorkflowRunner.php therefore groups workflows by owner and, per owner, temporarily impersonates them (IUserSession::setUser(), restored in a finally) before touching Deck. Don't remove this — without it, Deck's permission checks inside the cron run would be evaluated against no user (or the wrong user).
The filter-matching logic (WorkflowRunner::cardMatchesFilters(), ::isOverdue()) is deliberately static and side-effect-free so it's unit-testable without a real Deck installation — see tests/Unit/Service/WorkflowRunnerFilterTest.php.
Registration is declarative, not Bootstrap-based
Background jobs and classic personal settings are registered in appinfo/info.xml (<background-jobs>, <settings><personal>/<personal-section>), not via IRegistrationContext in lib/AppInfo/Application.php — that interface has no registerBackgroundJob()/registerSettings() methods on NC 34. Application.php is intentionally near-empty.
Commands
PHP and npm are not available in the local dev sandbox this repo is normally edited from. Do not try to run composer, php, or npm locally to verify changes — they will fail with "command not found". Verification happens by pushing to main (or opening a PR) and checking the Gitea Actions run; the self-hosted runner tag is gitea-runner-server03 (see .gitea/workflows/*.yml, all pinned to that runs-on: label).
Commands as they'd run in CI/on a machine that has the tools:
composer install
composer run lint # php -l over lib/ and tests/
composer run cs:check # nextcloud/coding-standard (php-cs-fixer); composer run cs:fix to auto-fix
composer run test:unit # PHPUnit — pure logic only, no live Deck/Nextcloud instance needed
npm install # no package-lock.json is committed (gitignored) — use install, not `npm ci`
npm run build # production build
npm run watch # rebuild on change during development
To run a single PHPUnit test: vendor/bin/phpunit --filter testUserFilterIsOrAgainstAssignedUsers tests/Unit/Service/WorkflowRunnerFilterTest.php.
There is no meaningful way to test the Deck integration itself outside a real Nextcloud+Deck instance (php occ background-job:worker workflow_deck_automation) — the unit tests intentionally stop at the pure filter logic.
Release process
- Bump
<version>inappinfo/info.xmlandversioninpackage.json(keep them in sync). - Commit, push to
main, confirm the lint/phpunit pipelines are green. git tag vX.Y.Z && git push origin vX.Y.Z— this triggers.gitea/workflows/build-release.yml, which builds the frontend and runsmake appstore, producingbuild/artifacts/appstore/workflow_deck_automation.tar.gzas a workflow artifact (not an attached Gitea Release asset — that would need an additional API step with a repo token, not currently wired up).
If the tagged build fails and no artifact was ever produced, fix the issue on main and move the existing tag to the fixed commit instead of bumping to a new version number (git tag -d vX.Y.Z && git tag -a vX.Y.Z -m "..." && git push origin :refs/tags/vX.Y.Z && git push origin vX.Y.Z) — this is how v0.1.0 was handled through several build-release.yml fixes before it first built successfully. Once a version has actually produced a published artifact, don't move its tag anymore; bump normally instead.
Data flow / architecture map
- UI:
src/PersonalSettings.vue(Vue 3, mounted fromtemplates/settings/personal.phpinto the section registered bylib/Settings/PersonalSection.php+lib/Settings/Personal.php) talks tolib/Controller/WorkflowController.php(anOCSController) viasrc/api.js, hitting OCS routes declared inappinfo/routes.php(/ocs/v2.php/apps/workflow_deck_automation/api/v1/...). - Storage:
lib/Db/Workflow.php(Entity) /lib/Db/WorkflowMapper.php(QBMapper) over thewfda_workflowstable, created inlib/Migration/Version1000Date20260813120000.php. Filter fields (filter_user_ids,filter_label_ids) are stored as JSON-encoded arrays in text columns, not join tables. - Automation:
lib/BackgroundJob/RunWorkflowsJob.php(5 minTimedJob) →lib/Service/WorkflowRunner.php(per-user impersonation + filtering, both filters OR-within-themselves and AND-between-each-other) →lib/Service/DeckIntegrationService.php(all actual Deck class calls) andlib/Service/NotificationMailer.php(email viaOCP\Mail\IMailerto the workflow owner's account address, never a manually-entered address). - Both the controller's board/stack/label/participant lookups (for populating the settings UI dropdowns) and the runner's card reads/moves go through the same
DeckIntegrationService— there is no separate read path.
Composer/npm lockfiles are intentionally not committed
.gitignore excludes composer.lock and package-lock.json. This means CI must use composer install/npm install, not composer install --no-dev assumptions tied to a lock file or npm ci (which hard-requires a lock file and will fail otherwise — this has already broken build-release.yml once). If you add a lockfile-dependent step, either commit the lockfile deliberately or keep using the non-ci/non-locked install form.
Frontend build gotchas already hit (don't reintroduce)
None of these were discoverable locally — there's no npm here (see "Local-only notes" below), so all three were only caught by an actual Gitea Actions run against build-release.yml:
viteversion must satisfy@nextcloud/vite-config's peer requirement.package.jsonpins@nextcloud/vite-configto^2.2.0, which currently resolves to2.5.4and peer-requiresvite@^7.3.6. If you bump@nextcloud/vite-config, check itspeerDependencies.viteand bump ourvitedevDependency to match, ornpm installfails withERESOLVE.package.jsonneeds"type": "module".vite.config.jsusesimport/exportsyntax and@nextcloud/vite-configis ESM-only; without"type": "module", Node treats.jsas CommonJS andvite buildfails trying torequire()an ESM-only package.- A
tsconfig.jsonmust exist at the repo root, even though this project has no TypeScript source.@nextcloud/vite-config'sindex.jsbarrel statically re-exportscreateLibConfigfromlibConfig.js, which importsvite-plugin-dtsat module scope — that import chain runs just from importingcreateAppConfig, regardless of whethercreateLibConfigis ever called.vite-plugin-dts/@volar/typescriptcrash (Cannot read properties of undefined (reading 'useCaseSensitiveFileNames')) while loadingvite.config.jsif there's no tsconfig for it to resolve. The minimaltsconfig.jsonin this repo exists solely to satisfy that, not because we write TypeScript.
Local-only notes
CLAUDE.local.md (gitignored) carries session-specific environment notes (e.g. "no PHP/npm available locally"). Check it at the start of work in this repo — it's not duplicated here since it can change independently of the committed guidance.