Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

Commit 0948497

Browse files
staticoclaude
andcommitted
Lazy-load config sections instead of loading all at once
Config tab now shows section headers as selectable items. Unloaded sections show "Enter to load" hint. Press Enter/Space on a section header to load just that section, or 'L' to load all sections at once. This avoids the slow bulk-loading that happened every time the config tab was entered. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> (cherry picked from commit 1ed0016)
1 parent 4382393 commit 0948497

3 files changed

Lines changed: 140 additions & 59 deletions

File tree

src/ui/App.tsx

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,6 +1685,54 @@ export function App({ address, packetStore, nodeStore, skipConfig = false, skipN
16851685
}
16861686
}, [myNodeNum, transport, showNotification]);
16871687

1688+
const requestSectionConfig = useCallback(async (sectionKey: string) => {
1689+
if (!transport || !myNodeNum) {
1690+
showNotification("Not connected");
1691+
return;
1692+
}
1693+
1694+
const opts = { myNodeNum };
1695+
1696+
try {
1697+
if (sectionKey === "channels") {
1698+
setPendingChannels(new Set([0, 1, 2, 3, 4, 5, 6, 7]));
1699+
for (let i = 0; i < 8; i++) {
1700+
const chBinary = adminHelper.createGetChannelRequest(i, opts);
1701+
await transport.send(chBinary);
1702+
}
1703+
showNotification("Loading channels...");
1704+
return;
1705+
}
1706+
1707+
if (sectionKey === "user") {
1708+
const ownerBinary = adminHelper.createGetOwnerRequest(opts);
1709+
await transport.send(ownerBinary);
1710+
showNotification("Loading user info...");
1711+
return;
1712+
}
1713+
1714+
// Radio config section
1715+
const configType = SECTION_TO_CONFIG_TYPE[sectionKey];
1716+
if (configType !== undefined) {
1717+
const binary = adminHelper.createGetConfigRequest(configType, opts);
1718+
await transport.send(binary);
1719+
showNotification(`Loading ${sectionKey}...`);
1720+
return;
1721+
}
1722+
1723+
// Module config section
1724+
const moduleType = SECTION_TO_MODULE_TYPE[sectionKey];
1725+
if (moduleType !== undefined) {
1726+
const binary = adminHelper.createGetModuleConfigRequest(moduleType, opts);
1727+
await transport.send(binary);
1728+
showNotification(`Loading ${sectionKey}...`);
1729+
return;
1730+
}
1731+
} catch {
1732+
showNotification(`Failed to load ${sectionKey}`);
1733+
}
1734+
}, [myNodeNum, transport, showNotification]);
1735+
16881736
const sendRebootRequest = useCallback(async (seconds: number = 2, reason: string = "Manual reboot") => {
16891737
if (!transport || !myNodeNum) return;
16901738
try {
@@ -2018,7 +2066,7 @@ export function App({ address, packetStore, nodeStore, skipConfig = false, skipN
20182066
if (input === "4") { setMode("dm"); return; }
20192067
if (input === "5") { setMode("log"); setChatInputFocused(false); setDmInputFocused(false); return; }
20202068
if (input === "6" && localMeshViewUrl) { setMode("meshview"); setChatInputFocused(false); setDmInputFocused(false); return; }
2021-
if (input === (localMeshViewUrl ? "7" : "6")) { setMode("config"); setChatInputFocused(false); setDmInputFocused(false); if (!batchEditMode) startBatchEdit(); requestAllConfigs(); return; }
2069+
if (input === (localMeshViewUrl ? "7" : "6")) { setMode("config"); setChatInputFocused(false); setDmInputFocused(false); if (!batchEditMode) startBatchEdit(); return; }
20222070
// Bracket keys for tab switching
20232071
const modes: AppMode[] = localMeshViewUrl
20242072
? ["packets", "nodes", "chat", "dm", "log", "meshview", "config"]
@@ -2029,7 +2077,7 @@ export function App({ address, packetStore, nodeStore, skipConfig = false, skipN
20292077
setMode(newMode);
20302078
setChatInputFocused(false);
20312079
setDmInputFocused(false);
2032-
if (newMode === "config") { if (!batchEditMode) startBatchEdit(); requestAllConfigs(); }
2080+
if (newMode === "config") { if (!batchEditMode) startBatchEdit(); }
20332081
return;
20342082
}
20352083
if (input === "]") {
@@ -2038,7 +2086,7 @@ export function App({ address, packetStore, nodeStore, skipConfig = false, skipN
20382086
setMode(newMode);
20392087
setChatInputFocused(false);
20402088
setDmInputFocused(false);
2041-
if (newMode === "config") { if (!batchEditMode) startBatchEdit(); requestAllConfigs(); }
2089+
if (newMode === "config") { if (!batchEditMode) startBatchEdit(); }
20422090
return;
20432091
}
20442092
}
@@ -2878,13 +2926,10 @@ export function App({ address, packetStore, nodeStore, skipConfig = false, skipN
28782926
return;
28792927
}
28802928

2881-
// Helper to find next/prev selectable row (skip section headers)
2929+
// Helper to find next/prev selectable row (section headers are selectable for lazy loading)
28822930
const findNextSelectable = (from: number, dir: 1 | -1): number => {
2883-
let idx = from + dir;
2884-
while (idx >= 0 && idx < flatConfigRows.length) {
2885-
if (!flatConfigRows[idx].isSectionHeader) return idx;
2886-
idx += dir;
2887-
}
2931+
const idx = from + dir;
2932+
if (idx >= 0 && idx < flatConfigRows.length) return idx;
28882933
return from;
28892934
};
28902935

@@ -2908,21 +2953,32 @@ export function App({ address, packetStore, nodeStore, skipConfig = false, skipN
29082953
return;
29092954
}
29102955

2911-
// g/G for first/last selectable row
2956+
// g/G for first/last row
29122957
if (input === "g") {
2913-
const first = flatConfigRows.findIndex(r => !r.isSectionHeader);
2914-
if (first >= 0) setSelectedConfigIndex(first);
2958+
if (flatConfigRows.length > 0) setSelectedConfigIndex(0);
29152959
return;
29162960
}
29172961
if (input === "G") {
2918-
for (let i = flatConfigRows.length - 1; i >= 0; i--) {
2919-
if (!flatConfigRows[i].isSectionHeader) { setSelectedConfigIndex(i); break; }
2962+
if (flatConfigRows.length > 0) setSelectedConfigIndex(flatConfigRows.length - 1);
2963+
return;
2964+
}
2965+
2966+
// Enter/Space on section header to load that section
2967+
const selectedRow = flatConfigRows[selectedConfigIndex];
2968+
if (selectedRow && selectedRow.isSectionHeader && selectedRow.sectionKey && !selectedRow.isLoaded) {
2969+
if (key.return || input === " ") {
2970+
requestSectionConfig(selectedRow.sectionKey);
2971+
return;
29202972
}
2973+
}
2974+
2975+
// 'L' to load all config sections at once
2976+
if (input === "L") {
2977+
requestAllConfigs();
29212978
return;
29222979
}
29232980

29242981
// Enter/Space to edit the selected field
2925-
const selectedRow = flatConfigRows[selectedConfigIndex];
29262982
if (selectedRow && !selectedRow.isSectionHeader && selectedRow.field) {
29272983
const field = selectedRow.field;
29282984

src/ui/components/ConfigPanel.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,19 @@ export function ConfigPanel({
9393

9494
if (row.isSectionHeader) {
9595
return (
96-
<Box key={`hdr-${row.sectionHeader}-${i}`}>
96+
<Box key={`hdr-${row.sectionHeader}-${i}`} backgroundColor={isSelected ? theme.bg.selected : undefined}>
97+
<Text color={isSelected ? theme.fg.accent : theme.fg.muted}>
98+
{isSelected ? "> " : " "}
99+
</Text>
97100
<Text color={theme.fg.muted}>── {row.sectionHeader} ──</Text>
101+
{row.sectionKey && !row.isLoaded && (
102+
<Text color={isSelected ? theme.fg.accent : theme.fg.muted}>
103+
{isSelected ? " [Enter] load" : " (not loaded)"}
104+
</Text>
105+
)}
106+
{row.isLoaded && (
107+
<Text color={theme.fg.muted}></Text>
108+
)}
98109
</Box>
99110
);
100111
}
@@ -141,9 +152,9 @@ export function ConfigPanel({
141152
{/* Footer */}
142153
<Box paddingX={1}>
143154
{(batchEditCount ?? 0) > 0 ? (
144-
<Text color={theme.fg.muted}>j/k nav | Enter edit | / filter | c commit | C discard | r reboot</Text>
155+
<Text color={theme.fg.muted}>j/k nav | Enter edit/load | L load all | / filter | c commit | C discard | r reboot</Text>
145156
) : (
146-
<Text color={theme.fg.muted}>j/k nav | Enter edit | / filter | r reboot</Text>
157+
<Text color={theme.fg.muted}>j/k nav | Enter edit/load | L load all | / filter | r reboot</Text>
147158
)}
148159
</Box>
149160
</Box>

src/ui/config-fields.ts

Lines changed: 55 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ export interface FieldDef {
1919
export interface FlatConfigRow {
2020
field: FieldDef | null; // null for section headers
2121
sectionHeader?: string;
22+
sectionKey?: string;
2223
value: unknown;
2324
displayValue: string;
2425
isSectionHeader: boolean;
26+
isLoaded?: boolean;
2527
}
2628

2729
export type ConfigStore = Map<string, Record<string, unknown>>;
@@ -419,37 +421,45 @@ export function buildFlatRows(
419421

420422
if (matchedFields.length === 0) continue;
421423

422-
// Section header
424+
// Section header - show load status
425+
const isLoaded = configData !== undefined;
423426
rows.push({
424427
field: null,
425428
sectionHeader: sectionInfo.label,
429+
sectionKey: sectionInfo.key,
426430
value: null,
427431
displayValue: "",
428432
isSectionHeader: true,
433+
isLoaded,
429434
});
430435

431-
for (const field of matchedFields) {
432-
const rawValue = configData ? configData[field.key] : undefined;
433-
rows.push({
434-
field,
435-
value: rawValue,
436-
displayValue: configData ? formatValue(field, rawValue) : "Loading...",
437-
isSectionHeader: false,
438-
});
436+
// Only show fields if section data is loaded
437+
if (isLoaded) {
438+
for (const field of matchedFields) {
439+
const rawValue = configData[field.key];
440+
rows.push({
441+
field,
442+
value: rawValue,
443+
displayValue: formatValue(field, rawValue),
444+
isSectionHeader: false,
445+
});
446+
}
439447
}
440448
}
441449

442450
// Channels section
443451
const validChannels = channels.filter(ch => ch != null).sort((a, b) => a.index - b.index);
444-
if (validChannels.length > 0) {
452+
{
445453
const channelFieldsMatch = !lowerFilter || "channel".includes(lowerFilter) || "channels".includes(lowerFilter);
446454
if (channelFieldsMatch) {
447455
rows.push({
448456
field: null,
449457
sectionHeader: "CHANNELS",
458+
sectionKey: "channels",
450459
value: null,
451460
displayValue: "",
452461
isSectionHeader: true,
462+
isLoaded: validChannels.length > 0,
453463
});
454464

455465
for (const ch of validChannels) {
@@ -502,47 +512,51 @@ export function buildFlatRows(
502512
}
503513

504514
// User section
505-
if (owner) {
515+
{
506516
const userFieldsMatch = !lowerFilter || "user".includes(lowerFilter) || "owner".includes(lowerFilter) || "name".includes(lowerFilter);
507517
if (userFieldsMatch) {
508518
rows.push({
509519
field: null,
510520
sectionHeader: "USER",
521+
sectionKey: "user",
511522
value: null,
512523
displayValue: "",
513524
isSectionHeader: true,
525+
isLoaded: !!owner,
514526
});
515527

516-
rows.push({
517-
field: { section: "user", key: "longName", label: "Long Name", type: "text", category: "user" },
518-
value: owner.longName,
519-
displayValue: owner.longName || "Not set",
520-
isSectionHeader: false,
521-
});
522-
rows.push({
523-
field: { section: "user", key: "shortName", label: "Short Name", type: "text", category: "user" },
524-
value: owner.shortName,
525-
displayValue: owner.shortName || "Not set",
526-
isSectionHeader: false,
527-
});
528-
rows.push({
529-
field: { section: "user", key: "id", label: "ID", type: "readonly", category: "user" },
530-
value: owner.id,
531-
displayValue: owner.id || "-",
532-
isSectionHeader: false,
533-
});
534-
rows.push({
535-
field: { section: "user", key: "hwModel", label: "Hardware Model", type: "readonly", category: "user" },
536-
value: owner.hwModel,
537-
displayValue: getHardwareModelName(owner.hwModel),
538-
isSectionHeader: false,
539-
});
540-
rows.push({
541-
field: { section: "user", key: "isLicensed", label: "Is Licensed", type: "readonly", category: "user" },
542-
value: owner.isLicensed,
543-
displayValue: owner.isLicensed ? "Yes" : "No",
544-
isSectionHeader: false,
545-
});
528+
if (owner) {
529+
rows.push({
530+
field: { section: "user", key: "longName", label: "Long Name", type: "text", category: "user" },
531+
value: owner.longName,
532+
displayValue: owner.longName || "Not set",
533+
isSectionHeader: false,
534+
});
535+
rows.push({
536+
field: { section: "user", key: "shortName", label: "Short Name", type: "text", category: "user" },
537+
value: owner.shortName,
538+
displayValue: owner.shortName || "Not set",
539+
isSectionHeader: false,
540+
});
541+
rows.push({
542+
field: { section: "user", key: "id", label: "ID", type: "readonly", category: "user" },
543+
value: owner.id,
544+
displayValue: owner.id || "-",
545+
isSectionHeader: false,
546+
});
547+
rows.push({
548+
field: { section: "user", key: "hwModel", label: "Hardware Model", type: "readonly", category: "user" },
549+
value: owner.hwModel,
550+
displayValue: getHardwareModelName(owner.hwModel),
551+
isSectionHeader: false,
552+
});
553+
rows.push({
554+
field: { section: "user", key: "isLicensed", label: "Is Licensed", type: "readonly", category: "user" },
555+
value: owner.isLicensed,
556+
displayValue: owner.isLicensed ? "Yes" : "No",
557+
isSectionHeader: false,
558+
});
559+
}
546560
}
547561
}
548562

0 commit comments

Comments
 (0)