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
54 changes: 54 additions & 0 deletions .tdd/spec-campus-project-work-summary-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Campus project work summary + GitHub Issue v1

Status: LOCKED
Issue: https://github.com/pirajoke/agent-dashboard/issues/34

## Goal

The read-only Pixel Verse Campus project inspector shows a concise description
of the current verified work and a direct GitHub Issue link for owner-view live
tasks, without publishing private task content.

## Acceptance criteria

- AC-01: A live project inspector can render `Над чем работаем` from a bounded,
privacy-safe `work_summary` value.
- AC-02: A live project inspector can render one keyboard-operable external
GitHub Issue link whose visible label is `Issue #N`.
- AC-03: Owner-view canonical Bridge tasks derive `work_summary` only from the
typed metadata `objective`, never from raw `description`, `result`, messages,
issue bodies, or logs.
- AC-04: Owner-view issue data is accepted only when metadata contains an
integer `github_issue_number`, a matching `github_issue_url`, and an exact
`github_repo` identity. The URL must be HTTPS `github.com/{owner}/{repo}/issues/N`
with no credentials, port, query, or fragment.
- AC-05: Verified MAIN MANAGER pixel events may carry the same owner-only
fields through the projection after identical validation.
- AC-06: An anonymous public-host `/api/manager/departments` response never
contains `work_summary`, `issue_url`, or `issue_number`; a view authenticated
with the existing dashboard owner token may contain those owner-only fields,
including when accessed through the public host.

## Edge cases

- EC-01: Missing summary or issue data hides only the corresponding row.
- EC-02: Summary text is whitespace-normalized and bounded without breaking
the existing responsive inspector.
- EC-03: The link opens in a new tab with `rel="noreferrer"` and remains
read-only and keyboard accessible.

## Errors and privacy

- ERR-01: A mismatched issue number/repository, non-GitHub URL, credentials,
port, query, fragment, malformed type, or unsafe summary fails closed.
- ERR-02: Raw task `description`, result, prompt/body, local paths, tokens,
credentials, logs, and private metadata never enter the projection or DOM.
- ERR-03: Existing project details, focus return, Escape close, empty state,
three-lane cap, and public privacy behavior remain unchanged.

## Constraints

- No new dependency.
- Source changes stay in `builder/`.
- Read-only UI only; no dispatch, edit, merge, deploy, or publication action.
- Draft PR only. Merge and production deployment are separate stages.
63 changes: 62 additions & 1 deletion builder/dashboard-assets/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,49 @@
unavailable: 'Команда на местах · live-данные временно недоступны',
active: 'Показаны свежие подтверждённые события',
};

function campusOwnerHeaders() {
const headers = {};
try {
const token = window.localStorage
.getItem('command-center.jarvis-run-token')
?.trim();
if (token) headers['X-Dashboard-Run-Token'] = token;
Comment on lines +118 to +121

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Forward the token into the cross-origin Campus iframe

When the supported dashboard is opened from file://, PIXEL_AGENTS_BASE loads this script in an iframe from https://command.meshly.fr, but the saved run token belongs to the parent file origin. Reading window.localStorage here therefore cannot see that token, so every Campus refresh receives the anonymous projection and the new summary/Issue rows remain unavailable to the owner; explicitly pass the token from the parent to the iframe or perform the authenticated request in the parent.

Useful? React with 👍 / 👎.

} catch (_error) {
// Storage can be unavailable; the endpoint then returns its public projection.
}
return headers;
}

function verifiedCampusIssue(event) {
const issueNumber = event?.issue_number;
const issueUrl = event?.issue_url;
if (!Number.isInteger(issueNumber) || issueNumber <= 0 || typeof issueUrl !== 'string') {
return null;
}
try {
const parsed = new URL(issueUrl);
const match = parsed.pathname.match(
/^\/[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*\/issues\/([1-9]\d*)$/,
);
if (
parsed.protocol !== 'https:'
|| parsed.hostname !== 'github.com'
|| parsed.username
|| parsed.password
|| parsed.port
|| parsed.search
|| parsed.hash
|| !match
|| Number(match[1]) !== issueNumber
) {
return null;
}
} catch (_error) {
return null;
}
return {number: issueNumber, url: issueUrl};
}
let lastTrigger = null;
let intervalId = null;
let refreshInFlight = false;
Expand Down Expand Up @@ -241,6 +284,10 @@
? event.next_step
: null;
const hasEvidence = Number.isInteger(event?.evidence_count) && event.evidence_count >= 0;
const workSummary = typeof event?.work_summary === 'string' && event.work_summary.trim()
? event.work_summary
: null;
const issue = verifiedCampusIssue(event);
projectDetailFields.project.textContent = (
folder.dataset.campusProjectLabel || folder.dataset.campusProject || '—'
);
Expand All @@ -251,14 +298,27 @@
projectDetailFields.status.textContent = event
? (statusLabels[event.status] || '—')
: 'нет активных задач';
projectDetailFields.work_summary.textContent = workSummary || '—';
projectDetailFields.issue_url.removeAttribute('href');
projectDetailFields.issue_url.textContent = '—';
if (issue) {
projectDetailFields.issue_url.setAttribute('href', issue.url);
projectDetailFields.issue_url.textContent = `Issue #${issue.number}`;
}
projectDetailFields.next_step.textContent = nextStep || '—';
projectDetailFields.evidence_count.textContent = hasEvidence
? String(event.evidence_count)
: '—';
projectLiveOnlyEls.forEach((row) => {
const field = row.querySelector('[data-campus-project-detail-field]')
?.dataset.campusProjectDetailField;
row.hidden = field === 'next_step' ? !nextStep : !hasEvidence;
const visible = {
work_summary: Boolean(workSummary),
issue_url: Boolean(issue),
next_step: Boolean(nextStep),
evidence_count: hasEvidence,
};
row.hidden = !visible[field];
});
projectDetailEl.hidden = false;
projectDetailCloseEl?.focus();
Expand Down Expand Up @@ -497,6 +557,7 @@
const response = await fetch('/api/manager/departments', {
cache: 'no-store',
signal: controller.signal,
headers: campusOwnerHeaders(),
});
if (!response.ok) throw new Error('unavailable');
const payload = await response.json();
Expand Down
60 changes: 50 additions & 10 deletions builder/dashboard-server-m4.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,10 +699,15 @@ def _github_bridge_reconciliation(issues: list[dict], bridge_tasks: list[dict])
}


