Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion builder/dashboard-assets/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
const taskPanelEl = campus.querySelector('[data-campus-task-panel]');
const routeLayerEl = campus.querySelector('[data-campus-route-layer]');
const managerMarkerEl = campus.querySelector('[data-campus-static-manager]');
const managerPresenceEl = campus.querySelector('[data-campus-manager-presence]');
const managerStatusEl = campus.querySelector('[data-campus-manager-status]');
const boulevardEl = campus.querySelector('.campus-boulevard');
const rosterEls = Array.from(campus.querySelectorAll('[data-campus-roster-agent]'));
const projectFolderEls = Array.from(campus.querySelectorAll('[data-campus-project-folder]'));
Expand Down Expand Up @@ -179,9 +181,19 @@
closeCampusDetails(false);
closeCampusProjectDetails(false);
resetCampusProjectFolders();
setManagerPresence();
lastTrigger = null;
}

function setManagerPresence(event = null) {
if (!managerPresenceEl || !managerStatusEl) return;
const status = event && Object.prototype.hasOwnProperty.call(statusLabels, event.status)
? statusLabels[event.status]
: 'ожидает задач';
managerStatusEl.textContent = status;
managerPresenceEl.dataset.campusManagerState = event?.status || 'idle';
}

function setCampusState(state, visibleTasks, agentCount, omittedTasks) {
if (stateEl) stateEl.textContent = stateMessages[state] || stateMessages.unavailable;
if (countEl) {
Expand Down Expand Up @@ -229,7 +241,9 @@
? event.next_step
: null;
const hasEvidence = Number.isInteger(event?.evidence_count) && event.evidence_count >= 0;
projectDetailFields.project.textContent = folder.dataset.campusProject || '—';
projectDetailFields.project.textContent = (
folder.dataset.campusProjectLabel || folder.dataset.campusProject || '—'
);
projectDetailFields.department_id.textContent = (
folder.dataset.campusProjectDepartmentLabel || '—'
);
Expand Down Expand Up @@ -430,6 +444,9 @@
setCampusState('empty', 0, 0, Number(payload?.omitted_task_count) || 0);
return;
}
setManagerPresence(
matchedEvents.find((event) => event.agent_id === 'COORDINATOR') || null,
);
const newJourneySignatures = new Set();
const managerOriginRect = managerMarkerEl?.getBoundingClientRect() || null;
matchedEvents.forEach((event) => {
Expand Down
95 changes: 94 additions & 1 deletion builder/dashboard-assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -4591,6 +4591,15 @@ tr:last-child td { border-bottom: none; }
gap: 0.4rem;
}

.campus-zone-hq > header {
display: inline-grid;
grid-template-columns: max-content minmax(0, 1fr);
align-items: stretch;
justify-content: start;
gap: 0;
max-width: 100%;
}

.campus-zone h3 {
margin: 0;
padding: 0.3rem 0.5rem;
Expand All @@ -4604,6 +4613,59 @@ tr:last-child td { border-bottom: none; }
box-shadow: inset 0 1px 0 rgba(255, 225, 169, 0.16), 0 3px 0 rgba(30, 20, 14, 0.58);
}

.campus-zone-presence {
min-width: 0;
padding: 0.24rem 0.42rem;
border: 1px solid rgba(209, 159, 91, 0.58);
border-left: 0;
background: linear-gradient(180deg, rgba(78, 54, 35, 0.94), rgba(45, 32, 24, 0.94));
color: #f1d8a7;
display: grid;
align-content: center;
gap: 0.08rem;
font-family: 'JetBrains Mono', monospace;
text-shadow: 0 1px 0 #291b12;
box-shadow: inset 0 1px 0 rgba(255, 225, 169, 0.12), 0 3px 0 rgba(30, 20, 14, 0.58);
}

.campus-zone-presence strong {
min-width: 0;
font-size: 0.48rem;
line-height: 1.05;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.campus-zone-presence-status {
min-width: 0;
color: #d8c49f;
display: flex;
align-items: center;
gap: 0.2rem;
font-size: 0.4rem;
line-height: 1.05;
white-space: nowrap;
}

.campus-zone-presence-status i {
width: 0.3rem;
height: 0.3rem;
flex: 0 0 auto;
border-radius: 50%;
background: #b98b43;
box-shadow: 0 0 0 1px rgba(43, 31, 21, 0.72);
}

.campus-zone-presence[data-campus-manager-state="active"] .campus-zone-presence-status i,
.campus-zone-presence[data-campus-manager-state="testing"] .campus-zone-presence-status i {
background: #65a873;
}

.campus-zone-presence[data-campus-manager-state="failed"] .campus-zone-presence-status i {
background: #c7554c;
}

.campus-owner-boundary {
max-width: 104px;
padding: 0.25rem 0.35rem;
Expand Down Expand Up @@ -4944,9 +5006,22 @@ tr:last-child td { border-bottom: none; }
height: 32px;
}

.campus-static-manager::after {
content: "";
position: absolute;
left: 6px;
bottom: -1px;
width: 20px;
height: 3px;
background: rgba(35, 24, 17, 0.58);
box-shadow: 0 1px 0 rgba(12, 9, 7, 0.38);
}

.campus-manager-sprite {
position: relative;
z-index: 1;
display: block;
filter: sepia(0.16) saturate(0.86) hue-rotate(-8deg)
filter: brightness(0.76) contrast(0.94) sepia(0.2) saturate(0.72) hue-rotate(-8deg)
drop-shadow(0 3px 1px rgba(43, 31, 21, 0.78));
}

Expand Down Expand Up @@ -5131,6 +5206,15 @@ tr:last-child td { border-bottom: none; }
font-size: clamp(0.44rem, 1.35vw, 0.58rem);
white-space: nowrap;
}
.campus-zone-presence {
padding: 0.2rem 0.28rem;
}
.campus-zone-presence strong {
font-size: clamp(0.34rem, 1.2vw, 0.44rem);
}
.campus-zone-presence-status {
font-size: clamp(0.3rem, 1vw, 0.38rem);
}
.campus-owner-boundary { display: none; }
.campus-project-shelf {
right: 0.35rem;
Expand Down Expand Up @@ -5213,6 +5297,15 @@ tr:last-child td { border-bottom: none; }
gap: 0.55rem;
padding: 0.5rem;
}
.campus-zone-hq {
background-color: #1b1513;
background-image:
linear-gradient(180deg, rgba(52, 36, 27, 0.48), rgba(27, 21, 19, 0.38)),
url("/dashboard-assets/pixel-verse-campus-bg.webp");
background-position: center, left 83%;
background-repeat: no-repeat;
background-size: auto, 410% auto;
}
.campus-task-lanes { grid-template-columns: minmax(0, 1fr); }
.campus-details dl > div { grid-template-columns: minmax(0, 1fr); gap: 0.2rem; }
.campus-project-detail { max-width: 100%; }
Expand Down
27 changes: 22 additions & 5 deletions builder/dashboard_builder/department_campus.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,18 @@ def _zone_html(department_id: str, zone: dict[str, Any]) -> str:
if zone["owner_permission_boundary"]
else ""
)
manager_presence = ""
if department_id == "hq":
manager = _CAMPUS_RESIDENT_PROFILES["COORDINATOR"]
manager_dom_id = "campus-resident-coordinator"
manager_presence = f"""
<div class="campus-zone-presence" data-campus-manager-presence>
<strong id="{manager_dom_id}-name" data-campus-manager-name>{manager['name']}</strong>
<span class="campus-zone-presence-status">
<i aria-hidden="true"></i>
<span id="{manager_dom_id}-status" data-campus-manager-status>ожидает задач</span>
</span>
</div>"""
residents = []
for resident in CAMPUS_RESIDENTS:
if resident["department_id"] != department_id:
Expand All @@ -568,6 +580,13 @@ def _zone_html(department_id: str, zone: dict[str, Any]) -> str:
f"--campus-walk-duration:{resident['walk_duration']};"
f"--campus-walk-delay:{resident['walk_delay']}"
)
resident_caption = ""
if not is_coordinator:
resident_caption = f"""
<span class="campus-resident-caption">
<strong id="{resident_dom_id}-name">{resident['name']}</strong>
<span id="{resident_dom_id}-status"><i></i>ожидает задач</span>
</span>"""
residents.append(
f"""
<div class="{classes}" data-campus-roster-agent="{resident['agent_id']}"
Expand All @@ -576,10 +595,7 @@ def _zone_html(department_id: str, zone: dict[str, Any]) -> str:
style="{style}"
aria-labelledby="{resident_dom_id}-name {resident_dom_id}-status">
<span class="{sprite_class}" aria-hidden="true"></span>
<span class="campus-resident-caption">
<strong id="{resident_dom_id}-name">{resident['name']}</strong>
<span id="{resident_dom_id}-status"><i></i>ожидает задач</span>
</span>
{resident_caption}
</div>"""
)
residents_html = "".join(residents)
Expand All @@ -601,6 +617,7 @@ def _zone_html(department_id: str, zone: dict[str, Any]) -> str:
f"""
<button class="campus-project-folder" type="button"
data-campus-project-folder data-campus-project="{escape(project_name)}"
data-campus-project-label="{escape(visible_name)}"
data-campus-project-agent="{escape(project['agent_id'])}"
data-campus-project-department="{escape(department_id)}"
data-campus-project-agent-label="{escape(agent_name)}"
Expand All @@ -626,7 +643,7 @@ def _zone_html(department_id: str, zone: dict[str, Any]) -> str:
return f"""
<section class="campus-zone campus-zone-{department_id}" id="{zone['zone_id']}"
data-department-id="{department_id}" aria-labelledby="{zone['zone_id']}-label">
<header><h3 id="{zone['zone_id']}-label">{zone['label']}</h3>{boundary}</header>
<header><h3 id="{zone['zone_id']}-label">{zone['label']}</h3>{manager_presence}{boundary}</header>
{residents_html}
{project_folders_html}
<div class="campus-furniture" aria-hidden="true"><i></i><i></i><i></i></div>
Expand Down
62 changes: 44 additions & 18 deletions builder/tests/test_department_campus_visual_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,9 @@ def handle_data(self, data: str) -> None:
if any(attributes.get("id") == "campus-zone-hq-label" for _, attributes in self._stack):
self.hq_heading_text.append(data)

