// Subscriptions const { Modal, Drawer, Toggle, RegionPill, useToast } = window.UI; function PageSubscriptions({ state, dispatch }) { const { pools } = state; const subs = pools.filter(p => p.source === "subscription"); const [addOpen, setAddOpen] = React.useState(false); const [openSub, setOpenSub] = React.useState(null); const toast = useToast(); return (

Subscriptions

Auto-fetched node sources. Region is detected from each node name; override or reject as needed.

{subs.map(s => { const total = s.nodes.length; const online = s.nodes.filter(n => n.alive && n.enabled).length; const regions = Array.from(new Set(s.nodes.map(n => n.region))).filter(Boolean); return (

{s.name}

{s.residential && residential}
refresh {s.update_interval} · last {s.last_updated} dispatch({type:"togglePool", name:s.name})}/>
Source
{s.url}
{s.last_error &&
{s.last_error}
}
Nodes online
{online}/{total}
Region groups
{regions.map(r => )}
Reject filters
{(s.reject_regex || []).slice(0,3).map((r,i) => {r})} {!(s.reject_regex || []).length && none}
Nodes {s.nodes.slice(0, 14).map(n => ( {n.name} ))} {s.nodes.length > 14 && +{s.nodes.length - 14}}
); })}
setOpenSub(null)} title={openSub?.name || ""}> {openSub && setOpenSub(null)} toast={toast}/>} setAddOpen(false)} dispatch={dispatch} toast={toast}/>
); } function SubEditor({ sub, dispatch, onClose, toast }) { const sourceForm = (pool) => ({ name: pool.name || "", url: pool.url || "", update_interval: pool.update_interval || "1h", residential: !!pool.residential, }); const regionsFromNodes = (nodes) => Object.fromEntries((nodes || []).map(n => [n.name, n.region || ""])); const tagsFromNodes = (nodes) => Object.fromEntries((nodes || []).map(n => [n.name, (n.tags || []).join(", ")])); const [tab, setTab] = React.useState("source"); const [source, setSource] = React.useState(() => sourceForm(sub)); const [regionDraft, setRegionDraft] = React.useState(() => regionsFromNodes(sub.nodes)); const [tagDraft, setTagDraft] = React.useState(() => tagsFromNodes(sub.nodes)); const [aliasNode, setAliasNode] = React.useState(sub.nodes[0]?.name || ""); const [alias, setAlias] = React.useState(sub.nodes[0]?.alias || ""); const [reject, setReject] = React.useState((sub.reject_regex || []).join("\n")); React.useEffect(() => { setTab("source"); setSource(sourceForm(sub)); setRegionDraft(regionsFromNodes(sub.nodes)); setTagDraft(tagsFromNodes(sub.nodes)); setAliasNode(sub.nodes[0]?.name || ""); setAlias(sub.nodes[0]?.alias || ""); setReject((sub.reject_regex || []).join("\n")); }, [sub.name]); const setSourceField = (key, value) => setSource(v => ({ ...v, [key]: value })); const tabs = [ {value:"source", label:"Source"}, {value:"regions", label:"Region overrides"}, {value:"aliases", label:"Aliases"}, {value:"tags", label:"Tags"}, {value:"reject", label:"Reject regex"}, ]; return ( <>
{tabs.map(t => ( ))}
{tab === "source" && (
setSourceField("name", e.target.value)} placeholder="airport-a"/>
Multiple URLs separated by newline, comma or |.
setSourceField("update_interval", e.target.value)} placeholder="1h"/>
setSourceField("residential", v)} label="Residential pool"/>
)} {tab === "regions" && (
Override auto-detected region codes for mis-tagged nodes.
{sub.nodes.slice(0, 12).map(n => (
{n.name}
setRegionDraft(v => ({...v, [n.name]: e.target.value.toUpperCase().slice(0,2)}))} maxLength={2} style={{width:64}}/>
))}
)} {tab === "aliases" && (
Map an alias to a specific node so callers can pin to it.
setAlias(e.target.value)}/>
)} {tab === "tags" && (
Manually tag nodes — including the special residential tag.
{sub.nodes.slice(0, 12).map(n => (
{n.name}
setTagDraft(v => ({...v, [n.name]: e.target.value}))} placeholder="streaming, residential" style={{width:200}}/>
))}
)} {tab === "reject" && (
Nodes whose names match any expression are excluded from the pool.
)} ); } function AddSubscriptionModal({ open, onClose, dispatch, toast }) { const [form, setForm] = React.useState({ name: "", url: "", update_interval: "1h", residential: false, reject_regex: "", }); const set = (k, v) => setForm(f => ({ ...f, [k]: v })); const submit = async () => { if (!form.name.trim() || !form.url.trim()) { toast("Pool name and URL are required"); return; } const ok = await dispatch({type:"addPool", name: form.name.trim(), config:{ source: "subscription", enabled: true, url: form.url.trim(), update_interval: form.update_interval, residential: form.residential, reject_regex: form.reject_regex.split("\n").map(s => s.trim()).filter(Boolean), }}); if (ok) onClose(); }; return ( }>
set("name", e.target.value)} placeholder="airport-c"/>
Multiple URLs separated by newline, comma or |. Failures don't block working sources.
set("residential", v)} label="Residential pool"/>
); } window.PageSubscriptions = PageSubscriptions;