From 50bc3819434ac5a21727b65f85fd24cb3da33c93 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Tue, 17 Mar 2026 00:11:25 -0500 Subject: [PATCH] add bookmarking clients, fix sorting issues, add sorting by country and only showing specific countries. --- .../public/assets/country-picker.js | 419 ++++++++++++++++++ Overlord-Server/public/assets/data.js | 1 + Overlord-Server/public/assets/main.js | 8 + Overlord-Server/public/assets/render.js | 39 +- Overlord-Server/public/assets/state.js | 1 + Overlord-Server/public/index.html | 3 + Overlord-Server/src/db.ts | 56 ++- .../src/server/routes/client-routes.ts | 42 +- Overlord-Server/src/types.ts | 1 + 9 files changed, 561 insertions(+), 9 deletions(-) create mode 100644 Overlord-Server/public/assets/country-picker.js diff --git a/Overlord-Server/public/assets/country-picker.js b/Overlord-Server/public/assets/country-picker.js new file mode 100644 index 0000000..f6a88b2 --- /dev/null +++ b/Overlord-Server/public/assets/country-picker.js @@ -0,0 +1,419 @@ +// Country data: code -> [name, continent] +const COUNTRIES = { + AF: ["Afghanistan", "Asia"], + AL: ["Albania", "Europe"], + DZ: ["Algeria", "Africa"], + AD: ["Andorra", "Europe"], + AO: ["Angola", "Africa"], + AG: ["Antigua & Barbuda", "North America"], + AR: ["Argentina", "South America"], + AM: ["Armenia", "Asia"], + AU: ["Australia", "Oceania"], + AT: ["Austria", "Europe"], + AZ: ["Azerbaijan", "Asia"], + BS: ["Bahamas", "North America"], + BH: ["Bahrain", "Asia"], + BD: ["Bangladesh", "Asia"], + BB: ["Barbados", "North America"], + BY: ["Belarus", "Europe"], + BE: ["Belgium", "Europe"], + BZ: ["Belize", "North America"], + BJ: ["Benin", "Africa"], + BT: ["Bhutan", "Asia"], + BO: ["Bolivia", "South America"], + BA: ["Bosnia & Herzegovina", "Europe"], + BW: ["Botswana", "Africa"], + BR: ["Brazil", "South America"], + BN: ["Brunei", "Asia"], + BG: ["Bulgaria", "Europe"], + BF: ["Burkina Faso", "Africa"], + BI: ["Burundi", "Africa"], + CV: ["Cabo Verde", "Africa"], + KH: ["Cambodia", "Asia"], + CM: ["Cameroon", "Africa"], + CA: ["Canada", "North America"], + CF: ["Central African Republic", "Africa"], + TD: ["Chad", "Africa"], + CL: ["Chile", "South America"], + CN: ["China", "Asia"], + CO: ["Colombia", "South America"], + KM: ["Comoros", "Africa"], + CG: ["Congo", "Africa"], + CD: ["Congo (DR)", "Africa"], + CR: ["Costa Rica", "North America"], + HR: ["Croatia", "Europe"], + CU: ["Cuba", "North America"], + CY: ["Cyprus", "Europe"], + CZ: ["Czechia", "Europe"], + DK: ["Denmark", "Europe"], + DJ: ["Djibouti", "Africa"], + DM: ["Dominica", "North America"], + DO: ["Dominican Republic", "North America"], + EC: ["Ecuador", "South America"], + EG: ["Egypt", "Africa"], + SV: ["El Salvador", "North America"], + GQ: ["Equatorial Guinea", "Africa"], + ER: ["Eritrea", "Africa"], + EE: ["Estonia", "Europe"], + SZ: ["Eswatini", "Africa"], + ET: ["Ethiopia", "Africa"], + FJ: ["Fiji", "Oceania"], + FI: ["Finland", "Europe"], + FR: ["France", "Europe"], + GA: ["Gabon", "Africa"], + GM: ["Gambia", "Africa"], + GE: ["Georgia", "Asia"], + DE: ["Germany", "Europe"], + GH: ["Ghana", "Africa"], + GR: ["Greece", "Europe"], + GD: ["Grenada", "North America"], + GT: ["Guatemala", "North America"], + GN: ["Guinea", "Africa"], + GW: ["Guinea-Bissau", "Africa"], + GY: ["Guyana", "South America"], + HT: ["Haiti", "North America"], + HN: ["Honduras", "North America"], + HU: ["Hungary", "Europe"], + IS: ["Iceland", "Europe"], + IN: ["India", "Asia"], + ID: ["Indonesia", "Asia"], + IR: ["Iran", "Asia"], + IQ: ["Iraq", "Asia"], + IE: ["Ireland", "Europe"], + IL: ["Israel", "Asia"], + IT: ["Italy", "Europe"], + JM: ["Jamaica", "North America"], + JP: ["Japan", "Asia"], + JO: ["Jordan", "Asia"], + KZ: ["Kazakhstan", "Asia"], + KE: ["Kenya", "Africa"], + KI: ["Kiribati", "Oceania"], + KP: ["North Korea", "Asia"], + KR: ["South Korea", "Asia"], + KW: ["Kuwait", "Asia"], + KG: ["Kyrgyzstan", "Asia"], + LA: ["Laos", "Asia"], + LV: ["Latvia", "Europe"], + LB: ["Lebanon", "Asia"], + LS: ["Lesotho", "Africa"], + LR: ["Liberia", "Africa"], + LY: ["Libya", "Africa"], + LI: ["Liechtenstein", "Europe"], + LT: ["Lithuania", "Europe"], + LU: ["Luxembourg", "Europe"], + MO: ["Macao", "Asia"], + MG: ["Madagascar", "Africa"], + MW: ["Malawi", "Africa"], + MY: ["Malaysia", "Asia"], + MV: ["Maldives", "Asia"], + ML: ["Mali", "Africa"], + MT: ["Malta", "Europe"], + MH: ["Marshall Islands", "Oceania"], + MR: ["Mauritania", "Africa"], + MU: ["Mauritius", "Africa"], + MX: ["Mexico", "North America"], + FM: ["Micronesia", "Oceania"], + MD: ["Moldova", "Europe"], + MC: ["Monaco", "Europe"], + MN: ["Mongolia", "Asia"], + ME: ["Montenegro", "Europe"], + MA: ["Morocco", "Africa"], + MZ: ["Mozambique", "Africa"], + MM: ["Myanmar", "Asia"], + NA: ["Namibia", "Africa"], + NR: ["Nauru", "Oceania"], + NP: ["Nepal", "Asia"], + NL: ["Netherlands", "Europe"], + NZ: ["New Zealand", "Oceania"], + NI: ["Nicaragua", "North America"], + NE: ["Niger", "Africa"], + NG: ["Nigeria", "Africa"], + MK: ["North Macedonia", "Europe"], + NO: ["Norway", "Europe"], + OM: ["Oman", "Asia"], + PK: ["Pakistan", "Asia"], + PW: ["Palau", "Oceania"], + PS: ["Palestine", "Asia"], + PA: ["Panama", "North America"], + PG: ["Papua New Guinea", "Oceania"], + PY: ["Paraguay", "South America"], + PE: ["Peru", "South America"], + PH: ["Philippines", "Asia"], + PL: ["Poland", "Europe"], + PT: ["Portugal", "Europe"], + QA: ["Qatar", "Asia"], + RO: ["Romania", "Europe"], + RU: ["Russia", "Europe"], + RW: ["Rwanda", "Africa"], + KN: ["Saint Kitts & Nevis", "North America"], + LC: ["Saint Lucia", "North America"], + VC: ["Saint Vincent & Grenadines", "North America"], + WS: ["Samoa", "Oceania"], + SM: ["San Marino", "Europe"], + ST: ["São Tomé & Príncipe", "Africa"], + SA: ["Saudi Arabia", "Asia"], + SN: ["Senegal", "Africa"], + RS: ["Serbia", "Europe"], + SC: ["Seychelles", "Africa"], + SL: ["Sierra Leone", "Africa"], + SG: ["Singapore", "Asia"], + SK: ["Slovakia", "Europe"], + SI: ["Slovenia", "Europe"], + SB: ["Solomon Islands", "Oceania"], + SO: ["Somalia", "Africa"], + ZA: ["South Africa", "Africa"], + SS: ["South Sudan", "Africa"], + ES: ["Spain", "Europe"], + LK: ["Sri Lanka", "Asia"], + SD: ["Sudan", "Africa"], + SR: ["Suriname", "South America"], + SE: ["Sweden", "Europe"], + CH: ["Switzerland", "Europe"], + SY: ["Syria", "Asia"], + TW: ["Taiwan", "Asia"], + TJ: ["Tajikistan", "Asia"], + TZ: ["Tanzania", "Africa"], + TH: ["Thailand", "Asia"], + TL: ["Timor-Leste", "Asia"], + TG: ["Togo", "Africa"], + TO: ["Tonga", "Oceania"], + TT: ["Trinidad & Tobago", "North America"], + TN: ["Tunisia", "Africa"], + TR: ["Turkey", "Asia"], + TM: ["Turkmenistan", "Asia"], + TV: ["Tuvalu", "Oceania"], + UG: ["Uganda", "Africa"], + UA: ["Ukraine", "Europe"], + AE: ["United Arab Emirates", "Asia"], + GB: ["United Kingdom", "Europe"], + US: ["United States", "North America"], + UY: ["Uruguay", "South America"], + UZ: ["Uzbekistan", "Asia"], + VU: ["Vanuatu", "Oceania"], + VE: ["Venezuela", "South America"], + VN: ["Vietnam", "Asia"], + YE: ["Yemen", "Asia"], + ZM: ["Zambia", "Africa"], + ZW: ["Zimbabwe", "Africa"], +}; + +const CONTINENT_ORDER = [ + "North America", + "South America", + "Europe", + "Africa", + "Asia", + "Oceania", +]; + +function flagHtml(code) { + if (!code) return ""; + const cc = String(code).toLowerCase(); + if (!/^[a-z]{2}$/.test(cc) || cc === "zz") return "🏴"; + return ``; +} + +export function initCountryPicker(onSelect) { + const anchor = document.getElementById("country-picker-anchor"); + if (!anchor) return; + + let currentCode = "all"; + let countryData = []; // [{ code, count }] + let panelOpen = false; + + const btn = document.createElement("button"); + btn.type = "button"; + btn.id = "country-picker-btn"; + btn.className = + "inline-flex items-center gap-2 bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-slate-100 hover:border-slate-600 transition-colors"; + btn.innerHTML = `All Countries`; + anchor.appendChild(btn); + + const panel = document.createElement("div"); + panel.id = "country-picker-panel"; + panel.className = + "hidden absolute z-[9999] mt-1 w-72 rounded-xl border border-slate-700 bg-slate-900 shadow-2xl flex flex-col overflow-hidden"; + panel.style.maxHeight = "420px"; + + const searchWrap = document.createElement("div"); + searchWrap.className = "p-2 border-b border-slate-800 flex-shrink-0"; + searchWrap.innerHTML = `
`; + panel.appendChild(searchWrap); + + const list = document.createElement("div"); + list.className = "overflow-y-auto flex-1"; + panel.appendChild(list); + + document.body.appendChild(panel); + + function positionPanel() { + const rect = btn.getBoundingClientRect(); + panel.style.top = `${rect.bottom + window.scrollY + 4}px`; + panel.style.left = `${rect.left + window.scrollX}px`; + } + + function openPanel() { + panelOpen = true; + panel.classList.remove("hidden"); + positionPanel(); + setTimeout(() => { + const inp = panel.querySelector("#country-search"); + if (inp) inp.focus(); + }, 0); + } + + function closePanel() { + panelOpen = false; + panel.classList.add("hidden"); + const inp = panel.querySelector("#country-search"); + if (inp) inp.value = ""; + renderList(""); + } + + btn.addEventListener("click", (e) => { + e.stopPropagation(); + if (panelOpen) { + closePanel(); + } else { + openPanel(); + } + }); + + document.addEventListener("click", (e) => { + if (!panelOpen) return; + if (!panel.contains(e.target) && e.target !== btn) { + closePanel(); + } + }); + + window.addEventListener("resize", () => { + if (panelOpen) positionPanel(); + }); + + const searchInput = searchWrap.querySelector("#country-search"); + searchInput.addEventListener("input", (e) => { + renderList(e.target.value.trim().toLowerCase()); + }); + + function setSelected(code, label, flagMarkup) { + currentCode = code; + const labelEl = document.getElementById("country-picker-label"); + if (labelEl) { + if (code === "all") { + labelEl.innerHTML = "All Countries"; + } else { + labelEl.innerHTML = `${flagMarkup} ${label}`; + } + } + onSelect(code); + closePanel(); + } + + function renderList(query) { + list.innerHTML = ""; + + const presentCodes = new Set(countryData.map((d) => d.code)); + const countMap = Object.fromEntries(countryData.map((d) => [d.code, d.count])); + + const allItem = makeItem( + ``, + "All Countries", + "", + "all", + null, + currentCode === "all", + ); + if (!query) list.appendChild(allItem); + + const continentGroups = {}; + + for (const code of presentCodes) { + if (code === "ZZ") continue; + const info = COUNTRIES[code]; + const name = info ? info[0] : code; + const continent = info ? info[1] : "Other"; + + if (query && !name.toLowerCase().includes(query) && !code.toLowerCase().includes(query)) continue; + + if (!continentGroups[continent]) continentGroups[continent] = []; + continentGroups[continent].push({ code, name, count: countMap[code] || 0 }); + } + + const continentKeys = Object.keys(continentGroups).sort((a, b) => { + const ai = CONTINENT_ORDER.indexOf(a); + const bi = CONTINENT_ORDER.indexOf(b); + if (ai === -1 && bi === -1) return a.localeCompare(b); + if (ai === -1) return 1; + if (bi === -1) return -1; + return ai - bi; + }); + + for (const continent of continentKeys) { + const countries = continentGroups[continent].sort((a, b) => + a.name.localeCompare(b.name), + ); + + const header = document.createElement("div"); + header.className = "px-3 py-1.5 text-xs font-semibold uppercase tracking-wider text-slate-500 bg-slate-950/50 sticky top-0"; + header.textContent = continent; + list.appendChild(header); + + for (const { code, name, count } of countries) { + const flag = flagHtml(code); + const item = makeItem(flag, name, code, code, count, currentCode === code); + list.appendChild(item); + } + } + + if (list.children.length === 0 || (list.children.length === 1 && !query)) { + const empty = document.createElement("div"); + empty.className = "px-4 py-6 text-center text-slate-500 text-sm"; + empty.textContent = query ? "No matching countries" : "No country data yet"; + list.appendChild(empty); + } + } + + function makeItem(flagMarkup, name, code, value, count, active) { + const el = document.createElement("button"); + el.type = "button"; + el.className = `w-full text-left flex items-center gap-3 px-3 py-2 text-sm hover:bg-slate-800 transition-colors ${active ? "bg-slate-800/80 text-blue-300" : "text-slate-200"}`; + + const flagSpan = document.createElement("span"); + flagSpan.className = "w-6 text-center flex-shrink-0 text-base"; + flagSpan.innerHTML = flagMarkup; + + const nameSpan = document.createElement("span"); + nameSpan.className = "flex-1 truncate"; + nameSpan.textContent = name; + + el.appendChild(flagSpan); + el.appendChild(nameSpan); + + if (count !== null) { + const badge = document.createElement("span"); + badge.className = "text-xs text-slate-400 font-mono flex-shrink-0"; + badge.textContent = count; + el.appendChild(badge); + } + + el.addEventListener("click", () => setSelected(value, name, flagMarkup)); + return el; + } + + async function loadCountries() { + try { + const res = await fetch("/api/clients/countries"); + if (!res.ok) return; + const data = await res.json(); + countryData = Array.isArray(data.countries) ? data.countries : []; + renderList(""); + } catch (err) { + console.error("country-picker: failed to load countries", err); + } + } + + loadCountries(); + setInterval(loadCountries, 30000); + + renderList(""); +} diff --git a/Overlord-Server/public/assets/data.js b/Overlord-Server/public/assets/data.js index e5d9073..0e127e6 100644 --- a/Overlord-Server/public/assets/data.js +++ b/Overlord-Server/public/assets/data.js @@ -25,6 +25,7 @@ export async function loadWithOptions(options = {}) { sort: state.sort, status: state.filterStatus || "all", os: state.filterOs || "all", + country: state.filterCountry || "all", }); const res = await fetch(`/api/clients?${params.toString()}`); if (!res.ok) throw new Error(`status ${res.status}`); diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index aec08f1..d823983 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -10,6 +10,7 @@ import { requestPreview, requestThumbnail, } from "./data.js"; +import { initCountryPicker } from "./country-picker.js"; const grid = document.getElementById("grid"); const totalPill = document.getElementById("total-pill"); @@ -445,6 +446,13 @@ filterOsSelect?.addEventListener("change", (e) => { loadWithOptions({ force: true }); }); +initCountryPicker((code) => { + state.filterCountry = code; + state.page = 1; + state.lastDigest = ""; + loadWithOptions({ force: true }); +}); + showOfflineToggle?.addEventListener("change", (e) => { if (e.target.checked) { state.filterStatus = lastNonOnlineStatus || "all"; diff --git a/Overlord-Server/public/assets/render.js b/Overlord-Server/public/assets/render.js index 4b4d7cd..a4d9482 100644 --- a/Overlord-Server/public/assets/render.js +++ b/Overlord-Server/public/assets/render.js @@ -212,6 +212,7 @@ export function createRenderer({ card.dataset.os = String(client.os || "").toLowerCase(); card.dataset.nickname = String(client.nickname || ""); card.dataset.customTag = String(client.customTag || ""); + card.dataset.bookmarked = String(!!client.bookmarked); card._customTagNote = String(client.customTagNote || ""); const os = osBadge(client.os || "unknown"); const arch = archBadge(client.arch || ""); @@ -226,7 +227,7 @@ export function createRenderer({ const hasTagNote = customTag.length > 0 && customTagNote.length > 0; const isTagNoteExpanded = hasTagNote && wasTagNoteExpanded; card.dataset.tagNoteExpanded = isTagNoteExpanded ? "true" : "false"; - card.className = `card rounded-xl border border-slate-800 bg-slate-900/70 p-4 shadow-lg ${client.online ? "" : "card-offline"} tone-${os.tone}`; + card.className = `card rounded-xl border ${client.bookmarked ? "border-yellow-600/60" : "border-slate-800"} bg-slate-900/70 p-4 shadow-lg ${client.online ? "" : "card-offline"} tone-${os.tone}`; const cardThumb = client.thumbnail ? (() => { const wrapper = document.createElement("div"); @@ -278,6 +279,7 @@ export function createRenderer({
+ ${formatPing(client.pingMs)} ${isViewer ? "" : ``} ${isViewer ? "" : ``} @@ -323,6 +325,41 @@ export function createRenderer({ }); } + card.querySelector(".bookmark-btn")?.addEventListener("click", async (e) => { + e.stopPropagation(); + const btn = e.currentTarget; + const id = btn.dataset.id; + const isBookmarked = card.dataset.bookmarked === "true"; + try { + const res = await fetch(`/api/clients/${id}/bookmark`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ bookmarked: !isBookmarked }), + }); + if (res.ok) { + card.dataset.bookmarked = String(!isBookmarked); + const icon = btn.querySelector("i"); + if (!isBookmarked) { + btn.classList.remove("border-slate-700", "bg-slate-800/50", "text-slate-500"); + btn.classList.add("border-yellow-600", "bg-yellow-900/50", "text-yellow-300"); + card.classList.remove("border-slate-800"); + card.classList.add("border-yellow-600/60"); + if (icon) { icon.classList.remove("fa-regular"); icon.classList.add("fa-solid"); } + btn.title = "Remove bookmark"; + } else { + btn.classList.remove("border-yellow-600", "bg-yellow-900/50", "text-yellow-300"); + btn.classList.add("border-slate-700", "bg-slate-800/50", "text-slate-500"); + card.classList.remove("border-yellow-600/60"); + card.classList.add("border-slate-800"); + if (icon) { icon.classList.remove("fa-solid"); icon.classList.add("fa-regular"); } + btn.title = "Bookmark"; + } + } + } catch (err) { + console.error("bookmark toggle failed", err); + } + }); + card.querySelector(".client-tag-toggle")?.addEventListener("click", (e) => { e.stopPropagation(); const notePanel = card.querySelector(".client-tag-note"); diff --git a/Overlord-Server/public/assets/state.js b/Overlord-Server/public/assets/state.js index 35fe762..843bcc9 100644 --- a/Overlord-Server/public/assets/state.js +++ b/Overlord-Server/public/assets/state.js @@ -5,6 +5,7 @@ export const state = { sort: "last_seen_desc", filterStatus: "all", filterOs: "all", + filterCountry: "all", lastDigest: "", isLoading: false, pendingForce: false, diff --git a/Overlord-Server/public/index.html b/Overlord-Server/public/index.html index 3807e62..5f921f4 100644 --- a/Overlord-Server/public/index.html +++ b/Overlord-Server/public/index.html @@ -87,6 +87,7 @@ > +