Skip to content

Commit 246b6a4

Browse files
committed
Added ADAR and Route Validator tests
1 parent 80eb867 commit 246b6a4

20 files changed

Lines changed: 62854 additions & 1097 deletions

README.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,27 @@ If you update legacy source HTML in `data/alias-guide-markup.html`, regenerate J
5757
npm run alias:convert
5858
```
5959

60+
To normalize long generated IDs into readable slug-based IDs:
61+
62+
```bash
63+
node scripts/normalize-alias-ids.mjs
64+
```
65+
6066
### Alias Module Notes
6167

6268
- In `data/alias-guide.json`, most content fields store both:
6369
- `html`: rendered UI content (supports formatting tags)
6470
- `text`: plain-text content used for search/filter
6571
- Keep `html` and `text` semantically aligned when editing content.
66-
- Current section layout behavior:
67-
- `CRC/ZHU Basics`: explorer layout (accordion groups + sticky detail panel + share links)
68-
- `Pilot Help Messages`: explorer layout (accordion groups + sticky detail panel + share links)
69-
- `Autotrack`: informational table layout
70-
- `Standard Routes`: informational table layout
71-
- Explorer permalink links use URL query param:
72-
- `/tools/alias-guide?alias=<entry-id>#<section-id>`
73-
- currently enabled for `CRC/ZHU Basics` and `Pilot Help Messages`
72+
- Alias sections now use one shared layout:
73+
- section header + intro (optional)
74+
- standardized reference tables
75+
- row-level `Link` button to copy direct hash links
76+
- Section-specific table label mapping lives in:
77+
- `components/alias-guide-page.js` -> `SECTION_TABLE_CONFIG`
78+
- Hash permalink format:
79+
- `/tools/alias-guide#<entry-id>`
80+
- example: `/tools/alias-guide#alias`
7481

7582
## Validate
7683

app/globals.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,9 @@ body {
253253
background: color-mix(in srgb, var(--accent) 14%, transparent);
254254
color: var(--accent-strong);
255255
}
256+
257+
.section-flyout {
258+
border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--surface-border));
259+
background: color-mix(in srgb, var(--accent) 18%, var(--surface));
260+
color: var(--foreground);
261+
}

app/tools/[id]/page.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getToolById, getTools } from "@/lib/tools";
44

55
export function generateStaticParams() {
66
return getTools()
7-
.filter((tool) => tool.id !== "alias-guide")
7+
.filter((tool) => !tool.liveUrl.startsWith("/"))
88
.map((tool) => ({ id: tool.id }));
99
}
1010

app/tools/adar-routes/page.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import AdarRoutesPage from "@/components/adar-routes-page";
2+
import adarRouteData from "@/data/adar-routes.json";
3+
import "./styles.css";
4+
5+
export const metadata = {
6+
title: "ADAR Routes | ZHU Controller Toolkit",
7+
description: "Search adapted departure and arrival routes between airport pairs.",
8+
};
9+
10+
export default function AdarRoutesToolPage() {
11+
return <AdarRoutesPage data={adarRouteData} />;
12+
}
13+

app/tools/adar-routes/styles.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.adar-table th,
2+
.adar-table td {
3+
border-bottom: 1px solid var(--surface-border);
4+
padding: 0.65rem 0.75rem;
5+
text-align: left;
6+
vertical-align: top;
7+
}
8+
9+
.adar-table th {
10+
color: var(--muted);
11+
font-size: 0.7rem;
12+
font-weight: 700;
13+
letter-spacing: 0.12em;
14+
text-transform: uppercase;
15+
}
16+
17+
.adar-table tbody tr:last-child td {
18+
border-bottom: 0;
19+
}
20+

app/tools/alias-guide/styles.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,11 @@
3232
vertical-align: middle;
3333
width: 1.1rem;
3434
}
35+
36+
.alias-anchor-hit {
37+
background: #fef9c3;
38+
}
39+
40+
:root[data-theme="dark"] .alias-anchor-hit {
41+
background: rgba(250, 204, 21, 0.2);
42+
}

