game-views ATLAS: mirror SCROLL log styling (strikethrough) + honour the redact filter

The merged ATLAS feed now reads its provenance rows the way SCROLL does:

- Struck (retracted/redacted) logs carry the strikethrough — buildAtlasFeed captures the source .drama-event-body.struck state and renderAtlasRow re-applies it as .atlas-row-body.struck (styled to match .drama-event-body.struck).
- A log hidden by the SCROLL Frame/Redact gear filter (display:none) is skipped in the ATLAS merge too — buildAtlasFeed checks getComputedStyle(ev).display, so unchecking Redact on SCROLL keeps those rows out of ATLAS.
- Also lays the source-toggle seam: atlasSources() reads the (forthcoming) ATLAS gear's view-checkboxes (scroll→provenance, post→post), defaulting to both when absent.

Verified: Jasmine renderAtlasRow struck spec + two FTs (struck row shows struck in ATLAS; redact-filtered rows absent from ATLAS) + the existing atlas aggregate FT, all green.

[[project-room-game-views-carousel]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-02 15:19:23 -04:00
parent ced324081f
commit 9754f6a54c
5 changed files with 119 additions and 21 deletions

View File

@@ -48,9 +48,12 @@
? '<span class="atlas-row-who">' + escapeHtml(r.whoText) + '</span>'
: '';
var time = r.timeHtml || '';
// Carry the source row's struck state (a retracted/redacted SCROLL log)
// so the merged copy reads the same strikethrough it does in SCROLL.
var bodyCls = 'atlas-row-body' + (r.struck ? ' struck' : '');
return '<div class="atlas-row atlas-row--' + r.source + '" data-source="'
+ r.source + '">' + who
+ '<span class="atlas-row-body">' + r.bodyHtml + '</span>' + time
+ '<span class="' + bodyCls + '">' + r.bodyHtml + '</span>' + time
+ '</div>';
}
@@ -317,31 +320,56 @@
return t ? t.outerHTML : '';
}
// Which sources the ATLAS gear has enabled (its source checkboxes map
// each reelhouse view → a row source: scroll→provenance, post→post).
// Defaults to both when the gear form isn't on the page yet.
function atlasSources() {
var form = document.getElementById('id_atlas_source_form');
if (!form) return { provenance: true, post: true };
var VIEW_SOURCE = { scroll: 'provenance', post: 'post' };
var on = {};
form.querySelectorAll('input[name="views"]:checked').forEach(function (cb) {
var src = VIEW_SOURCE[cb.value];
if (src) on[src] = true;
});
return on;
}
function buildAtlasFeed() {
var atlas = document.getElementById('id_room_atlas');
if (!atlas) return;
var srcOn = atlasSources();
var rows = [];
document.querySelectorAll('.room-view--scroll .drama-event').forEach(function (ev) {
var body = ev.querySelector('.drama-event-body');
rows.push({
source: 'provenance',
ts: nodeTime(ev, '.drama-event-time'),
whoText: '',
bodyHtml: body ? body.innerHTML : ev.innerHTML,
timeHtml: nodeTimeHtml(ev, '.drama-event-time'),
if (srcOn.provenance) {
document.querySelectorAll('.room-view--scroll .drama-event').forEach(function (ev) {
// Honour the SCROLL redact/frame filter: a log hidden there
// (display:none) must not surface in the merged ATLAS either.
if (getComputedStyle(ev).display === 'none') return;
var body = ev.querySelector('.drama-event-body');
rows.push({
source: 'provenance',
ts: nodeTime(ev, '.drama-event-time'),
whoText: '',
bodyHtml: body ? body.innerHTML : ev.innerHTML,
// Struck (retracted/redacted) carries over from SCROLL.
struck: !!(body && body.classList.contains('struck')),
timeHtml: nodeTimeHtml(ev, '.drama-event-time'),
});
});
});
document.querySelectorAll('.room-view--post #id_post_table .post-line').forEach(function (li) {
var who = li.querySelector('.post-line-author');
var txt = li.querySelector('.post-line-text');
rows.push({
source: 'post',
ts: nodeTime(li, '.post-line-time'),
whoText: who ? who.textContent.trim() : '',
bodyHtml: txt ? txt.innerHTML : '',
timeHtml: nodeTimeHtml(li, '.post-line-time'),
}
if (srcOn.post) {
document.querySelectorAll('.room-view--post #id_post_table .post-line').forEach(function (li) {
var who = li.querySelector('.post-line-author');
var txt = li.querySelector('.post-line-text');
rows.push({
source: 'post',
ts: nodeTime(li, '.post-line-time'),
whoText: who ? who.textContent.trim() : '',
bodyHtml: txt ? txt.innerHTML : '',
timeHtml: nodeTimeHtml(li, '.post-line-time'),
});
});
});
}
if (!rows.length) {
atlas.innerHTML =
'<p class="event-empty"><small>The atlas gathers . . .</small></p>';

View File

@@ -52,6 +52,12 @@ class GameViewsCarouselTest(FunctionalTest):
data={"token_type": "carte", "token_display": "Carte Blanche",
"slot_number": 1, "renewal_days": 7},
)
# A retracted SIG_READY (struck → data-label="redact") so the Atlas can
# be checked for SCROLL-style strikethrough + redact-filter coupling.
GameEvent.objects.create(
room=self.room, actor=self.viewer, verb=GameEvent.SIG_READY,
data={"retracted": True, "card_name": "The Nomad"},
)
self.room.table_status = Room.ROLE_SELECT
self.room.gate_status = Room.OPEN
self.room.save()
@@ -213,6 +219,45 @@ class GameViewsCarouselTest(FunctionalTest):
self.assertTrue(atlas.find_elements(
By.CSS_SELECTOR, "[data-source='post']"))
def test_atlas_provenance_rows_carry_scroll_strikethrough(self):
"""A retracted/redacted SCROLL log reads the same strikethrough in the
merged ATLAS as it does in SCROLL (`.atlas-row-body.struck`)."""
self._open()
self._scroll_to_views()
self._click_icon("atlas")
self.wait_for(lambda: self.assertTrue(
self._in_viewport(".room-view[data-view='atlas']")))
atlas = self.browser.find_element(
By.CSS_SELECTOR, ".room-view[data-view='atlas']")
self.assertTrue(atlas.find_elements(
By.CSS_SELECTOR, ".atlas-row-body.struck"))
def test_atlas_omits_redact_logs_filtered_out_on_scroll(self):
"""Unchecking Redact in the SCROLL gear filter hides the struck logs in
the feed — and they must not surface in the merged ATLAS either."""
self._open()
self._scroll_to_views() # lands on SCROLL → gear shows the log filter
gear = self.browser.find_element(
By.CSS_SELECTOR, ".gear-btn[data-menu-target='id_room_menu']")
self.browser.execute_script("arguments[0].click();", gear)
self.wait_for(lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_scroll_filter_form").is_displayed()))
redact = self.browser.find_element(
By.CSS_SELECTOR, "#id_scroll_filter_form input[value='redact']")
if redact.is_selected():
self.browser.execute_script("arguments[0].click();", redact)
self.browser.execute_script("arguments[0].click();", self.browser.find_element(
By.CSS_SELECTOR, "#id_scroll_filter_form button[type='submit']"))
self._click_icon("atlas")
self.wait_for(lambda: self.assertTrue(
self._in_viewport(".room-view[data-view='atlas']")))
atlas = self.browser.find_element(
By.CSS_SELECTOR, ".room-view[data-view='atlas']")
# Redact logs (the only struck rows) were filtered out → none in ATLAS.
self.assertFalse(atlas.find_elements(
By.CSS_SELECTOR, ".atlas-row-body.struck"))
def test_yarn_and_pulse_render_as_stubs(self):
"""YARN (fa-route) + PULSE (fa-chart-pie) are stub views this sprint —
each renders a placeholder, no backing model yet. The watermark icon

View File

@@ -70,6 +70,15 @@ describe("RoomViews atlas row rendering", () => {
expect(row).toContain("<em>safe</em>"); // body trusted (server-escaped)
expect(row).toContain("&lt;script&gt;"); // who escaped
});
it("marks struck (redacted) provenance rows so they read line-through like SCROLL", () => {
const struck = window.RoomViews.renderAtlasRow(
{ source: "provenance", bodyHtml: "withdraws yos Carte Blanche", whoText: "", timeHtml: "", struck: true });
expect(struck).toContain("struck");
const plain = window.RoomViews.renderAtlasRow(
{ source: "provenance", bodyHtml: "deposits a Carte Blanche", whoText: "", timeHtml: "" });
expect(plain).not.toContain("struck");
});
});
// The Text sub-btn swipe machine drives the reelhouse to GAME POST. Its from-

View File

@@ -247,7 +247,14 @@ html.sea-open #id_aperture_fill {
padding-inline-start: 0.5rem;
.atlas-row-who { font-weight: bold; color: rgba(var(--quaUser), 1); flex-shrink: 0; }
.atlas-row-body { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.atlas-row-body {
flex: 1;
min-width: 0;
overflow-wrap: anywhere;
// Struck (retracted/redacted) provenance rows read the same
// strikethrough they do in SCROLL (.drama-event-body.struck).
&.struck { text-decoration: line-through; opacity: 0.5; }
}
// The merged rows carry the ORIGINAL <time> from their source row
// (.drama-event-time from SCROLL, .post-line-time from POST). Those
// source rules are scoped to the feed/thread, so restate the shared

View File

@@ -70,6 +70,15 @@ describe("RoomViews atlas row rendering", () => {
expect(row).toContain("<em>safe</em>"); // body trusted (server-escaped)
expect(row).toContain("&lt;script&gt;"); // who escaped
});
it("marks struck (redacted) provenance rows so they read line-through like SCROLL", () => {
const struck = window.RoomViews.renderAtlasRow(
{ source: "provenance", bodyHtml: "withdraws yos Carte Blanche", whoText: "", timeHtml: "", struck: true });
expect(struck).toContain("struck");
const plain = window.RoomViews.renderAtlasRow(
{ source: "provenance", bodyHtml: "deposits a Carte Blanche", whoText: "", timeHtml: "" });
expect(plain).not.toContain("struck");
});
});
// The Text sub-btn swipe machine drives the reelhouse to GAME POST. Its from-