feat(events): Phase 2 — rename gRPC tavle types to board#803
Merged
Conversation
When clicking events near the top of the scrolled grid, slotTop could be very small or negative (getBoundingClientRect on a scrolled-away column). The overlay's close button was cut off above the viewport. Fix: clampAnchorY() ensures the anchor is always within 8px-margin of the viewport edges. Also added withFlexibleDimensions(true) to let CDK resize the overlay pane to fit within the viewport. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CDK's withPush/withFlexibleDimensions don't reliably prevent overlays from extending beyond the viewport, especially when using virtual anchor points. Added ensureOverlayInViewport() which runs after each overlay attach — measures the actual rendered bounding rect and applies a translateY correction if the top or bottom overflows. Applied to all 4 overlay attachment sites (3 create + 1 preview). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ight The previous setTimeout ran before Angular finished rendering. Now uses double requestAnimationFrame to ensure paint is complete, then: 1. Sets pane maxHeight to viewport - 16px (constrains content) 2. Measures actual rect after constraint 3. Shifts pane with translateY if top or bottom overflows This guarantees both close button and save button are always visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous approach applied translateY via transform, which CDK overwrites on any reflow (e.g. async eform template loading). Set pane.style.top directly and re-run on ResizeObserver events so content growth after initial attach still keeps pane in viewport. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mapRepeatType returned legacy values ('weekly', 'monthly', 'yearly')
that the edit dropdown never emits, leaving Gentag empty on existing
tasks. Align the CalendarRepeatRule union, mapRepeatType, and the
preview-modal label map with the dropdown's values
('weeklyOne', 'weeklyAll', 'monthlyDom', 'yearlyOne').
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Added 8 missing keys (Custom repeat, Repeat on, Ends, Never, On, After, occurrences, Done) so the calendar's custom-repeat modal renders in the user's language instead of falling back to English. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The create/update save subscribe silently ignored OperationResult with success=false, leaving the modal open with no feedback when preconditions (e.g. missing Logbøger folder for a fresh property, missing report headline) failed on the backend. Use ngx-toastr to show the server message, plus i18n keys 'Could not save the event' and 'Error'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new CI matrix slot 'm' that creates a property "Den glade gris <suffix>" + worker "foo bar" assigned to it, then saves a calendar event. Asserts the POST to /calendar/tasks returns success=true. When the reported silent-failure bug is present the test fails with the exact backend message (e.g. missing Logbøger folder), pinpointing which precondition needs fixing rather than guessing. Extends both dotnet-core-master.yml and dotnet-core-pr.yml test matrix from [a..l] to [a..l,m]. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous version used today's weekday and hour 10, but the calendar grid rejects clicks on past time slots. When CI ran late in the day (after 10:00 server time), the click was silently ignored and the create modal never opened, timing out at 600s. Navigate one week forward so every day is in the future, then click Monday 10:00 which is always safe. Also wait for #calendarEventTitle to appear before filling, so a genuine modal-open failure reports immediately instead of after 10 min. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CreateTask silently failed ("FolderIsRequired") on fresh properties
whose Logbøger area/folder wasn't provisioned during property creation.
Rewrites ResolveOrCreateLogbøgerFolderAsync to seed the Logbøger Area
(and its translations) from BackendConfigurationSeedAreas if missing,
then inlines the AreaProperty/Folder/ProperyAreaFolder/AreaRule
creation portion of BackendConfigurationPropertyAreasServiceHelper.Update
so we don't trigger its assignmentsForDelete side effect that would
destroy other active AreaProperties for the property.
Also: IsDisabled guard on seed lookup, LogError on missing seed,
FirstOrDefaultAsync on CheckListTranslations so missing eForms on
minimally-seeded DBs don't blow up rule creation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matrix l's calendar copy test failed consistently because
verifyEventExists() called .isVisible() synchronously, racing the
post-property-selection calendar re-render. Renamed to waitForEvent()
which awaits .waitFor({state:'visible'}) and returns false only on
timeout — call sites now include the missing title in the assertion
message for faster triage.
Matrix c's task-wizard.delete test failed intermittently because
several waitForTimeout(500) gaps between overlay interactions were
too tight for slow CI. Replaced every fixed sleep with explicit
visibility/hidden/count assertions, extracted the "open action menu,
click delete" sequence into a helper, gated the final row-disappear
assertion on the DELETE API response, and added dropdown-panel
count-0 waits between sequential ng-select interactions so the
second panel's visibility check doesn't match the first panel's
closing frame.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The worker create/edit modal's save button was gated on form.invalid, which stays true until the async language fetch populates the dropdown and patches the default languageCode. E2e tests polled the disabled state with a 30s timeout and failed intermittently when the async init lost the race — a silent click on an empty ng-select left the form permanently invalid. Add a formReady boolean set to true inside the getEnabledLanguages tap (on every emission, even API failure, so a broken backend surfaces as a clear missing-field assertion rather than a cryptic readiness timeout). Expose it on the root form as data-form-ready so tests can wait deterministically. Move the getEnabledLanguages call up near the top of ngOnInit (after form construction, safe against synchronously- replayed store values) so the HTTP request fires sooner. In the page object, after opening the create modal wait for data-form-ready=true before filling any field. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
c: Removed an incorrect toContainText(bindArea) assertion on the mat- select trigger after selecting a folder leaf — the trigger text does not reflect the selected folder in this form, so the assertion always timed out. The subsequent test steps succeed without it. l: The copy-event test asserted event visibility after only waiting on the folder-dtos response, but the events themselves are fetched by a separate POST /calendar/tasks/week. Added a waitForResponse for that request before asserting the event renders, so the 10s waitForEvent window starts from a known-complete fetch rather than racing it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous formReady only waited for languages to load. Tests still hit
flaky #saveCreateBtn disabled timeouts because the valueChanges
cascades on enableMobileAccess/webAccessEnabled/archiveEnabled each
re-ran updateEmailValidation() synchronously, leaving form.invalid
briefly true through several microtasks.
Replace the one-shot languages flag with a two-phase gate:
1. languagesLoaded$ (ReplaySubject<void>(1)) emits once getLanguages
returns, whether success or failure.
2. A formReadyWatcher$ subscription waits for that emission, then
subscribes to form.statusChanges with startWith(form.status),
debounceTime(200), filter(status !== 'PENDING'), first() —
flipping formReady exactly once when the form reaches a steady
non-pending state.
Also wrap the three email-toggle valueChanges subs with
debounceTime(50) so rapid programmatic fill() sequences coalesce
instead of each pushing the form through PENDING.
data-form-ready='true' now means "every synchronous init cascade has
settled", so e2e tests can fill and save without racing the form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The copy-event test still times out at 10s even after waiting on /calendar/tasks/week response. Two changes to diagnose + cover the simpler causes: - Log the week-tasks payload count/titles after the response arrives so we can see whether the event is actually in the backend data (rules out test 1 silently failing to persist) vs. pure UI render delay. - Bump the waitForEvent timeout from 10s → 30s. On slow CI runners the full calendar re-render after a fresh /tasks/week fetch may exceed 10s while still being under a sane upper bound. If the log shows the event is in the payload but it still times out at 30s, we have a real rendering bug to chase. If the log shows the event is missing, test 1 has a silent save-persistence issue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fetch
The previous run showed week-tasks count=0 after test 1 successfully
created the event. Expand diagnostics to distinguish three hypotheses:
1. Wrong captured request — Playwright's waitForResponse may have
matched an early pre-selection /tasks/week call.
2. UI filter excluding the event — activeBoardIds or other filter
narrowing the visible set.
3. Silent persistence failure — test 1's success=true was misleading.
E1: Log captured request URL + postData + full response body, plus
the parsed count/titles.
E2: From within the page context, fetch /tasks/week directly with
the property from the captured request and WIDE-OPEN filters
(empty boardIds/tagNames/siteIds). Log the result.
If E1 reveals a stale capture (propertyId missing/zero), we fix the
predicate. If E2 returns the event but the captured UI response
didn't, it's a filter bug. If E2 also returns 0, persistence is
the real issue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously a calendar event saved with no assignees was accepted (success=true) but then hidden by GetTasksForWeek's x.Status filter (task-wizard downgrades Status to NotActive when Sites is empty). Net effect: users created events that immediately vanished from the calendar. The diagnostic run confirmed this with count=0 payload. Enforce "at least one assignee" in three places: 1. Backend (BackendConfigurationCalendarService.CreateTask + UpdateTask): return OperationResult(false, "AtLeastOneWorkerMustBeAssigned") when Sites is null or empty. New i18n key added across all 26 supported locales in Resources/localization.json. 2. Frontend (task-create-edit-modal.component.html): the Save/Gem button [disabled] also gates on assigneeControl.value.length > 0. New plugin i18n key 'At least one worker must be assigned' used for the button's tooltip. Auto-translate script propagated to all non-English locales. 3. Tests: l/calendar.spec.ts now creates a worker assigned to the test property before saving events (mirrors m's pattern), and l/calendar.page.ts's fillCreateModal selects the first available assignee (new id=calendarEventAssignee on the mtx-select). Dropped the E1/E2 diagnostic block — we have the answer. afterAll now clears workers before properties. Task-wizard code is unchanged per user directive. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Assignee requirement is now enforced; test 1 creates the event with status=200 success=true, but test 2's waitForEvent still times out. With Status=Active guaranteed, the event should be in the /tasks/week payload. Log the captured request body, full response (first 1000 chars), and a direct-fetch result with wide-open filters to pin down whether the event is in the payload, filtered out, or missing entirely. Remove after the root cause is confirmed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Screenshot from the diagnostic run showed the event IS rendered on the calendar — so the locator 'app-calendar-task-block' (the Angular component tag) wasn't matching. Switch to .task-block (the actual rendered inner class, same selector I verified by hand earlier). Remove the E1+E2 diagnostic logging now that the root cause is known: the backend payload contains the event, the DOM contains the event, only the test selector was wrong. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
l copy-event assertion used toContain('Copy of') but the admin user's
locale is Danish ('da'), so the modal shows 'Kopi af Event-xxx'.
Switch to asserting the original title is still contained plus a
length-greater check — locale-agnostic and still verifies the copy
semantics (prefixed with SOMETHING, original title preserved).
afterAll cleanups were failing on worker-delete action-menu
interactions and aborting the whole suite AFTER the actual tests had
passed. Wrap clearTable calls in .catch so cleanup issues log as
non-fatal warnings instead of failing the matrix slot. Test matrices
get ephemeral DBs per job so leftover rows don't contaminate
anything.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
l: the post-copy visibility assertion still hardcoded "Copy of",
which fails in Danish locale ("Kopi af"). The copy save itself
passes (status=200, success=true, planning tag + eform preserved).
Switch the assertion to wait for any event containing the original
title — the locale-specific prefix is tested separately at the modal
title step.
l + m afterAll: wrap cleanup in Promise.race with a 60s cap so a
hung action-menu interaction in worker-delete never blocks the suite.
Any error inside cleanup is caught and logged as non-fatal. Each
matrix slot runs against an ephemeral DB so leftover rows can't
contaminate other jobs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After saveModal in the copy flow the modal closes, loadTasks refetches /calendar/tasks/week, and the grid re-renders — more ground to cover than the initial render. The post-copy visibility check was hitting the 10s default right as the re-render completed. Bump to 20s, same headroom the earlier check had before cleanup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… from compliance - Extend calendar.proto CalendarTaskItem with eform_id (21) and sdk_case_id (22) - Add SdkCaseId property to CalendarTaskResponseModel - Map EformId and SdkCaseId in CalendarGrpcService - Map compliance.MicrotingSdkCaseId in calendar service for compliance tasks Enables the Flutter app to call ReadComplianceCase with the SDK case ID to render real eForm content in task detail pages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When CI runs after the hour the test clicks (e.g. run at 10:31, click-target 10:00), the calendar's onCellClick rejects the past-time click and the modal never opens — locator.fill on #calendarEventTitle then blocks until the 10-min test timeout. Apply the same fix already used in m/: navigate one week forward with the chevron_right nav, then click Monday 10:00. All 7 days are in the future by construction so the fill can't race the wall clock. Test 2 navigates forward too so test 1's event (now in next week) is visible, waiting for the corresponding tasks/week POST. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After a copy operation two task-blocks contain the same title substring (the original and the 'Kopi af …' copy). Playwright's strict-mode default rejects locators with multiple matches, so the waitFor throws silently (returned false via our try/catch, looked like a plain timeout). Use .first() so the wait only cares that AT LEAST ONE matching event is visible — sufficient for our assertion (either the copy or the original must be rendered, and after save both exist). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add eform_id and sdk_case_id to calendar task proto
Two bugs on the event-modal date input: 1. Displayed '4/21/2026' (US short) instead of 'Mandag, 21. april' (weekday, day, month in Danish). 2. Clicking the input opened an empty mini calendar — month grid rendered with no day numbers. Both symptoms traced to MatNativeDateModule being imported in CalendarModule: it shadows the app-global EformMatDateFnsDateModule adapter, so the datepicker used the native adapter (no locale awareness → empty grid, US format). Fix: - Drop MatNativeDateModule from CalendarModule imports; rely on the global date-fns adapter that AuthStateService already drives with the user's locale (customDaLocale for Danish). - Add a module-scoped MAT_DATE_FORMATS provider overriding display.dateInput to 'EEEE, d. MMMM'. Keep parse.dateInput as 'P' so manual typing still accepts short dates. Scope is module-local so other plugins' datepickers keep the global short format. With customDaLocale loaded, EEEE → 'Mandag', MMMM → 'april', producing 'Mandag, 21. april'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously clicking an empty slot snapped to the nearest 15-minute position, which was too fine-grained — a click slightly above 10:00 could land on 9:45 or 10:15 unpredictably. Switch the week-grid and day-column click handlers to round to the nearest 30 minutes. Drag/resize handlers keep the 15-minute grid (users who explicitly pick up an event often want finer control), and the edit modal's start/stop time inputs remain unconstrained — users can still set any time when editing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
onNavigate shifted by 7 days regardless of view mode, which is wrong for day and list/schedule views — users expect one-day steps there. Gate the shift amount on viewMode: 7 for 'week', 1 for 'day' and 'schedule'. When switching FROM week view TO day or list view the user should land on today, not on an arbitrary day of whichever week they were viewing. Reset currentDate to today on that transition. onViewModeChange also now calls loadTasks so the freshly-scoped range is fetched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eteOpgave" This reverts commit ddd909d.
The angular GET /api/backend-configuration-pn/compliances/cases?id&templateId returns a ReplyElement built by SqlController.CheckRead, which materialises every Field's FieldValue and runs them through ReadFieldValue. For Number and NumberStepper fields, ReadFieldValue rewrites NULL → "" before JSON serialisation (eform-sdk SqlController.cs lines 2217-2231) — Date does the same (lines 2233-2247) but the round-trip parser rejects "" so it never emits a write pair. Other types keep NULL on the wire. Angular's PUT then runs CaseUpdateHelper.GetFieldValuesByRequestField (eFormApi.BasePn CaseUpdateHelper.cs lines 95-103). It emits a "[fieldValueId]|" pair for every Number whose Value is non-null on the wire — which after the GET-case rewrite includes every NULL Number FieldValue. Core.CaseUpdate (Core.cs lines 1649-1654) then writes "" to those FieldValues and PnBase.Update emits a Version row. Mobile's CompleteOpgave was skipping FieldValues entirely on empty-complete, so the parity-harness s3 scenario consistently showed: - 420_SDK.FieldValueVersions: row only in ANGULAR - 420_SDK.FieldValues pk=N: Value angular="" mobile=null The previous commit (reverted in 45b0dc4) tried to fix this by writing "" to ALL NULL FieldValues for the case, which over-fired and clobbered non-Number FVs that angular deliberately leaves untouched (regressing s2/s5 in the harness). This change mirrors the EXACT canonical filter: - Field is in the case's CheckList tree (Field.WorkflowState != Removed, Field.CheckListId IN (case CheckList ∪ subtree CheckLists)) - FieldValue.CaseId == foundCase.Id - FieldValue.Value IS NULL - FieldValue.WorkflowState != Removed - Field's FieldType is Number or NumberStepper Verified by parity-harness s2 / s3 / s5: all three scenarios produce byte-identical DB deltas across angular and mobile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ploadPhoto
Parity harness s_photo_upload_delete caught: mobile's UploadPhoto wrote
only to UploadedDatas + Cases.Custom JSON, while angular's AddNewImage
wrote to UploadedDatas + FieldValues. Net effect: a photo uploaded via
mobile was invisible to the angular admin (and vice versa) because the
read paths look at different storage.
Now mirrors angular's FieldValues write while keeping Cases.Custom for
the existing mobile read path (backward compat). Extension format
normalized to no-leading-dot ("png" not ".png") matching angular's
FileName.Split(".").Last() shape. FileName now follows angular's
two-phase Create-then-Update rename, and FileLocation is populated with
the same intermediate-path shape (Path.GetTempPath()/cases-temp-files
ticks) that angular writes — column metadata only; mobile remains
S3-only for the actual bytes.
Discovers the Picture-typed FieldId by walking the case's CheckList
descendant tree (BFS), matching the harness picker and the angular UI
which passes the fieldId from the rendered template.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lback can find retracted cases Two related defects surfaced during a live device test of compliance 9810 / case 17701 (a retracted missed-deadline rotation that completes correctly in the angular admin but failed from the mobile app): 1. ListOpgaver's ActionableOnly mode strips retracted compliances from compliancesInWeek but the recurrence loop re-emits the same logical row with ComplianceId=null. Device cached compliance_id=0 in Drift, so subsequent writes went out without IDs and hit the fallback. Fix: side-dict from the stripped compliances, populate ComplianceId + SdkCaseId on the recurrence-emit model. 2. The fallback fuzzy lookup in 4 write handlers excluded retracted cases (WorkflowState != Removed on validCaseIdsForSite), so any payload with compliance_id=0 could never resolve a missed-deadline compliance — even though the success path can revive the case. Fix: drop the Cases.WorkflowState filter; let the fallback find retracted cases. The PK branch and success path already handle revival correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CompleteOpgaveRequest now carries `repeated FieldValueWrite field_values` and `string comment`. Server applies the bundle AFTER case revival (WorkflowState='created') and BEFORE the closure cascade (PlanningCase/PlanningCaseSite Status=100 + core.CaseDelete soft-delete) — same lifecycle window the angular admin path uses (BackendConfigurationCompliancesService.Update lines 223-260). Bundle apply reuses the SetFieldValue handler's helpers verbatim: (caseId, fieldId) → FieldValues.Id lookup, CanonicalizeFieldValueAsync for CheckBox/Select normalization, single batched core.CaseUpdate + core.CaseUpdateFieldValues. Skips field_id <= 0 and missing FieldValue rows so legacy-cached fields don't fail the whole bundle. Comment apply mirrors SetComment's envelope-write shape — non-empty replaces OpgaverComment body verbatim; empty string is treated as "no change" so legacy clients pass through unchanged. Per-RPC SetFieldValue and SetComment handlers remain in place for legacy outbox rows still draining from older app builds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mobile's atomic-save bundle silently dropped writes when FieldValues rows hadn't been materialized for the case (case never read via GET /compliances/cases on the admin browser side). Angular's PUT runs after a GET that materializes via Core.CaseRead; mobile's ListTaskTracker doesn't trigger that path. CompleteOpgave now calls core.CaseRead before the per-field lookup, matching angular's lazy-materialization mechanism. Field values typed on the device now reach the SDK and surface in reports. Hotfix on top of the atomic-save commit (3a4c4db). Full convergence to angular's BackendConfigurationCompliancesService.Update tracked as a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mobile's CompleteOpgave was stamping DoneAt with the server's wall clock (DateTime.UtcNow / request.ClientTsUnix), which meant a worker completing a missed-deadline rotation produced reports dated today rather than the rotation's actual scheduled deadline. Now derives doneAtUtc = compliance.Deadline (with a != default fallback to DateTime.UtcNow for legacy / partially-populated rows, mirroring the existing pattern at lines 1681 / 1938 / 2734) once at the top of the closure and propagates to Case.DoneAt / DoneAtUserModifiable, PlanningCase.MicrotingSdkCaseDoneAt, PlanningCaseSite.MicrotingSdkCaseDoneAt, and the dayKey used for the post-completion calendar refresh. The bundled comment write keeps wall-clock semantics via a separate commentAtUtc local — the comment TsUnix tracks when the worker actually authored the comment on the device, which is genuinely distinct from when the rotation was scheduled (Deadline). Core.CaseUpdate / CaseUpdateFieldValues do NOT accept a doneAt parameter (SqlController.FieldValueUpdate writes Value only, no DoneAt stamp), so no changes are needed for FieldValue rows — they don't carry a DoneAt in this code path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Crash at report-table.component.ts:129 when caseField.value is null / undefined / empty for number+date columns: TypeError on value.replace and RangeError from parseISO(invalid). Mobile-completed cases sometimes carry empty FieldValues (per SDK convention) which the formatter wasn't guarding against. Coalesces value to '' at the top, skips number .replace and date parseISO when value is empty / invalid, adds explicit '' return for the 'unchecked' fall-through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous null-guard (commit 7d66aaa) used `?? ''` which only catches null/undefined — non-string values (numbers, booleans) passed through to parseISO which then threw "dateString.split is not a function". Now coerces via String(...) so any non-null value becomes a proper string before parseISO sees it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements PR-3 of the Google Drive integration: customer-side endpoints and services for receiving the OAuth envelope from the proxy, refreshing tokens via the proxy, and downloading attached Drive files into S3 via the existing UploadedData pipeline. - New endpoints under /api/backend-configuration-pn/google-drive: /start (build proxy URL with HMAC + nonce cookie), /oauth-finish (verify envelope JWT typ/nonce/user, persist GoogleOAuthToken with AES-GCM encrypted refresh token), /status (check connection), /attach (download Drive file and create AreaRulePlanningFile). - Services: GoogleDriveAuthService (envelope verification, AES-GCM at-rest crypto with zeroed-on-finally buffers, IMemoryCache for access tokens with proxy-supplied expiry), GoogleDriveFileService (Drive download mirroring UploadFile pattern; mime + size guards), OAuthProxyClient (typed HttpClient with HMAC-signed /refresh). - Nonce CSRF defense via IDataProtectionProvider-signed cookie bound to userId, single-use, 5-min lifetime, X-Forwarded-Proto aware Secure flag. - Strongly-typed GoogleDriveOptions with [Required, Url] and MinLength(32) annotations on the two key fields. - 4 new localization keys in 8 languages, plus 3 keys that were already referenced from BackendConfigurationCalendarService. - 8 integration tests using Testcontainers MariaDB + WireMock for proxy and Drive surfaces. WebViewLink is synthesized client-side from DriveFileId. - Spec updated: /google-drive/start now accepts ?date= query parameter (browser-driven flow can't set headers on top-level navigation); proxy v1.0.2 (deployed) implements both forms. Bumped Microting.EformBackendConfigurationBase 10.0.32 -> 10.0.33. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements PR-4: wires the placeholder "Add Google Drive file" link
in the calendar attach-file modal to the OAuth + Picker flow added
in PR-3, renders Drive badge + open-in-Drive link on attachment
rows, and adds a small backend /picker-token endpoint for the
Picker JS to call Drive API.
- BackendConfigurationPnGoogleDriveService (Angular wrapper) for
/status, /start, /picker-token, /attach.
- GoogleDriveOAuthFinishComponent — popup landing route, posts
same-origin pinned message back to opener and self-closes.
- Modal: dynamic gapi/picker JS load, OAuth popup with X-Frame
source check + opener.postMessage, mime filter pdf/png/jpeg
matching backend allowlist, single-use postMessage listener
cleaned up in ngOnDestroy, connecting flag prevents double-click
during OAuth dance, popup-closed watcher releases the flag.
- 11 i18n keys (auto-translated to 26 locales). The "Open in
Google Drive" key was renamed from an ambiguous "Open source"
phrasing that machine-translators interpreted as the FOSS idiom.
- Backend: localized GenericError for catch-all branches (was
leaking exception messages to clients), [Authorize]'d
/picker-token returning {accessToken, developerKey}, OAuthFinish
redirect points at the new google-drive-oauth-finish route.
Also fixes the integration-test SQL bootstrap snapshot to include
the v10.0.33 schema additions (DriveFileId/DriveModifiedTime/
GoogleOAuthTokenId on AreaRulePlanningFiles, plus four new tables
for GoogleOAuthToken/DriveWatchChannel + Versions). The previous
snapshot caused 10 schema-related test failures in CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nding Adds `int32 field_id = 7` to UploadPhotoMeta. When > 0 the handler binds the new FieldValue row to the client-specified Field.Id; when 0 it falls back to the prior BFS-first-Picture discovery so legacy outbox payloads in flight on existing devices keep working. Caller is already auth-scoped to the opgave's property via PropertyWorker check; out-of-tree field_id is at worst a self-inflicted misbinding the same worker can correct with another upload, so we don't reject. The Cases.Custom envelope write is unchanged. Mirrors angular's per-field photo flow: the angular admin's EFormFilesController.AddNewImage already takes (fieldId, caseId, file) and creates a FieldValue row with UploadedDataId. The mobile path now matches that contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements PR-5: ensures a Drive changes.watch subscription exists
after the first Drive-sourced attachment lands for a user, mints
an HS256 channel-token JWT (typ=channel) the proxy verifies on
fan-out, and adds the customer-side /notify endpoint that the
proxy POSTs to. Webhook is fire-and-forget per spec; PR-6's
reconcile cron is the safety net for missed deliveries.
- IGoogleDriveAuthService.EnsureWatchChannelAsync: idempotent on
attach (>24h reuse, ≤24h renew with old-row soft-delete first),
POSTs drive/v3/changes/watch with the proxy's /google-drive/notify
as the fan-out address, persists the response.
- GoogleDriveFileService.DownloadAndCacheFileAsync calls Ensure
for the first user-token-scoped Drive attachment. Watch failure
logged at Warning, file persists regardless.
- GoogleDriveController.Notify [AllowAnonymous]: HMAC verify
(canonical {channelId}|{resourceState}|{resourceId}|{messageNumber}|
{date}, ±2 min skew), JWT verify (sig + typ=channel + exp),
channel-row lookup (200 if stale per spec). Returns 200 fast,
no Drive calls.
- Spec PR-5 binding: presented JWT is FixedTimeEquals'd against
channel.SignedToken — defence-in-depth so future renewal paths
that reuse a ChannelId don't accept stale tokens.
- HMAC reference-vector test pins canonical-string + key + digest
to byte-exact values so a one-sided drift between proxy and
customer fails the test even if the test's own helper drifts
with the controller.
GoogleDriveOptions gains CustomerInstanceUrl for embedding in
the channel-token JWT (the proxy uses it to fan-out the webhook
back to the right customer instance).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements PR-7 of the Google Drive integration: a change processor that re-downloads modified Drive files into the existing UploadedData pipeline and handles the 404 / 403 lifecycle. Wired into PR-5's /notify webhook so each notification kicks the matching user's files on a fire-and-forget Task.Run with its own DI scope (the controller's request-scoped DbContext can't survive the await boundary). - GoogleDriveChangeProcessor.ProcessUserAsync iterates every Drive- sourced AreaRulePlanningFile for the user's active token, calls ProcessFileAsync per row, and tallies (Refreshed / Removed / NoChange / Errors). Token-revoked aborts the loop and inflates Errors by the remaining count so the invariant Refreshed+Removed+NoChange+Errors == input still holds. - ProcessFileAsync probes Drive metadata first; only re-downloads when modifiedTime > cached, then delegates to the existing RefreshFileAsync. 404 -> WorkflowState=Removed (file deleted by user). 403 -> same (permission revoked). 5xx -> Error and the reconcile cron retries. - Two new typed exceptions (DriveFileNotFoundException, DriveFilePermissionDeniedException) replace the previous bool return on RefreshFileAsync's metadata path so the processor can branch on outcome instead of HTTP-introspecting. - /notify now resolves the processor in a fresh DI scope and Task.Run's it, returns 200 immediately. Unhandled exceptions inside the background task are logged via ILogger; the host is not at risk. - Spec PR-7 step 4: structured Information audit log on every successful refresh (userId, driveFileId, file id, old/newModifiedTime, old/newSizeBytes) so PR-8's "last refreshed" tooltip and operator debugging both have a source. - 6 integration tests cover refresh, no-op, 404, 403, token-revoked, and a multi-file mixed-outcome tally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… comment
The CompleteOpgave handler was constructing the response Opgave via
inline `new Opgave { ... }` initializers and never populating Fields,
Attachments, Comment, EformId, ComplianceId, or MicrotingSdkCaseId.
Mobile's applyServerOpgave merges the response into Drift via
upsertOpgaver, which overwrites the cached form fields with the proto3
defaults (empty list, empty string), making form-field values
"disappear" from the UI on Complete.
Both response paths fixed:
- Main path (CompleteOpgave): refreshedTask branch now reuses the
same LoadEnvelopeByTaskIdAsync + LoadFieldsByTaskIdAsync +
PopulateAttachments helpers as ListOpgaver / ListTaskTracker /
LoadOpgaverAsync, so the response carries post-bundle truth.
- Idempotent path (BuildIdempotentCompleteOpgaveResponse): same
helper-call sequence applied. On outbox retries, the second
response no longer empties the cache the first response correctly
populated.
Order is preserved: bundle (CaseUpdate field values + comment write +
PlanningCase Status=100 + core.CaseDelete) is fully applied before the
response is constructed; the helpers re-read post-bundle state.
The synthesized "no longer actionable" branches (returned with empty
fields when refreshedTask is null) are unchanged — the mobile client's
applyServerOpgave drops the row from cache when proto.id is empty/0.
Known follow-up flagged by simplifier: the same `Opgave { ... } +
PopulateAttachments + Fields.AddRange` block now appears in 4 sites
(ListOpgaver, ListTaskTracker, LoadOpgaverAsync, CompleteOpgave). A
BuildOpgaveFromCalendarTask helper would prevent future drift; deferred
to a separate cleanup commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tors
Implements PR-8 (final PR) of the Google Drive integration: lets a
user disconnect a Google Drive account, surfaces connected accounts
on a dedicated settings page, and decorates calendar attachments
with last-refreshed timestamps + a Drive-disconnected badge when
the backing token is gone.
Backend:
- IGoogleDriveAuthService.DisconnectAsync(tokenId, currentUserId):
ownership check, clear in-memory access-token cache FIRST so any
in-flight thread cannot re-populate post-revoke, decrypt the
refresh token, POST to oauth2.googleapis.com/revoke (form-urlencoded
body so the token never lands in URL logs), best-effort POST to
drive/v3/channels/stop on every Created channel, soft-delete each
channel row, set RevokedAt on the token, clear the cache again
for defence-in-depth. Idempotent on Google's invalid_token reply
(treats as already-revoked success).
- GetAccessTokenAsync hot path now re-checks the DB for an active
token row before serving a cached access token — closes the race
where a cached token kept working for ~50 minutes after revoke.
- DELETE /google-drive/disconnect/{tokenId}, GET /google-drive/accounts
(active+revoked rows, ConnectedAt DESC) endpoints.
- CalendarTaskAttachmentDto gained LastRefreshedAt + DriveRevoked,
populated via .Include(...).ThenInclude(GoogleOAuthToken) in
MapAttachments — no N+1 on the calendar fetch.
- Navigation menu entry under Settings → "Connected Google Drive
accounts" so the page is discoverable from the UI (not just by
URL).
Frontend:
- BackendConfigurationPnGoogleDriveService.getAccounts/disconnect.
- GoogleDriveAccountsComponent (settings route): lists accounts
with email, connected date, last-used relative time, status
badge, and a Disconnect button on active rows that prompts a
confirm dialog before calling the API.
- Calendar attachment row now shows a red "Google Drive
disconnected" badge when f.driveRevoked, and a "Last refreshed N
ago" tooltip on every Drive row.
- 9 i18n keys added; auto-translated and hand-spotted IT/ES/NL/SV/
FR/DA/DE so "Drive" is preserved as the Google Drive brand
rather than mistranslated as "disk drive".
Tests:
- 4 new integration tests cover happy disconnect, Google-already-
revoked idempotency, cross-user authorization rejection, and
GET /accounts ordering.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…in case tree
Code-review found the new meta.FieldId > 0 path was looking up Fields by Id
without verifying (a) the field is Picture-typed and (b) it lives inside
the case's CheckList descendant tree. Both invariants were already enforced
by FindPictureFieldIdAsync for the legacy slot-based fallback; the new
client-id branch bypassed them, so a bogus meta.FieldId could land an
UploadedData reference on a Text/Number/Comment field's row or on a field
in an unrelated CheckList — corrupting the (CaseId, CheckListId, FieldId)
triple persisted to FieldValues.
Adds ValidateClientPictureFieldIdAsync — a targeted variant of the BFS
that loads the candidate field once (id + type filter + not-Removed) and
walks the CheckList parent/child tree from foundCase.CheckListId, returning
the field id iff its CheckListId is reachable from the root.
UploadPhoto's resolution path now:
- meta.FieldId > 0 -> validate; on rejection throw RpcException with
Status.InvalidArgument carrying the rejected id and the case's
root CheckListId for debuggability. Fail loud — matches the cycle's
fail-loud principle and consistent with the existing slot-range
rejection in this RPC.
- meta.FieldId == 0 -> legacy BFS unchanged.
No proto change; no API surface change. Test coverage for this guard is
tracked as a follow-up in the PR description.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ield-id # Conflicts: # eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn/Services/BackendConfigurationCalendarService/IBackendConfigurationCalendarService.cs
feat(opgaver): UploadPhoto fieldId binding + Complete response parity
…iple tavle_ids Replaces string tavle_id with repeated string tavle_ids on ListOpgaverRequest and StreamOpgaveChangesRequest. The C# handlers parse the repeated field directly into the existing BoardIds list that BackendConfigurationCalendarService.ShouldIncludeTask already filters on, so no business logic changes — only proto + glue. Hard cutover (same field number, type change). The flutter consumer is the only client of this proto; plugin and flutter PRs merge in lockstep. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Simplifier polish on TryParseBoardIds: file already imports System.Collections.Generic; bare List<int>/HashSet<int>/IEnumerable<string> match the rest of OpgaverGrpcService. XML doc trimmed to the load-bearing facts (per-entry skip semantics, dedup, empty=show-all contract). Behavior unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(opgaver): multi-tavle filter on ListOpgaver + StreamOpgaveChanges
…backend_configuration.Events Plugin side of the Danish->English migration per docs/superpowers/specs/2026-05-11-opgave-to-event-migration.md. Renames proto package, service, the opgave-noun messages (Opgave->Event, OpgaveChange->EventChange, CompleteOpgave*->CompleteEvent*), and the corresponding C# handler class + registration. The proto file itself moves opgaver.proto->events.proto. Intentionally mixed-language: Ejendom, Tavle, rode fields stay Danish this PR -- they're tracked in follow-up phases. Private locals inside the handler are also out of scope per the plan's smallest-viable diff ceiling. Helm chart already routes /backend_configuration.Events/ as h2c so the mobile client cutover is lockstep but the wire path is already wired through Traefik on the cluster. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The prior mechanical Opgaver->Event rename overshot and produced 6 references to non-existent Eventr* types. The private JSON envelope classes (OpgaverCustomEnvelope, OpgaverCommentBody, OpgaverPhotoBody) are explicitly carved out of the smallest-viable-diff per the migration plan - they stay Danish. Restore the references.
Simplifier pass: trimmed redundant header + collapsed paragraph structure from 16 to 11 lines. Same load-bearing info: spec link, exact noun scope (opgave only), Ejendom/Tavle/rode deferral, field- name preservation rationale, smallest-viable-diff framing.
feat(opgaver): rename gRPC surface from microting.opgaver.Opgaver to backend_configuration.Events
Previously the test tampered the LAST base64url char of the JWT signature. For a 32-byte HMAC-SHA256 → 43 base64url chars (no padding), the last char encodes only 4 significant bits of byte 31; the other 2 bits are padding bits the encoder always emits as 00. .NET's Convert.FromBase64String silently ignores those padding bits — so flipping the last char between 'A' (000000) and 'B' (000001) is a no-op for the decoded signature byte whenever byte 31's low-4-bits == 0. That happens ~1/16 of the time (uniform random HMAC output). Empirically: 6.33% undetected over 10000 trials. CI manifested the bug on https://github.com/microting/eform-backendconfiguration-plugin/actions/runs/25660940289 (stable HEAD after PR #802) — not a regression introduced by that rename, just a long-standing 6% flake that finally rolled snake-eyes. Fix: tamper a char in the MIDDLE of the signature segment instead. Middle chars use all 6 bits as data, so any flip is guaranteed to mutate the decoded signature. Empirically 0/10000 false-passes. Production verifier in GoogleDriveAuthService.VerifyEnvelopeJwt is correct as-is (FixedTimeEquals on the decoded byte arrays); only the test's tamper logic was unsound. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror of the Phase 1 plugin pattern (PR #802) for the next noun in the opgave→event migration: rename the gRPC message types and RPC method names from Tavle to Board. Wire field names stay (`tavler`, `tavle_id`, `tavle_ids`) so generated C# accessors don't churn — only the type names and RPC method move. Concrete proto changes: - rpc ListTavler(ListTavlerRequest) returns (ListTavlerResponse) → rpc ListBoards(ListBoardsRequest) returns (ListBoardsResponse) - message Tavle → message Board - message ListTavlerRequest/Response → ListBoardsRequest/Response - Top-of-file comment updated to document Phase 2 boundaries. Concrete handler changes (EventsGrpcService.cs): - public override Task<ListTavlerResponse> ListTavler(...) → Task<ListBoardsResponse> ListBoards(...) - new Tavle { ... } → new Board { ... } - response.Tavler.Add(...) kept (generated from carved wire field) - All 15 generated accessors (request.TavleIds reads, event.TavleId setters) untouched — they're driven by carved wire fields. Build: dotnet clean + build green from eFormAPI.Web (0 errors, 10 pre-existing warnings). Backend restarts cleanly on :5000+:5001. Embedded copy at eform-angular-frontend/eFormAPI/Plugins/BackendConfiguration.Pn/ synced (diff -q silent). Spec: docs/superpowers/specs/2026-05-11-opgave-to-event-migration.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 2 of the opgave→event migration: rename the gRPC message types and RPC method names from Tavle to Board on the plugin side. Mirrors the Phase 1 pattern (PR #802
8703e4ba) — wire field names stay so generated C# accessors don't churn.Spec:
docs/superpowers/specs/2026-05-11-opgave-to-event-migration.md(Phase 2 subsection).Renamed (gRPC type / RPC method)
rpc ListTavler(ListTavlerRequest) returns (ListTavlerResponse)→rpc ListBoards(ListBoardsRequest) returns (ListBoardsResponse)message Tavle→message Boardmessage ListTavlerRequest/Response→ListBoardsRequest/ResponseEventsGrpcService.ListTavler(...)handler →ListBoards(...)Stayed Danish (CARVE_WIRE_FIELD)
tavler(the repeated field onListBoardsResponse),tavle_id,tavle_ids,id,ejendom_id,name,color_hexrequest.TavleIdsreads,event.TavleId = ...setters) untouchedVerification
dotnet clean && dotnet buildfromeFormAPI.Web/— 0 errors (10 pre-existing warnings):5000+:5001cleanly, no plugin-load exceptionseform-angular-frontend/eFormAPI/Plugins/BackendConfiguration.Pn/synced (diff -qsilent)Test plan
🤖 Generated with Claude Code