Files
python-tdd/src/templates/apps/billboard/bud.html

98 lines
4.2 KiB
HTML
Raw Normal View History

bud landing page: /billboard/buds/<id>/ + my_buds tooltip portal + @mailman post-attribution anchor — TDD Replaces the @mailman invite Line's inline OK/BYE block w. a dedicated per-bud surface. Three new FTs (test_bill_bud_page, test_bill_my_buds_tooltip, test_bill_mailman_invite_post — landed red 2026-05-27 PM) drive: per-bud landing page rendering 4-btn apparatus + shoptalk textarea + invite-cascade glow handoff; my-buds row tooltip portal w. .tt-title/.tt-description/.tt-email/.tt-shoptalk/.tt-milestone slots; mailman Brief surfacing on any authenticated page-load via context processor + base.html JSON-script. Models: new `BudshipNote(user, bud, shoptalk[CharField max=160], edited_at)` w. unique_together — per-relation personal note about a bud, never visible to the bud. Lazy-created on first shoptalk save so absence of a row reads as 'never edited' (drives .tt-milestone slot presence). URLs (billboard): `buds/<uuid:bud_id>/` (bud_page), `buds/<uuid:bud_id>/shoptalk` (save_bud_shoptalk), `buds/<uuid:bud_id>/delete` (delete_bud). Views: bud_page auto-adds the bud on first visit (mirrors share_post implicit-add); resolves `pending_invite` as non-expired PENDING SeaInvite(owner=bud, invitee=request.user) → drives `sea_btn_active` + `sea_first_draw_pending` flags that _burger.html already reads on my_sea + room. my_buds enriches each bud w. `.shoptalk_text` + `.milestone_dt` so the row template can render data-tt-* attrs without an extra template tag. mail.py: INVITE_TEMPLATE now interpolates `owner_id` into an `<a class="post-attribution" href="/billboard/buds/{owner_id}/">{handle}</a>` wrapper around the owner's handle. post.html's existing safe-filter branch (gated on author username == 'mailman') passes it through unescaped. Removed the {% if line.sea_invite %} include path — _invite_actions.html left in place for archival. Templates: new bud.html (header + shoptalk form + apparatus + gear + burger fan + sea_btn nav inline JS); new _bud_gear.html (NVM→my_buds, DEL→guard portal "Delete this bud?" → POST delete_bud); new _bud_tooltip.html (portal w. .tt-* slots); _my_buds_item.html wraps `@handle` in an anchor to bud_page + carries data-tt-* attrs + " the {{ active_title_display }}"; my_buds.html includes the tooltip portal + loads my-buds-tooltip.js. JS: new my-buds-tooltip.js binds row clicks → .row-locked + populates #id_tooltip_portal from data-tt-* attrs; anchor clicks pass through to navigate; .tt-milestone is removed from DOM (not just emptied) when never-edited so the FT can distinguish absent vs cleared-after-edit. SCSS: extend landscape gear-btn rule + #id_*_menu rule w. `.bud-page` + `#id_bud_menu` (otherwise gear-btn collided w. bud-btn in landscape on bud.html). Bump active burger sub-btn z-index to 1 so click hit-test picks the active sub-btn during the 0.25s fan arc-out animation (otherwise a later-in-DOM inactive btn obscured the active target during transition). Cross-page Brief surface: new `mail_brief_payload` context processor injects the user's oldest unread MAIL_ACCEPTANCE Brief into every authenticated response; base.html renders the JSON-script + auto-fires Brief.showBanner. Mark-read still rides view_post's existing GET unread-flip — no new endpoint. Pre-existing MySeaInvitePostRenderTest (test_sea_invite_views.py) inverted to match the new contract: the .invite-actions sweep is unconditional (PENDING / ACCEPTED / DECLINED all carry prose only); pinned the post-attribution anchor + bud-page href in its place. 1518 ITs green (1475 app ITs + 43 sprint ITs), 23 sprint FTs green (5 my_buds tooltip + 13 bud page + 5 mailman invite post). Jasmine specs from sprint plan deferred — FT coverage of burger-glow / row-lock / portal-populate paths suffices and the textarea blur-POST flow isn't implemented in this sprint (form is server-action only, save-on-blur AJAX is a follow-on). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:45:20 -04:00
{% extends "core/base.html" %}
{% load static %}
{% load lyric_extras %}
{% block title_text %}Billbud{% endblock title_text %}
{% block header_text %}<span>Bill</span><span>bud</span>{% endblock header_text %}
{% block content %}
{# note.js exposes window.Brief — needed by the auto-fired @mailman Brief #}
{# when this page is the destination of an invite cascade. #}
<script src="{% static 'apps/dashboard/note.js' %}"></script>
<div class="bud-page">
<header class="bud-page-header">
<h3>{{ bud|at_handle }} the {{ bud.active_title_display }}</h3>
<p class="bud-page-email">{{ bud.email }}</p>
</header>
<form id="id_bud_shoptalk_form"
method="POST"
action="{% url 'billboard:save_bud_shoptalk' bud.id %}"
class="bud-shoptalk-form">
{% csrf_token %}
<label for="id_shoptalk" class="bud-shoptalk-label">Shoptalk</label>
<textarea id="id_shoptalk"
name="shoptalk"
maxlength="160"
class="form-control bud-shoptalk-input"
placeholder="Your personal note about this bud…">{{ shoptalk_text }}</textarea>
</form>
{# 4-btn apparatus mirrors my_sea.html: kit (from base.html) + bud + #}
{# gear + burger. Apparatus loads bud-btn.js (defines window. #}
{# bindBudBtn — does NOT auto-bind); the inline script below opens #}
{# the panel + stubs OK to disabled per the sprint plan. #}
{% include "apps/billboard/_partials/_bud_apparatus.html" with aria_label="Bud" include_suggestions=False %}
<script>
(function () {
var btn = document.getElementById('id_bud_btn');
var panel = document.getElementById('id_bud_panel');
var ok = document.getElementById('id_bud_ok');
if (!btn || !panel || !ok) return;
// bud-of-bud flow isn't wired yet — stub OK as disabled w. × text.
ok.classList.add('btn-disabled');
ok.textContent = '×';
ok.setAttribute('disabled', 'disabled');
btn.addEventListener('click', function () {
var open = document.documentElement.classList.toggle('bud-open');
btn.classList.toggle('active', open);
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && document.documentElement.classList.contains('bud-open')) {
document.documentElement.classList.remove('bud-open');
btn.classList.remove('active');
}
});
}());
</script>
{% include "apps/billboard/_partials/_bud_gear.html" %}
{# Burger fan — sea_btn_active + sea_first_draw_pending drive the #}
{# server-side .active / .glow-handoff classes (same vars my_sea + #}
{# room read). burger-btn.js auto-binds on DOMContentLoaded. #}
{% include "apps/gameboard/_partials/_burger.html" %}
</div>
<script src="{% static 'apps/epic/burger-btn.js' %}"></script>
{% if pending_invite %}
<script>
(function () {
bud page sea-btn cascade: live-invite window + accept-on-GET + glow handoff; my-buds tooltip clamp + row hover/lock — TDD Fixes the Bill Bud invite cascade so the sea sub-btn actually lights + leads to the bud's my_sea, and gives the My Buds row tooltip viewport clamping + hover/lock styling. SeaInvite.invitee_access_open (gameboard/models.py): new invitee-facing access window — a non-terminal invite (PENDING/ACCEPTED) within 24h of being proffered OR within 24h of the invitee's last gate token deposit. Re-arms on each deposit; DECLINED/LEFT/EXPIRED stay shut. Distinct from is_expired (which only models the PENDING lapse). 8 UTs. bud_page (billboard/views.py): sea_btn_active / sea_first_draw_pending now key on invitee_access_open across PENDING + ACCEPTED, not PENDING-only. Old design darkened the btn the instant the user accepted, so they could never reach the bud's sea from here post-accept — that was the red .fa-ban the user saw. ITs updated: accepted-within-window now lights; added stale-accepted-dark + recent-deposit-relights cases. my_sea_visit (gameboard/views.py): accept-on-GET — a still-pending, non-expired invite from the owner to the visitor is accepted implicitly on arrival (the sea-btn cascade + @mailman post-attribution anchor both land here, so the click IS the acceptance). Previously PENDING → 403, so the cascade dead-ended. ITs: pending-invitee now auto-accepts (200); expired-pending still 403s; stranger still 403s. bud.html: burger → sea_btn glow-handoff machine (the my_sea.html cascade minus the spread-modal stage) so the glow rides the affordance chain to the click target; active sea click clears glow, preserves .active, navigates. my-buds-tooltip.js: clamp the position:fixed #id_tooltip_portal to the viewport on row-lock — same 1rem-inset shape as game-kit.js / sky-wheel.js / wallet.js (measure after .active, clamp left, prefer above / flip below). Reset on clear. _billboard.scss: .bud-entry hover + .row-locked highlight (rows aren't .row-3col so the existing rule missed them) — fill --secUser, flip the --terUser handle to --quiUser, trailing title to readable --priUser. 520 billboard+gameboard ITs/UTs green; affected sea-btn-cascade FT green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:36:25 -04:00
var burger = document.getElementById('id_burger_btn');
bud landing page: /billboard/buds/<id>/ + my_buds tooltip portal + @mailman post-attribution anchor — TDD Replaces the @mailman invite Line's inline OK/BYE block w. a dedicated per-bud surface. Three new FTs (test_bill_bud_page, test_bill_my_buds_tooltip, test_bill_mailman_invite_post — landed red 2026-05-27 PM) drive: per-bud landing page rendering 4-btn apparatus + shoptalk textarea + invite-cascade glow handoff; my-buds row tooltip portal w. .tt-title/.tt-description/.tt-email/.tt-shoptalk/.tt-milestone slots; mailman Brief surfacing on any authenticated page-load via context processor + base.html JSON-script. Models: new `BudshipNote(user, bud, shoptalk[CharField max=160], edited_at)` w. unique_together — per-relation personal note about a bud, never visible to the bud. Lazy-created on first shoptalk save so absence of a row reads as 'never edited' (drives .tt-milestone slot presence). URLs (billboard): `buds/<uuid:bud_id>/` (bud_page), `buds/<uuid:bud_id>/shoptalk` (save_bud_shoptalk), `buds/<uuid:bud_id>/delete` (delete_bud). Views: bud_page auto-adds the bud on first visit (mirrors share_post implicit-add); resolves `pending_invite` as non-expired PENDING SeaInvite(owner=bud, invitee=request.user) → drives `sea_btn_active` + `sea_first_draw_pending` flags that _burger.html already reads on my_sea + room. my_buds enriches each bud w. `.shoptalk_text` + `.milestone_dt` so the row template can render data-tt-* attrs without an extra template tag. mail.py: INVITE_TEMPLATE now interpolates `owner_id` into an `<a class="post-attribution" href="/billboard/buds/{owner_id}/">{handle}</a>` wrapper around the owner's handle. post.html's existing safe-filter branch (gated on author username == 'mailman') passes it through unescaped. Removed the {% if line.sea_invite %} include path — _invite_actions.html left in place for archival. Templates: new bud.html (header + shoptalk form + apparatus + gear + burger fan + sea_btn nav inline JS); new _bud_gear.html (NVM→my_buds, DEL→guard portal "Delete this bud?" → POST delete_bud); new _bud_tooltip.html (portal w. .tt-* slots); _my_buds_item.html wraps `@handle` in an anchor to bud_page + carries data-tt-* attrs + " the {{ active_title_display }}"; my_buds.html includes the tooltip portal + loads my-buds-tooltip.js. JS: new my-buds-tooltip.js binds row clicks → .row-locked + populates #id_tooltip_portal from data-tt-* attrs; anchor clicks pass through to navigate; .tt-milestone is removed from DOM (not just emptied) when never-edited so the FT can distinguish absent vs cleared-after-edit. SCSS: extend landscape gear-btn rule + #id_*_menu rule w. `.bud-page` + `#id_bud_menu` (otherwise gear-btn collided w. bud-btn in landscape on bud.html). Bump active burger sub-btn z-index to 1 so click hit-test picks the active sub-btn during the 0.25s fan arc-out animation (otherwise a later-in-DOM inactive btn obscured the active target during transition). Cross-page Brief surface: new `mail_brief_payload` context processor injects the user's oldest unread MAIL_ACCEPTANCE Brief into every authenticated response; base.html renders the JSON-script + auto-fires Brief.showBanner. Mark-read still rides view_post's existing GET unread-flip — no new endpoint. Pre-existing MySeaInvitePostRenderTest (test_sea_invite_views.py) inverted to match the new contract: the .invite-actions sweep is unconditional (PENDING / ACCEPTED / DECLINED all carry prose only); pinned the post-attribution anchor + bud-page href in its place. 1518 ITs green (1475 app ITs + 43 sprint ITs), 23 sprint FTs green (5 my_buds tooltip + 13 bud page + 5 mailman invite post). Jasmine specs from sprint plan deferred — FT coverage of burger-glow / row-lock / portal-populate paths suffices and the textarea blur-POST flow isn't implemented in this sprint (form is server-action only, save-on-blur AJAX is a follow-on). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:45:20 -04:00
var sea = document.getElementById('id_sea_btn');
bud page sea-btn cascade: live-invite window + accept-on-GET + glow handoff; my-buds tooltip clamp + row hover/lock — TDD Fixes the Bill Bud invite cascade so the sea sub-btn actually lights + leads to the bud's my_sea, and gives the My Buds row tooltip viewport clamping + hover/lock styling. SeaInvite.invitee_access_open (gameboard/models.py): new invitee-facing access window — a non-terminal invite (PENDING/ACCEPTED) within 24h of being proffered OR within 24h of the invitee's last gate token deposit. Re-arms on each deposit; DECLINED/LEFT/EXPIRED stay shut. Distinct from is_expired (which only models the PENDING lapse). 8 UTs. bud_page (billboard/views.py): sea_btn_active / sea_first_draw_pending now key on invitee_access_open across PENDING + ACCEPTED, not PENDING-only. Old design darkened the btn the instant the user accepted, so they could never reach the bud's sea from here post-accept — that was the red .fa-ban the user saw. ITs updated: accepted-within-window now lights; added stale-accepted-dark + recent-deposit-relights cases. my_sea_visit (gameboard/views.py): accept-on-GET — a still-pending, non-expired invite from the owner to the visitor is accepted implicitly on arrival (the sea-btn cascade + @mailman post-attribution anchor both land here, so the click IS the acceptance). Previously PENDING → 403, so the cascade dead-ended. ITs: pending-invitee now auto-accepts (200); expired-pending still 403s; stranger still 403s. bud.html: burger → sea_btn glow-handoff machine (the my_sea.html cascade minus the spread-modal stage) so the glow rides the affordance chain to the click target; active sea click clears glow, preserves .active, navigates. my-buds-tooltip.js: clamp the position:fixed #id_tooltip_portal to the viewport on row-lock — same 1rem-inset shape as game-kit.js / sky-wheel.js / wallet.js (measure after .active, clamp left, prefer above / flip below). Reset on clear. _billboard.scss: .bud-entry hover + .row-locked highlight (rows aren't .row-3col so the existing rule missed them) — fill --secUser, flip the --terUser handle to --quiUser, trailing title to readable --priUser. 520 billboard+gameboard ITs/UTs green; affected sea-btn-cascade FT green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:36:25 -04:00
if (!burger || !sea) return;
// Glow-handoff machine — the bud-page counterpart of my_sea.html's
// burger → sea_btn cascade, minus the spread-modal stage (the active
// sea sub-btn navigates away instead of opening a modal). The burger
// carries .glow-handoff server-side (sea_first_draw_pending); opening
// it hands the glow off to the sea sub-btn so the nudge rides the
// affordance chain to the click target.
burger.addEventListener('click', function () {
if (!burger.classList.contains('glow-handoff')) return;
burger.classList.remove('glow-handoff');
sea.classList.add('glow-handoff');
});
// Active sea sub-btn click → the bud's spectator my-sea (which accepts
// the invite on GET). Clears the glow but PRESERVES .active. Fires in
// the target phase, BEFORE burger-btn.js's delegated fan-close.
bud landing page: /billboard/buds/<id>/ + my_buds tooltip portal + @mailman post-attribution anchor — TDD Replaces the @mailman invite Line's inline OK/BYE block w. a dedicated per-bud surface. Three new FTs (test_bill_bud_page, test_bill_my_buds_tooltip, test_bill_mailman_invite_post — landed red 2026-05-27 PM) drive: per-bud landing page rendering 4-btn apparatus + shoptalk textarea + invite-cascade glow handoff; my-buds row tooltip portal w. .tt-title/.tt-description/.tt-email/.tt-shoptalk/.tt-milestone slots; mailman Brief surfacing on any authenticated page-load via context processor + base.html JSON-script. Models: new `BudshipNote(user, bud, shoptalk[CharField max=160], edited_at)` w. unique_together — per-relation personal note about a bud, never visible to the bud. Lazy-created on first shoptalk save so absence of a row reads as 'never edited' (drives .tt-milestone slot presence). URLs (billboard): `buds/<uuid:bud_id>/` (bud_page), `buds/<uuid:bud_id>/shoptalk` (save_bud_shoptalk), `buds/<uuid:bud_id>/delete` (delete_bud). Views: bud_page auto-adds the bud on first visit (mirrors share_post implicit-add); resolves `pending_invite` as non-expired PENDING SeaInvite(owner=bud, invitee=request.user) → drives `sea_btn_active` + `sea_first_draw_pending` flags that _burger.html already reads on my_sea + room. my_buds enriches each bud w. `.shoptalk_text` + `.milestone_dt` so the row template can render data-tt-* attrs without an extra template tag. mail.py: INVITE_TEMPLATE now interpolates `owner_id` into an `<a class="post-attribution" href="/billboard/buds/{owner_id}/">{handle}</a>` wrapper around the owner's handle. post.html's existing safe-filter branch (gated on author username == 'mailman') passes it through unescaped. Removed the {% if line.sea_invite %} include path — _invite_actions.html left in place for archival. Templates: new bud.html (header + shoptalk form + apparatus + gear + burger fan + sea_btn nav inline JS); new _bud_gear.html (NVM→my_buds, DEL→guard portal "Delete this bud?" → POST delete_bud); new _bud_tooltip.html (portal w. .tt-* slots); _my_buds_item.html wraps `@handle` in an anchor to bud_page + carries data-tt-* attrs + " the {{ active_title_display }}"; my_buds.html includes the tooltip portal + loads my-buds-tooltip.js. JS: new my-buds-tooltip.js binds row clicks → .row-locked + populates #id_tooltip_portal from data-tt-* attrs; anchor clicks pass through to navigate; .tt-milestone is removed from DOM (not just emptied) when never-edited so the FT can distinguish absent vs cleared-after-edit. SCSS: extend landscape gear-btn rule + #id_*_menu rule w. `.bud-page` + `#id_bud_menu` (otherwise gear-btn collided w. bud-btn in landscape on bud.html). Bump active burger sub-btn z-index to 1 so click hit-test picks the active sub-btn during the 0.25s fan arc-out animation (otherwise a later-in-DOM inactive btn obscured the active target during transition). Cross-page Brief surface: new `mail_brief_payload` context processor injects the user's oldest unread MAIL_ACCEPTANCE Brief into every authenticated response; base.html renders the JSON-script + auto-fires Brief.showBanner. Mark-read still rides view_post's existing GET unread-flip — no new endpoint. Pre-existing MySeaInvitePostRenderTest (test_sea_invite_views.py) inverted to match the new contract: the .invite-actions sweep is unconditional (PENDING / ACCEPTED / DECLINED all carry prose only); pinned the post-attribution anchor + bud-page href in its place. 1518 ITs green (1475 app ITs + 43 sprint ITs), 23 sprint FTs green (5 my_buds tooltip + 13 bud page + 5 mailman invite post). Jasmine specs from sprint plan deferred — FT coverage of burger-glow / row-lock / portal-populate paths suffices and the textarea blur-POST flow isn't implemented in this sprint (form is server-action only, save-on-blur AJAX is a follow-on). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:45:20 -04:00
sea.addEventListener('click', function () {
if (!sea.classList.contains('active')) return;
bud page sea-btn cascade: live-invite window + accept-on-GET + glow handoff; my-buds tooltip clamp + row hover/lock — TDD Fixes the Bill Bud invite cascade so the sea sub-btn actually lights + leads to the bud's my_sea, and gives the My Buds row tooltip viewport clamping + hover/lock styling. SeaInvite.invitee_access_open (gameboard/models.py): new invitee-facing access window — a non-terminal invite (PENDING/ACCEPTED) within 24h of being proffered OR within 24h of the invitee's last gate token deposit. Re-arms on each deposit; DECLINED/LEFT/EXPIRED stay shut. Distinct from is_expired (which only models the PENDING lapse). 8 UTs. bud_page (billboard/views.py): sea_btn_active / sea_first_draw_pending now key on invitee_access_open across PENDING + ACCEPTED, not PENDING-only. Old design darkened the btn the instant the user accepted, so they could never reach the bud's sea from here post-accept — that was the red .fa-ban the user saw. ITs updated: accepted-within-window now lights; added stale-accepted-dark + recent-deposit-relights cases. my_sea_visit (gameboard/views.py): accept-on-GET — a still-pending, non-expired invite from the owner to the visitor is accepted implicitly on arrival (the sea-btn cascade + @mailman post-attribution anchor both land here, so the click IS the acceptance). Previously PENDING → 403, so the cascade dead-ended. ITs: pending-invitee now auto-accepts (200); expired-pending still 403s; stranger still 403s. bud.html: burger → sea_btn glow-handoff machine (the my_sea.html cascade minus the spread-modal stage) so the glow rides the affordance chain to the click target; active sea click clears glow, preserves .active, navigates. my-buds-tooltip.js: clamp the position:fixed #id_tooltip_portal to the viewport on row-lock — same 1rem-inset shape as game-kit.js / sky-wheel.js / wallet.js (measure after .active, clamp left, prefer above / flip below). Reset on clear. _billboard.scss: .bud-entry hover + .row-locked highlight (rows aren't .row-3col so the existing rule missed them) — fill --secUser, flip the --terUser handle to --quiUser, trailing title to readable --priUser. 520 billboard+gameboard ITs/UTs green; affected sea-btn-cascade FT green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:36:25 -04:00
sea.classList.remove('glow-handoff');
bud landing page: /billboard/buds/<id>/ + my_buds tooltip portal + @mailman post-attribution anchor — TDD Replaces the @mailman invite Line's inline OK/BYE block w. a dedicated per-bud surface. Three new FTs (test_bill_bud_page, test_bill_my_buds_tooltip, test_bill_mailman_invite_post — landed red 2026-05-27 PM) drive: per-bud landing page rendering 4-btn apparatus + shoptalk textarea + invite-cascade glow handoff; my-buds row tooltip portal w. .tt-title/.tt-description/.tt-email/.tt-shoptalk/.tt-milestone slots; mailman Brief surfacing on any authenticated page-load via context processor + base.html JSON-script. Models: new `BudshipNote(user, bud, shoptalk[CharField max=160], edited_at)` w. unique_together — per-relation personal note about a bud, never visible to the bud. Lazy-created on first shoptalk save so absence of a row reads as 'never edited' (drives .tt-milestone slot presence). URLs (billboard): `buds/<uuid:bud_id>/` (bud_page), `buds/<uuid:bud_id>/shoptalk` (save_bud_shoptalk), `buds/<uuid:bud_id>/delete` (delete_bud). Views: bud_page auto-adds the bud on first visit (mirrors share_post implicit-add); resolves `pending_invite` as non-expired PENDING SeaInvite(owner=bud, invitee=request.user) → drives `sea_btn_active` + `sea_first_draw_pending` flags that _burger.html already reads on my_sea + room. my_buds enriches each bud w. `.shoptalk_text` + `.milestone_dt` so the row template can render data-tt-* attrs without an extra template tag. mail.py: INVITE_TEMPLATE now interpolates `owner_id` into an `<a class="post-attribution" href="/billboard/buds/{owner_id}/">{handle}</a>` wrapper around the owner's handle. post.html's existing safe-filter branch (gated on author username == 'mailman') passes it through unescaped. Removed the {% if line.sea_invite %} include path — _invite_actions.html left in place for archival. Templates: new bud.html (header + shoptalk form + apparatus + gear + burger fan + sea_btn nav inline JS); new _bud_gear.html (NVM→my_buds, DEL→guard portal "Delete this bud?" → POST delete_bud); new _bud_tooltip.html (portal w. .tt-* slots); _my_buds_item.html wraps `@handle` in an anchor to bud_page + carries data-tt-* attrs + " the {{ active_title_display }}"; my_buds.html includes the tooltip portal + loads my-buds-tooltip.js. JS: new my-buds-tooltip.js binds row clicks → .row-locked + populates #id_tooltip_portal from data-tt-* attrs; anchor clicks pass through to navigate; .tt-milestone is removed from DOM (not just emptied) when never-edited so the FT can distinguish absent vs cleared-after-edit. SCSS: extend landscape gear-btn rule + #id_*_menu rule w. `.bud-page` + `#id_bud_menu` (otherwise gear-btn collided w. bud-btn in landscape on bud.html). Bump active burger sub-btn z-index to 1 so click hit-test picks the active sub-btn during the 0.25s fan arc-out animation (otherwise a later-in-DOM inactive btn obscured the active target during transition). Cross-page Brief surface: new `mail_brief_payload` context processor injects the user's oldest unread MAIL_ACCEPTANCE Brief into every authenticated response; base.html renders the JSON-script + auto-fires Brief.showBanner. Mark-read still rides view_post's existing GET unread-flip — no new endpoint. Pre-existing MySeaInvitePostRenderTest (test_sea_invite_views.py) inverted to match the new contract: the .invite-actions sweep is unconditional (PENDING / ACCEPTED / DECLINED all carry prose only); pinned the post-attribution anchor + bud-page href in its place. 1518 ITs green (1475 app ITs + 43 sprint ITs), 23 sprint FTs green (5 my_buds tooltip + 13 bud page + 5 mailman invite post). Jasmine specs from sprint plan deferred — FT coverage of burger-glow / row-lock / portal-populate paths suffices and the textarea blur-POST flow isn't implemented in this sprint (form is server-action only, save-on-blur AJAX is a follow-on). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:45:20 -04:00
window.location.href = '/gameboard/my-sea/visit/{{ bud.id }}/';
});
}());
</script>
{% endif %}
{% endblock content %}