- lyric/0005 RemoveField+AddField (RenameField doesn't rename the implicit M2M through table; field was new in 0004 so no data loss). Lyric.User.buddies → User.buds; related_name added_as_buddy → added_as_bud.
- applets/0007 renames Applet slug my-buddies → my-buds + name 'My Buddies' → 'My Buds'. UI rationale: BILLBUDDIES overflowed the page-header band; in-game term collapses to BILLBUDS.
- billboard/0006 alter Line.Meta.ordering = ('created_at', 'id') — was already in models.py, just generates the corresponding migration (formalizing the ordering decision from the May-8b refactor).
- global rename via sed: buddies → buds, buddy → bud across 16 files (templates, SCSS, JS, ITs, FTs, page object, view code). 4 file renames via git mv: my_buddies.html → my_buds.html, _applet-my-buddies.html → _applet-my-buds.html, _buddy_panel.html → _bud_panel.html, _buddy_add_panel.html → _bud_add_panel.html, _buddy.scss → _bud.scss. Test files renamed too: test_buddies.py → test_buds.py, test_my_buddies.py → test_my_buds.py, test_buddy_btn.py → test_bud_btn.py. core.scss @import 'buddy' → 'bud'.
- new shared partial templates/apps/applets/_partials/_applet-list-shell.html — vertical-rotated <h2> + scrollable <ul> aperture, parameterised via {% include %} so a single page can invoke it more than once. Params: shell_title, shell_items, shell_item_template, shell_list_id, shell_empty.
- my_buds.html: single shell invocation w. add-bud panel below (page_class page-billbuds).
- my_posts.html: two shell invocations (own posts + posts shared with me) inside .applet-list-page--two-up — portrait stacks them; landscape lays side-by-side via @media (orientation: landscape) flex-direction: row (page_class page-billposts).
- SCSS: drop the bottom-anchored .buds-page block; new shared .applet-list-page (extends %billboard-page-base, flex-column + padding) w. .applet-scroll inside (extends %applet-box) and .applet-list inside that (flex: 1, overflow-y: auto). .applet-list-page--two-up flips to row layout in landscape. Body class trio gains page-billposts.
- 841 ITs + 5 my_buds/my_posts FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small fixes close out the OK→banner gap:
1. Anchor over h2: base.html drops <div id="id_brief_banner_anchor"></div> right before {% block content %} (after the messages block). note.js's showBanner now prefers the explicit anchor over the first <h2> — keeps the banner in the visible content flow on pages where the first h2 is position:absolute (post.html's rotated navbar header was the immediate motivator; sky.html's rotated h2 is the same shape, so this catches that pre-emptively too).
2. window.Brief explicit assignment: const Brief = (...) at script-tag scope is reachable as a bare name but does NOT auto-attach to window. The buddy panel's OK handler gates banner reveal on `if (window.Brief && data.brief)` — that gate was always false, so Brief.showBanner never fired on share-OK even though the chip + Line append in DOM proved the fetch.then() was running. Explicit window.Brief = Brief; window.Note = Note; in note.js (post-IIFE) closes the gap.
Also picks up the deferred page-object update — functional_tests.post_page.PostPage.share_post_with() now drives the buddy-btn flow (click #id_buddy_btn → type → click #id_buddy_panel .btn.btn-confirm → wait for recipient chip), so legacy SharingTest exercises the new pipeline end-to-end.
NoteSpec.js T10 split into T10a/T10b: a covers the anchor-preferred path, b covers the <h2> fallback.
16/16 buddy FTs green (previously 15/16). 12/12 sharing + Jasmine + my_notes FTs green. 818-test IT sweep green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
Closes the C3 brief sprint. Three event sources (note unlock, share invite, login messages) now route through the Brief slide-down, & the legacy .alert-success/.alert-warning rendering in base.html is retired.
C3.b — share-post async Line + Brief:
- billboard.share_post detects Accept: application/json. JSON path appends a Line (text="Shared with X at <isoformat>", isoformat carries microseconds so two rapid shares of the same email don't collide on Line.unique_together(post,text)), spawns a Brief(kind=SHARE_INVITE) for the sharer, and returns {brief: brief.to_banner_dict() | None, line_text, recipient_display}. Sharer-shares-with-themselves stays a silent no-op (response carries brief: null). Legacy form-submit path preserved for non-AJAX (still redirects + flashes the privacy-safe message — kept for older FTs / no-JS fallback).
- billboard.Brief.to_banner_dict() (moved from dashboard.views helper to a model method) shapes the JSON the banner JS consumes.
- post.html: share form intercepted by JS — fetches POST w. Accept:application/json, then appends `data.line_text` as the next row in #id_post_table, calls Brief.showBanner(data.brief), and (when registered) appends a fresh `<span class="post-recipient">` to the new #id_post_recipients box. No page reload — the alert-success flash is gone.
- 10 new ITs (SharePostAsyncTest + SharePostLegacyRedirectTest) cover the JSON path, line append, brief creation w. SHARE_INVITE kind, registered/unregistered recipient behaviour, sharer-self skip, line dedupe via timestamp, and that the legacy form-submit redirect path still works.
- functional_tests.test_sharing line numbering updated: the share now records its own Line so the alice-reply lands at row 3 instead of 2.
C3.c+d — magic-link confirmation + invalid-link error use Brief banner styling:
- base.html's {% if messages %} block stops rendering .alert-success/.alert-warning divs. Instead each message renders as a transient Brief-styled banner: <div class="note-banner note-banner--message note-banner--{{level_tag}}"> with .note-banner__body / __description carrying the message text and a .btn-cancel NVM that removes the banner via inline onclick. No DB Brief row; no FYI; no square. Same Gaussian-glass look as note-unlock + share-invite Briefs.
- _note.scss adds the note-banner--message variant (full-opacity description) + note-banner--error/--warning border-color override (priRd 0.6) so the invalid-link banner reads as red/abandon.
C3.e — .alert-success/.alert-warning retired in markup; the SCSS class blocks aren't referenced anywhere else in templates so they sit dormant (left in place — base form styling keeps .form-control etc. working; no need to ripple into _base.scss).
Banner JS (note.js / Brief module) was untouched in C3.b+c+d — the Brief.showBanner contract from C3.a already handles all three kinds (NOTE_UNLOCK / USER_POST / SHARE_INVITE) by reading kind off the brief; the message-banner path doesn't go through showBanner because there's no Brief row.
Tests: 218 dashboard+billboard+api ITs + 322 lyric+dashboard+billboard ITs + 2 sharing FTs + 9 my_notes FTs + 1 Jasmine FT all green. Existing lyric.test_views login message-text assertions unchanged (they pull from messages framework — not the rendered HTML — so the markup swap doesn't affect them).
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
Wires the C2 Brief model into the existing Note-unlock pipeline. Per the per-category Post model: each user has a single Post(owner=user, kind=NOTE_UNLOCK) titled by the header Line "Look! — new Note unlocked"; each unlock appends a per-event Line ("Stargazer, 5:21:00 PM") + spawns a Brief FK'd to that Line.
Server:
- billboard.Post gains a `kind` enum (NOTE_UNLOCK / USER_POST / SHARE_INVITE, default USER_POST) so the per-category Post is deterministically discoverable via (owner, kind=NOTE_UNLOCK).
- drama.Note.grant_if_new now returns (note, created, brief) — backwards-incompat tuple shape; the new third slot is None on idempotent re-grants. On a fresh grant it: get-or-creates the user's Note Unlocks Post; ensures the header Line; appends a per-event Line w. timestamp; creates a Brief(kind=NOTE_UNLOCK, owner, post, line, title=note.display_title).
- dashboard.views.sky_save's response shape flips {note: {...}|null} → {brief: {...}|null}. The new helper _brief_to_banner_dict exposes {id, kind, title, line_text, post_url, square_url, created_at}; for NOTE_UNLOCK kind, square_url = reverse('billboard:my_notes').
Banner JS (apps/dashboard/note.js):
- Module renamed Note → Brief (Note kept as an alias during the C3 sprint; will retire in C3.e). showBanner now consumes the Brief shape: title from brief.title, body from brief.line_text, time from brief.created_at, FYI href = brief.post_url, NVM dismisses, .note-banner__image is rendered as a clickable <a href=brief.square_url> when square_url is set. The CSS class names (.note-banner, .note-banner__title, etc.) stay so all existing SCSS + FT selectors keep working.
- sky.html + _applet-my-sky.html flip Note.handleSaveResponse → Brief.handleSaveResponse.
Tests:
- new IT class GrantIfNewSpawnsBriefTest (5 tests): first grant creates Post+Line+Brief, idempotent on second grant returns no brief, two different slugs share one Post, line text carries note title, post.kind == NOTE_UNLOCK. PostKindFieldTest (2 tests): default user_post + all three choices present.
- existing drama test_models.GrantIfNew tests updated to unpack the third tuple element.
- dashboard.tests.integrated.test_sky_views: 5 tests flipped from data["note"] → data["brief"] w/ the new payload shape (kind=note_unlock, title=Stargazer, line_text contains Stargazer, post_url under /billboard/post/, square_url=/billboard/my-notes/).
- NoteSpec.js (Jasmine): 12 specs rewritten for the Brief shape — SAMPLE_BRIEF carries the seven fields, T6 asserts the square is a clickable <a>, T8 asserts FYI points at brief.post_url (was hardcoded /billboard/my-notes/).
- functional_tests.test_applet_my_notes: T2 split — FYI now navigates to /billboard/post/<uuid>/ (the post detail / mark-read contract), the .note-banner__image square preserves the legacy jump direct to /billboard/my-notes/.
billboard/0003_post_kind migration auto-generated. 808 ITs + 9 my_notes FTs + 1 Jasmine FT all green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
The Post/Line models always read more like billboard tenants than dashboard ones (1st-person personal vs. 2nd-person provenance feed); the upcoming Brief model needs them in the billboard namespace as the canonical surface they FK into. C1 is a pure relocation w. zero new behavior: 789 ITs + 20 sky/Post FTs green against the moved code.
- billboard.models adds Post (owner + shared_with) + Line (text + post FK), schema mirroring the legacy dashboard models 1:1; Post.get_absolute_url now reverses to `billboard:view_post`.
- billboard.forms adds LineForm + ExistingPostLineForm (moved from dashboard.forms; dashboard/forms.py removed).
- billboard.views absorbs new_post / view_post / share_post / my_posts (templates rendered from apps/billboard/post.html + my_posts.html).
- billboard.urls adds the namespaced routes: /billboard/new-post, /billboard/post/<uuid>/, /billboard/post/<uuid>/share-post, /billboard/users/<uuid>/. dashboard.urls drops the corresponding entries.
- _applet-my-posts + _applet-new-post URL refs now use the billboard: namespace; templates/apps/dashboard/{post,my_posts}.html removed.
- api/serializers + api/views + api/tests/integrated/test_views imports flip dashboard.models → billboard.models (PostSerializer / PostDetailAPI / PostLinesAPI / PostsAPI all retain identifiers — the model rename to Brief lands in C2).
- dashboard/tests/integrated/test_{models,views,forms} + dashboard/tests/unit/test_{models,forms} swap imports; test_views URL strings flip /dashboard/post/ → /billboard/post/, /dashboard/new_post → /billboard/new-post, /dashboard/users/ → /billboard/users/, share_post → share-post (path) / billboard:share_post (reverser). Tests stay in dashboard.tests/ for now — relocation TBD.
- functional_tests/my_posts_page.py URL string flips to /billboard/users/.
- Auto-generated migrations: billboard/0001_initial (CreateModel Post + Line), dashboard/0003_remove_post_* (drops legacy Post + Line), drama/0004_alter_gameevent_verb (incidental — choices field caught up).
This commit drops the dashboard Post/Line tables w/o data preservation; user has confirmed staging-side wipe is acceptable. C2 introduces the Brief model + read-tracking + slide-down banner unification. C3 hooks Note-unlock + share-post-invite + magic-link / invalid-link `messages` calls into the new Brief / banner pipeline.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
A user-typed TZ override fed through schedulePreview's `if (tz) params.set('tz', tz)` path made PySwiss compute the chart against a TZ that didn't match the lat/lon, so a partial edit (e.g. "America/New_Yo|") returned HTTP 400. Mirror the lat/lon convention: tz field gets readonly + tabindex:-1 across all three sky contexts (Dashsky sky.html, in-room PICK SKY _sky_overlay.html, My Sky applet _applet-my-sky.html). Auto-population still works because the JS writes via .value rather than via user input. The <small id="id_nf_tz_hint"> "Auto-detected from coordinates." line is removed; that copy now lives on the <input>'s placeholder so an empty TZ field self-explains. JS purges every tzHint reference (const declaration + 4 .textContent writes per file × 3 files).
SkyViewTest.test_tz_input_is_readonly_and_carries_auto_detect_placeholder pins the rendered Dashsky markup: id_nf_tz carries `readonly`, the placeholder is "auto-detected from coordinates", and `id="id_nf_tz_hint"` no longer appears anywhere. Existing MySkyTimezoneRefreshTest still passes — it asserts the field auto-fills via JS, which still works on a readonly input.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
DEL btn (.btn-danger, "Forget sky?" data-confirm wired to the global #id_guard_portal) sits absolutely centered inside .sky-wheel-col; OK submits a POST to the new sky_delete view, which clears every sky_* field on the User model & redirects back to /dashboard/sky/.
The sky.html aperture is now uniform across saved/unsaved: form-col is always flex-column align-center justify-center so the fields + SAVE SKY pair sits visually centered. body.sky-saved adds *only* the snap-binary scroll layer (scroll-snap-type:y, modal-body display:contents, cols min-height:100% scroll-snap-align:start, wheel-col aspect-ratio cap released, form-col flex:0 0 auto so the snap basis wins) — the column-stacking is no longer gated.
Async save: SAVE SKY's success branch now calls _activateSavedState(), which adds body.sky-saved, draws the wheel from _lastChartData, pins overlay.scrollTop to the form section's offsetTop, then runs the existing _scrollApertureToTop ease-out so the wheel reveals from above instead of replacing the form with a hard cut. The wheel preview that previously redrew during typing is now gated on _savedSky — pre-first-save typing fetches the chart data (so SAVE SKY enables) but does not render the wheel, mirroring the My Sky applet's "no wheel until saved" UX. The in-room PICK SKY overlay (_sky_overlay.html) still previews live, deliberately untouched.
Pre-save the wheel-col is hidden via `body:not(.sky-saved) .sky-page .sky-wheel-col { display: none }`, so the empty SVG can't shunt the form below the fold (& the DEL btn rides the same selector since it lives inside .sky-wheel-col).
Tests: SkyDeleteTest IT class (5: clears fields, redirects, 405 on GET, login required, preserves unrelated user fields). MySkyDeleteFlowTest FT class (3: DEL btn visibility gated on sky data, NVM dismisses w. data intact, OK clears + reverts body class). MySkyAsyncSaveTest FT (1: fresh user → SAVE SKY → body picks up sky-saved, wheel SVG populates, DEL btn becomes visible — all without a page reload). All 13 sky FTs + sky ITs green; existing MySkyApertureSnapScrollTest & MySkyTimezoneRefreshTest still pass.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
post-save the .sky-page aperture flips into scroll-snap-y-mandatory mode: wheel-col & form-col each fill the aperture & carry scroll-snap-align:start, so vertical scroll toggles between them rather than free-flowing through both. Modal-body uses display:contents so the cols become direct flex children of .sky-page (where min-height:100% resolves against the explicit aperture height); wheel-col's aspect-ratio/max-height caps are released under body.sky-saved so the section actually fills the aperture instead of clipping at 480px. SAVE SKY's success branch calls _scrollApertureToTop(), a 280ms RAF loop w. ease-out cubic so the user lands back on the wheel after confirming from the form section. New FT class MySkyApertureSnapScrollTest covers (T1) snap-type:y mandatory + scroll-snap-align:start on both cols, (T2) scrollTop returns to 0 after SAVE SKY click; both red before the SCSS+JS, green after. Snap behavior is gated on body.sky-saved (set by sky_view based on user.sky_chart_data) so the pre-save form-only flow is untouched.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
- User.pronouns CharField w. choices=[pluralism (default), bawlmorese, misogyny, misandry, misanthropy] + pronoun_subj/obj/poss properties; PRONOUN_TABLE single source of truth in apps.lyric.models; mig 0002_user_pronouns
- drama.GameEvent.to_prose() drops module-level PRONOUN_* constants; SIG_READY/SIG_UNREADY/ROLE_SELECTED now resolve poss/subj from self.actor.pronouns at render time, so flipping a user's preference rewrites all their existing scroll prose; default actor → "their"
- SIG_READY prose strips a leading "The " from card_name so "the The Wanderer" reads "the Wanderer" and "the Engraven The Nomad" reads "the Engraven Nomad"; minor arcana ("Maid of Brands") untouched
- new applets/0005 seeds 'pronouns' applet (3x3, game-kit, default visible); _game_kit_sections.html grows a #id_gk_pronouns block w. 5 .gk-pronoun-card items labeled by ideology slug (italic) and tagged data-pronoun + data-trio
- card click → window.showGuard(card, "Set pronoun preference?<span class='guard-pronoun-trio'>{trio}</span>", commitCb); on OK fetches POST /dashboard/set-pronouns w. CSRF cookie + reloads so .active class moves and provenance prose re-renders; NVM dismisses
- dashboard.set_pronouns view (POST-only, login_required, 204/400/405) at /dashboard/set-pronouns; rejects choices not in PRONOUN_TABLE
- _game-kit.scss extends shared card rule to .gk-pronoun-card w. .active fill state + italic ideology label; #id_guard_portal .guard-pronoun-trio styled small/dim/centered under the question
- billscroll aperture: padding-right 0.75rem on #id_drama_scroll inside .applet-scroll so the timestamp column no longer sits beneath the scrollbar
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
- Delete all incremental migration files across all apps; regenerate 0001_initial.py
per app via makemigrations (schema unchanged, no model edits)
- applets/0003_seed_applets.py: all 20 Applet rows in one migration
- epic/0003_seed_fiorentine_deck.py: Fiorentine Minchiate DeckVariant + 78 cards
- epic/0004_seed_earthman_deck.py: Earthman DeckVariant + 50 major + 56 minor/middle
arcana (106 cards); stray PENTACLES courts absent — clean from the start
- epic/0005_seed_astro_reference_tables.py: 12 signs, 10 planets, 9 aspect types
(incl. Semisquare + Sesquiquadrate), 12 house labels
- 706 ITs green on fresh DB
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
sky_save was re-fetching from PySwiss using sky_birth_dt, which was
stored as local time treated as UTC — giving a different (wrong) asc
than the chart computed by sky_preview. sky_natus_data then served this
wrong chart, rotating the applet wheel by the timezone offset.
Fix: sky_save stores body.get('chart_data') directly (client _lastChartData
is already enriched from sky_preview with correct UTC). sky_natus_data
returns the stored chart with fresh distinctions — no PySwiss call needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sky_save now re-fetches from PySwiss server-side on save so stored
chart_data always carries enriched element format (contributors/stellia/
parades). New sky/data endpoint serves fresh PySwiss data to the My Sky
applet on load, replacing the stale inline json_script approach.
natus-wheel.js: sign ring slices (data-sign-name) and house ring slices
(data-house) now have click handlers with _activateSign/_activateHouse;
em-dash fallback added for classic elements with empty contributor lists.
Action URLs sky/preview, sky/save, sky/data lose trailing slashes.
Jasmine: T12 sign tooltip, T13 house tooltip, T14 enriched element
contributor display (symbols, Stellium/Parade formations, em-dash fallback).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move #id_natus_tooltip out of #id_applets_container (container-type:
inline-size breaks position:fixed) → add to home.html alongside
#id_tooltip_portal
- Move #id_natus_tooltip out of .natus-modal-wrap (transform breaks
position:fixed) → place as sibling of .natus-overlay in room.html
- Add _positionTooltip() helper in natus-wheel.js: flips tooltip to
left of cursor when it would overflow right edge; clamps both axes
- Replace hardcoded 280px in dashboard.js palette tooltip with measured
offsetWidth; add left-edge floor (Math.max margin)
- Planet tooltip format: @14.0° Capricorn (<svg-icon>) using preloaded
_signPaths; falls back to unicode symbol if not yet loaded
- Add .tt-sign-icon SCSS: fill:currentColor, vertical-align:middle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clicking a swatch instantly swaps the body palette class for a live
preview; OK commits silently (POST, no reload); click-elsewhere or
10 s auto-dismiss reverts. Tooltip portal shows label, shoptalk,
lock state. Locked swatches show × (disabled). 20 FTs green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New/extended tests across billboard, dashboard, drama, epic, gameboard,
and lyric to cover previously untested branches: dev_login view, scroll
position endpoints, sky preview error paths, drama to_prose/to_activity
branches, consumer broadcast handlers, tarot deck draw/shuffle, astrology
model __str__, character model, sig reserve/ready/confirm views, natus
preview/save views, and the full tasks.py countdown scheduler.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- sky_view passes page_class="page-sky" so the footer pins correctly
- _natus.scss: page-sky aperture block (mirrors page-wallet pattern);
sky-page stacks wheel above form via flex order + page-level scroll;
wheel col uses aspect-ratio:1/1 so it takes natural square size without
compressing to fit the form
- natus-wheel.js: _layout() sets viewBox + preserveAspectRatio="xMidYMid meet"
so the wheel is always centred inside the SVG element regardless of its
aspect ratio (fixes left-alignment in the dashboard applet)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- User model: sky_birth_dt/lat/lon/place/house_system/chart_data fields (lyric migration 0018)
- Applet seed: my-sky (6×6, dashboard context) via applets migration 0009
- dashboard/sky/ — full monoapplet page: natus form + D3 wheel, LS key scoped to dashboard:sky; saves to User model
- dashboard/sky/preview/ — PySwiss proxy (same logic as epic:natus_preview, no seat check)
- dashboard/sky/save/ — persists 6 sky fields to User via update_fields
- _applet-my-sky.html: tile with h2 link to /dashboard/sky/
- 2 FTs (applet→link→form, localStorage persistence) + 12 ITs — all green
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>