app/tools/route-validator/page.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import RouteValidatorPage from "@/components/route-validator-page";
2+
import routeData from "@/data/zhu-routing-rules.json";
3+
import "./styles.css";
4+
5+
export const metadata = {
6+
title: "Route Validator | ZHU Controller Toolkit",
7+
description:
8+
"Validate VATSIM flight plan routes against ZHU preferred routing aliases for controlled departures.",
9+
};
10+
11+
export default function RouteValidatorToolPage() {
12+
return <RouteValidatorPage routeData={routeData} />;
13+
}
14+
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
.route-validator-table th,
2+
.route-validator-table td {
3+
border-bottom: 1px solid var(--surface-border);
4+
padding: 0.62rem 0.75rem;
5+
text-align: left;
6+
vertical-align: top;
7+
}
8+
9+
.route-validator-table th {
10+
color: var(--muted);
11+
font-size: 0.7rem;
12+
font-weight: 700;
13+
letter-spacing: 0.12em;
14+
text-transform: uppercase;
15+
}
16+
17+
.route-validator-table tbody tr:last-child td {
18+
border-bottom: 0;
19+
}
20+
21+
.rv-chip {
22+
border: 1px solid var(--surface-border);
23+
border-radius: 9999px;
24+
display: inline-block;
25+
font-size: 0.65rem;
26+
font-weight: 700;
27+
letter-spacing: 0.12em;
28+
padding: 0.22rem 0.48rem;
29+
text-transform: uppercase;
30+
}
31+
32+
.rv-chip-good {
33+
background: color-mix(in srgb, #16a34a 18%, transparent);
34+
border-color: color-mix(in srgb, #16a34a 45%, transparent);
35+
color: #166534;
36+
}
37+
38+
.rv-chip-warn {
39+
background: color-mix(in srgb, #f59e0b 20%, transparent);
40+
border-color: color-mix(in srgb, #f59e0b 45%, transparent);
41+
color: #92400e;
42+
}
43+
44+
.rv-chip-bad {
45+
background: color-mix(in srgb, #ef4444 15%, transparent);
46+
border-color: color-mix(in srgb, #ef4444 40%, transparent);
47+
color: #991b1b;
48+
}
49+
50+
.rv-chip-neutral {
51+
background: color-mix(in srgb, var(--accent) 10%, transparent);
52+
border-color: var(--surface-border);
53+
color: var(--muted);
54+
}
55+
56+
.rv-chip-revision {
57+
background: color-mix(in srgb, #facc15 24%, transparent);
58+
border-color: color-mix(in srgb, #eab308 52%, transparent);
59+
color: #854d0e;
60+
}
61+
62+
:root[data-theme="dark"] .rv-chip-good {
63+
color: #86efac;
64+
}
65+
66+
:root[data-theme="dark"] .rv-chip-warn {
67+
color: #fcd34d;
68+
}
69+
70+
:root[data-theme="dark"] .rv-chip-bad {
71+
color: #fca5a5;
72+
}
73+
74+
:root[data-theme="dark"] .rv-chip-revision {
75+
color: #fde68a;
76+
}
77+

components/adar-routes-page.js

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { useMemo, useState } from "react";
5+
6+
function normalizeAirport(value) {
7+
return value.trim().toUpperCase();
8+
}
9+
10+
function includesAirport(list, airport) {
11+
if (!airport) {
12+
return true;
13+
}
14+
15+
const normalized = normalizeAirport(airport);
16+
const stripped = normalized.startsWith("K") ? normalized.slice(1) : normalized;
17+
18+
return list.some((code) => {
19+
const normalizedCode = normalizeAirport(code);
20+
if (normalizedCode === normalized) {
21+
return true;
22+
}
23+
24+
if (normalizedCode.startsWith("K") && normalizedCode.slice(1) === stripped) {
25+
return true;
26+
}
27+
28+
return normalizedCode === stripped;
29+
});
30+
}
31+
32+
function routeText(route) {
33+
return route.routeString || "";
34+
}
35+
36+
export default function AdarRoutesPage({ data }) {
37+
const [departure, setDeparture] = useState("");
38+
const [arrival, setArrival] = useState("");
39+
const [routeQuery, setRouteQuery] = useState("");
40+
41+
const normalizedRouteQuery = routeQuery.trim().toUpperCase();
42+
43+
const results = useMemo(() => {
44+
return (data.routes || []).filter((route) => {
45+
const departureMatch = includesAirport(route.departures || [], departure);
46+
const arrivalMatch = includesAirport(route.arrivals || [], arrival);
47+
const routeMatch =
48+
!normalizedRouteQuery || routeText(route).toUpperCase().includes(normalizedRouteQuery);
49+
50+
return departureMatch && arrivalMatch && routeMatch;
51+
});
52+
}, [arrival, data.routes, departure, normalizedRouteQuery]);
53+
54+
const swapAirports = () => {
55+
setDeparture(arrival);
56+
setArrival(departure);
57+
};
58+
59+
return (
60+
<main className="relative min-h-screen overflow-hidden px-6 py-8 md:px-10">
61+
<div className="ambient-bg" />
62+
<div className="mx-auto max-w-7xl space-y-6">
63+
<header className="panel">
64+
<div className="flex items-start justify-between gap-3">
65+
<p className="text-accent text-sm font-semibold uppercase tracking-[0.24em]">Reference</p>
66+
<Link className="button-secondary text-sm" href="/">
67+
Back to Toolkit
68+
</Link>
69+
</div>
70+
<h1 className="font-heading text-main mt-1 text-4xl font-bold tracking-wide md:text-5xl">
71+
ADAR Route Lookup
72+
</h1>
73+
<p className="text-muted mt-2 max-w-4xl">
74+
Search adapted departure/arrival routes by airport pair. This view uses parsed ERAM ADAR
75+
adaptation data.
76+
</p>
77+
78+
<div className="mt-5 grid gap-3 lg:grid-cols-[1fr_auto_1fr_1fr]">
79+
<label className="space-y-1">
80+
<span className="text-muted text-xs uppercase tracking-[0.16em]">Departure</span>
81+
<input
82+
className="search w-full"
83+
list="adar-airports"
84+
onChange={(event) => setDeparture(event.target.value)}
85+
placeholder="KIAH or IAH"
86+
type="search"
87+
value={departure}
88+
/>
89+
</label>
90+
91+
<div className="flex items-end">
92+
<button className="button-secondary w-full" onClick={swapAirports} type="button">
93+
Swap
94+
</button>
95+
</div>
96+
97+
<label className="space-y-1">
98+
<span className="text-muted text-xs uppercase tracking-[0.16em]">Arrival</span>
99+
<input
100+
className="search w-full"
101+
list="adar-airports"
102+
onChange={(event) => setArrival(event.target.value)}
103+
placeholder="KAUS or AUS"
104+
type="search"
105+
value={arrival}
106+
/>
107+
</label>
108+
109+
<label className="space-y-1">
110+
<span className="text-muted text-xs uppercase tracking-[0.16em]">Route Contains</span>
111+
<input
112+
className="search w-full"
113+
onChange={(event) => setRouteQuery(event.target.value)}
114+
placeholder="DRLLR5, V568, SAT"
115+
type="search"
116+
value={routeQuery}
117+
/>
118+
</label>
119+
</div>
120+
121+
<datalist id="adar-airports">
122+
{(data.airports || []).map((airport) => (
123+
<option key={airport} value={airport} />
124+
))}
125+
</datalist>
126+
127+
<div className="mt-4 flex flex-wrap gap-2 text-xs uppercase tracking-[0.14em]">
128+
<span className="border-default bg-surface-soft text-muted rounded-full border px-3 py-1">
129+
{data.meta.routeCount} routes
130+
</span>
131+
<span className="border-default bg-surface-soft text-muted rounded-full border px-3 py-1">
132+
{data.meta.airportCount} airports
133+
</span>
134+
<span className="border-default bg-surface-soft text-muted rounded-full border px-3 py-1">
135+
{results.length} matches
136+
</span>
137+
</div>
138+
</header>
139+
140+
<section className="panel">
141+
{results.length === 0 ? (
142+
<p className="text-muted">No matching ADAR records for this filter combination.</p>
143+
) : (
144+
<div className="overflow-x-auto">
145+
<table className="adar-table min-w-full text-sm">
146+
<thead className="bg-surface-soft">
147+
<tr>
148+
<th>ID</th>
149+
<th>Departure</th>
150+
<th>Arrival</th>
151+
<th>Route</th>
152+
<th>AC Criteria</th>
153+
<th>Altitudes</th>
154+
</tr>
155+
</thead>
156+
<tbody>
157+
{results.map((route) => (
158+
<tr key={route.adarId}>
159+
<td className="font-mono font-semibold">{route.adarId}</td>
160+
<td>{(route.departures || []).join(", ")}</td>
161+
<td>{(route.arrivals || []).join(", ")}</td>
162+
<td className="font-mono">{routeText(route)}</td>
163+
<td>
164+
{(route.aircraftCriteriaDetails || []).length === 0
165+
? "N/A"
166+
: route.aircraftCriteriaDetails
167+
.map((criteria) => {
168+
const suffix = criteria.isExcluded ? " (Excluded)" : "";
169+
return `${criteria.id} (${criteria.facility})${suffix}`;
170+
})
171+
.join(", ")}
172+
</td>
173+
<td>
174+
{route.lowerAltitude}-{route.upperAltitude}
175+
</td>
176+
</tr>
177+
))}
178+
</tbody>
179+
</table>
180+
</div>
181+
)}
182+
</section>
183+
</div>
184+
</main>
185+
);
186+
}
187+

0 commit comments

Comments
 (0)