inside_coordinator = any(
attributes.get("data-campus-static-manager") == "true"
for _, attributes in self._stack
)
if not inside_coordinator:
return
if any(tag == "strong" for tag, _attributes in self._stack):
if any("data-campus-manager-name" in attributes for _, attributes in self._stack):
self.coordinator_name_text.append(data)
elif any(
tag == "span" and "campus-manager-sprite" not in (attributes.get("class") or "").split()
for tag, attributes in self._stack
):
elif any("data-campus-manager-status" in attributes for _, attributes in self._stack):
self.coordinator_status_text.append(data)

def handle_endtag(self, tag: str) -> None:
Expand Down Expand Up @@ -315,12 +306,16 @@ def test_ac_4_coordinator_sprite_keeps_native_pixel_scale_and_environment_tone(s
def test_ac_4_coordinator_has_a_floor_contact_shadow(self):
manager_css = self._css_declarations(".campus-static-manager")
sprite_css = self._css_declarations(".campus-manager-sprite")
contact_shadow_css = self._css_declarations(".campus-static-manager::after")

self.assertRegex(
manager_css + sprite_css,
r"(?:drop-shadow\(|box-shadow:)",
"the coordinator needs a contact shadow that visually anchors it to the room",
)
self.assertRegex(contact_shadow_css, r"content:\s*[\"']{2}")
self.assertRegex(contact_shadow_css, r"bottom:\s*-?\d")
self.assertRegex(contact_shadow_css, r"background:\s*rgba\(")

def test_ac_5_file_dashboard_uses_reachable_campus_and_matching_message_origin(self):
required_contracts = (
Expand Down Expand Up @@ -414,6 +409,12 @@ def test_ac_8_mobile_viewport_fits_the_real_campus_height_without_data_leakage(s
self.dashboard_html,
r"@media\s*\(max-width:\s*760px\)[\s\S]*?\.pixel-agents-viewport\.has-content-height",
)
self.assertRegex(
self.css,
r"@media\s*\(max-width:\s*560px\)[\s\S]*?\.campus-zone-hq\s*\{"
r"[\s\S]*?pixel-verse-campus-bg\.webp[\s\S]*?background-position:\s*center,\s*left",
"the narrow HQ room must retain its own management-room crop",
)

def test_ac_9_configured_roster_is_visible_without_claiming_live_work(self):
expected = {
Expand Down Expand Up @@ -471,20 +472,45 @@ def test_ac_11_live_event_replaces_matching_resident_without_duplication(self):
"clearing live routes must not erase the persistent team roster",
)

def test_ac_12_coordinator_plaque_moves_with_the_room_local_character(self):
def test_ac_12_coordinator_identity_lives_in_the_room_header_not_over_the_sprite(self):
hq = re.search(
r'<section class="campus-zone campus-zone-hq"[\s\S]*?</section>',
self.campus_html,
)
self.assertIsNotNone(hq)
self.assertNotIn("campus-zone-presence", hq.group(0))
self.assertRegex(
hq.group(0),
r'data-campus-static-manager="true"[\s\S]*campus-resident-caption',
hq_html = hq.group(0)
room_header = re.search(r"<header>[\s\S]*?</header>", hq_html)
manager = re.search(
r'<div class="[^"]*campus-static-manager[^"]*"[\s\S]*?</div>',
hq_html,
)
self.assertIsNotNone(room_header)
self.assertIsNotNone(manager)
self.assertIn("data-campus-manager-presence", room_header.group(0))
self.assertIn("data-campus-manager-name", room_header.group(0))
self.assertIn("data-campus-manager-status", room_header.group(0))
self.assertIn("Главный координатор", room_header.group(0))
self.assertIn("ожидает задач", room_header.group(0))
self.assertNotIn("campus-resident-caption", manager.group(0))
self.assertNotIn("Главный координатор", manager.group(0))
self.assertNotIn("ожидает задач", manager.group(0))
self.assertIn('data-campus-project-label="Координация"', hq_html)
self.assertIn(
"folder.dataset.campusProjectLabel || folder.dataset.campusProject || '—'",
self.script,
)
for contract in (
"const managerPresenceEl = campus.querySelector('[data-campus-manager-presence]');",
"const managerStatusEl = campus.querySelector('[data-campus-manager-status]');",
"function setManagerPresence(event = null)",
"setManagerPresence();",
"matchedEvents.find((event) => event.agent_id === 'COORDINATOR') || null",
):
with self.subTest(manager_state_contract=contract):
self.assertIn(contract, self.script)
self.assertLess(
hq.group(0).index("campus-manager-sprite"),
hq.group(0).index("Главный координатор"),
hq_html.index("data-campus-manager-presence"),
hq_html.index("data-campus-static-manager"),
)

def test_ec_1_seven_zone_public_shell_stays_read_only_and_unsynthesized(self):
Expand Down