feat/frontend #1
@@ -78,6 +78,26 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./services/frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: audio_frontend_dev
|
||||||
|
ports:
|
||||||
|
- "3000:5173"
|
||||||
|
environment:
|
||||||
|
VITE_API_URL: http://localhost:8000
|
||||||
|
VITE_WS_URL: ws://localhost:8001/ws/live
|
||||||
|
# VITE_API_URL: http://api:8000
|
||||||
|
# VITE_WS_URL: ws://api:8001
|
||||||
|
volumes:
|
||||||
|
- ./services/frontend:/app
|
||||||
|
- /app/node_modules
|
||||||
|
networks:
|
||||||
|
- audio_network
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
timescale_data:
|
timescale_data:
|
||||||
|
|||||||
13
services/frontend/Dockerfile
Normal file
13
services/frontend/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Node 20 + Vite dev server
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 5173
|
||||||
|
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ const WINDOW_OPTIONS_MS = [15_000, 30_000, 60_000, 120_000, 300_000] as const;
|
|||||||
const DEFAULT_WINDOW_MS = 60_000;
|
const DEFAULT_WINDOW_MS = 60_000;
|
||||||
|
|
||||||
const MIN_HZ = 0.1;
|
const MIN_HZ = 0.1;
|
||||||
const MAX_HZ = 60;
|
// const MAX_HZ = 60;
|
||||||
const DEFAULT_REQUESTED_HZ = 10;
|
const DEFAULT_REQUESTED_HZ = 10;
|
||||||
|
|
||||||
// If there are more than 600 points in selected window -> downsample
|
// If there are more than 600 points in selected window -> downsample
|
||||||
@@ -56,7 +56,7 @@ let flushTimer: number | null = null;
|
|||||||
|
|
||||||
let lastSeenSample: AudioSample | null = null;
|
let lastSeenSample: AudioSample | null = null;
|
||||||
|
|
||||||
function clampInt(v: number, min: number, max: number): number {
|
function clampInt(v: number, min: number): number {
|
||||||
if (!Number.isFinite(v)) return min;
|
if (!Number.isFinite(v)) return min;
|
||||||
return Math.max(min, v);
|
return Math.max(min, v);
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ function isAllowedWindowMs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildWsUrl(base: string, hz: number): string {
|
function buildWsUrl(base: string, hz: number): string {
|
||||||
const safeHz = clampInt(hz, MIN_HZ, MAX_HZ);
|
const safeHz = clampInt(hz, MIN_HZ);
|
||||||
|
|
||||||
// Prefer URL() for correctness (keeps existing params)
|
// Prefer URL() for correctness (keeps existing params)
|
||||||
try {
|
try {
|
||||||
@@ -188,7 +188,7 @@ export const useLiveStreamStore = create<LiveStreamState>()((set, get) => {
|
|||||||
|
|
||||||
requestedHz: DEFAULT_REQUESTED_HZ,
|
requestedHz: DEFAULT_REQUESTED_HZ,
|
||||||
setRequestedHz: (hz) => {
|
setRequestedHz: (hz) => {
|
||||||
const next = clampInt(hz, MIN_HZ, MAX_HZ);
|
const next = clampInt(hz, MIN_HZ);
|
||||||
const prev = get().requestedHz;
|
const prev = get().requestedHz;
|
||||||
if (next === prev) return;
|
if (next === prev) return;
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ export const useLiveStreamStore = create<LiveStreamState>()((set, get) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
loadLatest: async (limit = 300) => {
|
loadLatest: async (limit = 300) => {
|
||||||
const safeLimit = clampInt(limit, 1, 5000);
|
const safeLimit = clampInt(limit, 1);
|
||||||
const base = env.apiUrl.replace(/\/$/, "");
|
const base = env.apiUrl.replace(/\/$/, "");
|
||||||
const url = `${base}/api/v1/audio/latest?limit=${safeLimit}`;
|
const url = `${base}/api/v1/audio/latest?limit=${safeLimit}`;
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ export function AudioLiveWidget() {
|
|||||||
<FrequencyCurrentDisplay
|
<FrequencyCurrentDisplay
|
||||||
latest={latest}
|
latest={latest}
|
||||||
history={chartHistory}
|
history={chartHistory}
|
||||||
windowMs={windowMs}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:col-span-3 grid gap-4">
|
<div className="lg:col-span-3 grid gap-4">
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
type Props = {
|
type Props = {
|
||||||
latest: AudioSample | null;
|
latest: AudioSample | null;
|
||||||
history: AudioSample[]; // последние ~15 секунд
|
history: AudioSample[]; // последние ~15 секунд
|
||||||
windowMs: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const HISTORY_WINDOW_MS = 15_000; // 15 секунд истории
|
const HISTORY_WINDOW_MS = 15_000; // 15 секунд истории
|
||||||
@@ -68,7 +67,6 @@ function getActivityColor(freq: number, rms: number): string {
|
|||||||
export const FrequencyCurrentDisplay = memo(function FrequencyCurrentDisplay({
|
export const FrequencyCurrentDisplay = memo(function FrequencyCurrentDisplay({
|
||||||
latest,
|
latest,
|
||||||
history,
|
history,
|
||||||
windowMs,
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const recentHistory = useMemo(() => {
|
const recentHistory = useMemo(() => {
|
||||||
if (!latest) return [];
|
if (!latest) return [];
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": false,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user