def _dashboard_run_token() -> str:
def _dashboard_run_token(*, create_if_missing: bool = True) -> str:
token = os.environ.get("DASHBOARD_RUN_TOKEN", "").strip()
if token:
return token
if not create_if_missing:
try:
return JARVIS_DASHBOARD_RUN_TOKEN_FILE.read_text().strip()
except FileNotFoundError:
return ""
JARVIS_DASHBOARD_RUN_TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
if not JARVIS_DASHBOARD_RUN_TOKEN_FILE.exists() or not JARVIS_DASHBOARD_RUN_TOKEN_FILE.read_text().strip():
JARVIS_DASHBOARD_RUN_TOKEN_FILE.write_text(secrets.token_urlsafe(24) + "\n")
Expand Down Expand Up @@ -1250,7 +1255,7 @@ def _campus_bridge_event(task: dict) -> dict | None:
if not isinstance(task_id, str):
return None
zone = DEPARTMENT_ZONES[project["department_id"]]
return {
event = {
"event_id": task_id,
"task_id": task_id,
"department_id": project["department_id"],
Expand All @@ -1265,16 +1270,27 @@ def _campus_bridge_event(task: dict) -> dict | None:
"ephemeral": True,
"zone_id": zone["zone_id"],
}
metadata = _manager_metadata(task)
event["work_summary"] = metadata.get("objective")
event["github_repo"] = metadata.get("github_repo")
event["github_issue_number"] = metadata.get("github_issue_number")
event["github_issue_url"] = metadata.get("github_issue_url")
return event


def _department_campus_payload(data: object, *, now: datetime | None = None) -> dict:
def _department_campus_payload(
data: object,
*,
now: datetime | None = None,
owner_view: bool = False,
) -> dict:
"""Project a verified manager snapshot or safe canonical Bridge tasks."""
current = now or datetime.now(timezone.utc)
if current.tzinfo is None:
current = current.replace(tzinfo=timezone.utc)
current = current.astimezone(timezone.utc)
if not isinstance(data, dict) or not isinstance(data.get("tasks"), list):
return department_campus_projection(None, now=current)
return department_campus_projection(None, now=current, owner_view=owner_view)

candidates: list[tuple[int, datetime, list]] = []
malformed_verified_snapshot = False
Expand Down Expand Up @@ -1307,21 +1323,31 @@ def _department_campus_payload(data: object, *, now: datetime | None = None) ->

if not candidates:
if malformed_verified_snapshot:
return department_campus_projection(None, now=current)
return department_campus_projection(None, now=current, owner_view=owner_view)
events = [
event
for task in data["tasks"]
if isinstance(task, dict)
for event in [_campus_bridge_event(task)]
if event is not None
]
return department_campus_projection(events, now=current, max_tasks=3)
return department_campus_projection(
events,
now=current,
max_tasks=3,
owner_view=owner_view,
)

# max() preserves the first source item when timestamps tie.
_, snapshot_time, events = max(candidates, key=lambda item: item[1])
if (current - snapshot_time).total_seconds() > 30 * 60:
return _department_campus_state("stale", now=current)
return department_campus_projection(events, now=current, max_tasks=3)
return department_campus_projection(
events,
now=current,
max_tasks=3,
owner_view=owner_view,
)


def _runtime_asset_block(filename: str, start_marker: str, end_marker: str) -> str:
Expand Down Expand Up @@ -1392,9 +1418,19 @@ def _read_json_body(self, max_bytes: int = 4096) -> dict:
def _dashboard_run_authorized(self) -> bool:
if not self._is_public_request():
return True
expected = _dashboard_run_token()
write_method = getattr(self, "command", "").upper() in {
"POST",
"PUT",
"PATCH",
"DELETE",
}
expected = _dashboard_run_token(create_if_missing=write_method)
Comment on lines +1421 to +1427

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Provision the owner token before authenticated writes

On a fresh deployment or after the token file is removed, every GET now calls _dashboard_run_token(create_if_missing=False), while the deployment script never creates the file and mm-command-center-auth immediately tries to read it. The public UI also disables its write action until a token is supplied, so the documented authorization flow cannot bootstrap without first issuing an undocumented unauthenticated POST solely to create the token; provision it during deployment/startup instead.

Useful? React with 👍 / 👎.

provided = self.headers.get("X-Dashboard-Run-Token", "").strip()
return bool(provided) and secrets.compare_digest(provided, expected)
return (
bool(expected)
and bool(provided)
and secrets.compare_digest(provided, expected)
)

def _require_dashboard_run_auth(self) -> bool:
if self._dashboard_run_authorized():
Expand Down Expand Up @@ -2244,7 +2280,11 @@ def do_GET(self):
if parsed.path == '/api/manager/departments':
try:
data = _bridge_request("GET", "/api/tasks?limit=24&include_messages=1")
self._json_response(200, _department_campus_payload(data))
if self._dashboard_run_authorized():
payload = _department_campus_payload(data, owner_view=True)
else:
payload = _department_campus_payload(data)
self._json_response(200, payload)
except Exception:
self._json_response(
200,
Expand Down
Loading