feat: unify card runtime and event-driven web ui
This commit is contained in:
parent
0edf8c3fef
commit
4dfb7ca3cc
105 changed files with 17382 additions and 8505 deletions
494
examples/cards/templates/today-briefing-live/card.js
Normal file
494
examples/cards/templates/today-briefing-live/card.js
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
export function mount({ root, state, host }) {
|
||||
state = state || {};
|
||||
const __cleanup = [];
|
||||
const __setInterval = (...args) => {
|
||||
const id = window.setInterval(...args);
|
||||
__cleanup.push(() => window.clearInterval(id));
|
||||
return id;
|
||||
};
|
||||
const __setTimeout = (...args) => {
|
||||
const id = window.setTimeout(...args);
|
||||
__cleanup.push(() => window.clearTimeout(id));
|
||||
return id;
|
||||
};
|
||||
if (!(root instanceof HTMLElement)) return;
|
||||
|
||||
const dateEl = root.querySelector('[data-today-date]');
|
||||
const tempEl = root.querySelector('[data-today-temp]');
|
||||
const unitEl = root.querySelector('[data-today-unit]');
|
||||
const feelsLikeEl = root.querySelector('[data-today-feels-like]');
|
||||
const aqiChipEl = root.querySelector('[data-today-aqi-chip]');
|
||||
const countEl = root.querySelector('[data-today-events-count]');
|
||||
const emptyEl = root.querySelector('[data-today-empty]');
|
||||
const listEl = root.querySelector('[data-today-events-list]');
|
||||
const updatedEl = root.querySelector('[data-today-updated]');
|
||||
if (!(dateEl instanceof HTMLElement) || !(tempEl instanceof HTMLElement) || !(unitEl instanceof HTMLElement) || !(feelsLikeEl instanceof HTMLElement) || !(aqiChipEl instanceof HTMLElement) || !(countEl instanceof HTMLElement) || !(emptyEl instanceof HTMLElement) || !(listEl instanceof HTMLElement) || !(updatedEl instanceof HTMLElement)) return;
|
||||
|
||||
const configuredWeatherToolName = typeof state.weather_tool_name === 'string' ? state.weather_tool_name.trim() : '';
|
||||
const configuredCalendarToolName = typeof state.calendar_tool_name === 'string' ? state.calendar_tool_name.trim() : '';
|
||||
const calendarNames = Array.isArray(state.calendar_names)
|
||||
? state.calendar_names.map((value) => String(value || '').trim()).filter(Boolean)
|
||||
: [];
|
||||
const weatherPrefix = typeof state.weather_prefix === 'string' ? state.weather_prefix.trim() : 'OpenWeatherMap';
|
||||
const temperatureName = typeof state.temperature_name === 'string' ? state.temperature_name.trim() : '';
|
||||
const apparentTemperatureName = typeof state.apparent_temperature_name === 'string' ? state.apparent_temperature_name.trim() : '';
|
||||
const aqiName = typeof state.aqi_name === 'string' ? state.aqi_name.trim() : '';
|
||||
const maxEventsRaw = Number(state.max_events);
|
||||
const maxEvents = Number.isFinite(maxEventsRaw) && maxEventsRaw >= 1 ? Math.min(maxEventsRaw, 8) : 6;
|
||||
const refreshMsRaw = Number(state.refresh_ms);
|
||||
const refreshMs = Number.isFinite(refreshMsRaw) && refreshMsRaw >= 60000 ? refreshMsRaw : 15 * 60 * 1000;
|
||||
const emptyText = typeof state.empty_text === 'string' && state.empty_text.trim() ? state.empty_text.trim() : 'Nothing scheduled today.';
|
||||
|
||||
emptyEl.textContent = emptyText;
|
||||
|
||||
const updateLiveContent = (snapshot) => {
|
||||
host.setLiveContent(snapshot);
|
||||
};
|
||||
|
||||
const stripQuotes = (value) => {
|
||||
const text = String(value ?? '').trim();
|
||||
if ((text.startsWith("'") && text.endsWith("'")) || (text.startsWith('"') && text.endsWith('"'))) {
|
||||
return text.slice(1, -1);
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const normalizeText = (value) => String(value || '').trim().toLowerCase();
|
||||
|
||||
const parseLiveContextEntries = (payloadText) => {
|
||||
const text = String(payloadText || '').replace(/\r/g, '');
|
||||
const startIndex = text.indexOf('- names: ');
|
||||
const relevant = startIndex >= 0 ? text.slice(startIndex) : text;
|
||||
const entries = [];
|
||||
let current = null;
|
||||
let inAttributes = false;
|
||||
|
||||
const pushCurrent = () => {
|
||||
if (current) entries.push(current);
|
||||
current = null;
|
||||
inAttributes = false;
|
||||
};
|
||||
|
||||
for (const rawLine of relevant.split('\n')) {
|
||||
if (rawLine.startsWith('- names: ')) {
|
||||
pushCurrent();
|
||||
current = {
|
||||
name: stripQuotes(rawLine.slice(9)),
|
||||
domain: '',
|
||||
state: '',
|
||||
areas: '',
|
||||
attributes: {},
|
||||
};
|
||||
continue;
|
||||
}
|
||||
if (!current) continue;
|
||||
const trimmed = rawLine.trim();
|
||||
if (!trimmed) continue;
|
||||
if (trimmed === 'attributes:') {
|
||||
inAttributes = true;
|
||||
continue;
|
||||
}
|
||||
if (rawLine.startsWith(' domain:')) {
|
||||
current.domain = stripQuotes(rawLine.slice(rawLine.indexOf(':') + 1));
|
||||
inAttributes = false;
|
||||
continue;
|
||||
}
|
||||
if (rawLine.startsWith(' state:')) {
|
||||
current.state = stripQuotes(rawLine.slice(rawLine.indexOf(':') + 1));
|
||||
inAttributes = false;
|
||||
continue;
|
||||
}
|
||||
if (rawLine.startsWith(' areas:')) {
|
||||
current.areas = stripQuotes(rawLine.slice(rawLine.indexOf(':') + 1));
|
||||
inAttributes = false;
|
||||
continue;
|
||||
}
|
||||
if (inAttributes && rawLine.startsWith(' ')) {
|
||||
const separatorIndex = rawLine.indexOf(':');
|
||||
if (separatorIndex >= 0) {
|
||||
const key = rawLine.slice(4, separatorIndex).trim();
|
||||
const value = stripQuotes(rawLine.slice(separatorIndex + 1));
|
||||
current.attributes[key] = value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
inAttributes = false;
|
||||
}
|
||||
|
||||
pushCurrent();
|
||||
return entries;
|
||||
};
|
||||
|
||||
const extractLiveContextText = (toolResult) => {
|
||||
if (toolResult && toolResult.parsed && typeof toolResult.parsed === 'object') {
|
||||
const parsed = toolResult.parsed;
|
||||
if (typeof parsed.result === 'string') return parsed.result;
|
||||
}
|
||||
if (typeof toolResult?.content === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(toolResult.content);
|
||||
if (parsed && typeof parsed === 'object' && typeof parsed.result === 'string') {
|
||||
return parsed.result;
|
||||
}
|
||||
} catch {
|
||||
return toolResult.content;
|
||||
}
|
||||
return toolResult.content;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const normalizeDateValue = (value) => {
|
||||
if (typeof value === 'string') return value;
|
||||
if (value && typeof value === 'object') {
|
||||
if (typeof value.dateTime === 'string') return value.dateTime;
|
||||
if (typeof value.date === 'string') return value.date;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const formatHeaderDate = (value) => value.toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' });
|
||||
const formatTime = (value) => {
|
||||
const raw = normalizeDateValue(value);
|
||||
if (!raw) return '--:--';
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return 'All day';
|
||||
const date = new Date(raw);
|
||||
if (Number.isNaN(date.getTime())) return '--:--';
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
const formatEventDay = (value) => {
|
||||
const raw = normalizeDateValue(value);
|
||||
if (!raw) return '--';
|
||||
const date = /^\d{4}-\d{2}-\d{2}$/.test(raw) ? new Date(`${raw}T00:00:00`) : new Date(raw);
|
||||
if (Number.isNaN(date.getTime())) return '--';
|
||||
return date.toLocaleDateString([], { weekday: 'short' });
|
||||
};
|
||||
const isAllDay = (start, end) => /^\d{4}-\d{2}-\d{2}$/.test(normalizeDateValue(start)) || /^\d{4}-\d{2}-\d{2}$/.test(normalizeDateValue(end));
|
||||
|
||||
const extractEvents = (toolResult) => {
|
||||
if (toolResult && toolResult.parsed && typeof toolResult.parsed === 'object') {
|
||||
const parsed = toolResult.parsed;
|
||||
if (Array.isArray(parsed.result)) return parsed.result;
|
||||
}
|
||||
if (typeof toolResult?.content === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(toolResult.content);
|
||||
if (parsed && typeof parsed === 'object' && Array.isArray(parsed.result)) {
|
||||
return parsed.result;
|
||||
}
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const eventTime = (value) => {
|
||||
const raw = normalizeDateValue(value);
|
||||
if (!raw) return Number.MAX_SAFE_INTEGER;
|
||||
const normalized = /^\d{4}-\d{2}-\d{2}$/.test(raw) ? `${raw}T00:00:00` : raw;
|
||||
const time = new Date(normalized).getTime();
|
||||
return Number.isFinite(time) ? time : Number.MAX_SAFE_INTEGER;
|
||||
};
|
||||
|
||||
const findEntry = (entries, candidates) => {
|
||||
const normalizedCandidates = candidates.map((value) => normalizeText(value)).filter(Boolean);
|
||||
if (normalizedCandidates.length === 0) return null;
|
||||
const exactMatch = entries.find((entry) => normalizedCandidates.includes(normalizeText(entry.name)));
|
||||
if (exactMatch) return exactMatch;
|
||||
return entries.find((entry) => {
|
||||
const entryName = normalizeText(entry.name);
|
||||
return normalizedCandidates.some((candidate) => entryName.includes(candidate));
|
||||
}) || null;
|
||||
};
|
||||
|
||||
const findByDeviceClass = (entries, deviceClass) => entries.find((entry) => normalizeText(entry.attributes?.device_class) === normalizeText(deviceClass)) || null;
|
||||
const parseNumericValue = (entry) => {
|
||||
const value = Number(entry?.state);
|
||||
return Number.isFinite(value) ? value : null;
|
||||
};
|
||||
const formatMetric = (value, unit) => {
|
||||
if (!Number.isFinite(value)) return '--';
|
||||
return `${Math.round(value)}${unit ? ` ${unit}` : ''}`;
|
||||
};
|
||||
|
||||
const buildAqiStyle = (aqiValue) => {
|
||||
if (!Number.isFinite(aqiValue)) {
|
||||
return { label: 'AQI --', tone: 'Unavailable', background: 'color-mix(in srgb, var(--theme-card-neutral-border) 65%, white)', color: 'var(--theme-card-neutral-subtle)' };
|
||||
}
|
||||
if (aqiValue <= 50) return { label: `AQI ${Math.round(aqiValue)}`, tone: 'Good', background: 'color-mix(in srgb, var(--theme-status-live) 16%, white)', color: 'var(--theme-status-live)' };
|
||||
if (aqiValue <= 100) return { label: `AQI ${Math.round(aqiValue)}`, tone: 'Moderate', background: 'color-mix(in srgb, var(--theme-status-warning) 16%, white)', color: 'var(--theme-status-warning)' };
|
||||
if (aqiValue <= 150) return { label: `AQI ${Math.round(aqiValue)}`, tone: 'Sensitive', background: 'color-mix(in srgb, var(--theme-status-warning) 24%, white)', color: 'var(--theme-status-warning)' };
|
||||
if (aqiValue <= 200) return { label: `AQI ${Math.round(aqiValue)}`, tone: 'Unhealthy', background: 'color-mix(in srgb, var(--theme-status-danger) 14%, white)', color: 'var(--theme-status-danger)' };
|
||||
if (aqiValue <= 300) return { label: `AQI ${Math.round(aqiValue)}`, tone: 'Very unhealthy', background: 'color-mix(in srgb, var(--theme-accent) 14%, white)', color: 'var(--theme-accent-strong)' };
|
||||
return { label: `AQI ${Math.round(aqiValue)}`, tone: 'Hazardous', background: 'color-mix(in srgb, var(--theme-accent-strong) 18%, white)', color: 'var(--theme-accent-strong)' };
|
||||
};
|
||||
|
||||
const startTime = (value) => {
|
||||
const raw = normalizeDateValue(value);
|
||||
if (!raw) return Number.NaN;
|
||||
const normalized = /^\d{4}-\d{2}-\d{2}$/.test(raw) ? `${raw}T12:00:00` : raw;
|
||||
const time = new Date(normalized).getTime();
|
||||
return Number.isFinite(time) ? time : Number.NaN;
|
||||
};
|
||||
|
||||
const computeTodayScore = (events, status) => {
|
||||
if (status === 'Unavailable') return 5;
|
||||
if (!Array.isArray(events) || events.length === 0) {
|
||||
return status === 'Partial' ? 18 : 24;
|
||||
}
|
||||
const now = Date.now();
|
||||
const soonestMs = events
|
||||
.map((event) => startTime(event?.start))
|
||||
.filter((time) => Number.isFinite(time) && time >= now)
|
||||
.sort((left, right) => left - right)[0];
|
||||
const soonestHours = Number.isFinite(soonestMs) ? (soonestMs - now) / (60 * 60 * 1000) : null;
|
||||
let score = 34;
|
||||
if (soonestHours !== null) {
|
||||
if (soonestHours <= 1) score = 80;
|
||||
else if (soonestHours <= 3) score = 70;
|
||||
else if (soonestHours <= 8) score = 58;
|
||||
else if (soonestHours <= 24) score = 46;
|
||||
}
|
||||
score += Math.min(events.length, 3) * 3;
|
||||
if (status === 'Partial') score -= 8;
|
||||
return Math.max(0, Math.min(100, Math.round(score)));
|
||||
};
|
||||
|
||||
const renderEvents = (events) => {
|
||||
listEl.innerHTML = '';
|
||||
if (!Array.isArray(events) || events.length === 0) {
|
||||
emptyEl.style.display = 'block';
|
||||
countEl.textContent = 'No events';
|
||||
return;
|
||||
}
|
||||
emptyEl.style.display = 'none';
|
||||
countEl.textContent = `${events.length} ${events.length === 1 ? 'event' : 'events'}`;
|
||||
|
||||
for (const [index, event] of events.slice(0, maxEvents).entries()) {
|
||||
const item = document.createElement('li');
|
||||
item.style.padding = index === 0 ? '10px 0 0' : '10px 0 0';
|
||||
item.style.borderTop = '1px solid var(--theme-card-neutral-border)';
|
||||
|
||||
const timing = document.createElement('div');
|
||||
timing.style.fontSize = '0.76rem';
|
||||
timing.style.lineHeight = '1.2';
|
||||
timing.style.textTransform = 'uppercase';
|
||||
timing.style.letterSpacing = '0.05em';
|
||||
timing.style.color = 'var(--theme-card-neutral-muted)';
|
||||
timing.style.fontWeight = '700';
|
||||
const timeLabel = isAllDay(event.start, event.end) ? 'All day' : `${formatEventDay(event.start)} · ${formatTime(event.start)}`;
|
||||
timing.textContent = timeLabel;
|
||||
item.appendChild(timing);
|
||||
|
||||
const summary = document.createElement('div');
|
||||
summary.style.marginTop = '4px';
|
||||
summary.style.fontSize = '0.95rem';
|
||||
summary.style.lineHeight = '1.35';
|
||||
summary.style.color = 'var(--theme-card-neutral-text)';
|
||||
summary.style.fontWeight = '700';
|
||||
summary.textContent = String(event.summary || '(No title)');
|
||||
item.appendChild(summary);
|
||||
|
||||
listEl.appendChild(item);
|
||||
}
|
||||
};
|
||||
|
||||
const resolveToolName = async (configuredName, pattern, fallbackName) => {
|
||||
if (configuredName) return configuredName;
|
||||
if (!host.listTools) return fallbackName;
|
||||
try {
|
||||
const tools = await host.listTools();
|
||||
const tool = Array.isArray(tools)
|
||||
? tools.find((item) => pattern.test(String(item?.name || '')))
|
||||
: null;
|
||||
return tool?.name || fallbackName;
|
||||
} catch {
|
||||
return fallbackName;
|
||||
}
|
||||
};
|
||||
|
||||
const resolveCalendarNames = async (toolName) => {
|
||||
if (calendarNames.length > 0) return calendarNames;
|
||||
if (!host.listTools) return calendarNames;
|
||||
try {
|
||||
const tools = await host.listTools();
|
||||
const tool = Array.isArray(tools)
|
||||
? tools.find((item) => String(item?.name || '') === toolName)
|
||||
: null;
|
||||
const enumValues = Array.isArray(tool?.parameters?.properties?.calendar?.enum)
|
||||
? tool.parameters.properties.calendar.enum.map((value) => String(value || '').trim()).filter(Boolean)
|
||||
: [];
|
||||
return enumValues;
|
||||
} catch {
|
||||
return calendarNames;
|
||||
}
|
||||
};
|
||||
|
||||
const loadWeather = async (toolName) => {
|
||||
const toolResult = await host.callTool(toolName, {});
|
||||
const entries = parseLiveContextEntries(extractLiveContextText(toolResult)).filter((entry) => normalizeText(entry.domain) === 'sensor');
|
||||
const temperatureEntry = findEntry(entries, [
|
||||
temperatureName,
|
||||
`${weatherPrefix} Temperature`,
|
||||
]) || findByDeviceClass(entries, 'temperature');
|
||||
const apparentEntry = findEntry(entries, [
|
||||
apparentTemperatureName,
|
||||
`${weatherPrefix} Apparent temperature`,
|
||||
`${weatherPrefix} Feels like`,
|
||||
]);
|
||||
const aqiEntry = findEntry(entries, [
|
||||
aqiName,
|
||||
'Air quality index',
|
||||
]) || findByDeviceClass(entries, 'aqi');
|
||||
|
||||
return {
|
||||
toolName,
|
||||
temperature: parseNumericValue(temperatureEntry),
|
||||
temperatureUnit: String(temperatureEntry?.attributes?.unit_of_measurement || state.unit || '°F'),
|
||||
feelsLike: parseNumericValue(apparentEntry),
|
||||
feelsLikeUnit: String(apparentEntry?.attributes?.unit_of_measurement || temperatureEntry?.attributes?.unit_of_measurement || state.unit || '°F'),
|
||||
aqi: parseNumericValue(aqiEntry),
|
||||
};
|
||||
};
|
||||
|
||||
const loadEvents = async (toolName) => {
|
||||
const selectedCalendars = await resolveCalendarNames(toolName);
|
||||
if (!toolName) throw new Error('Calendar tool unavailable');
|
||||
if (!Array.isArray(selectedCalendars) || selectedCalendars.length === 0) {
|
||||
throw new Error('No calendars configured');
|
||||
}
|
||||
|
||||
const start = new Date();
|
||||
start.setHours(0, 0, 0, 0);
|
||||
const end = new Date(start);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
const endExclusiveTime = end.getTime() + 1;
|
||||
const allEvents = [];
|
||||
|
||||
for (const calendarName of selectedCalendars) {
|
||||
const toolResult = await host.callTool(toolName, {
|
||||
calendar: calendarName,
|
||||
range: 'today',
|
||||
});
|
||||
const events = extractEvents(toolResult);
|
||||
for (const event of events) {
|
||||
const startTime = eventTime(event?.start);
|
||||
if (startTime < start.getTime() || startTime >= endExclusiveTime) continue;
|
||||
allEvents.push({ ...event, _calendarName: calendarName });
|
||||
}
|
||||
}
|
||||
|
||||
allEvents.sort((left, right) => eventTime(left?.start) - eventTime(right?.start));
|
||||
return {
|
||||
toolName,
|
||||
calendarNames: selectedCalendars,
|
||||
events: allEvents.slice(0, maxEvents),
|
||||
};
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
const now = new Date();
|
||||
dateEl.textContent = formatHeaderDate(now);
|
||||
|
||||
const [weatherToolName, calendarToolName] = await Promise.all([
|
||||
resolveToolName(configuredWeatherToolName, /(^|_)GetLiveContext$/i, 'mcp_home_assistant_GetLiveContext'),
|
||||
resolveToolName(configuredCalendarToolName, /(^|_)calendar_get_events$/i, 'mcp_home_assistant_calendar_get_events'),
|
||||
]);
|
||||
|
||||
const [weatherResult, eventsResult] = await Promise.allSettled([
|
||||
loadWeather(weatherToolName),
|
||||
loadEvents(calendarToolName),
|
||||
]);
|
||||
|
||||
const snapshot = {
|
||||
kind: 'today_briefing',
|
||||
date_label: dateEl.textContent || null,
|
||||
weather_tool_name: weatherToolName || null,
|
||||
calendar_tool_name: calendarToolName || null,
|
||||
updated_at: null,
|
||||
status: null,
|
||||
weather: null,
|
||||
events: [],
|
||||
errors: {},
|
||||
};
|
||||
|
||||
let successCount = 0;
|
||||
|
||||
if (weatherResult.status === 'fulfilled') {
|
||||
successCount += 1;
|
||||
const weather = weatherResult.value;
|
||||
tempEl.textContent = Number.isFinite(weather.temperature) ? String(Math.round(weather.temperature)) : '--';
|
||||
unitEl.textContent = weather.temperatureUnit || '°F';
|
||||
feelsLikeEl.textContent = formatMetric(weather.feelsLike, weather.feelsLikeUnit);
|
||||
const aqiStyle = buildAqiStyle(weather.aqi);
|
||||
aqiChipEl.textContent = `${aqiStyle.tone} · ${aqiStyle.label}`;
|
||||
aqiChipEl.style.background = aqiStyle.background;
|
||||
aqiChipEl.style.color = aqiStyle.color;
|
||||
snapshot.weather = {
|
||||
temperature: Number.isFinite(weather.temperature) ? Math.round(weather.temperature) : null,
|
||||
temperature_unit: weather.temperatureUnit || null,
|
||||
feels_like: Number.isFinite(weather.feelsLike) ? Math.round(weather.feelsLike) : null,
|
||||
aqi: Number.isFinite(weather.aqi) ? Math.round(weather.aqi) : null,
|
||||
aqi_tone: aqiStyle.tone,
|
||||
};
|
||||
} else {
|
||||
tempEl.textContent = '--';
|
||||
unitEl.textContent = '°F';
|
||||
feelsLikeEl.textContent = '--';
|
||||
aqiChipEl.textContent = 'AQI unavailable';
|
||||
aqiChipEl.style.background = 'color-mix(in srgb, var(--theme-card-neutral-border) 65%, white)';
|
||||
aqiChipEl.style.color = 'var(--theme-card-neutral-subtle)';
|
||||
snapshot.errors.weather = String(weatherResult.reason);
|
||||
}
|
||||
|
||||
if (eventsResult.status === 'fulfilled') {
|
||||
successCount += 1;
|
||||
const eventsData = eventsResult.value;
|
||||
renderEvents(eventsData.events);
|
||||
snapshot.events = eventsData.events.map((event) => ({
|
||||
summary: String(event.summary || '(No title)'),
|
||||
start: normalizeDateValue(event.start) || null,
|
||||
end: normalizeDateValue(event.end) || null,
|
||||
all_day: isAllDay(event.start, event.end),
|
||||
}));
|
||||
} else {
|
||||
renderEvents([]);
|
||||
countEl.textContent = 'Unavailable';
|
||||
emptyEl.style.display = 'block';
|
||||
emptyEl.textContent = 'Calendar unavailable.';
|
||||
snapshot.errors.events = String(eventsResult.reason);
|
||||
}
|
||||
|
||||
const updatedText = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
updatedEl.textContent = updatedText;
|
||||
snapshot.updated_at = updatedText;
|
||||
|
||||
if (successCount === 2) {
|
||||
snapshot.status = 'Ready';
|
||||
} else if (successCount === 1) {
|
||||
snapshot.status = 'Partial';
|
||||
} else {
|
||||
snapshot.status = 'Unavailable';
|
||||
}
|
||||
|
||||
snapshot.score = computeTodayScore(snapshot.events, snapshot.status);
|
||||
|
||||
updateLiveContent(snapshot);
|
||||
};
|
||||
|
||||
host.setRefreshHandler(() => {
|
||||
void refresh();
|
||||
});
|
||||
void refresh();
|
||||
__setInterval(() => { void refresh(); }, refreshMs);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
host.setRefreshHandler(null);
|
||||
host.setLiveContent(null);
|
||||
host.clearSelection();
|
||||
for (const cleanup of __cleanup.splice(0)) cleanup();
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue