Commit Graph

28 Commits

Author SHA1 Message Date
Disco DeDisco
be919c7aff bud panels duplicate-add guard: server-side already_present flag + client-side error Brief w. FYI flash highlight on the existing entry — for each of the three #id_bud_btn panels (My Buds / post-share / gatekeeper-invite), the JSON response from add_bud / share_post / invite_gamer now carries {already_present, recipient_display, recipient_user_id}; bud-btn.js branches on already_present → calls new Brief.showDuplicateBanner({display_name, target_selector}) instead of the normal onSuccess append; banner title reads @<username> is already present, NVM dismisses, FYI dismisses AND eases in the .bud-duplicate-flash class (color: var(--terUser); text-shadow: 0 0 .5em var(--ninUser); transition: 600ms) onto the existing element (.bud-entry .bud-name / .post-recipient[data-user-id=…] / .gate-slot.filled[data-user-id=…]); gatekeeper "already present" = recipient is either GateSlot.FILLED + gamer OR has TableSeat OR has a pending RoomInvite (highlight target only set when seated — pending invites have no visible slot); .post-recipient chips + .gate-slot.filled cells gain data-user-id so the FYI selector can find them; my_buds.html now loads note.js via the {% block scripts %} pattern (Brief module is required by the duplicate banner path); bonus: latent test_jasmine.py bug fixed — "0 failures" in result.text matched "10 failures" / "20 failures" / etc, silently passing up to 99 failed specs; replaced w. re.search(r"(?<!\d)0 failures\b", …) (caught my new red specs, would've caught any prior Jasmine regression); 18 new ITs + 10 new Jasmine specs + 3 new FTs (one per panel) — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:40:15 -04:00
Disco DeDisco
22d0507c3f post.html header prose branches on viewer-vs-owner: invitees see "shared with me, @viewer the {title}" + "created by @owner the {title}" instead of the owner-centric "just me / shared between" lines; owner view unchanged — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
- billboard.views.view_post adds viewer_is_owner + other_recipients context vars. is_real_invitee = (auth AND post has owner AND viewer != owner). Anon viewers + ownerless-post legacy path fall through to owner-style rendering (which renders empty gracefully via the at_handle / display_name AnonymousUser guards).
  - other_recipients = post.shared_with.exclude(viewer) when invitee; .all() otherwise.
  - post.html .post-header branches:
    • viewer_is_owner: existing prose ("just me, @owner …" / "shared between {recipients} & me, @owner …").
    • sole invitee: "shared with me, @viewer the {viewer.title}" + "created by @owner the {owner.title}".
    • multi invitee: "shared with {other_recipients}" + "& me, @viewer the {viewer.title}" + "created by @owner the {owner.title}".
  - lyric_extras at_handle + display_name: guard against AnonymousUser (no .email attribute) — return "" rather than crash. Preserves the Percival ch. 18 anon-views-ownerless-post path.
  - 12 new ITs in test_post_invitee_view (context vars: viewer_is_owner, other_recipients exclude/include; template prose: sole + multi invitee phrasing, owner unchanged).
  - 878 IT regression + 8 post-html FT regression green (1 Marionette flake on multi-run that passes in isolation).

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 01:14:11 -04:00
Disco DeDisco
eb0369f0b7 buds Phase 2: top-3 username|email autocomplete on #id_recipient (post share + my_buds add); implicit symmetric auto-add on share_post (sharer ↔ recipient buds graph); recipient field accepts username OR email — TDD
- billboard.views.search_buds(GET /billboard/buds/search?q=...) — top-3 prefix match against request.user.buds via Q(username__istartswith) | Q(email__istartswith). Returns {buds: [{id, username, email}]}. Privacy: only the user's own buds are searched, no leak of strangers.
  - _resolve_recipient(raw) helper resolves a free-form recipient (email if "@" present, else username, both case-insensitive). Wired into add_bud + share_post so #id_recipient accepts either form.
  - share_post implicit auto-add (per-spec): when recipient is registered + first-time-shared, both directions of buds M2M get the link — request.user.buds.add(recipient) AND recipient.buds.add(request.user). Idempotent, no auto-add on reshare/self/unregistered.
  - new bud-autocomplete.js shared module (apps/billboard/static/apps/billboard/) — bindBudAutocomplete(input, suggestionsEl, {searchUrl}). Mirrors sky.html birth-place picker: 250ms debounced fetch from MIN_CHARS=1, click-to-fill, Escape closes, click-outside closes, late-response drop. e.stopPropagation on suggestion-click so the bud-panel's outside-click handler doesn't fire and clear the input.
  - SCSS .bud-suggestions / .bud-suggestion-item mirrors .sky-suggestions but position:fixed bottom:4rem (aligned above the bud panel, with overflow:hidden on the panel forcing the dropdown to live as a sibling rather than a child). Landscape breakpoints clear the navbar/footer 4rem sidebars, 8rem at min-width 1800px.
  - both _bud_panel.html (post share) + _bud_add_panel.html (my_buds add) get the suggestions div sibling + script tags. Each panel's existing document click-outside handler now skips the suggestions container so a click inside doesn't close+clear. type="email" → type="text" since usernames are accepted; placeholder "friend@example.com or username".
  - new test classes in test_buds.py: SearchBudsViewTest (6 — prefix match, cap-3, email prefix, non-bud leakproof, empty-q, anon redirect) + SharePostImplicitAutoAddTest (4 — sharer.buds += recipient, recipient.buds += sharer, username-typed share, unregistered no-add) + AddBudViewTest.test_add_resolves_username_too. test_my_buds.py FT adds test_autocomplete_suggests_buds_by_username_prefix. test_sharing.py placeholder assertion updated to "friend@example.com or username".
  - 852 ITs (+11) + 5 my_buds 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>
