Skip to content

New release#215

Merged
gregv merged 15 commits into
mainfrom
develop
May 10, 2026
Merged

New release#215
gregv merged 15 commits into
mainfrom
develop

Conversation

@gregv
Copy link
Copy Markdown
Contributor

@gregv gregv commented May 10, 2026

No description provided.

gregv and others added 15 commits May 8, 2026 07:37
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>
@gregv gregv merged commit 6eb169e into main May 10, 2026
5 checks passed
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.

1 participant