"""Build the content-addressed browser shell served by the dashboard."""
from __future__ import annotations
import gzip
import hashlib
import re
from dataclasses import dataclass
from pathlib import Path
import rjsmin
SCRIPT_TAG = re.compile(r'^$', re.MULTILINE)
COMMONJS_EXPORT_LINE = re.compile(
rb"^\s*if \(typeof module[^\n]+module\.exports[^\n]+;\s*$(?!\n\s*else)", re.MULTILINE
)
COMMONJS_BROWSER_BRANCH = re.compile(
rb"^\s*if \(typeof module[^\n]+module\.exports[^\n]+;\s*\n\s*else\s*\{",
re.MULTILINE,
)
WORKER_RUNTIME_SOURCE = "static/background-issue-sync.js"
FEATURE_SOURCES = {
"comment-actions": ("static/conversation.js", "static/comment-actions.js"),
"issue-capture": (
"static/create-issue-sheet.js", "static/update-follow-up.js", "static/shared-image-capture.js",
),
"pull-workflow": ("static/pull-sheet.js", "static/review-sheet.js"),
"push-notifications": ("static/push-notifications.js",),
"device-setup": ("static/install-app.js", "static/mobile-device-setup.js"),
"security-center": ("static/security-center.js",),
"today-timer": (
"static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/search-preview.js", "static/search-defer.js", "static/mobile-search-viewport.js", "static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-queue-launcher.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js",
"static/today-rollover.js", "static/later-work.js", "static/drafts.js", "static/unfiled-captures.js",
"static/assign-and-start.js", "static/queue-today.js",
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
"static/today-work.js", "static/pick-work.js", "static/batch-find-work.js",
"static/search-batch-plan.js", "static/issue-evidence-review.js",
),
}
CACHE_DECLARATION = re.compile(
r"const CACHE = 'stackchain-dashboard-shell-(?:v\d+|[0-9a-f]{16})';"
)
@dataclass(frozen=True)
class RuntimeBundle:
runtime_bytes: bytes
runtime_gzip_bytes: bytes
runtime_digest: str
runtime_name: str
@dataclass(frozen=True)
class FrontendBuild:
dashboard_html: str
runtime_bytes: bytes
runtime_gzip_bytes: bytes
runtime_digest: str
runtime_name: str
shell_digest: str
page_sources: tuple[str, ...]
feature_bundles: dict[str, RuntimeBundle]
service_worker_source: str
def _bundle(frontend_dir: Path, sources: tuple[str, ...]) -> bytes:
chunks = []
for source in sources:
path = frontend_dir / source.removeprefix("static/")
chunks.append(f"/* {source} */\n".encode() + path.read_bytes() + b"\n;\n")
source = b"".join(chunks)
# Node-only export shims support source-level unit tests but are unreachable
# in the browser. Strip the simple one-line form from shipped bundles.
source = COMMONJS_BROWSER_BRANCH.sub(b"{", source)
source = COMMONJS_EXPORT_LINE.sub(b"", source)
revision = hashlib.sha256(source).hexdigest()
minified = rjsmin.jsmin(source.decode()).encode()
return minified + f';"source-sha256:{revision}";'.encode()
def _runtime(frontend_dir: Path, sources: tuple[str, ...], prefix: str) -> RuntimeBundle:
content = _bundle(frontend_dir, sources)
digest = hashlib.sha256(content).hexdigest()[:16]
return RuntimeBundle(
runtime_bytes=content,
runtime_gzip_bytes=gzip.compress(content, compresslevel=6, mtime=0),
runtime_digest=digest,
runtime_name=f"{prefix}-{digest}.js",
)
def build_frontend(frontend_dir: Path) -> FrontendBuild:
"""Return one internally consistent HTML, runtime, and offline-worker build."""
source_html = (frontend_dir / "index.html").read_text()
sources = tuple(SCRIPT_TAG.findall(source_html))
if not sources:
raise ValueError("dashboard entry document has no page scripts")
feature_source_names = {source for group in FEATURE_SOURCES.values() for source in group}
core_sources = tuple(source for source in sources if source not in feature_source_names)
core = _runtime(frontend_dir, core_sources, "runtime")
feature_bundles = {
name: _runtime(frontend_dir, feature_sources, f"feature-{name}")
for name, feature_sources in FEATURE_SOURCES.items()
}
dashboard_html = SCRIPT_TAG.sub("", source_html)
feature_metadata = "\n".join(
f''
for name, bundle in feature_bundles.items()
)
dashboard_html = dashboard_html.replace("", feature_metadata + "\n")
dashboard_html = dashboard_html.replace(
"