Skip to content

T2: Minutes and Decisions Core Features#116

Closed
rubenvdlinde wants to merge 767 commits into
mainfrom
feature/73/p2-minutes-and-decisions-core-t2
Closed

T2: Minutes and Decisions Core Features#116
rubenvdlinde wants to merge 767 commits into
mainfrom
feature/73/p2-minutes-and-decisions-core-t2

Conversation

@rubenvdlinde
Copy link
Copy Markdown
Contributor

Closes #73

Summary

Implements T2 collaboration layer: notification subscriptions, version control, approval workflow, portal publication, search integration, and reference provider.

Implementation Complete

✓ Backend services (DecisionNotificationService, MinutesVersionService, MinutesApprovalService, DecisionService)
✓ REST controllers and routes
✓ Frontend components (DecisionDetail, MinutesDetail, MinutesVersionPanel)
✓ Translations (EN/NL)
✓ Unit tests for all services
✓ Search provider and reference provider

rubenvdlinde and others added 30 commits April 14, 2026 17:05
…ment

feat: meeting lifecycle state machine (p2-meeting-management)
…and security reviews (#18)

- appinfo/info.xml: remove duplicate <background-jobs> blocks (three → one)
- appinfo/routes.php: rename route voting#create → voting#open to match VotingController::open()
- src/router/index.js: remove duplicate const declarations (MotionDetail, AmendmentDetail) and duplicate routes
- tests/Unit/Service/VotingServiceTest.php: fix mock addMethods (getObject/findObjects instead of find/findAll), remove motionService from VotingService constructor call, add missing meetingId arg to openVotingRound, fix exception messages to Dutch, fix grantProxy exception class to InvalidArgumentException, rewrite closeVotingRound test to verify motion lifecycle transition
- lib/Controller/MotionController.php: add requireChairOrSecretary() guard to coSignRequest and budgetImpact; always derive displayName from authenticated session in coSignConfirm (prevent identity spoofing)
- lib/Service/MotionService.php: add relations.motion filter to detectConflicts findAll query (prevent full-table scan)
- src/components/VotingRoundPanel.vue: fix isRoundOpen to compare closedAt as Date (allow future deadline rounds to remain open)
- lib/Service/OriPublicationService.php: replace file_get_contents with IClientService for HTTP (SSRF defence, timeout control); add URL validation (HTTPS-only, no RFC-1918/loopback); track oriPublishedAt on publish success so getPublicationStatus distinguishes published from merely closed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ew + Security Review (#17)

Code Review CRITICALs:
- DecisionDetail.vue: add motion relation card (task 6.2)
- DecisionDetail.vue: add text property to propertyItems (task 6.2)
- ActionItemDetail.vue: add decision + meeting relation cards (task 7.2)
- ActionItemDetail.vue: add description to propertyItems (task 7.2)

Code Review WARNINGs:
- MinutesGenerationService: resolveMeeting() re-throws fetch exception as RuntimeException(503)
- MinutesGenerationService: transition() returns saveObject() return value (not stale local array)
- MinutesGenerationService: fetchRelatedObjects() uses paginated loop (matches OverdueActionItemsJob)
- MinutesControllerTest: add testTransitionWhenOpenRegisterUnavailableReturns503() (task 9.3)

Security Review WARNINGs:
- MinutesGenerationService: enforce OpenRegister session-based ACL via setRegister/setSchema+find(id:)
  in both generateDraft() and transition() — matches MeetingService pattern (OWASP A01)
- Add DecisionController::publish() with server-side admin check + outcome/isPublished validation
  (OWASP A01 / ADR-005 frontend-only bypass fix); update decisions.js + DecisionDetail.vue
  to call POST /api/decisions/{id}/publish instead of generic CRUD PUT
- appinfo/info.xml: remove duplicate <background-jobs> blocks (triple-register bug)
- appinfo/routes.php: rename voting#create → voting#open to match controller method
- src/router/index.js: remove duplicate const declarations and duplicate route entries
- lib/Controller/MotionController.php: add requireChairOrSecretary() guard to coSignRequest
  and budgetImpact; always derive displayName from session in coSignConfirm (prevent spoofing)
- lib/Service/MotionService.php: add relations.motion filter to detectConflicts() findAll call
- lib/Service/OriPublicationService.php: fix getPublicationStatus() to check oriPublishedAt
  instead of closedAt; stamp oriPublishedAt on successful publish; add SSRF validation
  (HTTPS-only, block RFC-1918/loopback addresses)
- src/components/VotingRoundPanel.vue: fix isRoundOpen to compare closedAt date vs now
- tests/Stubs/ObjectService.php: add stub with correct named-parameter signatures
- tests/bootstrap-unit.php: load ObjectService stub for unit tests
- tests/Unit/Service/VotingServiceTest.php: fix constructor call (remove nonexistent
  motionService param); fix openVotingRound arg count; fix Dutch exception messages;
  update mock from stdClass/addMethods to ObjectService stub; fix findObjects return structure
- tests/Unit/Service/MotionServiceTest.php: update mock to ObjectService stub;
  fix saveObject with() callback positions to match named-parameter call order
- src/views/AmendmentDetail.vue: fix unnecessarily quoted Accept header key (ESLint)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AgendaController: return HTTP 401 (not 403) for unauthenticated requests
- AgendaController: wrap entire advanceBobPhase body in try/catch(\Throwable)
- AgendaService: normalize participant objects via toArray() before array access
- phpstan.neon: scope broad wildcard ignore to specific \$calendarEventService
- tests/integration/agenda.json: add 401 unauthenticated test per endpoint
- appinfo/routes.php: remove dead metrics#index and health#index routes
- SettingsController: add #[NoAdminRequired] PHP 8 attribute to index()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove duplicate oriPublishedAt stamp block and redundant SSRF validation
in getEndpoint() introduced by merge — isValidOriEndpoint() already covers
the check and is called from publish(). Resolve trivial whitespace conflicts
in routes.php, MotionController.php, and MotionService.php.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…and security reviews (#18)

- [CRITICAL] Rename route voting#grantProxy → voting#proxy to match VotingController::proxy() method name; prevents 404 on grant-proxy endpoint
- [CRITICAL] Fix integration test assertions for open (201) and cast (201) voting-round endpoints; was checking for 200
- [WARNING] Add requesttoken CSRF header to AmendmentDetail.vue transition() fetch; matches MotionDetail.vue pattern
- [WARNING] Populate participantCount in VotingRoundPanel.fetchCurrentRound() by fetching participants for the meeting in parallel with the round fetch
- [WARNING] Delete src/views/MotionIndex.vue — unreachable duplicate of Motions.vue (not imported in router)
- [WARNING] Fix SSRF bypass in OriPublicationService.isValidOriEndpoint(): return false when gethostbyname() fails to resolve instead of allowing through
- [WARNING] Omit participant relation from Vote objects when isSecret=true to preserve secret ballot anonymity (VotingService.castVote)
- [WARNING] Validate participantId against OpenRegister before castVote() in MailReplyHandler to prevent unverified _mail metadata from casting votes (OWASP A07:2021)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nts (#17)

Extends the security fixes from iteration 3 (which already added the
DecisionController publish endpoint and server-side validation):

- MinutesController: generateDraft() now requires admin (prevents cross-tenant
  information disclosure — OWASP A01 / ADR-005)
- MinutesController: RESTRICTED_TRANSITIONS now includes 'review', so ALL
  lifecycle transitions require admin (not just approved/signed/published)
- MinutesControllerTest: add isAdmin=true mock to all generateDraft tests
  that expect the service to be reached (tests were silently passing before
  because the admin check did not exist)
- MinutesControllerTest: add testGenerateDraftByNonAdminReturns403()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ons' into feature/17/p2-minutes-and-decisions
…reviews (#15)

- AgendaController: return HTTP 401 (not 403) for unauthenticated requests in requireChairOrAdmin
- AgendaController: wrap full advanceBobPhase body in try/catch with \Throwable fallback matching other methods
- AgendaService: normalize participant objects via toArray() in publishAgenda loop, matching all other data loops
- phpstan.neon: scope unused-property ignore to $calendarEventService only; remove wildcard that suppressed all AgendaService property warnings
- SettingsController: add #[NoAdminRequired] PHP attribute to index() alongside existing @NoAdminRequired docblock
- tests/integration/agenda.json: add 401 (unauthenticated) and 403 (unprivileged user) test cases for all four protected endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolved merge conflicts with existing fix iteration 5 commit on remote.
Added 403 (unprivileged user) test cases alongside 401 (unauthenticated)
tests for all four protected endpoints — both scenarios now covered per
the reviewer's requirement to test requireChairOrAdmin at integration level.
Retained meeting#lifecycle route from development branch merge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rubenvdlinde and others added 21 commits April 19, 2026 07:50
* feat: Add OpenSpec change p2-minutes-and-decisions-core-t3 from Specter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: Add OpenSpec change p2-minutes-and-decisions-core-t3 from Specter

* chore: add hydra.json for p2-minutes-and-decisions-core-t3

* chore(spec): mark p2-minutes-and-decisions-core-t3 as extension of p2-minutes-and-decisions

* chore(spec): mark p2-minutes-and-decisions-core-t3 as extension of p2-minutes-and-decisions

* chore(spec): mark p2-minutes-and-decisions-core-t3 as extension of p2-minutes-and-decisions

---------

Co-authored-by: Specter Intelligence <specter@conduction.nl>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: Add OpenSpec change p2-minutes-and-decisions-other-t1 from Specter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: Add OpenSpec change p2-minutes-and-decisions-other-t1 from Specter

* chore: add hydra.json for p2-minutes-and-decisions-other-t1

* chore(spec): mark p2-minutes-and-decisions-other-t1 as extension of p2-minutes-and-decisions

* chore(spec): mark p2-minutes-and-decisions-other-t1 as extension of p2-minutes-and-decisions

* chore(spec): mark p2-minutes-and-decisions-other-t1 as extension of p2-minutes-and-decisions

---------

Co-authored-by: Specter Intelligence <specter@conduction.nl>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: Add OpenSpec change p2-minutes-and-decisions-other-t2 from Specter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: Add OpenSpec change p2-minutes-and-decisions-other-t2 from Specter

* chore: add hydra.json for p2-minutes-and-decisions-other-t2

* chore(spec): mark p2-minutes-and-decisions-other-t2 as extension of p2-minutes-and-decisions

* chore(spec): mark p2-minutes-and-decisions-other-t2 as extension of p2-minutes-and-decisions

* chore(spec): mark p2-minutes-and-decisions-other-t2 as extension of p2-minutes-and-decisions

---------

Co-authored-by: Specter Intelligence <specter@conduction.nl>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: Add OpenSpec change p2-motion-and-voting from Specter

* chore: add hydra.json for p2-motion-and-voting

---------

Co-authored-by: Specter Intelligence <specter@conduction.nl>
* feat: Add OpenSpec change p2-motion-and-voting-core-t1 from Specter

* feat: Add OpenSpec change p2-motion-and-voting-core-t1 from Specter

* chore: add hydra.json for p2-motion-and-voting-core-t1

---------

Co-authored-by: Specter Intelligence <specter@conduction.nl>
* feat: Add OpenSpec change p2-motion-and-voting-core-t2 from Specter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: Add OpenSpec change p2-motion-and-voting-core-t2 from Specter

* chore: add hydra.json for p2-motion-and-voting-core-t2

---------

Co-authored-by: Specter Intelligence <specter@conduction.nl>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: Add OpenSpec change p2-motion-and-voting-core-t3 from Specter

Adds proposal, design, specs, and tasks for Motion and Voting Core T3:
motion execution tracking (lifecycle extension: execution-pending →
executing → executed), vote anonymisation (GDPR/AVG compliance),
quorum calculator service + API endpoint, and written/circular
resolution approval (BW 2:238). No ADR-000 schema changes — all
features use existing entity fields and built-in OpenRegister mechanisms.

* feat: Add OpenSpec change p2-motion-and-voting-core-t3 from Specter

* chore: add hydra.json for p2-motion-and-voting-core-t3

---------

Co-authored-by: Specter Intelligence <specter@conduction.nl>
* feat: Add OpenSpec change p2-motion-and-voting-other-t1 from Specter

* chore: add hydra.json for p2-motion-and-voting-other-t1

---------

Co-authored-by: Specter Intelligence <specter@conduction.nl>
* feat: Add OpenSpec change p3-citizen-participation from Specter

* chore: add hydra.json for p3-citizen-participation

---------

Co-authored-by: Specter Intelligence <specter@conduction.nl>
* feat: Add OpenSpec change p4-integration from Specter

* chore: add hydra.json for p4-integration

---------

Co-authored-by: Specter Intelligence <specter@conduction.nl>
* feat: Add OpenSpec change p4-reporting-and-analytics from Specter

* chore: add hydra.json for p4-reporting-and-analytics

---------

Co-authored-by: Specter Intelligence <specter@conduction.nl>
* feat: Add OpenSpec change p4-standards-hardening from Specter

* chore: add hydra.json for p4-standards-hardening

---------

Co-authored-by: Specter Intelligence <specter@conduction.nl>
…issues

Hydra's supervisor dep-check walks hydra.json's 'issue' field to verify
a dependency is merged (closed on GitHub). Two P2 umbrella specs had
`issue: null` even though matching issues exist and are closed:

- p2-minutes-and-decisions → #17 (closed 2026-04-14 as COMPLETED)
- p2-motion-and-voting     → #72 (open; required for tiered impls to
                                   unblock, and we want it buildable)

Every tiered spec under these umbrellas (p2-minutes-and-decisions-*,
p2-motion-and-voting-*) was reporting 'deps not merged' and never
dispatching — purely because the dep resolver couldn't find an issue
to check.

No spec content changed. Only the 'issue' field in two hydra.json files.
fix(openspec): link p2 umbrella specs to their GitHub issues
Follow-up to #112. The intelligence DB and development hydra.json files
were missing issue linkage for three tier specs even though matching
GitHub issues exist:

- p2-meeting-management-core-t3    -> #61
- p2-meeting-management-other-t2   -> #42
- p2-minutes-and-decisions-core-t1 -> #21

When the umbrella specs (p2-meeting-management, p2-motion-and-voting)
eventually merge, the supervisor's dep-check walks down to these tier
specs. With issue:null they'd report 'deps not merged' forever, exactly
the block pattern PR #112 fixed for the umbrellas.

No spec content changed — only the 'issue' field in three hydra.json files.
fix(openspec): link 3 more p2 tier specs to their GitHub issues
…ydra (#115)

App repos should carry ONLY repo-specific ADRs (in openspec/architecture/),
not stale copies of hydra's org-wide ADRs. These copies had drifted —
adr-004-frontend.md in this repo still said 'fetch() not axios' while
hydra master says the opposite since commit e4cf8a2. That caused Hydra's
code reviewer on decidesk#71 to flag a real ADR contradiction.

Per user direction (2026-04-19): delete all per-repo copies of hydra's
org-wide ADRs. Reviewer + builder containers already COPY the relevant
ADRs from hydra into the image at build time — agents operating in the
repo outside a container should read hydra master directly.

openspec/architecture/ stays — that's where repo-specific ADRs (authored
by Specter during research) should live.
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 642290f

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 215/215
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-19 14:55 UTC

Download the full PDF report from the workflow artifacts.

…subscriptions, version control, approval workflow, and portal publication (#73)
@rubenvdlinde rubenvdlinde force-pushed the feature/73/p2-minutes-and-decisions-core-t2 branch from 44289af to eeaee86 Compare April 19, 2026 15:07
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 0678713

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 416/416
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-19 15:08 UTC

Download the full PDF report from the workflow artifacts.

…control, approval workflow, and portal publication (#73)

- Add notification subscription toggle to Decision and Minutes detail views
- Create MinutesVersionPanel component for version history and diff viewing
- Implement Minutes approval workflow UI with dual sign-off (chair + secretary)
- Add Decision portal publication UI with share link management
- Add comprehensive Dutch and English translation keys for all new features
- Fix DecisionControllerTest to include DecisionService constructor parameter

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 6b00b0b

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 416/416
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-19 15:15 UTC

Download the full PDF report from the workflow artifacts.

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.

Implement: Minutes and Decisions — Core T2

1 participant