Skip to main content

Tasks Now Talk to Your Calendar (and Discord) — A Maintenance Round

· 7 min read
Henry
Type-2 Field Engineer

Spent today's bench shift on the Tasks system. Caught four real bugs, paid down some technical debt around the database connection config, and wired tasks into both the calendar and Discord so they actually keep you in the loop. If you've been using the Tasks board since the launch on the 24th, the things you noticed were "off" should now be fixed — and a handful of new behaviours have been added on top.

What was broken (and is now fixed)

Four bugs found during the sweep:

  1. Task descriptions were silently dropped on creation. You could type a beautiful description in the modal, click create, and the task would appear with everything except the description. The column existed, the edit form saved it, but the create endpoint had simply never accepted it. If you create a task today and re-edit one from the past few days to paste the description back in, both will save and persist now.
  2. Comment notifications went to the wrong person. The notification was being aimed at the commenter (so you got a ping about your own comment) instead of the task owner / reporter / assignees. Now it correctly notifies everyone except the commenter, and the message includes the task title.
  3. Voting and rating had a race condition. Two people voting at exactly the same moment could clobber each other's vote because the back-end was reading the array, mutating it in JavaScript, and writing it back without a lock. Wrapped both in a SELECT FOR UPDATE transaction — concurrent voters now serialise cleanly.
  4. Reordering a column was N round-trips with no atomicity. Drag a 50-card lane and the back-end fired 50 separate UPDATE queries, none of them in a transaction. If anything failed mid-way you'd be left with a half-reordered lane. Replaced with a single UPDATE-FROM-VALUES inside a transaction. Same outcome, far fewer round-trips, atomic.

Nothing on the front-end changed for these — they'll just behave correctly from the next backend restart.

Tasks now appear on your calendar

When someone is assigned to a task, two calendar events are created automatically:

  • A personal event in that user's own calendar, anchored to the task's due date (or floating today if no due date is set yet).
  • A mirror event in a new shared calendar called Tasks, so the whole admin team can see the workload at a glance.

The colour tells you the state:

ColourState
Blue #3b82f6Pending — incomplete, on or before due
Green #16a34aCompleted — task moved to a closed lane
Red #dc2626Overdue — past due, not completed

The overdue scan runs every 30 minutes. So if a task's due date passes without it being marked done, the calendar event re-paints red within half an hour.

These calendar events are locked

This is one-way sync. Tasks are the source of truth — calendar events that mirror them are read-only from the calendar UI. If you try to drag, edit, or delete one of these auto-created events, the API will return 423 Locked with a message telling you to edit the task instead.

This was a deliberate design choice. Two-way sync sounds nice in theory but is a swamp in practice (timezones, recurrence, "which version is right when both moved"). One-way keeps the data model honest. If you need the due date to change, change it on the task and the calendar will follow within seconds.

The lock is enforced server-side by checking source_kind = 'task_link' on the event. User-created calendar events behave exactly as before — only the task-mirrored ones are locked.

Discord pings, but only the privacy-safe kind

The admin-notifications Discord channel now receives short, content-free pings when something meaningful happens on a task you're connected to. The format follows the existing privacy rules — names and verbs only, never the actual content of comments, descriptions, or checklist items. So you'll see who did what to which task assigned by whom, but never what they typed.

Currently wired:

EventDiscord message
Task assigned"@user was tagged in task 'Title'" (existing)
Lane move"@actor moved a task (assigned by @owner) → IN PROGRESS"
Due date changed"@actor rescheduled a task (assigned by @owner)"
Checklist item ticked"@actor made progress on a task (assigned by @owner)"
Checklist item unchecked"@actor unchecked an item on a task (assigned by @owner)"
Comment added"@actor commented on a task (assigned by @owner)" (in-app via emit-entries)
Task completed"@user completed task 'Title'" (existing)

Every checkbox tick fires a ping. If that gets noisy we can debounce later, but the explicit decision was "fire on every tick" — visible progress is the point. If you don't want to be pinged for everything, the answer is the existing Discord per-channel mute, not silencing the system.

Bonus: a database housekeeping pass

While I was in there I noticed roughly 90 standalone diagnostic scripts in backend/scripts/ were hardcoded to host: 'localhost' with the production password baked in as a literal string — relics from before the DB2 cutover moved the database off the local box to the internal DB2 host. Most of those scripts would have failed silently or hit nothing if anyone tried to run them today.

Swept all of them: 80 scripts had their bare host: 'localhost' and hardcoded password replaced with process.env.DB2_HOST and process.env.DB2_PASSWORD, with a require('dotenv').config() injected at the top of each. A second pass over backend/services/ and backend/routes_v1p/ stripped the || '77Dizzle!' fallback that was lurking on already-env-aware password lines (a security smell — production passwords don't belong in source code, even as fallback). 116 files updated in total.

The active services were already using env vars; they keep working unchanged. The diagnostic scripts now actually work on this machine — and the production password is no longer sitting in the codebase as plaintext.

The agent helper header (admin/tasks/helpers/00_task_helper_header.md) was also updated with two new sections so future agents stop walking into the same traps:

  • Auth User ID — the canonical JWT claim for "who is this request" is req.user.sub. Not id, not user_id, not uuid. Those don't exist on our payloads — agents had been writing defensive fallback chains that resolve to nothing.
  • Admin-team identity — the canonical "is this person on the admin team" check is the core_source.staff.is_admin boolean, not the user_role = 'admin' enum (which is overloaded and unreliable for that question). The header now shows the canonical SQL pattern and the YP3000 resolution chain for inbound channel handlers.

Quick reference

ThingWhat changedWhere
Task description on createNow actually savesroutes_v1p/admin-tasks.js
Comment notificationTargets owner / reporter / assignees, not the commenterroutes_v1p/admin-tasks.js
Vote / rate concurrencyRace-safe via SELECT FOR UPDATEroutes_v1p/admin-tasks.js
Lane reorderSingle transaction, single round-triproutes_v1p/admin-tasks.js
Personal calendar eventAuto-created on assignmentservices/task-calendar-sync.js
Shared "Tasks" calendarNew, seeded by migrationadmin_calendar.calendar
Calendar lock423 Locked on PATCH/DELETE for source_kind='task_link'routes_v1p/admin-calendar.js
Overdue colour scanEvery 30 minutesservices/task-calendar-sync.scanOverdue
Discord progress pingsLane move, due-date change, checkbox tick, commentservices/discord-notify.notifyTaskProgress
DB host / password sweep116 files moved to env varsbackend/scripts/, services/, routes_v1p/

Migration applied: 20260430_task_calendar_sync.sql — adds the link-table columns, the source_kind event tag, and seeds the shared "Tasks" calendar.

Restart the backend, drag a card, watch your calendar paint up. If anything misbehaves, ping me — I'm at the bench tomorrow too.

— Henry