Initial commit

This commit is contained in:
2026-06-30 18:30:24 +00:00
commit 67e0a90b15
54 changed files with 6184 additions and 0 deletions
Binary file not shown.
+62
View File
@@ -0,0 +1,62 @@
from fastapi import FastAPI, HTTPException, Form
from fastapi.responses import HTMLResponse
import os, json, uuid
from datetime import datetime
DATA = "/data/freqs.json"
app = FastAPI(redirect_slashes=False)
def load():
if os.path.exists(DATA):
with open(DATA) as f: return json.load(f)
return {}
def save(d):
with open(DATA, "w") as f: json.dump(d, f, indent=2)
@app.get("/api/list")
def list_freqs():
items = load()
return sorted(
[{"id": k, **v} for k, v in items.items()],
key=lambda x: (x.get("band", ""), float(x.get("freq", 0) or 0))
)
@app.post("/api/add")
def add(freq: str = Form(""), name: str = Form(""), mode: str = Form(""),
band: str = Form(""), tone: str = Form(""), offset: str = Form(""),
description: str = Form(""), tag: str = Form("")):
items = load()
fid = uuid.uuid4().hex[:12]
items[fid] = {
"freq": freq, "name": name, "mode": mode, "band": band,
"tone": tone, "offset": offset, "description": description,
"tag": tag, "added": datetime.utcnow().isoformat()
}
save(items)
return {"id": fid}
@app.patch("/api/update/{fid}")
def update(fid: str, freq: str = Form(None), name: str = Form(None),
mode: str = Form(None), band: str = Form(None), tone: str = Form(None),
offset: str = Form(None), description: str = Form(None), tag: str = Form(None)):
items = load()
if fid not in items: raise HTTPException(404)
for k, v in {"freq": freq, "name": name, "mode": mode, "band": band,
"tone": tone, "offset": offset, "description": description, "tag": tag}.items():
if v is not None: items[fid][k] = v
save(items)
return {"ok": True}
@app.delete("/api/delete/{fid}")
def delete(fid: str):
items = load()
if fid not in items: raise HTTPException(404)
del items[fid]
save(items)
return {"ok": True}
@app.get("/", response_class=HTMLResponse)
def root():
with open("/static/index.html") as f:
return f.read()
+409
View File
@@ -0,0 +1,409 @@
{
"dca821d3b8ce": {
"freq": "162.400",
"name": "NOAA Weather Radio - Fort Worth (Primary)",
"mode": "FM",
"band": "NOAA",
"tone": "",
"offset": "N/A",
"description": "Primary NOAA WX freq for Ellis County. Channel 1.",
"tag": "noaa,weather",
"added": "2026-06-30T05:10:57.809929"
},
"268c6f32a65e": {
"freq": "162.550",
"name": "NOAA Weather Radio - Fort Worth (Backup)",
"mode": "FM",
"band": "NOAA",
"tone": "",
"offset": "N/A",
"description": "Secondary NOAA WX freq for Ellis County. Channel 7.",
"tag": "noaa,weather",
"added": "2026-06-30T05:10:57.817404"
},
"040dd4d93450": {
"freq": "162.525",
"name": "NOAA Weather Radio - Fort Worth (Tertiary)",
"mode": "FM",
"band": "NOAA",
"tone": "",
"offset": "N/A",
"description": "Tertiary NOAA WX freq for Ellis County. Channel 6.",
"tag": "noaa,weather",
"added": "2026-06-30T05:10:57.825125"
},
"5273ebab24fd": {
"freq": "145.410",
"name": "WD5DDH SKYWARN - Midlothian",
"mode": "FM",
"band": "VHF-2m",
"tone": "110.9 PL",
"offset": "-0.6",
"description": "Ellis County ARC SKYWARN repeater. Severe weather net.",
"tag": "repeater,skywarn",
"added": "2026-06-30T05:10:57.832547"
},
"7d283c84981d": {
"freq": "145.410",
"name": "WD5DDH SKYWARN - Waxahachie",
"mode": "FM",
"band": "VHF-2m",
"tone": "162.2 PL",
"offset": "-0.6",
"description": "Ellis County ARC SKYWARN repeater. Severe weather net.",
"tag": "repeater,skywarn",
"added": "2026-06-30T05:10:57.840343"
},
"89f140dc63b9": {
"freq": "145.410",
"name": "WD5DDH SKYWARN - Ennis",
"mode": "FM",
"band": "VHF-2m",
"tone": "131.8 PL",
"offset": "-0.6",
"description": "Ellis County ARC repeater - Ennis side, severe weather net",
"tag": "repeater,skywarn",
"added": "2026-06-30T05:19:55.454888"
},
"4a88dc83e6f1": {
"freq": "441.650",
"name": "WD5DDH UHF - Waxahachie",
"mode": "FM",
"band": "UHF-70cm",
"tone": "110.9 PL",
"offset": "+5",
"description": "Ellis County ARC UHF repeater, Waxahachie area",
"tag": "repeater,club",
"added": "2026-06-30T05:19:55.468042"
},
"a9b79a80d1bf": {
"freq": "442.525",
"name": "WD5DDH - Italy",
"mode": "FM",
"band": "UHF-70cm",
"tone": "88.5 PL",
"offset": "+5",
"description": "Ellis County ARC UHF repeater, Italy water tower",
"tag": "repeater,club",
"added": "2026-06-30T05:19:55.479919"
},
"205864b19829": {
"freq": "145.250",
"name": "KD5OL - Palmer",
"mode": "FM",
"band": "VHF-2m",
"tone": "100.0 PL",
"offset": "-0.6",
"description": "Yaesu Fusion / WIRES-X, Palmer TX",
"tag": "repeater,fusion",
"added": "2026-06-30T05:19:55.491816"
},
"3380bd95ef1d": {
"freq": "146.880",
"name": "W5FC DARC - Dallas",
"mode": "FM",
"band": "VHF-2m",
"tone": "110.9 PL",
"offset": "-0.6",
"description": "Dallas Amateur Radio Club - most active 2m in DFW",
"tag": "repeater,dfw",
"added": "2026-06-30T05:19:55.501171"
},
"13a4ed10d6c6": {
"freq": "442.425",
"name": "W5FC Fusion - Dallas",
"mode": "FM",
"band": "UHF-70cm",
"tone": "110.9 PL",
"offset": "+5",
"description": "DARC Yaesu Fusion AMS, WIRES-X linked",
"tag": "repeater,fusion",
"added": "2026-06-30T05:19:55.511745"
},
"e9889d52ba8b": {
"freq": "145.110",
"name": "K5FTW - Fort Worth",
"mode": "FM",
"band": "VHF-2m",
"tone": "110.9 PL",
"offset": "-0.6",
"description": "Fort Worth area - very popular, wide coverage",
"tag": "repeater,dfw",
"added": "2026-06-30T05:19:55.524403"
},
"c70278ca1638": {
"freq": "146.940",
"name": "K5FTW - Fort Worth RACES",
"mode": "FM",
"band": "VHF-2m",
"tone": "110.9 PL",
"offset": "-0.6",
"description": "Tarrant County RACES nightly net",
"tag": "repeater,races",
"added": "2026-06-30T05:19:55.536858"
},
"1d31637df8e1": {
"freq": "146.720",
"name": "WA5CKF - Irving",
"mode": "FM",
"band": "VHF-2m",
"tone": "110.9 PL",
"offset": "-0.6",
"description": "DFW Traffic Nets nightly, Irving",
"tag": "repeater,nets",
"added": "2026-06-30T05:19:55.544339"
},
"aca9862288c9": {
"freq": "147.120",
"name": "K5RWK - Richardson",
"mode": "FM",
"band": "VHF-2m",
"tone": "110.9 PL",
"offset": "+0.6",
"description": "Richardson Wireless Klub AllStar linked",
"tag": "repeater,allstar",
"added": "2026-06-30T05:19:55.551990"
},
"51c0947ec40f": {
"freq": "462.650",
"name": "WRXB290 - Waxahachie GMRS",
"mode": "FM",
"band": "GMRS",
"tone": "131.8 PL",
"offset": "+5",
"description": "Ellis County REACT GMRS - east of Waxahachie",
"tag": "gmrs,repeater",
"added": "2026-06-30T05:19:55.564920"
},
"32dd8b2b6d67": {
"freq": "462.625",
"name": "WRVB840 - Nash/Forreston GMRS",
"mode": "FM",
"band": "GMRS",
"tone": "131.8 PL",
"offset": "+5",
"description": "Ellis County REACT GMRS - Nash/Forreston",
"tag": "gmrs,repeater",
"added": "2026-06-30T05:19:55.577459"
},
"1f69a5691fba": {
"freq": "462.700",
"name": "WRWT247 - Palmer GMRS",
"mode": "FM",
"band": "GMRS",
"tone": "131.8 PL",
"offset": "+5",
"description": "Ellis County REACT GMRS - Palmer area",
"tag": "gmrs,repeater",
"added": "2026-06-30T05:19:55.585068"
},
"a1542dbbe73c": {
"freq": "462.675",
"name": "GMRS Emergency / Travel Calling",
"mode": "FM",
"band": "GMRS",
"tone": "141.3 PL",
"offset": "",
"description": "GMRS channel 20 - standard emergency/travel calling freq",
"tag": "calling,emergency",
"added": "2026-06-30T05:19:55.592761"
},
"b48d9242feb2": {
"freq": "151.940",
"name": "MURS Channel 3",
"mode": "FM",
"band": "MURS",
"tone": "",
"offset": "",
"description": "MURS no-license-required VHF simplex",
"tag": "simplex,unlicensed",
"added": "2026-06-30T05:19:55.600318"
},
"582cd42a42bd": {
"freq": "154.600",
"name": "MURS Channel 5 (Blue Dot)",
"mode": "FM",
"band": "MURS",
"tone": "",
"offset": "",
"description": "MURS Blue Dot - commonly used Walmart channel",
"tag": "simplex,unlicensed",
"added": "2026-06-30T05:19:55.608153"
},
"076de1dd8e31": {
"freq": "27.065",
"name": "CB Channel 9 - Emergency",
"mode": "AM",
"band": "CB",
"tone": "",
"offset": "",
"description": "CB emergency / motorist assistance channel",
"tag": "calling,emergency",
"added": "2026-06-30T05:19:55.615832"
},
"d1f88010eaed": {
"freq": "27.185",
"name": "CB Channel 19 - Truckers",
"mode": "AM",
"band": "CB",
"tone": "",
"offset": "",
"description": "CB truckers channel - highway info",
"tag": "simplex,trucker",
"added": "2026-06-30T05:19:55.623960"
},
"135625984e19": {
"freq": "155.580",
"name": "Ellis County Fire Dispatch",
"mode": "FM",
"band": "Public Safety",
"tone": "77.0 PL",
"offset": "",
"description": "Countywide fire dispatch frequency",
"tag": "scanner,fire",
"added": "2026-06-30T05:19:55.631683"
},
"0cf09960a8dc": {
"freq": "159.3675",
"name": "Ellis County EMS (AMR)",
"mode": "FM",
"band": "Public Safety",
"tone": "532 DPL",
"offset": "",
"description": "Ellis County EMS dispatch (operated by AMR)",
"tag": "scanner,ems",
"added": "2026-06-30T05:19:55.643258"
},
"b12945bfdc82": {
"freq": "155.040",
"name": "Ennis Police Dispatch",
"mode": "FM",
"band": "Public Safety",
"tone": "466 DPL",
"offset": "",
"description": "Ennis PD dispatch",
"tag": "scanner,police",
"added": "2026-06-30T05:19:55.651695"
},
"c2e37fee36b4": {
"freq": "155.310",
"name": "Glenn Heights Police",
"mode": "FM",
"band": "Public Safety",
"tone": "151.4 PL",
"offset": "",
"description": "Glenn Heights PD analog dispatch",
"tag": "scanner,police",
"added": "2026-06-30T05:19:55.659366"
},
"9bb2eaa50fbd": {
"freq": "155.880",
"name": "Ferris Police/Fire",
"mode": "FM",
"band": "Public Safety",
"tone": "94.8 PL",
"offset": "",
"description": "Ferris combined police/fire dispatch",
"tag": "scanner,multi",
"added": "2026-06-30T05:19:55.667174"
},
"a9ea8248551c": {
"freq": "151.0175",
"name": "Palmer Police",
"mode": "FM",
"band": "Public Safety",
"tone": "343 DPL",
"offset": "",
"description": "Palmer PD dispatch",
"tag": "scanner,police",
"added": "2026-06-30T05:19:55.675102"
},
"b808d11a5903": {
"freq": "151.460",
"name": "Midlothian Fire Dispatch",
"mode": "FM",
"band": "Public Safety",
"tone": "",
"offset": "",
"description": "Midlothian Fire (legacy VHF channel)",
"tag": "scanner,fire",
"added": "2026-06-30T05:19:55.682738"
},
"73434d455c05": {
"freq": "155.790",
"name": "Waxahachie Police (legacy)",
"mode": "FM",
"band": "Public Safety",
"tone": "",
"offset": "",
"description": "Waxahachie PD legacy VHF dispatch",
"tag": "scanner,police",
"added": "2026-06-30T05:19:55.690597"
},
"3e03b296356d": {
"freq": "462.675",
"name": "FRS Channel 1 (call)",
"mode": "FM",
"band": "FRS",
"tone": "",
"offset": "",
"description": "FRS channel 1 - common family/group use",
"tag": "simplex,family",
"added": "2026-06-30T05:19:55.698509"
},
"3404401304c4": {
"freq": "446.000",
"name": "2m Calling Frequency",
"mode": "FM",
"band": "UHF-70cm",
"tone": "",
"offset": "",
"description": "National 70cm FM simplex calling frequency",
"tag": "simplex,calling",
"added": "2026-06-30T05:19:55.706494"
},
"bc9a34941d2c": {
"freq": "146.520",
"name": "2m Calling Frequency",
"mode": "FM",
"band": "VHF-2m",
"tone": "",
"offset": "",
"description": "National 2m FM simplex calling frequency",
"tag": "simplex,calling",
"added": "2026-06-30T05:19:55.714369"
},
"7b59114a2767": {
"freq": "14.300",
"name": "Maritime Mobile Service Net",
"mode": "SSB-USB",
"band": "HF-20m",
"tone": "",
"offset": "",
"description": "Daily maritime/emergency traffic net, USB",
"tag": "hf,emergency",
"added": "2026-06-30T05:19:55.722718"
},
"b82361198ff7": {
"freq": "7.268",
"name": "HF SATERN Net",
"mode": "SSB-LSB",
"band": "HF-40m",
"tone": "",
"offset": "",
"description": "Salvation Army Team Emergency Radio Network, LSB",
"tag": "hf,emergency",
"added": "2026-06-30T05:19:55.730777"
},
"b836211376cd": {
"freq": "904.625",
"name": "Meshtastic LongFast (US)",
"mode": "LoRa",
"band": "Mesh",
"tone": "",
"offset": "",
"description": "Default Meshtastic channel for North America",
"tag": "mesh,data",
"added": "2026-06-30T05:19:55.739037"
}
}
+201
View File
@@ -0,0 +1,201 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Frequencies - The Dark Elite</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
:root { --bg:#060708; --border:rgba(80,90,100,0.2); --accent:#22d3ee; --text:#e5e7eb; --muted:#9ca3af; --danger:#dc2626; }
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;min-height:100vh}
.container{max-width:1300px;margin:0 auto;padding:2rem}
header{display:flex;justify-content:space-between;align-items:center;padding-bottom:1.5rem;border-bottom:1px solid var(--border);margin-bottom:2rem}
header h1{margin:0;font-size:1.5rem;letter-spacing:0.2em;text-transform:uppercase;color:var(--accent);text-shadow:0 0 12px rgba(34,211,238,0.4)}
header a{color:var(--muted);text-decoration:none;font-size:0.85rem}
header a:hover{color:var(--accent)}
.section-title{font-size:0.75rem;letter-spacing:0.25em;text-transform:uppercase;color:var(--accent);border-left:3px solid var(--accent);padding-left:0.75rem;margin:2rem 0 1rem 0;display:flex;justify-content:space-between;align-items:center}
.controls{display:flex;gap:0.5rem;flex-wrap:wrap}
.controls input,.controls select{background:rgba(10,11,13,0.6);border:1px solid var(--border);color:var(--text);padding:0.4rem 0.7rem;border-radius:4px;font-size:0.8rem;font-family:ui-monospace,monospace}
.controls input:focus,.controls select:focus{outline:none;border-color:var(--accent)}
.add-form{background:linear-gradient(135deg,rgba(18,20,24,0.7),rgba(10,11,13,0.85));border:1px solid var(--border);border-radius:8px;padding:1.25rem;display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:0.75rem;margin-bottom:2rem}
.add-form input,.add-form select{background:rgba(10,11,13,0.6);border:1px solid var(--border);color:var(--text);padding:0.55rem 0.7rem;border-radius:4px;font-size:0.85rem;font-family:ui-monospace,monospace}
.add-form input:focus,.add-form select:focus{outline:none;border-color:var(--accent)}
.add-form .full{grid-column:1/-1}
.add-form button{background:rgba(34,211,238,0.15);border:1px solid var(--accent);color:var(--accent);padding:0.55rem;border-radius:4px;font-size:0.8rem;cursor:pointer;letter-spacing:0.1em;text-transform:uppercase;font-family:ui-monospace,monospace}
.add-form button:hover{background:rgba(34,211,238,0.25)}
table{width:100%;border-collapse:collapse;font-size:0.85rem;font-family:ui-monospace,monospace}
th{text-align:left;color:var(--muted);font-weight:400;text-transform:uppercase;letter-spacing:0.1em;font-size:0.7rem;padding:0.7rem 0.5rem;border-bottom:1px solid var(--border);position:sticky;top:0;background:var(--bg);cursor:pointer;user-select:none}
th:hover{color:var(--accent)}
td{padding:0.7rem 0.5rem;border-bottom:1px solid rgba(80,90,100,0.1)}
tr:hover td{background:rgba(34,211,238,0.04)}
td.freq{color:var(--accent);font-weight:600}
td.editable{cursor:text}
td.editable:hover{background:rgba(34,211,238,0.08)}
td input{background:rgba(10,11,13,0.9);border:1px solid var(--accent);color:var(--accent);padding:0.2rem 0.4rem;border-radius:3px;font:inherit;width:100%;outline:none}
.del{color:var(--muted);cursor:pointer;font-size:0.7rem;text-transform:uppercase;letter-spacing:0.1em}
.del:hover{color:var(--danger)}
.empty{color:var(--muted);text-align:center;padding:2rem;font-style:italic}
.toast{position:fixed;bottom:2rem;right:2rem;background:rgba(18,20,24,0.95);border:1px solid var(--accent);color:var(--accent);padding:0.8rem 1.2rem;border-radius:4px;font-family:ui-monospace,monospace;font-size:0.85rem;z-index:100;display:none}
.tag-pill{display:inline-block;background:rgba(34,211,238,0.12);border:1px solid rgba(34,211,238,0.3);color:rgb(165,235,247);font-size:0.65rem;padding:0.1rem 0.4rem;border-radius:3px;text-transform:uppercase;letter-spacing:0.1em}
</style>
</head>
<body>
<div class="container">
<header>
<div><h1>Frequencies</h1></div>
<a href="/" style="color:#9ca3af;text-decoration:none;font-size:0.85rem">← Hub</a>
</header>
<div class="section-title">Add Frequency</div>
<form class="add-form" id="addForm">
<input name="freq" placeholder="Freq (MHz)" required>
<input name="name" placeholder="Name / Callsign">
<select name="band">
<option value="">Band</option>
<option>HF-160m</option><option>HF-80m</option><option>HF-40m</option><option>HF-20m</option><option>HF-15m</option><option>HF-10m</option>
<option>VHF-6m</option><option>VHF-2m</option><option>UHF-70cm</option><option>UHF-33cm</option><option>UHF-23cm</option>
<option>GMRS</option><option>FRS</option><option>MURS</option><option>CB</option>
<option>NOAA</option><option>Marine</option><option>Aircraft</option>
<option>Public Safety</option><option>Business</option><option>Mil-Air</option>
<option>Mesh</option><option>Other</option>
</select>
<select name="mode">
<option value="">Mode</option>
<option>FM</option><option>AM</option><option>SSB-USB</option><option>SSB-LSB</option>
<option>CW</option><option>DMR</option><option>D-STAR</option><option>YSF</option>
<option>P25</option><option>NXDN</option><option>DSTAR</option><option>FT8</option>
<option>FT4</option><option>PSK31</option><option>RTTY</option><option>APRS</option>
<option>Packet</option><option>LoRa</option>
</select>
<input name="tone" placeholder="Tone (CTCSS/DCS)">
<input name="offset" placeholder="Offset (+/- MHz)">
<input name="tag" placeholder="Tag (repeater, simplex, net)">
<input name="description" class="full" placeholder="Description / notes">
<button type="submit" class="full">+ Add Frequency</button>
</form>
<div class="section-title">
Saved Frequencies
<div class="controls">
<select id="filterBand"><option value="">All Bands</option></select>
<input type="text" id="search" placeholder="Filter...">
</div>
</div>
<table>
<thead>
<tr>
<th data-sort="freq">Freq</th>
<th data-sort="name">Name</th>
<th data-sort="band">Band</th>
<th data-sort="mode">Mode</th>
<th data-sort="tone">Tone</th>
<th data-sort="offset">Offset</th>
<th data-sort="tag">Tag</th>
<th data-sort="description">Description</th>
<th></th>
</tr>
</thead>
<tbody id="rows"><tr><td colspan="9" class="empty">Loading...</td></tr></tbody>
</table>
</div>
<div id="toast" class="toast"></div>
<script>
let all=[]; let sortKey="band"; let sortDir=1;
const FIELDS=["freq","name","band","mode","tone","offset","tag","description"];
function toast(m){const t=document.getElementById("toast");t.textContent=m;t.style.display="block";setTimeout(()=>t.style.display="none",2000);}
async function load(){
const r=await fetch("/freqs/api/list");
all=await r.json();
populateBands();
render();
}
function populateBands(){
const bands=[...new Set(all.map(x=>x.band).filter(Boolean))].sort();
const sel=document.getElementById("filterBand");
const cur=sel.value;
sel.innerHTML='<option value="">All Bands</option>'+bands.map(b=>'<option>'+b+'</option>').join("");
sel.value=cur;
}
function render(){
const q=document.getElementById("search").value.toLowerCase();
const band=document.getElementById("filterBand").value;
let items=all.filter(x=>{
if(band && x.band!==band)return false;
if(q){const s=FIELDS.map(f=>(x[f]||"")).join(" ").toLowerCase();if(!s.includes(q))return false;}
return true;
});
items.sort((a,b)=>{
const va=(a[sortKey]||""), vb=(b[sortKey]||"");
if(sortKey==="freq"){return ((parseFloat(va)||0)-(parseFloat(vb)||0))*sortDir;}
return String(va).localeCompare(String(vb))*sortDir;
});
const tbody=document.getElementById("rows");
if(!items.length){tbody.innerHTML='<tr><td colspan="9" class="empty">No frequencies yet. Add one above.</td></tr>';return;}
tbody.innerHTML=items.map(x=>'<tr data-id="'+x.id+'">'+
'<td class="freq editable" data-f="freq">'+(x.freq||"")+'</td>'+
'<td class="editable" data-f="name">'+(x.name||"")+'</td>'+
'<td class="editable" data-f="band">'+(x.band?'<span class="tag-pill">'+x.band+'</span>':"")+'</td>'+
'<td class="editable" data-f="mode">'+(x.mode||"")+'</td>'+
'<td class="editable" data-f="tone">'+(x.tone||"")+'</td>'+
'<td class="editable" data-f="offset">'+(x.offset||"")+'</td>'+
'<td class="editable" data-f="tag">'+(x.tag||"")+'</td>'+
'<td class="editable" data-f="description" style="max-width:280px">'+(x.description||"")+'</td>'+
'<td><span class="del" data-id="'+x.id+'">Del</span></td>'+
'</tr>').join("");
}
document.getElementById("addForm").addEventListener("submit",async e=>{
e.preventDefault();
const fd=new FormData(e.target);
const r=await fetch("/freqs/api/add",{method:"POST",body:fd});
if(r.ok){toast("Added");e.target.reset();load();}
else toast("Failed");
});
document.querySelectorAll("th[data-sort]").forEach(th=>{
th.addEventListener("click",()=>{
const k=th.dataset.sort;
if(sortKey===k)sortDir*=-1; else {sortKey=k;sortDir=1;}
render();
});
});
document.getElementById("search").addEventListener("input",render);
document.getElementById("filterBand").addEventListener("change",render);
document.getElementById("rows").addEventListener("click",async e=>{
if(e.target.classList.contains("del")){
const id=e.target.dataset.id;
if(!confirm("Delete this frequency?"))return;
await fetch("/freqs/api/delete/"+id,{method:"DELETE"});
toast("Deleted");load();
return;
}
const td=e.target.closest("td.editable");
if(!td || td.querySelector("input"))return;
const id=td.parentElement.dataset.id;
const field=td.dataset.f;
const cur=field==="band"?(td.textContent||"").trim():td.textContent;
td.innerHTML='<input type="text" value="'+cur.replace(/"/g,"&quot;")+'">';
const inp=td.querySelector("input"); inp.focus(); inp.select();
const save=async()=>{
const val=inp.value;
const fd=new FormData(); fd.append(field,val);
await fetch("/freqs/api/update/"+id,{method:"PATCH",body:fd});
toast("Updated");load();
};
inp.addEventListener("blur",save);
inp.addEventListener("keydown",ev=>{if(ev.key==="Enter")inp.blur();if(ev.key==="Escape"){td.textContent=cur;}});
});
load();
</script>
</body>
</html>