2026-05-08 23:34:35 -04:00
Disco DeDisco
11ff109d1e my_posts: titles → @{handle}'s Posts + Posts by Others (view-side string build); applet-list link colour --terUser w. hover/active shifting to --ninUser + --terUser glow halo
- billboard.views.my_posts adds owner_posts_title (f"@{handle}'s Posts") + others_posts_title ("Posts by Others") to context. handle = owner.username or owner.email matches the navbar @-handle pattern.
  - my_posts.html shell invocations use the new vars instead of in-template |add: filter chains.
  - SCSS .applet-list .applet-list-entry > a: base color rgba(var(--terUser), 1), text-decoration none, font-weight bold; on :hover/:active color shifts to rgba(var(--ninUser), 1) + text-shadow 0 0 0.55rem rgba(var(--terUser), 0.7) for the lift halo. transition: text-shadow 0.15s ease.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:16:31 -04:00
Disco DeDisco
246e45e55d buds rename + applet-list shell — Buddies → Buds everywhere (model field, slug, URL, view, DOM, CSS); my_buds.html + my_posts.html share new _applet-list-shell.html partial — vertical-title applet-scroll card; my_posts hosts two side-by-side in landscape, stacked in portrait — TDD
- 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>
2026-05-08 23:08:33 -04:00
Disco DeDisco
5f6002aa70 buddies sprint phase 1: User.buddies M2M(self,symm=False) + my_buddies aperture page + add_buddy JSON endpoint + buddy btn slide-out — TDD; My Contacts applet renamed → My Buddies (slug + name + partial)
- lyric/0004 adds User.buddies = ManyToManyField('self', symmetrical=False, blank=True, related_name='added_as_buddy'). Asymmetric one-way add: A.buddies.add(B) doesn't reciprocate. Reverse via B.added_as_buddy.all() — load-bearing for the future "buddy changed username" snapshot-accept flow noted in design.
  - applets/0006 renames slug my-contacts → my-buddies + name 'Contacts' → 'My Buddies'. Existing migrations 0003/0004 untouched (historical artifacts).
  - billboard.views.my_buddies + add_buddy:
    • my_buddies: GET /billboard/my-buddies/ → renders the aperture page with request.user.buddies.all().
    • add_buddy: POST /billboard/buddies/add → JSON {buddy: {id, username, email}|null}. Privacy: returns null when email isn't a registered User OR is the requester's own; never leaks membership. Idempotent on re-add (M2M dedup).
  - templates:
    • _applet-my-contacts.html → _applet-my-buddies.html (heading + link to /billboard/my-buddies/).
    • my_buddies.html — bottom-anchored aperture list of buddies w. {% empty %} fallback "No buddies yet."
    • _buddy_add_panel.html — bottom-left handshake btn + slide-out, mirrors _buddy_panel.html (post share) but POSTs to add_buddy and appends to #id_buddies_list. Skips append if data-buddy-id already in DOM (race-safe). Drops the .buddy-entry--empty row on first add.
  - SCSS: page-billbuddies joins the body-class aperture trio; .buddies-page extends %billboard-page-base + flex-column + bottom-anchor for #id_buddies_list. id_applet_my_contacts → id_applet_my_buddies (test references + grid placement).
  - tests: new test_buddies.py — 14 ITs covering UserBuddiesM2MTest (asymmetric, idempotent), MyBuddiesViewTest (lists own buddies only, anon redirect), AddBuddyViewTest (registered/unregistered/self/idempotent/email-fallback/405). Existing test_views/test_billboard/test_game_kit references swapped to my-buddies. New test_my_buddies.py FT — 4 tests: pre-existing buddies render, empty state, add via panel appends entry w. username, unregistered silent no-op.
  - 841 ITs (+14) + 4 my_buddies 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>
