Conversation
New package api/planning/planning_views.py — 20 REST endpoints:
GET /api/planning/{event_id} board snapshot + ETag
POST/PATCH lists, cards, labels require_plan_editor
POST/DELETE comments require_logged_in_on_enabled_plan
PATCH editors, config; POST seed-template require_admin_on_event
POST cards/{id}/rebalance LexoRank batch rebalance
POST run-of-show/sync + GET preview preserves source:manual countdowns
POST _flush_digests (cron, token-gated)
POST slack/notify, editing-heartbeat
Auth foundations (services/hackathon_planning_service.py):
require_plan_editor decorator — @auth.require_user + doc lookup +
planning.enabled check + admin-or-editor check; editor list never cached
require_admin_on_event, require_logged_in_on_enabled_plan
g.hackathon stash prevents cross-event card mutations
Services:
planning_template_service.py — 8-list OHack default template with seed cards
planning_slack_notifier.py — Firestore-backed pending digest queue;
lazy flush (next mutation after 60s window) + read-driven flush (GET board);
no cron required; Redis accelerates deadline lookup when available
planning_ros_service.py — Run of Show sync: planning cards → countdowns;
preserves entries where source != "planning"
Schema (model/planning.py):
ALLOWED_BUDGET_BUCKETS, ALLOWED_CARD_KINDS, MAX_* constants,
default_planning_subobject()
validators.py — validate_planning_subobject (editors dedup, Slack channel regex,
planning.enabled/template_seeded/budget_widget booleans)
hackathons_service.py — persist planning subobject through PATCH
api/__init__.py — register planning blueprint
firestore.indexes.json — composite indexes for planning_cards, planning_comments,
planning_pending_digests subcollections
Security:
_flush_digests requires PLANNING_INTERNAL_TOKEN or rejects non-loopback
All subcollection lookups path through g.hackathon (URL's event_id),
preventing cross-event card_id injection
planning.enabled === false returns 404 (not 403) on all mutation routes
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add hackathon planning board backend — /api/planning/*
…estore indexes
The get_board endpoint queried planning_lists and planning_cards with
.where("archived", False).order_by("position") — a compound query that
requires composite indexes not yet deployed to production Firestore.
Sorts in Python instead to eliminate the index dependency.
Also adds the missing (archived, position) composite indexes for both
collections to firestore.indexes.json so firebase deploy --only
firestore:indexes makes them available in production.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
org_info.get("user_permissions") always returned None because OrgMemberInfo
uses attribute access. Switch to getattr(org_info, "user_permissions", [])
so volunteer.admin permission is correctly detected, unblocking the 403
on seed-template, editors PATCH, and all other admin-only planning routes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous fix targeted the wrong attribute name (org_id_to_org_info), so getattr(...) silently returned None and the loop never executed — is_admin always returned False, blocking every admin-only planning route (seed-template, editors PATCH, config PATCH) with 403. The actual attribute on PropelAuth's User class is org_id_to_org_member_info. Also use OrgMemberInfo.user_has_permission() helper as the primary check, falling back to direct attribute access. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GET /api/planning/_users/search?q=... — admin-only, substring match across Firestore users by name/email/nickname, returns up to 25 candidates with the propel user_id needed by editors[]. Q must be at least 2 chars to avoid dumping the user table. Powers the new Autocomplete on the admin Planning Board accordion so admins no longer need to know or paste PropelAuth user IDs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PEP 604 union syntax (X | None) requires Python 3.10+. The OHack backend runs on an older interpreter, so importing the module raised TypeError at parse time and the manual Slack digest button (and every digest flush path) returned 500. Use typing.Optional instead — supported on every interpreter the rest of the backend already targets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…snapshot
Status:
- Adds ALLOWED_CARD_STATUSES = {planned, in_progress, blocked, completed}
to model/planning.py
- PATCH /cards/{id} now accepts a status field (null/empty clears it)
Assignee profiles:
- get_board now includes a `users` map keyed by propel_user_id with
{name, nickname, profile_image} for everyone referenced in
cards.assignees + planning.editors. Lets the frontend render avatars
without per-user round-trips.
- 60-second in-process cache on the user-list resolution so polling
boards don't pay for fetch_users() on every snapshot.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds mention search + notification dispatch:
- GET /api/planning/_users/mention-search?q= — any-logged-in endpoint
returning {user_id, name, profile_image} only. No email, no Slack ID
ever surfaced to clients. Reuses the existing 60s _USER_PROFILES_CACHE
so an active board doesn't hit fetch_users() per keystroke.
- New services/planning_mention_notifier.py:
- parse_mention_ids(body) — finds @[Name](propel_user_id) tokens.
- notify_mentions(...) — best-effort dispatch. For each mentioned user
we resolve their PropelAuth OAuth identity (cached 5 min via TTLCache)
and send:
* Slack DM AND email when Slack OAuth → both channels (Slack-buried
mentions remain durable in the inbox).
* Email only when Google OAuth (no Slack ID).
* Drop with warning when neither is available.
- Skips actor self-mentions silently.
- create_comment hook: parses mentions from the saved body and fires
notifications best-effort. Failures are logged but cannot break the
comment write — the comment is already persisted by the time the
dispatcher runs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ists
Two changes to _resolve_public_user_profiles:
1. Bundle the Firestore document id (db_id) on each profile so the
frontend can build canonical /profile/{db_id} links.
2. Fall back to a one-shot PropelAuth OAuth lookup for any propel_id
that isn't in the cached fetch_users() index. Previously, a user
who had just authenticated but never triggered profile creation
(never visited /profile, never saved metadata) would show as
"Unknown ?" on cards they assigned themselves to.
The fallback caches per-user for 5 minutes so a hot board with
many self-assigners doesn't hammer PropelAuth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Things that bit us hard during planning-board development and would silently re-bite anyone who didn't have this context: - Python 3.9 forbids PEP 604 union syntax (X | None) — use Optional[X] - PropelAuth User attribute is org_id_to_org_member_info (NOT org_id_to_org_info), each value is an OrgMemberInfo object with attribute access (NOT a dict) - Firestore compound queries (where + order_by on different fields) need composite indexes deployed via firebase deploy — emulator allows them silently, prod returns 500 - The users collection is populated lazily on first profile interaction; fetch_users() does NOT cover everyone with a propel_user_id - OAuth provider response shapes differ (Slack has user_id field, Google has picture); detect by presence of https://slack.com/user_id - User.id (Firestore doc) vs User.user_id (propel_id) are different values — bundle both when sending user data to the frontend
The new SSR permalink page (/hack/{event_id}/plan/c/{card_id}) needs to
fetch a single card server-side to emit per-card OG meta tags so Slack /
Twitter / LinkedIn unfurls show the card title + description preview.
Public read; gated on planning.enabled (returns 404 when disabled, same
as get_board). Archived cards return 404.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GET /api/planning/_users/by-ids?ids=p1,p2,... — any logged-in user can
batch-resolve up to 50 propel_user_ids to {name, profile_image, db_id}.
Replaces a hacky workaround in the admin editors manager that searched
for the first 8 chars of the propel_id (which never matched anyone's
name, so editors always rendered as "Unknown user").
Reuses _resolve_public_user_profiles, so the PropelAuth fallback added
for board snapshots also covers admin-side editor display — a freshly-
added editor who has never visited /profile still shows the right name
and avatar, not the opaque oauth2|slack|... ID.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…pshot Three bugs in the manual 'Send digest now' button — combined effect was that clicking returned 200 but no Slack message ever arrived: 1. send_slack (in common/utils/slack.py) silently swallows SlackApiError and returns nothing. Most importantly, when the bot isn't a member of the target channel Slack returns not_in_channel — the route looked like success but the message was dropped. 2. send_manual_digest called _collect_and_clear_events (which CLEARS the pending queue) and then _do_flush (which collects again from an empty queue and returns 0). Even with Slack working, the manual digest path was a no-op on a board with no pending changes. 3. The route handler returned 200 unconditionally — no failure ever bubbled up to the admin UI, so debugging was opaque. Fixes: - New _post_to_channel() helper joins via add_bot_to_channel() before posting and surfaces the underlying SlackApiError as (ok, error_string). Specifically translates not_in_channel to a friendly hint. - _do_flush uses the new helper. - send_manual_digest rewritten per the design plan: "post the current state, one line per non-empty list with card counts" — returns (ok, message) so the route can surface success/error. - /slack/notify returns 502 with the error message when Slack rejects the post (vs. 500 for unexpected exceptions). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
No description provided.