Social sync: add ghost overlay sharing with realtime updates - #1273
Social sync: add ghost overlay sharing with realtime updates#1273Alinapanyue wants to merge 2 commits into
Conversation
Introduce tokenized shared timetable links, a ghost overlay API, and WebSocket-driven live updates so viewers can see owner schedule changes in real time while preserving existing share flow compatibility. Made-with: Cursor
Reviewer test guide (local)If you want to validate the realtime ghost-share flow end-to-end, here is a quick checklist:
Happy to help reproduce any issues if a step fails. |
| try { | ||
| const payload = JSON.parse(event.data); | ||
| if (payload.type === "timetable.updated") { | ||
| dispatch(fetchGhostTimetableBySlug(slug)); |
There was a problem hiding this comment.
from my understanding, the websocket will send an event, then fetchGhostTimetableBySlug will trigger another HTTP request to fetch the new changes -- is it possible to just convey this within the WS event itself?
There was a problem hiding this comment.
Good call — addressed in a97a41647. WebSocket timetable.updated now includes sharedTimetable + courses + permission + updatedAt, and frontend applies that payload directly instead of always issuing a follow-up HTTP fetch. I kept a backward-compatible HTTP fallback path in case older payload shapes are encountered.
| def ensure_share_token(self): | ||
| if self.share_token: | ||
| return self.share_token | ||
| token = secrets.token_urlsafe(24) |
There was a problem hiding this comment.
re. the token generation logic, is there a potential race condition here? i feel that two users generating the same token_urlsafe at the same time is extremely unlikely, but what happens in the unlikely case that they do?
There was a problem hiding this comment.
Great catch — addressed in a97a41647. I updated ensure_share_token to retry on IntegrityError from the unique constraint, so even in the extremely unlikely collision case we regenerate and retry instead of failing.
| return; | ||
| } | ||
| const raw = window.prompt( | ||
| "Paste a shared timetable link or slug to overlay as ghost." |
There was a problem hiding this comment.
to us developers this makes sense, but to a non-technical user this could be confusing.
possible alternative:
Paste a shared timetable link or code.
Example link: jhu.sem.ly/timetables/links/abc123
Example code: abc123
There was a problem hiding this comment.
Agreed — updated prompt copy in a97a41647 to be more user-friendly with concrete examples: Paste a shared timetable link or code.
Example link: jhu.sem.ly/timetables/links/abc123
Example code: abc123
| enabled: boolean; | ||
| shareSlug: string | null; | ||
| timetable: Timetable | null; | ||
| permission: "view" | "edit"; |
There was a problem hiding this comment.
i dont see this 'edit' permission enforced anywhere, is this for later?
There was a problem hiding this comment.
Yes, this is currently for later phases. MVP in this PR is intentionally read-only ghost overlay (view), while edit is scaffolded metadata for follow-up collaborative editing permissions so we do not need another schema change later.
| default="view", | ||
| choices=(("view", "View"), ("edit", "Edit")), | ||
| ) | ||
| expires_at = models.DateTimeField(null=True, blank=True, default=None) |
There was a problem hiding this comment.
i see conditional logic for checking share validity (is_share_access_valid) but can't find where expires_at and revoked_at get changed, is that implemented?
There was a problem hiding this comment.
Correct — expiry/revoke mutation flows are not included in this MVP yet. This PR adds the enforcement checks + fields so links can be invalidated/expired once we add management UI/API in a follow-up. Current default behavior is non-revoked + no expiry unless set manually.
Send full ghost timetable payloads over websocket events to remove follow-up HTTP fetches, add collision-safe share token retries, and clarify ghost link prompt copy for non-technical users. Made-with: Cursor
|
@Alinapanyue re. the linter changes, see these docs: https://semesterly-v2.readthedocs.io/en/latest/contributing.html?highlight=npx%20prettier commands to resolve the above workflows should be at the very bottom, try giving those a run and re-commit :) |


Why this change
The original ghost-share flow only refreshed after manual reload (or polling fallback), so shared timetable viewers could miss changes while the owner edited their schedule. This update moves the feature to true push-based updates so ghost overlays behave more like collaborative documents: once a viewer opens a share link, updates appear automatically.
To support real-time updates end-to-end, we also needed to run Django in an ASGI-capable server path instead of WSGI-only dev flow.
How we implemented it
/ws/...connections, which is required for browser WebSocket handshakesASGIStaticFilesHandlerso static assets continue to load correctly in local ASGI developmentMajor backend changes
SharedTimetablewith social-sync metadata (share_token,source_timetable, permission/expiry/revoke fields,updated_at) and migration/timetables/links/<slug>/ghost/) with access validation and school scopingPersonalTimetablesave, refresh linked shared snapshotstimetable.updatedevents to the corresponding share room after transaction commitMajor frontend changes
timetable.updatedInfra/config changes
docker-compose: add Redis service and run web with Uvicorn ASGI appsemesterly/settings.py: configure Channels + channel layers + feature flag for ghost mode rolloutbuild/caddy/Caddyfile: add explicit websocket proxy route for/wsTest plan
docker compose up --buildand verify app loads under ASGI with static assetsdocker compose exec web python manage.py test timetable.testsghost_timetable_slice.test.ts