2026-05-08 22:31:42 -04:00
Disco DeDisco
b3eb14140c admin Posts (NOTE_UNLOCK): readonly input + 'No response needed' placeholder + secUser focus glow + buddy btn suppressed + view POST 403 + Line.admin_solicited listener nukes errant writes; share Lines: drop ts suffix, author = sharer (adman fallback for anon legacy), silent no-op on re-share — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
- billboard/0005 adds Line.admin_solicited (BooleanField default False); RunPython backfills existing note_unlock Lines to True. Note.grant_if_new sets admin_solicited=True on its system prose.
  - billboard.models post_save signal: any Line saved on a Post.kind=NOTE_UNLOCK without admin_solicited=True is deleted (defense-in-depth alongside the view guard).
  - billboard.views.view_post hard-rejects POST on NOTE_UNLOCK kind (HTTP 403) — clean view-level contract; the post_save listener is the safety net for ORM/API paths that bypass it.
  - templates/apps/billboard/post.html: NOTE_UNLOCK branch renders the input as readonly w. 'No response needed at this time' placeholder + no method/action; user_post branch keeps the regular composer. Buddy panel include guarded behind `{% if post.kind != 'note_unlock' %}` — friend invites don't apply to admin threads.
  - SCSS: .post-line-form input.form-control[readonly]:focus uses --secUser glow (cooler than the regular --terUser composer focus).
  - share_post: drop the iso-timestamp suffix on Line.text (just 'Shared with {email}'); author = request.user (anon legacy fallback to adman so AnonymousUser doesn't break the FK); re-share of an already-in-shared_with recipient is a silent no-op (no second Line, brief: null in JSON response). Buddy panel JS now reads data-sharer-name from server-rendered display_name so the optimistic _appendLine matches the post-refresh state.
  - new ITs: test_admin_posts (PostRejectsAdminWritesTest, UnsolicitedLineListenerTest, NoteGrantSetsAdminSolicitedTest) — 7 tests; share_post tests rewritten for the new contract (drop ts, author=sharer, silent re-share dedup) — 12 tests; new FT test_admin_post_readonly w. AdminPostInputReadonlyTest + AdminPostHasNoBuddyBtnTest + UserPostInputUnaffectedTest — 4 tests. 827 ITs + 18 buddy/sharing 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>
2026-05-08 21:52:34 -04:00
Disco DeDisco
6f76f6c176 post aperture refactor (May-8b): Post.title field; Line.author PROTECT FK + created_at; Note.grant_if_new admin-vs-Look! format dispatch w. note-ref anchor; bottom-anchored aperture w. shared-between header + per-Line user/timestamp; dotted-? Brief square; reserved adman seed — TDD
- schema: billboard/0004 adds Post.title (CharField 35) + Line.author (PROTECT FK, related_name=authored_lines) + Line.created_at (auto_now_add); RunPython backfill stamps existing rows (note_unlock → "Notes & recognitions" + author=adman; user_post → first-line glean + author=Post.owner).
  - lyric/0003 seeds adman User (system author for note unlock + share invite Lines); apps.lyric.models gains RESERVED_USERNAMES = {"adman"}, is_reserved_username() guard in dashboard.set_profile, get_or_create_adman() lazy fetch (TransactionTestCase flushes the seed).
  - drama: Note.grant_if_new dispatches via _ADMIN_NOTE_SLUGS = {"super-schizo","super-nomad"} — admin slugs use "The administration recognizes…" prose; everyone else uses "Look!—new Note unlocked." Both wrap Note name in `<a class="note-ref">`. Header Line dropped (test_two_different_grants_share_one_post asserts 2 lines, not 3). Note.display_name property added (slug.title() default — "super-schizo" → "Super-Schizo"). User.active_title_display returns donned recognition title or "Earthman" default.
  - billboard models: Post.name property removed → my_posts.html, _applet-my-posts.html, PostSerializer switched to Post.title. LineForm.save(for_post, author) + ExistingPostLineForm.save(author) signature + all callers (api.views, billboard.views.new_post + view_post + share_post). billboard.views.share_post authors via get_or_create_adman; new_post truncates first line for Post.title via _truncate_post_title.
  - post.html: <h3> post title heading; .post-shared-recipients (commas only) + .post-shared-self lines ("just me, X the Earthman" / "& me, X the Y" 0/≥1 split); #id_post_table is now a <ul> w. justify-content: flex-end + per-Line 3-col grid (author/text/time); adman Lines render |safe + .post-line--system italic; #id_text → #id_post_line_text rename (post.html only — /billboard/ new-post applet keeps #id_text); page_class page-billpost (joins billboard+billscroll body-class trio).
  - SCSS _billboard.scss: .post-page extends %billboard-page-base, adds bottom-anchored flex-column scroll + 3-col .post-line grid + .post-line-form pinned at bottom. _note.scss: a.note-banner__image picks up .note-item__image-box dashed-? styling for the Brief square.
  - _buddy_panel.html JS rewired for new layout: _appendLine builds <li class="post-line post-line--system"> w. adman+timestamp; _appendRecipientChip handles 0→1+ transition (rewrites "just me," → "& me,", inserts .post-shared-recipients line above self).
  - FT post_page.py: get_table_rows queries .post-line; wait_for_row_in_post_table matches by text containment (line_number arg ignored — kept for backwards compat); get_line_input_box probes #id_post_line_text first, falls back to #id_text; get_post_owner reads textContent (hidden span). test_applet_new_post_line_validation switched to input[name="text"]:invalid/:valid for cross-page selectors.
  - rootvars.scss: minor plutonium + fuschia tweaks (pre-existing).
  - 818 ITs + 35 FTs (buddy/new-post/sharing/validation/layout/jasmine/my-notes/my-posts) green.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:29:21 -04:00
Disco DeDisco
e465b6a3b3 buddy btn sprint scaffolding: TDD spec + partial template + SCSS + page_class — 15/16 FTs green, 1 captured-red for post-compaction handoff
Pre-compaction handoff for the bottom-left handshake btn that replaces the inline share form on post.html. The full spec lives in functional_tests/test_buddy_btn.py — running it as a red-first TDD checklist for the next agent (or future Disco) to pick up after compaction.

Scaffolding landed:
- functional_tests/test_buddy_btn.py — 16 tests across 6 classes covering presence-only-on-post.html (B1–B3), bottom-left fixed positioning matching kit-btn dimensions, slide-out panel structure (closed/open width, OK btn = .btn-confirm, recipient input left-padding clears the glyph), kit↔buddy mutual-exclusion via opacity, click-outside + Escape dismiss + clear, OK→async share creates Brief + appends Line + chips the recipient + closes the panel + clears the field, and post.html / my_posts.html body class picks up the aperture marker (page-billboard).
- templates/apps/billboard/_partials/_buddy_panel.html — the partial: <button id="id_buddy_btn"><i class="fa-solid fa-handshake"></i></button> + #id_buddy_panel housing #id_recipient and #id_buddy_ok (.btn.btn-confirm). Inline JS mirrors the game-kit.js click/escape/click-outside pattern, toggles html.buddy-open + .active on the btn, intercepts OK to POST share-post w. Accept:application/json (reuses C3.b shape — line_text + brief.to_banner_dict() + recipient_display), appends the chip + line in-DOM, Brief.showBanner shows the slide-down banner, _close clears the input.
- templates/apps/billboard/post.html — drops the inline #id_share_form / #id_recipient / SHARE-primary block + its JS; includes the buddy panel partial at the end of {% block content %}.
- billboard.views.view_post + my_posts now set page_class="page-billboard" so the body class hooks into the aperture SCSS group (the user noted post.html wasn't in that group; this brings it in).
- static_src/scss/_buddy.scss — new partial: #id_buddy_btn fixed bottom-left mirror of #id_kit_btn (3rem circle, secUser border, .active state, transition: opacity 0.15s); #id_buddy_panel slide-out spans calc(100vw - 3rem) (1.5rem each side w. landscape sidebar carve-outs), transform: scaleX(0)→1 from left center on html.buddy-open, opacity 0→1, the recipient input gets padding 0 1rem 0 3.5rem so the glyph doesn't overlap. Mutual exclusion: html.buddy-open #id_kit_btn → opacity:0; html:has(#id_kit_bag_dialog[open]) #id_buddy_btn → opacity:0 (uses :has() per project convention; no JS-side kit-open class needed).
- core.scss imports buddy after game-kit.

15/16 FTs green; the lone red is BuddyBtnOkSubmitsAsyncShareTest.test_ok_creates_brief_appends_line_and_chip — server flow works (Brief is created, recipient chip + line append in DOM both visible in the screendump), only the .note-banner injection isn't surfacing on post.html. Likely cause: note.js inserts after the first <h2>, but post.html's only h2 is the rotated navbar header which is position:absolute, so the banner's geometry parents to that and falls outside the visible aperture. Two clean follow-ups for the post-compaction agent: (a) make Brief.showBanner pick a different anchor when h2.parentElement is position:absolute, or (b) define a #id_brief_banner_anchor in base.html under the page content and have showBanner prefer it.

Also pending for post-compaction: update functional_tests.post_page.PostPage.share_post_with() to drive the new buddy-btn flow (click btn → type → click OK → wait for chip) so the legacy test_sharing FT keeps working — currently it still operates on the inline form selectors that no longer exist.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 19:00:28 -04:00
Disco DeDisco
14bab444ff brief sprint C3.b+c+d+e: share-post Line+Brief async, magic-link / invalid-link banners use Brief styling, .alert-* retired — TDD
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>
2026-05-08 18:15:43 -04:00
Disco DeDisco
7f9ff36d1d brief sprint C2: introduce billboard.Brief notification model + view_post marks-read on GET — TDD
Brief is the slide-down-banner record that connects an event (a Line freshly appended to a Post) to a user who needs to see it. It's the C3 attachment point for note-unlock + share-invite + future event sources; the banner JS (C3) reads the Brief shape to render kind-specific affordances. C2 lays the schema + the FYI-read contract; C3 hooks the senders.

Schema (billboard.Brief):
- owner FK→lyric.User (related_name='briefs') — required; whose attention this is for
- post  FK→billboard.Post (related_name='briefs') — required; where FYI navigates
- line  FK→billboard.Line (related_name='briefs', null=True) — the appended Line that triggered the Brief; nullable for share-invite-style flows where the Line write races behind the Brief
- is_unread BooleanField default=True — flips on view_post GET
- kind CharField (note_unlock | user_post | share_invite, default=user_post) — drives banner-side affordances
- title CharField (blank=True) — banner display title
- created_at DateTimeField (default=timezone.now) — Meta.ordering='-created_at'

view_post (the post-detail GET) now bulk-updates is_unread=False on every Brief where owner == request.user AND post == our_post AND is_unread=True. POST (the compose-a-new-Line path) intentionally does NOT mark read — the user is authoring, not reviewing.

Tests: BriefModelTest (7) covers defaults, kind choices include all three values, line nullability, owner+post requiredness, title field, __str__ shape. ViewPostMarksReadTest (5) covers the GET flips owner's unread Brief to read; doesn't flip other users' Briefs on the same post; doesn't flip Briefs on unrelated posts; idempotent for already-read; POST request does NOT mark read.

Auto migration billboard/0002_brief creates the table. 801-test IT regression green (789 + 12 new).

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 17:35:46 -04:00
Disco DeDisco
d192b1522d brief sprint C1: relocate Post + Line from dashboard → billboard (no behavior change) — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
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>
2026-05-08 17:20:06 -04:00
Disco DeDisco
5413e63585 billboard Most Recent Scroll: fix SQLite NULL drop on SIG_READY exclude; pronouns flow FT; Blades middle reversal Nervous → Fickle — TDD
- billboard/views.py _billboard_context: `.exclude(verb=SIG_READY, data__retracted=True)` was silently dropping every SIG_READY event whose data had no `retracted` key — `WHERE NOT (NULL AND verb='sig_ready')` evaluates to NULL via JSON_EXTRACT, which the SQL engine treats as "row not satisfying WHERE", so the row was excluded. Fix: pull a 100-row buffer w. only the SIG_UNREADY exclude at the SQL level, then post-filter retracted SIG_READY in Python before slicing to 36; PostgreSQL handles the lookup correctly so this is a SQLite-only manifestation that explained intermittent "No events yet" in Most Recent Scroll
- CLAUDE.md gotchas: new entry warning that `.exclude(data__key=value)` / `.filter(data__key=value)` on SQLite JSONField bites on missing keys; if the predicate must require key existence, post-filter in Python
- functional_tests/test_game_kit.py PronounsAppletFlowTest: end-to-end profile-wide pronoun flip — start on per-room billscroll seeing "their" cognates, navigate to Game Kit, click bawlmorese card, assert guard portal active w. "yo/yo/yos" preview, click OK, navigate to billboard + see Most Recent Scroll re-rendered w. "yos", navigate back to billscroll + see same flip; covers the whole render-time-pronoun-resolution path on real DOM
- epic/0008_blades_reversal_fickle.py: rename Middle Arcana Blades reversal_qualifier "Nervous" → "Fickle" (RunPython forward+reverse on arcana=MIDDLE, suit=BLADES, number ∈ {11,12,13,14}); SigSelectSpec.js hardcoded "Nervous" updated to "Fickle" + collected static

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:27:17 -04:00
Disco DeDisco
2dc68c41a7 billboard applets: drop billboard- prefix from partials & ids; Most Recent → Most Recent Scroll; room_scroll → scroll — TDD
- Slug renames (mig 0004): billboard-my-scrolls → my-scrolls; billboard-my-contacts → my-contacts; billboard-most-recent → most-recent-scroll (name → "Most Recent Scroll"); billboard-notes → notes
- Partial filenames lose the `billboard-` token to mirror dashboard/gameboard convention; element ids follow (id_applet_my_scrolls, id_applet_my_contacts, id_applet_most_recent_scroll, id_applet_notes, id_applet_scroll); .applet-billboard-scroll → .applet-scroll
- View fn billboard.views.room_scroll → scroll; template apps/billboard/room_scroll.html → scroll.html (URL name `billboard:scroll` already correct)
- ITs + FTs updated to new identifiers; SCSS selectors retitled

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 23:22:01 -04:00
Disco DeDisco
75fcc5b34d billboard applets: single-root wrapper for HTMX swap; full context on toggle — TDD
- _applets.html wraps menu + container in one #id_billboard_applets_wrapper div; form's hx-target is now the wrapper, so OK no longer leaves a stale duplicate menu in the DOM (which previously caused the next OK to revert prior toggles)
- toggle_billboard_applets passes full context (recent_room, recent_events, viewer, my_rooms) via factored _billboard_context helper, so Most Recent + My Scrolls keep their content after a toggle instead of falling through to the empty fallback
- applets.js: register id_billboard_applets_wrapper as an applet container so post-swap menu cleanup runs
- BillboardAppletsTest: portrait viewport in setUp; FT covers content preservation, no-revert on second toggle, & post-refresh state
- 4 new ITs: Most Recent renders Coin-on-a-String after toggle; My Scrolls renders room name; response has single menu div; second toggle preserves prior hidden state

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 17:15:26 -04:00
Disco DeDisco
e78bbb873b Billnotes note-page interaction: hover glow, click-lock, DON/DOFF; note titles + card-ref styling — TDD
- note-page.js: click-lock (_lockedItem + notes-locked body class); DON auto-DOFFs prev donned; _setGreeting updates navbar; _donnedItem exposed for test API
- NotePageSpec.js: 18 Jasmine specs covering lock/unlock, DON/DOFF state, auto-DOFF, greeting update, initial load; flushPromises helper for chained fetch .then()
- _note.scss: DON/DOFF opacity:0 by default; hover + locked + donned states show them; body:not(.notes-locked) hover suppression
- views.py: Super-Schizo/Super-Nomad card titles; recognition_title field (display_title) separate from card title; mark_safe descriptions w. card-ref spans
- my_notes.html: |safe on description; recognition_title for Recognitions block
- _navbar.html: id_greeting_prefix/id_greeting_name spans for JS greeting update
- _base.scss: global .card-ref rule (--terUser, font-weight 600, !important)

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 01:54:57 -04:00
Disco DeDisco
6ad736413b super-schizo + super-nomad Notes: auto-grant to superusers; sig unlock; navbar titles — TDD
- drama/models.py: _NOTE_DISPLAY dict; Note.display_title / .display_greeting
  properties; super-schizo → "21st Century" + "Schizoid Man";
  super-nomad → "Howdy," + "Stranger"
- billboard/views.py: _NOTE_META super-schizo/nomad entries with mark_safe
  HTML descriptions ("card-ref"-styled card names), swatch_label "I"/"0",
  no palette_options; swatch_label added to note_items context
- lyric/models.py post_save: new superusers get super-schizo + super-nomad
  Notes automatically; setup_sig_session grants them explicitly too
- epic/models.py _filter_major_unlocks: accepts super-nomad / super-schizo
  as valid unlocks alongside their plain counterparts
- _navbar.html: display_greeting|safe + display_title replace slug|capfirst
- my_notes.html: note-item__image-box--label branch for swatch_label
- _note.scss: .note-item__image-box--label modifier (bold italic, solid border)
- _base.scss: .ord global ordinal superscript class (21st etc.)
- ITs: SuperuserNoteGrantTest (3); SigSelectRenderingTest +2 (super- variants)

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 01:30:02 -04:00
Disco DeDisco
214120ef2d note title equip: DON/DOFF buttons outside top-left corner; User.active_title FK; don/doff views; greeting updated via JS — TDD
- lyric/models.py: active_title = ForeignKey('drama.Note', null=True, SET_NULL)
- lyric migration 0020_add_active_title
- billboard/views.py: don_title + doff_title views; is_equipped per note_item context
- billboard/urls.py: note/<slug>/don + note/<slug>/doff routes
- _navbar.html: id_greeting_name span; shows active_title.slug|capfirst when set
- my_notes.html: .note-don-doff buttons (DON/DOFF, × when disabled); data-don-url/doff-url/title attrs
- note-page.js: _bindDonDoff() — DON POST sets greeting + swaps btn state; DOFF restores Earthman
- _note.scss: .note-don-doff position:absolute left:-1rem top:0; flex-direction:column gap:1.25rem
- ITs: NoteEquipTitleViewTest (5 tests); UserModelTest.test_active_title_* (3 tests)
- FT: NoteEquipTitleTest.test_don_equips_title_greeting_and_doff_restores

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 01:44:58 -04:00
Disco DeDisco
7d4389a74a note palette modal: NVM closes confirm only; confirm visibility via style.display; swatch labels from _PALETTE_DEFS; recognitions block — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
- note-page.js: NVM hides confirm (style.display='none') — swatch modal stays open;
  _showConfirm/_hideConfirm use style.display to bypass CSS specificity issues;
  _doSetPalette reads data-palette-label before modal closes; appends
  .note-recognitions__palette-line w. dim+bold markup after OK
- billboard/views.py: import _PALETTE_DEFS; _PALETTE_LABELS dict; _palette_opts()
  enriches palette_options w. {name, label}; my_notes adds palette_label to note_items
- _note.scss: confirmed palette swatch uses gradient (palette vars cascade from
  palette-* class); hardcoded bardo/sheol bg overrides removed; .note-recognitions block
  w. .note-recognitions__header (tt-sign-section-header style) & __dim (tt-dim style);
  .note-swatch-label in terUser bold; .note-item__palette gradient; confirm display:none default
- my_notes.html: p.name/p.label replaces slice hack; data-palette-label on swatch rows;
  Recognitions block w. dim spans & strong values; removes hidden attr from confirm
- IT: test_palette_modal_renders_swatch_labels; test_also_saves_user_palette
- FT: NVM test corrected — modal stays open, confirm is_displayed() False; T2a URL fix

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 01:31:19 -04:00
Disco DeDisco
cd5252c185 note palette: swatch previews body palette, NVM reverts, OK saves sitewide; note_set_palette also saves user.palette — TDD
- note-page.js: body class swap on swatch click; 10s auto-revert timer; NVM reverts;
  .note-item--active persists border/glow while modal open; .previewing on swatch
- billboard/views.py: note_set_palette also saves user.palette via _unlocked_palettes_for_user
- _note.scss: .note-swatch-body gradient (palette vars cascade from parent palette-* class);
  .previewing state; .note-item--active; note-palette-modal tooltip glass;
  note-palette-confirm floats below modal (position:absolute, out of flow)
- my_notes.html: note-item__body wrapper; image-box right; swatch row OK buttons removed
- FTs: T2a URL fix (/recognition → /my-notes); T2b split into preview+persist & NVM tests;
  NoteSetPaletteViewTest.test_also_saves_user_palette IT

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 23:54:05 -04:00
Disco DeDisco
473e6bc45a rename: Note→Post/Line (dashboard); Recognition→Note (drama); new-post/my-posts to billboard
- dashboard: Note→Post, Item→Line across models, forms, views, API, urls & tests
- new-post (9×3) & my-posts (3×3) applets migrate from dashboard→billboard context; billboard view passes form & recent_posts
- drama: Recognition→Note, related_name notes; billboard URL /recognition/→/my-notes/, set-palette at /note/<slug>/set-palette
- recognition.js→note.js (module Note, data.note key); recognition-page.js→note-page.js; .recog-*→.note-*
- _recognition.scss→_note.scss; BillNotes page header; applet slug billboard-recognition→billboard-notes (My Notes)
- NoteSpec.js replaces RecognitionSpec.js; test_recognition.py→test_applet_my_notes.py
- 4 migrations applied: dashboard 0004, applets 0011+0012, drama 0005; 683 ITs green

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 22:32:34 -04:00
Disco DeDisco
6d9d3d4f54 recognition: page, palette modal, & dashboard palette unlock — TDD
- billboard/recognition/ view + template; recognition/<slug>/set-palette endpoint (no trailing slash)
- recognition.html: <template>-based modal (clone on open, remove on close — Selenium find_elements compatible)
- recognition-page.js: image-box → modal → swatch preview → body-click restore → OK → confirm → POST set-palette
- _palettes_for_user() replaces static PALETTES; Recognition.palette unlocks swatch + populates data-shoptalk
- _unlocked_palettes_for_user() wires dynamic unlock check into set_palette view
- _applet-palette.html: data-shoptalk from context instead of hard-coded "Placeholder"
- _recognition.scss: banner, recog-list/item, image-box, modal, palette-confirm; :not([hidden]) pattern avoids display override
- FT T2 split into T2a (banner → FYI → recog page), T2b (palette modal flow), T2c (dashboard palette applet)
- 684 ITs green; 7 FTs green

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 04:02:14 -04:00
Disco DeDisco
7c249500bd refactor: extract apply_applet_toggle, rooms_for_user & natus helpers to utils; DRY toggle views
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
- epic/utils.py (new): _planet_house, _compute_distinctions, rooms_for_user
- applets/utils.py: apply_applet_toggle replaces 5 copy-pasted toggle loops
- dashboard/views.py: use apply_applet_toggle; fix double free_tokens/tithe_tokens query in wallet(); promote _compute_distinctions import to module level
- gameboard/views.py: use apply_applet_toggle & rooms_for_user; fix double free_tokens query in toggle_game_applets
- billboard/views.py: use apply_applet_toggle & rooms_for_user
- SCSS: %tt-token-fields placeholder in _tooltips.scss; _gameboard & _game-kit @extend it
- epic/tests/unit/test_utils.py (new): coverage for _planet_house fallback path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 15:46:30 -04:00
Disco DeDisco
32d8d97360 wired PICK SKY server-side polarity countdown via threading.Timer (tasks.py); fixed polarity_done overlay gating on refresh; cleared sig-select floats on overlay dismiss; filtered Redact events from Most Recent applet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 00:34:05 -04:00
Disco DeDisco
cde231d43c billscroll should now remember user's position across devices 2026-03-24 17:44:34 -04:00
Disco DeDisco
c71f4eb68c styled more of Most Recent applet, allowing for scrolling of 36 most recent events and Load More link 2026-03-24 17:19:09 -04:00
Disco DeDisco
189d329e76 new applet structure for apps.billboard, incl. My Scrolls, Contacts & Most Recent applets; completely revamped _billboard.scss, tho some styling inconsistencies persist; ensured #id_billboard_applets_container inherited base styles found in _applets.scss; a pair of new migrations in apps.applets to support new applet models & fields; billboard gets its first ITs, new urls & views; pair of new FT classes in FTs.test_billboard 2026-03-24 16:46:46 -04:00
Disco DeDisco
91e0eaad8e new DRAMA & BILLBOARD apps to start provenance system; new billboard.html & _scroll.html templates; admin area now displays game event log; new CLAUDE.md file to free up Claude Code's memory.md space; minor additions to apps.epic.views to ensure new systems just described adhere to existing game views
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-19 15:48:59 -04:00