function clampInt(value, min, max, fallback) {
if (value === null || value === undefined || String(value).trim() === "") return fallback
var n = Number(value)
return isFinite(n) ? Math.max(min, Math.min(max, Math.round(n))) : fallback
}
function normalizeHours(value) { return clampInt(value, 1, 24, 3) }
function normalizeAlertPercent(value) { return clampInt(value, 1, 100, 50) }
function normalizeAlertHours(value) { return clampInt(value, 1, 168, 24) }
function choices(presets, current, label) {
var values = presets.slice()
if (values.indexOf(current) < 0) values.push(current)
values.sort(function(a, b) { return a - b })
return values.map(function(v) { return { value: String(v), label: label(v) } })
}
function reasonKey(row) {
var key = String((row && (row.reason_code || row.status)) || "unknown")
return ["priority", "on_track", "blocked", "unavailable", "unknown"].indexOf(key) >= 0 ? key : "unknown"
}
function stateKey(row) {
return row && row.urgent === true && row.status === "priority" ? "urgent" : reasonKey({status: row && row.status})
}
function duration(hours, tr) {
var h = Math.max(0, Math.round(hours))
return h < 24 ? tr("priority.hours", { hours: h })
: tr("priority.days", { days: Math.floor(h / 24), hours: h % 24 })
}
function summary(row, nowMs, tr) {
if (!hasForecast(row)) return tr("priority.reason." + reasonKey(row))
var hours = (Date.parse(row.reset_at) - nowMs) / 3600000
return tr("priority.summary", { unused: num(row.unused_percent, tr), left: num(row.remaining_percent, tr), when: duration(hours, tr) })
}
function hasForecast(row) {
return !!(row && row.unused_percent !== null && row.unused_percent !== undefined && row.reset_at)
}
function num(value, tr) {
var n = Math.round(Number(value) * 10) / 10
if (!isFinite(n)) return "–"
var text = n === Math.round(n) ? String(Math.round(n)) : n.toFixed(1)
return text.replace(".", tr("priority.decimal"))
}
function rate(value, tr) { return tr("priority.rate", { rate: num(value, tr) }) }
function facts(row, nowMs, historyAvailable, tr) {
if (!hasForecast(row)) return []
var when = duration((Date.parse(row.reset_at) - nowMs) / 3600000, tr)
var measured = row.recent_percent_per_hour !== null && row.recent_percent_per_hour !== undefined
return [
{ label: tr("priority.fact.window"), value: tr("priority.window_value", { window: String(row.window || ""), when: when }) },
{ label: tr("priority.fact.left"), value: tr("priority.percent", { value: num(row.remaining_percent, tr) }) },
{ label: tr("priority.fact.unused"), value: tr("priority.percent", { value: num(row.unused_percent, tr) }), strong: true },
{ label: tr("priority.fact.pace"), value: rate(row.pace_percent_per_hour, tr) },
{ label: measured ? tr("priority.fact.recent", { hours: num(row.recent_hours, tr) }) : tr("priority.fact.recent_none"),
value: measured ? rate(row.recent_percent_per_hour, tr) : tr(historyAvailable ? "priority.collecting" : "priority.history_error") },
{ label: tr("priority.fact.cycle"), value: rate(row.cycle_percent_per_hour, tr) },
{ label: tr("priority.fact.needed"), value: rate(row.required_percent_per_hour, tr) }
]
}
function notes(row, nowMs, tr) {
if (!row) return []
var out = [tr("priority.reason." + reasonKey(row))]
if (stateKey(row) === "urgent")
out.push(tr("priority.urgent_help", { unused: row.alert_unused_percent, hours: row.alert_hours }))
if (row.session_exhausted === true) out.push(tr("priority.session_exhausted"))
if (row.stale === true && row.fetched_at)
out.push(tr("priority.stale", { when: duration((nowMs - Date.parse(row.fetched_at)) / 3600000, tr) }))
return out
}