Session Recap · 23 Jul 2026
Notičky
recheck (GL #199)
A pass over the whole notification surface for GL #199 „Zkontrolovat znovu notičky". The PM's one complaint — the candidate instantly gets a „firma už o tom ví"-style message after a match — turned into a full copy + lifecycle audit: reworded the overclaiming push, fixed leftover branding, added a company welcome email, and closed two real correctness gaps (stale iOS badge, push tokens firing after logout). Two deeper items were filed as follow-ups.
01At a glance
Seven commits on feature/199-zkontrolovat-znovu-noticky. Four backend/i18n, two mobile, one spanning both. Not pushed.
- The literal string „firma už o tom ví" does not exist — the PM was paraphrasing the candidate's interest-confirmation push. The „instant" timing is by design (synchronous in the same request), so the fix was the copy, not the timing.
- The confirmation copy overclaimed: „Potvrdili jsme váš zájem… v konverzaci s firmou" implied the company had already seen it. Company-side dispatch is fire-and-forget with swallowed failures, so that claim can be false. Reworded to only state the interest was sent.
- Two correctness gaps surfaced during the audit and got fixed: the iOS app-icon badge was set but never cleared, and push devices kept receiving an account's notifications after logout.
- Two things too big for a copy pass were filed as open backend follow-ups: sender name in new-message notifications (
pracino-gkc) and wiring the already-built package-expiry reminder to a cron (pracino-6is).
02The #199 complaint, traced
What the candidate actually receives after tapping „Mám zájem“, and why „instant“ is not the bug.
The candidate taps „Mám zájem" → POST /conversations/context. Inside that one request, CreateContextConversationUseCase inserts the conversation row and then dispatches both notifications concurrently in a single Promise.all — one to the candidate (confirmation), one to every company member. No queue, no delay, no ordering guarantee anywhere in the chain.
The candidate is told „confirmed" at the same instant the company is notified — not after. And because the company dispatch swallows failures (bad token, DB error), the candidate can be told the company knows when it literally never was notified. The fix is the wording, not the send.
123234"candidate_interest_confirmed": { "body": "Potvrdili jsme váš zájem o nabídku {jobOfferTitle}. Další komunikace bude probíhat v konverzaci s firmou.", "title": "Kandidát projevil zájem o nabídku" "body": "Váš zájem o nabídku {jobOfferTitle} jsme odeslali firmě. Další komunikace bude probíhat v konverzaci.", "title": "Zájem o nabídku odeslán"}No newline at end of fileNo newline at end of fileThe old title was a second bug on its own: „Kandidát projevil zájem o nabídku" is the company-facing wording, shown to the candidate about themselves in third person. The new title is candidate-facing.
03What shipped
Seven commits, oldest first. Copy and cleanup up top, the two correctness fixes at the bottom.
| Commit | Type | Area | What it does |
|---|---|---|---|
| e15a765a | fix | backend · i18n | Reword candidate interest confirmation so it stops claiming the company already saw it; fix the third-person title. |
| 3cccb1cb | fix | backend · i18n | Magic-link email said „Pracino/Pracina“ — leftover from the rename. Now „Parino/Parina“, matching every other string. |
| d5244a9e | chore | backend · i18n | Delete dead i18n keys (member-added message, new_message action_label/email_subject) + regenerate types. |
| d0297b30 | feat | backend | Company variant of the welcome email — „Začni firemním profilem“ + publish-offers copy instead of the candidate CV wording. |
| 6e02c49d | fix | mobile | Clear the iOS app-icon badge on launch, on foreground, and on notification open — nothing reset it before. |
| 714167f1 | fix | backend + mobile | Unregister the push device on logout: new DELETE endpoint disables the row; logout fires it best-effort. Has e2e coverage. |
| db394678 | refactor | mobile | Remove the unreachable notification-center screen (bell icon, screen, tabs, route, 7 dead i18n keys) — 319 lines out. |
04The welcome email split
Company owners were getting candidate copy — „fill in your profile and we'll build your CV“ — on registration.
Account completion sent one welcome email for both roles, with body text written entirely for candidates. A WelcomeRegistrationVariant (candidate | company) is now threaded from the complete-account use case through the email payload into the template, selecting role-appropriate heading and body. The candidate path is unchanged (variant: "candidate").
flowchart LR
A[CompleteAccountUseCase] -->|role| B{variant}
B -->|candidate| C["Začni svým profilem — CV copy"]
B -->|company| D["Začni firemním profilem — publish-offers copy"]05The two correctness fixes
Not copy — actual wrong behavior found while reading the notification code.
The foreground handler set shouldSetBadge: true but setBadgeCountAsync was never called, so a badge from a push stuck forever. Now cleared iOS-only on launch, on every foreground transition, and on notification open. Android is unaffected — its launcher badge is handler-driven.
A signed-out device kept receiving the old account's pushes — no DELETE endpoint existed and logout never touched push state. Now DELETE /notifications/push-devices/:deviceId disables just that user's row (verified an unrelated account with the same deviceId stays enabled), and logout fires it fire-and-forget so it can never block sign-out. Re-login re-enables via the existing upsert.
- Confirmation copy no longer implies the company has seen the interest
- Candidate-facing title on the candidate's own notification
- Magic-link branding consistent (Parino)
- Company welcome email verified via preview + unit spec
- iOS badge clears on launch / foreground / open
- Logout unregister endpoint + e2e passing (13/13 notifications e2e)
- Dead notification-center screen removed, shared hooks kept
06Deferred — filed, not done
Two items surfaced by the audit that need more than a copy edit. Both open, both area:backend, both tagged gl-199.
| Issue | Title | Why it was deferred |
|---|---|---|
| pracino-gkc | Enrich new-message notification with sender name | Body is generic („Máte novou zprávu v konverzaci“). Needs a repo query change to load the counterpart identity, plus i18n + generated types + the matching email template — real plumbing, not a string swap. |
| pracino-6is | Wire job_posting_package_expiring_soon reminder to a cron | The reminder service is fully built but has zero callers. Needs a scheduled job, a cadence decision, and de-dup so a package isn't re-notified every run. It's currently the app's only lifecycle/retention notification — the rest are all event-triggered. |
The bigger picture the PM asked about: the backend has exactly one cron today (a monthly unlock-request cleanup) and zero retention/re-engagement notifications. pracino-6is would be the first scheduled notification in the app.
07Status
Where the branch stands.
All seven commits are on feature/199-zkontrolovat-znovu-noticky, tree clean, not pushed. Backend lint + typecheck green; touched unit suites and the full notifications e2e (13/13) pass. Mobile typecheck + lint + jest green. Two pre-existing, unrelated test failures were confirmed to fail identically at HEAD.
One iOS note for the pre-MR checklist: the badge-clear and logout-unregister changes are platform-divergent and worth a device spot-check.