const { useEffect, useMemo, useState } = React;
const CONFIG = window.ADMIN_CONFIG || {};
const STORAGE_KEY = 'ur_admin_auth_v1';
const DEFAULT_ADMIN_HASH = 'bc1daab2688c7bffd8f764426ac81028e8b4288f07522aa9dbf61555fae6c849';
const NAV_ITEMS = [
{ key: 'stats', title: '统计', copy: '总览供应、地区分布与同步状态', emoji: '◫' },
{ key: 'logs', title: '查看日志', copy: '审计每一轮 refresh 的结果与变化', emoji: '◭' },
];
function getSession() {
try {
return JSON.parse(sessionStorage.getItem(STORAGE_KEY) || 'null');
} catch (error) {
return null;
}
}
function setSession(nextValue) {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(nextValue));
}
function clearSession() {
sessionStorage.removeItem(STORAGE_KEY);
}
function compactNumber(value) {
if (!Number.isFinite(value)) return '0';
return new Intl.NumberFormat('zh-CN', { maximumFractionDigits: value > 100 ? 0 : 1 }).format(value);
}
function formatPercent(value) {
if (!Number.isFinite(value)) return '0%';
return `${(value * 100).toFixed(value >= 0.1 ? 1 : 2)}%`;
}
function formatDateTime(value) {
if (!value) return '未提供';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).format(date);
}
function formatRelative(value) {
if (!value) return '未提供';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
const diffMinutes = Math.round((date.getTime() - Date.now()) / 60000);
const formatter = new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' });
if (Math.abs(diffMinutes) < 60) return formatter.format(diffMinutes, 'minute');
const diffHours = Math.round(diffMinutes / 60);
if (Math.abs(diffHours) < 24) return formatter.format(diffHours, 'hour');
const diffDays = Math.round(diffHours / 24);
return formatter.format(diffDays, 'day');
}
function buildQuery(params = {}) {
const search = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === null || value === '') return;
search.set(key, String(value));
});
const query = search.toString();
return query ? `?${query}` : '';
}
async function apiFetch(path, params = {}) {
const base = CONFIG.apiBase || 'https://api.repilot.jp';
const token = CONFIG.apiToken || '';
const url = base + path + buildQuery(params);
const response = await fetch(url, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
if (!response.ok) {
const text = await response.text();
throw new Error(text || `API ${response.status}`);
}
return response.json();
}
async function sha256(value) {
const data = new TextEncoder().encode(value);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((item) => item.toString(16).padStart(2, '0')).join('');
}
function toneForStatus(status) {
if (status === 'success') return 'success';
if (status === 'failed') return 'danger';
return 'neutral';
}
function labelForStatus(status) {
if (status === 'success') return '成功';
if (status === 'failed') return '失败';
if (status === 'running') return '进行中';
return status || '未知';
}
function summarizeChanges(run) {
const summary = run?.summary || {};
return {
listedRooms: summary.listedRoomCount || 0,
delistedRooms: summary.delistedRoomCount || 0,
listedProperties: summary.listedPropertyCount || 0,
delistedProperties: summary.delistedPropertyCount || 0,
activeRoomDelta: summary.activeRoomDelta || 0,
};
}
function entryLabel(entry) {
if (!entry) return '未命名记录';
return entry.displayName || entry.propertyName || entry.roomName || entry.propertyKey || entry.roomKey || entry.canonicalRoomId || '未命名记录';
}
function renderTrigger(trigger) {
if (trigger === 'manual') return '手动';
if (trigger === 'scheduled') return '定时';
return trigger || '未标注';
}
function getDefaultSetupActive() {
const users = CONFIG.adminUsers || [];
return users.length === 1 && users[0].username === 'admin' && users[0].passwordHash === DEFAULT_ADMIN_HASH;
}
function App() {
const [session, setSessionState] = useState(getSession());
const [view, setView] = useState('stats');
const [statsState, setStatsState] = useState({ loading: false, error: '', data: null });
const [logsState, setLogsState] = useState({
loading: false,
error: '',
data: null,
filters: {
regionKey: '',
trigger: '',
changedOnly: false,
since: '',
until: '',
},
});
const authed = Boolean(session?.username);
useEffect(() => {
if (!authed) return;
refreshStats();
refreshLogs(logsState.filters);
}, [authed]);
async function refreshStats() {
setStatsState((current) => ({ ...current, loading: true, error: '' }));
try {
const [health, summary, regions, latestLog, allLogs] = await Promise.all([
apiFetch('/health'),
apiFetch('/api/v1/summary'),
apiFetch('/api/v1/regions'),
apiFetch('/api/v1/refresh-logs/latest'),
apiFetch('/api/v1/refresh-logs'),
]);
setStatsState({
loading: false,
error: '',
data: {
health,
summary,
regions: regions?.data || [],
latestLog,
logs: allLogs?.data || [],
},
});
} catch (error) {
setStatsState({ loading: false, error: error.message || '加载统计失败', data: null });
}
}
async function refreshLogs(filters) {
setLogsState((current) => ({ ...current, loading: true, error: '' }));
try {
const params = {
regionKey: filters.regionKey,
trigger: filters.trigger,
changedOnly: filters.changedOnly ? 1 : '',
since: filters.since ? new Date(filters.since).toISOString() : '',
until: filters.until ? new Date(filters.until).toISOString() : '',
};
const response = await apiFetch('/api/v1/refresh-logs', params);
setLogsState((current) => ({
...current,
loading: false,
error: '',
data: response?.data || [],
}));
} catch (error) {
setLogsState((current) => ({
...current,
loading: false,
error: error.message || '加载日志失败',
data: [],
}));
}
}
function handleLogin(nextSession) {
setSession(nextSession);
setSessionState(nextSession);
}
function handleLogout() {
clearSession();
setSessionState(null);
}
if (!authed) {
return
{view === 'stats' ? '聚焦当前 UR 全局供应、地区分布和同步健康度。' : '筛选每次 refresh 的执行结果,定位新增、下架和异常原因。'}
独立于前台站点的后台入口,用于查看 refresh 审计日志、全局物业供应统计,以及当前同步健康状态。