User report: hex felt smaller than the aperture even at portrait (off-centered, room to spare on top + bottom), and chair labels overlapped the hex edges at landscape — progressively worse as the hex grew at larger viewports. Three contributors stacked: (1) `.room-shell { max-height: 80vh }` capped the shell at 80% of viewport height even when the .room-page aperture had more room — at 1789×1031 this donated 228px (1053→825) of aperture height to dead margin; (2) scene design 360×300 was wider than tall (1.2 aspect) but landscape aperture is narrower than tall (~1.4), so the height cap bottlenecked scene-scale at min(aperture_w/360, aperture_h/300) instead of letting the hex grow; (3) chair font-size scales w. rem (clamp(14,2.4vmin,22)) but chair position scales w. --table-scale — at large viewports rem maxes at 22 so labels widen and push chair icons further from box-center toward the hex (visual "creep") ; fix: remove the 80vh cap (`max-height: 80vh` → `height: 100%` on .room-shell L340) so the shell stretches to fill the .room-page aperture; bump hex from 160×231 to 200×231 (regular pointy-top w. width = height × √3/2 = 200 * 1.1547 — comment in _room.scss updated); apothem of 200-wide pointy-top regular hex is 100px exact (200/√3 × √3/2), so `$pos-d` 110px → 140px gives 40px design-units of radial chair clearance (was 30); derived `$pos-d-x: round(140*0.5) = 70`, `$pos-d-y: round(140*0.866) = 121` for slot 2/3/5/6 diagonal anchors at 60° from horizontal (matches existing geometry approach); scene design height 300 → 320 to leave enough vertical headroom at large landscape that the rem-driven (font-size 1.6rem × scale) chair icons + labels don't clip the aperture top/bottom edges — at 1789×1111 w. scene_H=300 the AC/BC label tops sat AT aperture top (y=-21 vs aperture y=-22), bumping to 320 drops scale from 4.05 → 3.54 and leaves 76px of headroom; SCENE_H in room.js bumped to 320 to match (Math.min(w/SCENE_W, h/SCENE_H) sets --table-scale CSS var via transform: scale on .room-table-scene) ; visual verification via Claudezilla across three viewports (no test layer per user preference — layout regression coverage via spot-check on next room render) — iPhone-14 portrait 566×875: hex 243×281 → 314×363 (+29% wider, fills 55% of aperture width vs 44% before); mid landscape 1149×781: hex 333×385 → 493×569 (+48% wider, 56% vs 38% before); large landscape 1789×1111: hex 440×509 → 708×818 (+61% wider, 48% vs 30% before — the most dramatic improvement, matching user's "progressively worse the larger the hex grows" observation). Chair clearance now uniform 40 design-units radially across all scales; AC/BC labels stay 76px inside aperture top at the largest viewport ; dead `$seat-r`/`$seat-r-x`/`$seat-r-y` consts at L357-359 left in place (unused elsewhere in codebase but out of scope for this layout fix) ; full IT/UT 999 green in 46s — no regressions; .table-hex / .table-hex-border / .room-table-scene / .table-seat positioning consts are the only refs to these dimensions across SCSS & JS so no cascade beyond room layout. Unblocks Sprint 2+ (My Sea applet will share the same hex CSS, parameterized, per user's intent for future friend-invite up-to-6-person rooms)
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
CAST SKY btn click handler in sig-select.js init() was bound to `Tray.open()` — wrong on two counts: (1) the tray was already played during the `polarity_room_done` → `Tray.placeSig` sequence (sig stage card slides into the tray cell before the overlay dismisses), so re-opening it on CAST SKY click pops the tray a second time; (2) Sky Select never opens — `_sky_overlay.html` is only `{% include %}`d server-side when `room.table_status == "SKY_SELECT"`, so during SIG_SELECT the partial + its `openSky` handler aren't in the DOM and `Tray.open()` is the only thing the click does. Bug surfaced symmetrically in both polarity rooms regardless of which finished first ; fix: replace `Tray.open()` w. `window.location.reload()` so the server re-renders the room w. table_status=SKY_SELECT — which surfaces the sky overlay partial + the `openSky` handler bound at _sky_overlay.html:192-193. Same pattern as `_onSkyConfirmed` in the sky partial (location.reload after sky save) ; testability hook mirrors `RoleSelect.setReload` (role-select.js:236): `var _reload = function () { window.location.reload(); };` at module scope, listener calls `_reload()` (closure looks up the var at click time so reassignment works), `setReload(fn)` exposed on the module's test API. SigSelectSpec.js adds `describe("CAST SKY click (post pick_sky_available)")` w. 2 specs — reload spy hit on click + `Tray.open` spy NOT hit on click; the negative assertion catches the original bug, the positive verifies the fix's intent. Existing 363 specs untouched ; Jasmine FT green in 8.6s; full IT/UT 999 green in 44s ; collectstatic mirror at src/static/apps/epic/sig-select.js refreshed in same commit so the served JS carries the fix (Django serves from STATIC_ROOT, not from app static dirs, in StaticLiveServerTestCase)
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
pipeline fix — `_post_gear.html` (commit 6a7464e) gated the NVM target on `{% url 'billboard:my_posts' user_id=request.user.id %}`, which exploded w. NoReverseMatch when an anonymous user (Percival ch.18 anonymous-post lab — ownerless `Post.objects.create()`) hit view_post (which has no @login_required); whole gear-include now wrapped in `{% if request.user.is_authenticated %}` since anonymous viewers can't DEL/BYE/back-to-my-posts anyway; AnonymousPostViewerTest pins the 200-render + gear-absence contract so future ownerless-post regressions surface in ITs (pipeline run #298 fixed) — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
- Brief schema (billboard/0007): post FK becomes nullable + new room FK to epic.Room + KIND_GAME_INVITE enum value. to_banner_dict resolves post_url to reverse('epic:gatekeeper', room.id) when post is null and room is set.
- epic.invite_gamer view refactor:
• Accepts `recipient` (matches bud-panel field; legacy `invitee_email` still works for full backwards compat).
• Resolves via apps.billboard.views._resolve_recipient (email if "@" present, else username).
• RoomInvite stores the resolved User's email (or raw input if unregistered).
• Auto-adds inviter ↔ recipient to each others' buds (symmetric per Phase 2 spec) when recipient is a registered User.
• Spawns a Brief w. owner=request.user, kind=GAME_INVITE, room=room, post=null.
• Accept: application/json → {brief, recipient_display}; otherwise redirects to gatekeeper as before.
• Self-invite + blank recipient: 200 w. brief=null, no RoomInvite, no buds touch.
- _gatekeeper.html: gate-invite-panel block (lines 62-71) removed.
- new templates/apps/billboard/_partials/_bud_invite_panel.html: clone of _bud_panel.html w. data-invite-url + autocomplete from request.user.buds. JS posts to invite_gamer + Brief.showBanner. room.html includes it owner-only when not table_status and gate_status != RENEWAL_DUE.
- room.html scripts block now loads apps/dashboard/note.js so window.Brief is defined for the slide-down banner.
- Tests: new test_invite_gamer.py (14 ITs) covering ajax + legacy form-submit paths, recipient resolution, RoomInvite creation, Brief w. room FK + GAME_INVITE kind, symmetric buds auto-add, unregistered/self/blank silent no-op cases. New test_gatekeeper_bud_btn.py FT (9 tests) covers presence (owner-only), absence of legacy #id_invite_email, async invite flow end-to-end (RoomInvite, Brief, banner, panel close, username resolve, buds auto-add).
- test_brief.test_brief_owner_post_required relaxed to test_brief_owner_required (post is now nullable).
- test_room_gatekeeper.test_second_gamer_drops_token_into_open_slot updated to drive the bud-btn flow (drops the #id_invite_email/#id_invite_btn references).
- 866 ITs (+14) + 9 gatekeeper FTs + 28 existing room-gatekeeper 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>
- _game-kit.scss #id_kit_btn landscape rule now uses `right: calc((var(--sidebar-w) - 3rem) / 2)` — same formula as gear-btn (_applets.scss) and bud-btn (_bud.scss). All three 3rem-wide circular btns now share the same horizontal-centre math against the fluid sidebar.
- Drops `@media (orientation: landscape) and (min-width: 1800px) { right: 2.5rem }` which was a leftover from the old doubled-8rem-sidebar regime; the rem clamp ceiling now caps the sidebar without per-breakpoint overrides.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- portrait h2 spans get padding-inline-end / padding-inline-start (0.4em each) at the 45/55 boundary; box-sizing: border-box keeps the flex basis honest. Solves the "B I L L B O A R D" run-together where the L of BILL touches the B of BOARD.
- landscape h2 drops the `display: block` override + the > span resets that nuked text-align: justify and flex. Now inherits the portrait flex 45/55 + per-span justify + padding-inline. With writing-mode: vertical-rl, the flex axis runs vertically (45% bottom for BILL post-rotate, 55% top for POST/BOARD/etc.); padding-inline-end resolves to the bottom edge of the first span = natural gap between the two rotated words.
- Explicit h2 height: 80vh in landscape so the flex 45/55 percentages have a defined basis to resolve against (block height isn't auto-derived in writing-mode: vertical-rl).
- 8 layout/navbar FTs still green; assertions are categorical (position: fixed, etc.) not exact-px.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- html { font-size: clamp(14px, 2.4vmin, 22px) } — single sliding scale; everything in rem (sidebar widths, h2 font-size, paddings) scales together. Phone rotation swaps width/height but vmin stays the same → 1rem stays the same → navbar/footer/h2 hold their size between portrait + landscape.
- :root --sidebar-w: 5rem (replaces the locally-scoped $sidebar-w SCSS var that lived inside @media blocks); --h2-col-w: 3rem for the rotated wordmark column in landscape. var(--sidebar-w) + var(--h2-col-w) are the only knobs that move the layout.
- Landscape container: margin-left = calc(var(--sidebar-w) + var(--h2-col-w)); margin-right = var(--sidebar-w). Applets are now clipped INSIDE the h2 column, so the rotated "BILLPOST" / "DASHBOARD" wordmark never has content bleeding behind it (the original complaint).
- h2 markup refactor across 13 templates: <span>BILL</span><span>POST</span> instead of <span>BILL</span>POST. Portrait styling: display: flex; first span flex 0 0 45% + --quaUser colour; second span flex 0 0 55% + --secUser inherited. Per-span text-align: justify + text-justify: inter-character keeps the inter-letter spacing within each span. Landscape resets the flex (single rotated wordmark, not split).
- Drop the four h2 font-size jumps (min-height: 400/500/800px) — single font-size: 3rem now scales fluidly via root rem. Drop the @media (orientation: landscape) and (max-width: 1100px) h1 override (rem-fluid handles cramped widths). Drop the entire @media (orientation: landscape) and (min-width: 1800px) sidebar-doubling block in _base.scss / _applets.scss / _bud.scss — the rem clamp ceiling already caps the size.
- _bud.scss + _applets.scss: bud-btn / bud-panel / bud-suggestions / gear-btn / applet menus all switch to var(--sidebar-w)-based positioning; landscape rules are single (no per-breakpoint duplication).
- Per-spec tradeoff: non-.btn-primary buttons (BYE / NVM / OK / kit-btn / etc.) inherit rem-fluid like everything else and will scale slightly w. viewport. User explicitly OK'd this — they don't need to stay px-fixed.
- 852 ITs + 24 layout/navbar/bud FTs green; existing geometry assertions are relative or categorical (not exact-px) so the rem clamp doesn't surface failures at the 800x1200 FT viewport.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tonal shift to lean into the bureaucratic-flavour of the @adman entity. Going-forward only; existing super-schizo / super-nomad Lines in DB keep the old prose.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- new lyric_extras.at_handle filter: '@{username}' if user.username, else truncate_email(user.email). Companion to display_name (which has no @-prefix). Used by post.html line author col + self/shared self lines.
- post.html updates: line author span renders {{ line.author|at_handle }}; .post-shared-recipients chips render {{ r|at_handle }} + .post-attribution; .post-shared-self wraps "{handle} the {title}" in <span class="post-attribution">. The 'just me' / '& me' prose stays plain (only the handle+title combo is coloured).
- Note.grant_if_new prose wraps both the @-handle (or bare email fallback) AND the title in <span class="post-attribution">. Standard format wraps the combo "{handle} the {title}" together; admin format wraps each independently since the prose splits them ("recognizes @disco for ... customary title of Schizoid Man"). Existing Lines unchanged — going-forward styling only.
- SCSS: .post-attribution { color: rgba(var(--quaUser), 1); } scoped at .post-page so it lights up in both .post-header descendants and #id_post_table descendants. .post-line-author also switches from opacity-based dim to the same --quaUser key (drops opacity 0.75 since the colour change reads as the de-emphasis on its own).
- 852 ITs still green — line.text inclusions ("Stargazer", "alice@test.io" etc.) still substring-match through the wrapping spans.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
- 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>
- 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>
- 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>
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>
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>