FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
fersLogStore.ts
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
3
4import { create } from 'zustand';
5
6export type FersLogLevel =
7 | 'TRACE'
8 | 'DEBUG'
9 | 'INFO'
10 | 'WARNING'
11 | 'ERROR'
12 | 'FATAL'
13 | 'OFF'
14 | 'UNKNOWN';
15
16export type FersLogEntry = {
17 sequence: number;
18 level: FersLogLevel;
19 line: string;
20};
21
22export type ConfigurableFersLogLevel = Exclude<FersLogLevel, 'UNKNOWN'>;
23
24type FersLogStore = {
25 entries: FersLogEntry[];
26 droppedCount: number;
27 maxLines: number;
28 logLevel: ConfigurableFersLogLevel;
29 isOpen: boolean;
30 drawerWidth: number;
31 appendLog: (entry: FersLogEntry) => void;
32 clearLogs: () => void;
33 setMaxLines: (maxLines: number) => void;
34 setLogLevel: (logLevel: ConfigurableFersLogLevel) => void;
35 setOpen: (isOpen: boolean) => void;
36 toggleOpen: () => void;
37 setDrawerWidth: (drawerWidth: number) => void;
38};
39
40export const DEFAULT_LOG_MAX_LINES = 2000;
41export const MIN_LOG_MAX_LINES = 100;
42export const MAX_LOG_MAX_LINES = 20_000;
43export const DEFAULT_LOG_LEVEL: ConfigurableFersLogLevel = 'INFO';
44export const DEFAULT_LOG_DRAWER_WIDTH = 560;
45export const MIN_LOG_DRAWER_WIDTH = 360;
46export const MAX_LOG_DRAWER_WIDTH = 960;
47const LOG_DRAWER_STORAGE_KEY = 'fers.logViewer.v1';
48export const LOG_LEVEL_OPTIONS: ConfigurableFersLogLevel[] = [
49 'TRACE',
50 'DEBUG',
51 'INFO',
52 'WARNING',
53 'ERROR',
54 'FATAL',
55 'OFF',
56];
57
58export const isConfigurableFersLogLevel = (
59 level: FersLogLevel
60): level is ConfigurableFersLogLevel =>
61 LOG_LEVEL_OPTIONS.some((option) => option === level);
62
63export const clampLogMaxLines = (maxLines: number) => {
64 if (!Number.isFinite(maxLines)) {
65 return DEFAULT_LOG_MAX_LINES;
66 }
67
68 return Math.max(
69 MIN_LOG_MAX_LINES,
70 Math.min(MAX_LOG_MAX_LINES, Math.floor(maxLines))
71 );
72};
73
74function canUseLocalStorage(): boolean {
75 return typeof globalThis.localStorage !== 'undefined';
76}
77
78export const clampLogDrawerWidth = (drawerWidth: number) => {
79 if (!Number.isFinite(drawerWidth)) {
80 return DEFAULT_LOG_DRAWER_WIDTH;
81 }
82
83 return Math.max(
84 MIN_LOG_DRAWER_WIDTH,
85 Math.min(MAX_LOG_DRAWER_WIDTH, Math.round(drawerWidth))
86 );
87};
88
89export const getEffectiveLogDrawerWidth = (
90 drawerWidth: number,
91 maxAllowedWidth: number
92) => {
93 const preferredWidth = clampLogDrawerWidth(drawerWidth);
94
95 if (!Number.isFinite(maxAllowedWidth)) {
96 return preferredWidth;
97 }
98
99 return Math.max(0, Math.min(preferredWidth, Math.floor(maxAllowedWidth)));
100};
101
102export function readStoredLogDrawerWidth(): number {
103 if (!canUseLocalStorage()) {
104 return DEFAULT_LOG_DRAWER_WIDTH;
105 }
106
107 const raw = globalThis.localStorage.getItem(LOG_DRAWER_STORAGE_KEY);
108 if (!raw) {
109 return DEFAULT_LOG_DRAWER_WIDTH;
110 }
111
112 try {
113 const parsed = JSON.parse(raw) as { drawerWidth?: unknown };
114 return clampLogDrawerWidth(Number(parsed.drawerWidth));
115 } catch {
116 return DEFAULT_LOG_DRAWER_WIDTH;
117 }
118}
119
120function writeStoredLogDrawerWidth(drawerWidth: number): void {
121 if (!canUseLocalStorage()) {
122 return;
123 }
124
125 globalThis.localStorage.setItem(
126 LOG_DRAWER_STORAGE_KEY,
127 JSON.stringify({ drawerWidth })
128 );
129}
130
131const trimEntries = (
132 entries: FersLogEntry[],
133 maxLines: number
134): { entries: FersLogEntry[]; dropped: number } => {
135 if (entries.length <= maxLines) {
136 return { entries, dropped: 0 };
137 }
138
139 return {
140 entries: entries.slice(entries.length - maxLines),
141 dropped: entries.length - maxLines,
142 };
143};
144
145export const useFersLogStore = create<FersLogStore>()((set) => ({
146 entries: [],
147 droppedCount: 0,
148 maxLines: DEFAULT_LOG_MAX_LINES,
149 logLevel: DEFAULT_LOG_LEVEL,
150 isOpen: false,
151 drawerWidth: readStoredLogDrawerWidth(),
152 appendLog: (entry) =>
153 set((state) => {
154 const trimmed = trimEntries(
155 [...state.entries, entry],
156 state.maxLines
157 );
158
159 return {
160 entries: trimmed.entries,
161 droppedCount: state.droppedCount + trimmed.dropped,
162 };
163 }),
164 clearLogs: () => set({ entries: [], droppedCount: 0 }),
165 setMaxLines: (maxLines) =>
166 set((state) => {
167 const nextMaxLines = clampLogMaxLines(maxLines);
168 const trimmed = trimEntries(state.entries, nextMaxLines);
169
170 return {
171 maxLines: nextMaxLines,
172 entries: trimmed.entries,
173 droppedCount: state.droppedCount + trimmed.dropped,
174 };
175 }),
176 setLogLevel: (logLevel) => set({ logLevel }),
177 setOpen: (isOpen) => set({ isOpen }),
178 toggleOpen: () => set((state) => ({ isOpen: !state.isOpen })),
179 setDrawerWidth: (drawerWidth) => {
180 const nextDrawerWidth = clampLogDrawerWidth(drawerWidth);
181 writeStoredLogDrawerWidth(nextDrawerWidth);
182 set({ drawerWidth: nextDrawerWidth });
183 },
184}));