Services: - ja4sentinel: TLS/JA4 fingerprint capture daemon (Go, libpcap) - logcorrelator: JA4 log correlation engine (Go, ClickHouse) - mod_reqin_log: Apache module (C, JSON request logging) - bot_detector: ML bot detection pipeline (Python) - dashboard: FastAPI/Streamlit analytics UI (Python) Shared libraries: - shared/go/ja4common: logger, config, shutdown, ipfilter (Go module) - shared/python/ja4_common: ClickHouseClient, ClickHouseSettings (Python package) - shared/clickhouse/: canonical SQL migrations (10 files) Build & packaging: - Unified 3-stage Dockerfile.package for Go RPMs (el8/el9/el10) - go.work workspace linking sentinel, correlator, ja4common - Makefile with test-all, build-all, rpm-* targets Fixes applied: - go.work: 1.21 → 1.24.6 (required by sentinel) - correlator Dockerfiles: golang:1.21 → golang:1.24 - replace directives in go.mod for ja4common local path - pyproject.toml: setuptools.backends → setuptools.build_meta - Removed static libpcap linking (unavailable on Rocky 9) - Fixed data races in output/writers_test.go (sync.Mutex + atomic.Int32) - Rewrote corrupted test files (logger_test.go × 2) Test coverage: - correlator: 67.1% total (unixsocket 80.5%, config 91.7%, app 83.3%, multi 87.7%, stdout 100%) - sentinel: all 10 packages pass (api, capture, config, fingerprint, ipfilter, logging, output, tlsparse) Documentation: - README.md + docs/ (architecture, development, 5 services, shared libs, DB schema & migrations) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
75 lines
2.0 KiB
TypeScript
75 lines
2.0 KiB
TypeScript
import { createContext, useContext, useEffect, useState } from 'react';
|
|
import { CONFIG } from './config';
|
|
|
|
export type Theme = 'dark' | 'light' | 'auto';
|
|
|
|
interface ThemeContextValue {
|
|
theme: Theme;
|
|
resolved: 'dark' | 'light';
|
|
setTheme: (t: Theme) => void;
|
|
}
|
|
|
|
const ThemeContext = createContext<ThemeContextValue>({
|
|
theme: CONFIG.DEFAULT_THEME,
|
|
resolved: 'dark',
|
|
setTheme: () => {},
|
|
});
|
|
|
|
const STORAGE_KEY = CONFIG.THEME_STORAGE_KEY;
|
|
|
|
function resolveTheme(theme: Theme): 'dark' | 'light' {
|
|
if (theme === 'auto') {
|
|
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
}
|
|
return theme;
|
|
}
|
|
|
|
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
const [theme, setThemeState] = useState<Theme>(() => {
|
|
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
|
|
return stored ?? CONFIG.DEFAULT_THEME;
|
|
});
|
|
|
|
const [resolved, setResolved] = useState<'dark' | 'light'>(() => resolveTheme(
|
|
(localStorage.getItem(STORAGE_KEY) as Theme | null) ?? CONFIG.DEFAULT_THEME
|
|
));
|
|
|
|
const applyTheme = (t: Theme) => {
|
|
const r = resolveTheme(t);
|
|
setResolved(r);
|
|
document.documentElement.setAttribute('data-theme', r);
|
|
};
|
|
|
|
const setTheme = (t: Theme) => {
|
|
setThemeState(t);
|
|
localStorage.setItem(STORAGE_KEY, t);
|
|
applyTheme(t);
|
|
};
|
|
|
|
// Apply on mount
|
|
useEffect(() => {
|
|
applyTheme(theme);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
// Watch system preference changes when in 'auto' mode
|
|
useEffect(() => {
|
|
if (theme !== 'auto') return;
|
|
const mq = window.matchMedia('(prefers-color-scheme: light)');
|
|
const handler = () => applyTheme('auto');
|
|
mq.addEventListener('change', handler);
|
|
return () => mq.removeEventListener('change', handler);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [theme]);
|
|
|
|
return (
|
|
<ThemeContext.Provider value={{ theme, resolved, setTheme }}>
|
|
{children}
|
|
</ThemeContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useTheme() {
|
|
return useContext(ThemeContext);
|
|
}
|