// 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 |.
)}
{tab === "regions" && (
Override auto-detected region codes for mis-tagged nodes.
{sub.nodes.slice(0, 12).map(n => (
))}
)}
{tab === "aliases" && (
)}
{tab === "tags" && (
Manually tag nodes — including the special residential tag.
{sub.nodes.slice(0, 12).map(n => (
))}
)}
{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;