Improve DICOM UPP relation visualizer
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
const app = {
|
||||
token: localStorage.getItem("pacs_relation_token") || "",
|
||||
user: null,
|
||||
rows: [],
|
||||
active: null,
|
||||
filter: "all",
|
||||
search: "",
|
||||
algorithmModel: "",
|
||||
dicomModel: "",
|
||||
dicomPart: "",
|
||||
viewerUrl: "http://127.0.0.1:8107",
|
||||
searchTimer: null,
|
||||
};
|
||||
|
||||
@@ -19,8 +22,15 @@ function escapeHtml(value) {
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
async function json(path) {
|
||||
const res = await fetch(path);
|
||||
async function request(path, options = {}) {
|
||||
const headers = { ...(options.headers || {}) };
|
||||
if (app.token) headers.Authorization = `Bearer ${app.token}`;
|
||||
if (options.body) headers["Content-Type"] = "application/json";
|
||||
const res = await fetch(path, { ...options, headers });
|
||||
if (res.status === 401) {
|
||||
showLogin(true);
|
||||
throw new Error("unauthorized");
|
||||
}
|
||||
if (!res.ok) {
|
||||
let detail = res.statusText;
|
||||
try {
|
||||
@@ -31,9 +41,39 @@ async function json(path) {
|
||||
}
|
||||
throw new Error(detail);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async function json(path, options = {}) {
|
||||
const res = await request(path, options);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function showLogin(show) {
|
||||
$("loginOverlay").classList.toggle("hidden", !show);
|
||||
}
|
||||
|
||||
function showViewer() {
|
||||
$("viewerPage").classList.remove("hidden");
|
||||
$("settingsPage").classList.add("hidden");
|
||||
}
|
||||
|
||||
async function showSettings() {
|
||||
if (!isAdmin()) return;
|
||||
$("viewerPage").classList.add("hidden");
|
||||
$("settingsPage").classList.remove("hidden");
|
||||
await renderSettingsPage();
|
||||
}
|
||||
|
||||
function isAdmin() {
|
||||
return app.user?.role === "管理员";
|
||||
}
|
||||
|
||||
function applyAuthUi() {
|
||||
$("userBadge").textContent = app.user ? `${app.user.username} · ${app.user.role}` : "未登录";
|
||||
$("settingsBtn").classList.toggle("hidden", !isAdmin());
|
||||
}
|
||||
|
||||
function fmtDate(value) {
|
||||
const text = String(value || "");
|
||||
if (!text) return "";
|
||||
@@ -65,9 +105,9 @@ function uniq(list) {
|
||||
|
||||
function statusLabel(value) {
|
||||
return {
|
||||
complete: "PACS + STL",
|
||||
no_stl: "PACS + 列表",
|
||||
pacs_only: "仅 PACS",
|
||||
complete: "DICOM + STL",
|
||||
no_stl: "DICOM + UPP",
|
||||
pacs_only: "仅 DICOM",
|
||||
stl_only: "仅 STL",
|
||||
list_only: "列表无STL",
|
||||
unknown: "未知",
|
||||
@@ -88,7 +128,18 @@ function partLabel(value) {
|
||||
}[value] || value;
|
||||
}
|
||||
|
||||
function dicomParts(row) {
|
||||
const labels = asList(row.dicom_body_parts);
|
||||
if (labels.length) return labels;
|
||||
return asList(row.body_parts).map(partLabel);
|
||||
}
|
||||
|
||||
function pendingDICOM(row) {
|
||||
return Number(row.undetermined_series || 0);
|
||||
}
|
||||
|
||||
function setDbStatus(data) {
|
||||
app.viewerUrl = data.viewer_url || app.viewerUrl;
|
||||
const pill = $("dbStatus");
|
||||
if (data.database?.ok) {
|
||||
pill.textContent = `${data.database.database} 已连接`;
|
||||
@@ -101,12 +152,11 @@ function setDbStatus(data) {
|
||||
|
||||
function renderMetrics(counts = {}) {
|
||||
const items = [
|
||||
["CT 索引", counts.total_ct],
|
||||
["PACS", counts.pacs_count],
|
||||
["STL资产", counts.stl_asset_count],
|
||||
["STL", counts.stl_count],
|
||||
["完整关联", counts.complete_count],
|
||||
["待判别序列", counts.undetermined_series],
|
||||
["DICOM", counts.pacs_count],
|
||||
["缺 STL", counts.no_stl_count],
|
||||
["仅 DICOM", counts.pacs_only_count],
|
||||
["待处理DICOM", counts.pending_dicom_count],
|
||||
];
|
||||
$("metrics").innerHTML = items
|
||||
.map(([label, value]) => `<div class="metric"><span>${escapeHtml(label)}</span><strong>${Number(value || 0)}</strong></div>`)
|
||||
@@ -116,21 +166,27 @@ function renderMetrics(counts = {}) {
|
||||
function cardTitle(row) {
|
||||
return [
|
||||
`CT号:${row.ct_key}`,
|
||||
`PACS:${row.pacs_ct_number || "无"}`,
|
||||
`DICOM:${row.pacs_ct_number || "无"}`,
|
||||
`STL:${row.stl_ct_number || "无"}`,
|
||||
`状态:${statusLabel(row.relation_status)}`,
|
||||
`PACS患者:${row.pacs_patient_name || "无"}`,
|
||||
`STL列表患者:${row.upp_patient_name || "无"}`,
|
||||
`DICOM患者:${row.pacs_patient_name || "无"}`,
|
||||
`UPP患者:${row.upp_patient_name || "无"}`,
|
||||
`重建模型:${row.algorithm_model || "无"}`,
|
||||
`DICOM模型:${asList(row.dicom_models).join("、") || "无"}`,
|
||||
`部位标注:${dicomParts(row).join("、") || "无"}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderPartTags(parts) {
|
||||
if (!parts.length) return `<span class="subtle">无部位标注</span>`;
|
||||
return parts.map((part) => `<em title="${escapeHtml(part)}">${escapeHtml(part)}</em>`).join("");
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
const list = $("relationList");
|
||||
$("resultCount").textContent = String(app.rows.length);
|
||||
if (!app.rows.length) {
|
||||
list.innerHTML = `<p class="empty">没有匹配记录</p>`;
|
||||
clearDetail();
|
||||
return;
|
||||
}
|
||||
list.innerHTML = "";
|
||||
@@ -141,16 +197,18 @@ function renderList() {
|
||||
card.title = cardTitle(row);
|
||||
const date = row.study_date ? `${fmtDate(row.study_date)} ${fmtTime(row.study_time)}` : fmtDate(row.exam_date || row.task_created_at);
|
||||
const stlCount = Number(row.stl_file_count || row.stl_file_count_agg || 0);
|
||||
const dicomModels = asList(row.dicom_models).join("、") || "无";
|
||||
const parts = dicomParts(row);
|
||||
const pending = pendingDICOM(row);
|
||||
card.innerHTML = `
|
||||
<div class="card-row">
|
||||
<strong>${escapeHtml(row.ct_key)}</strong>
|
||||
<i class="mini-badge ${escapeHtml(row.relation_status)}">${escapeHtml(statusLabel(row.relation_status))}</i>
|
||||
</div>
|
||||
<span>${escapeHtml(row.pacs_patient_name || row.upp_patient_name || "无姓名")} · ${escapeHtml(row.patient_id || row.patient_id_masked || "无ID")}</span>
|
||||
<small>${escapeHtml(date || "无时间")} · PACS ${Number(row.dicom_file_count || 0)} 张 · STL ${stlCount} 个</small>
|
||||
<small>重建 ${escapeHtml(row.algorithm_model || "无")} · DICOM ${escapeHtml(dicomModels)}</small>
|
||||
<small>${escapeHtml(matchLabel(row.match_type))}${Number(row.undetermined_series || 0) ? ` · ${Number(row.undetermined_series)} 待判别` : ""}</small>
|
||||
<small>${escapeHtml(date || "无时间")} · DICOM ${Number(row.dicom_file_count || 0)} 张 · STL ${stlCount} 个</small>
|
||||
<small>重建 ${escapeHtml(row.algorithm_model || "无")}</small>
|
||||
<div class="inline-tags">${renderPartTags(parts)}</div>
|
||||
<small>${escapeHtml(matchLabel(row.match_type))}${pending ? ` · ${pending} 待处理DICOM` : ""}</small>
|
||||
`;
|
||||
card.onclick = () => selectRelation(row.ct_key);
|
||||
list.appendChild(card);
|
||||
@@ -175,50 +233,54 @@ function markNode(id, state, strong, meta) {
|
||||
node.querySelector("em").textContent = meta;
|
||||
}
|
||||
|
||||
function groupSegments(row) {
|
||||
const names = asList(row.segment_names);
|
||||
const families = asList(row.segment_families);
|
||||
const categories = asList(row.segment_categories);
|
||||
const groups = new Map();
|
||||
names.forEach((name, index) => {
|
||||
const key = categories[index] || families[index] || "未分类";
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key).push(name);
|
||||
});
|
||||
return groups;
|
||||
function dicomViewerUrl(row = app.active) {
|
||||
if (!row?.pacs_ct_number) return "";
|
||||
return `${app.viewerUrl.replace(/\/$/, "")}/?ct_number=${encodeURIComponent(row.pacs_ct_number)}`;
|
||||
}
|
||||
|
||||
function renderSegments(row) {
|
||||
const groups = groupSegments(row);
|
||||
const count = asList(row.segment_names).length;
|
||||
$("segmentCount").textContent = `${count} 个`;
|
||||
if (!count) {
|
||||
$("segmentGroups").innerHTML = `<p class="empty">没有 STL 分割文件</p>`;
|
||||
return;
|
||||
}
|
||||
$("segmentGroups").innerHTML = Array.from(groups.entries())
|
||||
.map(
|
||||
([group, names]) => `
|
||||
<div class="segment-group">
|
||||
<strong>${escapeHtml(group)}</strong>
|
||||
<div class="segment-tags">${names.map((name) => `<em title="${escapeHtml(name)}">${escapeHtml(name)}</em>`).join("")}</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
function updateOpenDicomButton() {
|
||||
const button = $("openDicomBtn");
|
||||
const url = dicomViewerUrl();
|
||||
button.disabled = !url;
|
||||
button.title = url || "当前记录没有 DICOM 检查号";
|
||||
}
|
||||
|
||||
function clearDetail() {
|
||||
app.active = null;
|
||||
$("activeCt").textContent = "未选择 CT";
|
||||
$("activeSubtitle").textContent = "从左侧选择一个 CT 号查看 DICOM 与 UPP STL 之间的关系";
|
||||
$("relationBadge").className = "relation-badge idle";
|
||||
$("relationBadge").textContent = "等待选择";
|
||||
markNode("nodePacs", "missing", "未选择", "检查与序列");
|
||||
$("nodeCtValue").textContent = "-";
|
||||
markNode("nodeStl", "missing", "未选择", "含UPP列表信息");
|
||||
renderDl("pacsDetails", []);
|
||||
renderDl("stlDetails", []);
|
||||
updateOpenDicomButton();
|
||||
}
|
||||
|
||||
function renderDetail(row) {
|
||||
app.active = row;
|
||||
$("activeCt").textContent = row.ct_key || "未选择 CT";
|
||||
$("activeSubtitle").textContent = `${row.pacs_ct_number || "无 PACS"} ↔ ${row.stl_ct_number || "无 STL"} · ${matchLabel(row.match_type)}`;
|
||||
$("activeSubtitle").textContent = `${row.pacs_ct_number || "无 DICOM"} ↔ ${row.stl_ct_number || "无 STL"} · ${matchLabel(row.match_type)}`;
|
||||
const badge = $("relationBadge");
|
||||
badge.className = `relation-badge ${row.relation_status || "idle"}`;
|
||||
badge.textContent = statusLabel(row.relation_status);
|
||||
|
||||
const pending = pendingDICOM(row);
|
||||
$("nodeCtValue").textContent = row.ct_key || "-";
|
||||
markNode("nodePacs", row.pacs_present ? "ok" : "missing", row.pacs_ct_number || "缺失", `${Number(row.dicom_file_count || 0)} 张 · ${Number(row.pacs_series_count || 0)} 序列`);
|
||||
markNode("nodeStl", row.stl_asset_present ? (row.stl_present ? "ok" : "warn") : "missing", row.stl_ct_number || "缺失", `${row.algorithm_model || "无模型"} · ${Number(row.stl_file_count || row.stl_file_count_agg || 0)} STL`);
|
||||
markNode(
|
||||
"nodePacs",
|
||||
row.pacs_present ? "ok" : "missing",
|
||||
row.pacs_ct_number || "缺失",
|
||||
`${Number(row.dicom_file_count || 0)} 张 · ${Number(row.pacs_series_count || 0)} 序列${pending ? ` · ${pending} 待处理DICOM` : ""}`,
|
||||
);
|
||||
markNode(
|
||||
"nodeStl",
|
||||
row.stl_asset_present ? (row.stl_present ? "ok" : "warn") : "missing",
|
||||
row.stl_ct_number || "缺失",
|
||||
`${row.algorithm_model || "无重建模型"} · ${Number(row.stl_file_count || row.stl_file_count_agg || 0)} STL`,
|
||||
);
|
||||
|
||||
renderDl("pacsDetails", [
|
||||
["CT号", row.pacs_ct_number],
|
||||
@@ -227,10 +289,9 @@ function renderDetail(row) {
|
||||
["检查时间", `${fmtDate(row.study_date)} ${fmtTime(row.study_time)}`.trim()],
|
||||
["描述", row.study_description],
|
||||
["序列/文件", `${Number(row.pacs_series_count || 0)} / ${Number(row.dicom_file_count || 0)}`],
|
||||
["标注", `${Number(row.annotated_series || 0)} 已标注 · ${Number(row.undetermined_series || 0)} 待判别`],
|
||||
["标注", `${Number(row.annotated_series || 0)} 已标注 · ${pending} 待处理DICOM`],
|
||||
["完成", row.completed ? "已完成" : "待处理"],
|
||||
["部位", asList(row.body_parts).map(partLabel).join("、")],
|
||||
["DICOM模型", asList(row.dicom_models).join("、")],
|
||||
["部位标注", dicomParts(row).join("、")],
|
||||
]);
|
||||
|
||||
renderDl("stlDetails", [
|
||||
@@ -242,16 +303,15 @@ function renderDetail(row) {
|
||||
["年龄/性别", [row.patient_age, row.upp_patient_sex].filter(Boolean).join(" / ")],
|
||||
["检查时间", fmtDate(row.exam_date)],
|
||||
["任务时间", fmtDate(row.task_created_at)],
|
||||
["列表记录", row.list_present ? `${Number(row.list_record_count || 0)} 条` : "无"],
|
||||
["UPP列表记录", row.list_present ? `${Number(row.list_record_count || 0)} 条` : "无"],
|
||||
["文件数", Number(row.stl_file_count || row.stl_file_count_agg || 0)],
|
||||
["总大小", fmtBytes(row.stl_total_bytes || row.stl_total_bytes_agg)],
|
||||
["目录", row.processed_stl_dir],
|
||||
["分类", uniq(asList(row.segment_categories)).join("、")],
|
||||
["Family", uniq(asList(row.segment_families)).join("、")],
|
||||
["分割分类", uniq(asList(row.segment_categories)).join("、")],
|
||||
["更新时间", fmtDate(row.stl_updated_at || row.upp_updated_at)],
|
||||
]);
|
||||
|
||||
renderSegments(row);
|
||||
updateOpenDicomButton();
|
||||
renderList();
|
||||
}
|
||||
|
||||
@@ -272,13 +332,14 @@ async function loadRelations() {
|
||||
params.set("limit", "500");
|
||||
if (app.search) params.set("q", app.search);
|
||||
if (app.algorithmModel) params.set("algorithm_model", app.algorithmModel);
|
||||
if (app.dicomModel) params.set("dicom_model", app.dicomModel);
|
||||
if (app.dicomPart) params.set("dicom_part", app.dicomPart);
|
||||
app.rows = await json(`/api/relations?${params.toString()}`);
|
||||
if (app.rows.length && !app.rows.some((row) => row.ct_key === app.active?.ct_key)) {
|
||||
await selectRelation(app.rows[0].ct_key);
|
||||
} else {
|
||||
renderList();
|
||||
if (!app.active && app.rows.length) await selectRelation(app.rows[0].ct_key);
|
||||
if (!app.rows.length) clearDetail();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,8 +353,174 @@ async function refreshAll() {
|
||||
}
|
||||
}
|
||||
|
||||
async function login(event) {
|
||||
event.preventDefault();
|
||||
$("loginError").textContent = "";
|
||||
try {
|
||||
const data = await json("/api/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username: $("username").value.trim(), password: $("password").value }),
|
||||
});
|
||||
app.token = data.token;
|
||||
app.user = { username: data.username, role: data.role };
|
||||
localStorage.setItem("pacs_relation_token", app.token);
|
||||
applyAuthUi();
|
||||
showLogin(false);
|
||||
showViewer();
|
||||
await refreshAll();
|
||||
} catch (_) {
|
||||
$("loginError").textContent = "登录失败";
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
app.token = "";
|
||||
app.user = null;
|
||||
app.rows = [];
|
||||
app.active = null;
|
||||
localStorage.removeItem("pacs_relation_token");
|
||||
applyAuthUi();
|
||||
showViewer();
|
||||
clearDetail();
|
||||
showLogin(true);
|
||||
}
|
||||
|
||||
async function loadCurrentUser() {
|
||||
if (!app.token) {
|
||||
showLogin(true);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
app.user = await json("/api/auth/me");
|
||||
applyAuthUi();
|
||||
showLogin(false);
|
||||
return true;
|
||||
} catch (_) {
|
||||
app.token = "";
|
||||
localStorage.removeItem("pacs_relation_token");
|
||||
applyAuthUi();
|
||||
showLogin(true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function settingsTable(users) {
|
||||
if (!users?.length) return `<p class="empty">暂无账号</p>`;
|
||||
return `
|
||||
<table class="settings-table">
|
||||
<thead><tr><th>账号</th><th>角色</th><th>状态</th><th>更新时间</th><th>重置密码</th></tr></thead>
|
||||
<tbody>
|
||||
${users
|
||||
.map(
|
||||
(user) => `
|
||||
<tr>
|
||||
<td>${escapeHtml(user.username)}</td>
|
||||
<td>${escapeHtml(user.role)}</td>
|
||||
<td>${escapeHtml(user.status)}</td>
|
||||
<td>${escapeHtml(fmtDate(user.updated_at))}</td>
|
||||
<td>
|
||||
<div class="password-reset">
|
||||
<input data-password-user="${escapeHtml(user.username)}" placeholder="新密码" type="password" />
|
||||
<button data-reset-user="${escapeHtml(user.username)}" type="button">更新</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
async function renderSettingsPage() {
|
||||
const data = await json("/api/settings");
|
||||
$("settingsContent").innerHTML = `
|
||||
<section class="settings-section">
|
||||
<div class="settings-title">
|
||||
<h3>账号创建</h3>
|
||||
<span>当前登录:${escapeHtml(app.user?.username || "-")}</span>
|
||||
</div>
|
||||
<form id="createUserForm" class="settings-form">
|
||||
<input name="username" placeholder="新账号" />
|
||||
<input name="password" type="password" placeholder="初始密码" />
|
||||
<select name="role">
|
||||
<option value="阅片员">阅片员</option>
|
||||
<option value="管理员">管理员</option>
|
||||
<option value="访客">访客</option>
|
||||
</select>
|
||||
<select name="status">
|
||||
<option value="启用">启用</option>
|
||||
<option value="停用">停用</option>
|
||||
</select>
|
||||
<button class="primary-btn" type="submit">保存账号</button>
|
||||
</form>
|
||||
${settingsTable(data.users || [])}
|
||||
</section>
|
||||
<section class="settings-section split">
|
||||
<div>
|
||||
<div class="settings-title"><h3>系统管理</h3><span>跳转与联动</span></div>
|
||||
<dl class="settings-dl">
|
||||
<dt>阅片系统</dt><dd>${escapeHtml(data.viewer_url || "-")}</dd>
|
||||
<dt>数据库</dt><dd>${escapeHtml(data.database?.host || "-")}:${escapeHtml(data.database?.port || "-")} / ${escapeHtml(data.database?.database || "-")}</dd>
|
||||
<dt>DICOM表</dt><dd>${escapeHtml(data.tables?.dicom || "-")}</dd>
|
||||
<dt>摘要表</dt><dd>${escapeHtml(data.tables?.dicom_summary || "-")}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div>
|
||||
<div class="settings-title"><h3>权限说明</h3><span>页面级控制</span></div>
|
||||
<div class="role-grid">
|
||||
<div class="role-card"><strong>管理员</strong><span>查看关联、系统设置、账号密码管理</span></div>
|
||||
<div class="role-card"><strong>阅片员</strong><span>查看关联与跳转阅片系统</span></div>
|
||||
<div class="role-card"><strong>访客</strong><span>只读查看关联信息</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
$("createUserForm").addEventListener("submit", saveUser);
|
||||
document.querySelectorAll("[data-reset-user]").forEach((button) => {
|
||||
button.addEventListener("click", () => resetPassword(button.dataset.resetUser));
|
||||
});
|
||||
}
|
||||
|
||||
async function saveUser(event) {
|
||||
event.preventDefault();
|
||||
const form = new FormData(event.currentTarget);
|
||||
await json("/api/settings/users", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
username: String(form.get("username") || "").trim(),
|
||||
password: String(form.get("password") || ""),
|
||||
role: String(form.get("role") || "阅片员"),
|
||||
status: String(form.get("status") || "启用"),
|
||||
}),
|
||||
});
|
||||
await renderSettingsPage();
|
||||
}
|
||||
|
||||
async function resetPassword(username) {
|
||||
const input = Array.from(document.querySelectorAll("[data-password-user]")).find((item) => item.dataset.passwordUser === username);
|
||||
const password = input?.value || "";
|
||||
if (!password) return;
|
||||
await json(`/api/settings/users/${encodeURIComponent(username)}/password`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ password }),
|
||||
});
|
||||
await renderSettingsPage();
|
||||
}
|
||||
|
||||
function openDicomViewer() {
|
||||
const url = dicomViewerUrl();
|
||||
if (url) window.open(url, "_blank", "noopener");
|
||||
}
|
||||
|
||||
function wire() {
|
||||
$("loginForm").addEventListener("submit", login);
|
||||
$("logoutBtn").addEventListener("click", logout);
|
||||
$("settingsBtn").addEventListener("click", showSettings);
|
||||
$("backToViewer").addEventListener("click", showViewer);
|
||||
$("refreshBtn").addEventListener("click", refreshAll);
|
||||
$("openDicomBtn").addEventListener("click", openDicomViewer);
|
||||
$("searchInput").addEventListener("input", () => {
|
||||
clearTimeout(app.searchTimer);
|
||||
app.searchTimer = setTimeout(() => {
|
||||
@@ -305,8 +532,8 @@ function wire() {
|
||||
app.algorithmModel = $("algorithmFilter").value;
|
||||
loadRelations();
|
||||
});
|
||||
$("dicomModelFilter").addEventListener("change", () => {
|
||||
app.dicomModel = $("dicomModelFilter").value;
|
||||
$("dicomPartFilter").addEventListener("change", () => {
|
||||
app.dicomPart = $("dicomPartFilter").value;
|
||||
loadRelations();
|
||||
});
|
||||
document.querySelectorAll("[data-filter]").forEach((button) => {
|
||||
@@ -318,5 +545,12 @@ function wire() {
|
||||
});
|
||||
}
|
||||
|
||||
wire();
|
||||
refreshAll();
|
||||
async function boot() {
|
||||
clearDetail();
|
||||
applyAuthUi();
|
||||
wire();
|
||||
const ok = await loadCurrentUser();
|
||||
if (ok) await refreshAll();
|
||||
}
|
||||
|
||||
boot();
|
||||
|
||||
Reference in New Issue
Block a user