Skip to content

Commit 9929d8c

Browse files
committed
Added logging to TFMS
1 parent ae32513 commit 9929d8c

3 files changed

Lines changed: 93 additions & 20 deletions

File tree

app/tools/tfms/styles.css

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,33 @@
3535

3636
.tfms-specialty-name-button {
3737
cursor: pointer;
38-
text-decoration: underline;
39-
text-decoration-color: color-mix(in srgb, var(--accent) 35%, transparent);
40-
text-underline-offset: 0.18em;
4138
}
4239

43-
.tfms-specialty-name-button:hover {
44-
text-decoration-color: color-mix(in srgb, var(--accent) 70%, transparent);
40+
.tfms-specialty-chip-button {
41+
display: inline-flex;
42+
align-items: center;
43+
justify-content: center;
44+
border: 1px solid var(--chip-border);
45+
border-radius: 9999px;
46+
padding: 0.18rem 0.62rem;
47+
font-size: 0.72rem;
48+
font-weight: 700;
49+
letter-spacing: 0.08em;
50+
text-transform: uppercase;
51+
white-space: nowrap;
52+
background-color: var(--chip-fill);
53+
color: var(--chip-text);
54+
transition: filter 120ms ease;
55+
}
56+
57+
.tfms-specialty-chip-button:hover {
58+
filter: brightness(0.94);
59+
}
60+
61+
:root[data-theme="dark"] .tfms-specialty-chip-button {
62+
background-color: color-mix(in srgb, var(--chip-border) 18%, var(--surface));
63+
border-color: color-mix(in srgb, var(--chip-border) 55%, var(--surface-border));
64+
color: var(--chip-border);
4565
}
4666

4767
.tfms-compact-card {
@@ -133,14 +153,20 @@
133153
}
134154

135155
:root[data-theme="dark"] .tfms-band-green {
156+
background: color-mix(in srgb, #16a34a 22%, var(--surface));
157+
border-color: color-mix(in srgb, #16a34a 45%, var(--surface-border));
136158
color: #86efac;
137159
}
138160

139161
:root[data-theme="dark"] .tfms-band-yellow {
162+
background: color-mix(in srgb, #f59e0b 22%, var(--surface));
163+
border-color: color-mix(in srgb, #f59e0b 45%, var(--surface-border));
140164
color: #fcd34d;
141165
}
142166

143167
:root[data-theme="dark"] .tfms-band-red {
168+
background: color-mix(in srgb, #ef4444 18%, var(--surface));
169+
border-color: color-mix(in srgb, #ef4444 42%, var(--surface-border));
144170
color: #fca5a5;
145171
}
146172

components/tfms-viewer-page.js

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,9 @@ function getSpecialtyRowTone(row, thresholds) {
157157
function getSpecialtyChipStyle(specialty) {
158158
const colors = getSpecialtyColors(specialty);
159159
return {
160-
backgroundColor: "transparent",
161-
borderColor: colors.iconFill,
162-
color: colors.iconFill,
160+
"--chip-fill": colors.sectorFill,
161+
"--chip-border": colors.iconFill,
162+
"--chip-text": colors.chipText || colors.iconFill,
163163
};
164164
}
165165

@@ -195,9 +195,9 @@ function applyTraconExclusionToProjections(projections, allTraconPolygons) {
195195
return {
196196
...flight,
197197
specialty: nowInside ? null : flight.specialty,
198-
proj10Specialty: p10Inside ? null : flight.proj10Specialty,
199-
proj20Specialty: p20Inside ? null : flight.proj20Specialty,
200-
proj30Specialty: p30Inside ? null : flight.proj30Specialty,
198+
proj10Specialty: (nowInside || p10Inside) ? null : flight.proj10Specialty,
199+
proj20Specialty: (nowInside || p20Inside) ? null : flight.proj20Specialty,
200+
proj30Specialty: (nowInside || p30Inside) ? null : flight.proj30Specialty,
201201
};
202202
});
203203
}
@@ -627,7 +627,7 @@ function buildTowerStaffingByAirport(vatsim) {
627627
if (!callsign || !callsign.endsWith("_TWR")) {
628628
continue;
629629
}
630-
const match = callsign.match(/^([A-Z0-9]{3,4})_(?:\d{1,3}_)?TWR$/);
630+
const match = callsign.match(/^([A-Z0-9]{3,4})_(?:[A-Z0-9]{1,4}_)?TWR$/);
631631
if (!match) {
632632
continue;
633633
}
@@ -729,6 +729,9 @@ export default function TfmsViewerPage() {
729729
const projectedFlightsRef = useRef([]);
730730
const pilotMotionByCallsignRef = useRef({});
731731
const specialtyTickerTokenRef = useRef(0);
732+
const [specialtyLogActive, setSpecialtyLogActive] = useState(false);
733+
const [specialtyLogEntries, setSpecialtyLogEntries] = useState([]);
734+
const specialtyLogColumnsRef = useRef([]);
732735
const eventSplitTickerTokenRef = useRef(0);
733736
const airportQueueTrackerRef = useRef({});
734737
const previousAirportQueueByIcaoRef = useRef({});
@@ -854,7 +857,6 @@ export default function TfmsViewerPage() {
854857
if (!sector || sector === "zhu") {
855858
continue;
856859
}
857-
const floor = Number(props.floor);
858860
const ceiling = Number(props.ceiling);
859861
const ring = feature?.geometry?.coordinates?.[0] || [];
860862
const points = ring
@@ -947,6 +949,39 @@ export default function TfmsViewerPage() {
947949
() => (specialtySummary.length > 0 ? specialtySummary : defaultSpecialtySummary),
948950
[defaultSpecialtySummary, specialtySummary],
949951
);
952+
953+
// Append a log entry whenever data refreshes while logging is active
954+
useEffect(() => {
955+
if (!specialtyLogActive || specialtyDisplay.length === 0) return;
956+
const time = new Date().toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false });
957+
const counts = Object.fromEntries(specialtyDisplay.map((row) => [row.specialty, row.now]));
958+
setSpecialtyLogEntries((prev) => [...prev, { time, counts }]);
959+
// eslint-disable-next-line react-hooks/exhaustive-deps
960+
}, [specialtyLogActive, specialtySummary]); // intentionally use specialtySummary (not display) so TRACON toggle doesn't add duplicate rows
961+
962+
const handleSpecialtyLogToggle = useCallback(() => {
963+
if (!specialtyLogActive) {
964+
specialtyLogColumnsRef.current = specialtyDisplay.map((row) => row.specialty);
965+
setSpecialtyLogEntries([]);
966+
setSpecialtyLogActive(true);
967+
} else {
968+
const columns = specialtyLogColumnsRef.current;
969+
const header = ["time", ...columns].join(",");
970+
const rows = specialtyLogEntries.map((entry) =>
971+
[entry.time, ...columns.map((col) => String(entry.counts[col] ?? 0))].join(","),
972+
);
973+
const csv = [header, ...rows].join("\n");
974+
const blob = new Blob([csv], { type: "text/csv" });
975+
const url = URL.createObjectURL(blob);
976+
const link = document.createElement("a");
977+
link.href = url;
978+
link.download = `zhu-specialty-log-${new Date().toISOString().slice(0, 10)}.csv`;
979+
link.click();
980+
URL.revokeObjectURL(url);
981+
setSpecialtyLogActive(false);
982+
}
983+
}, [specialtyLogActive, specialtyDisplay, specialtyLogEntries]);
984+
950985
const airportQueueDisplay = useMemo(
951986
() => (airportQueueSummary.length > 0 ? airportQueueSummary : defaultAirportQueueSummary),
952987
[airportQueueSummary, defaultAirportQueueSummary],
@@ -1619,6 +1654,17 @@ export default function TfmsViewerPage() {
16191654
<article className="panel tfms-compact-card">
16201655
<div className="flex flex-wrap items-center justify-between gap-2">
16211656
<h2 className="font-heading text-main text-2xl">Specialty Summary</h2>
1657+
<div className="flex items-center gap-2">
1658+
<button
1659+
onClick={handleSpecialtyLogToggle}
1660+
className={`rounded-lg border px-3 py-1 text-xs font-semibold transition-colors ${
1661+
specialtyLogActive
1662+
? "border-red-500/40 text-red-400 hover:border-red-500/70 hover:bg-red-500/10 hover:text-red-300"
1663+
: "border-emerald-500/40 text-emerald-400 hover:border-emerald-500/70 hover:bg-emerald-500/10 hover:text-emerald-300"
1664+
}`}
1665+
>
1666+
{specialtyLogActive ? `Stop & Export (${specialtyLogEntries.length})` : "Start Logging"}
1667+
</button>
16221668
<label className="toggle-chip border-default bg-surface-soft text-muted inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs">
16231669
<input
16241670
className="sr-only peer"
@@ -1629,6 +1675,7 @@ export default function TfmsViewerPage() {
16291675
<span className="toggle-chip-dot" aria-hidden="true" />
16301676
<span>Exclude TRACON Volumes</span>
16311677
</label>
1678+
</div>
16321679
</div>
16331680
<div className="mt-3 overflow-x-auto">
16341681
<table className="tfms-table tfms-specialty-table tfms-compact-table min-w-full">

lib/tfms/specialty-colors.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
const SPECIALTY_PALETTE = {
2-
AUS: { sectorFill: "#FFDAE1", iconFill: "#F39AB0", iconStroke: "#FFE8ED" },
3-
CRP: { sectorFill: "#FFDDD1", iconFill: "#F1A98F", iconStroke: "#FFEAE3" },
4-
LCH: { sectorFill: "#C7F7C7", iconFill: "#72C57A", iconStroke: "#DEFBDD" },
5-
LFK: { sectorFill: "#EBDFEB", iconFill: "#BA9CC9", iconStroke: "#F3EBF3" },
6-
NEW: { sectorFill: "#FEFCE5", iconFill: "#D8D08A", iconStroke: "#FFFEEF" },
7-
OCN: { sectorFill: "#A6DAF0", iconFill: "#5CA9CF", iconStroke: "#C6E8F6" },
8-
RSG: { sectorFill: "#D2C6EC", iconFill: "#9E86C9", iconStroke: "#E4DCF5" },
2+
AUS: { sectorFill: "#FFDAE1", iconFill: "#F39AB0", iconStroke: "#FFE8ED", chipText: "#9B3255" },
3+
CRP: { sectorFill: "#FFDDD1", iconFill: "#F1A98F", iconStroke: "#FFEAE3", chipText: "#9B4A2A" },
4+
LCH: { sectorFill: "#C7F7C7", iconFill: "#72C57A", iconStroke: "#DEFBDD", chipText: "#276B2E" },
5+
LFK: { sectorFill: "#EBDFEB", iconFill: "#BA9CC9", iconStroke: "#F3EBF3", chipText: "#6B3F82" },
6+
NEW: { sectorFill: "#FEFCE5", iconFill: "#D8D08A", iconStroke: "#FFFEEF", chipText: "#7A6C1A" },
7+
OCN: { sectorFill: "#A6DAF0", iconFill: "#5CA9CF", iconStroke: "#C6E8F6", chipText: "#1A5E80" },
8+
RSG: { sectorFill: "#D2C6EC", iconFill: "#9E86C9", iconStroke: "#E4DCF5", chipText: "#4E2D80" },
99
};
1010

1111
export function getSpecialtyColors(specialty) {

0 commit comments

Comments
 (0)