Files
ompweb/next.config.ts
Andre Brait a44946dab8 feat(dictation): recording deck for voice dictation (timer, waveform, pause, retry, send modes) (#109)
* feat(dictation): recording deck with timer, waveform, pause, retry and send modes

* fix(dictation): remove stray pulse ring and let waveform shrink on narrow composers

* test(systemd): scrub ambient OMP_WEB_OMP_BIN so install test is hermetic

* feat(dictation): mic becomes cancel while recording, composer Send transcribes+queues, deck keeps only pause and stop

* fix(dictation): apply review findings — theme tokens, audio lifecycle, attachment staleness, dynamic waveform width

- Use the theme's --status-error token instead of undefined --danger
  custom properties so dictation colors follow light/dark themes.
- Close the AudioContext and release the analyser when capture finishes,
  not only on cleanup, so transcription no longer holds the mic graph.
- Read attachments through refs inside handleSend/sendQueued so files
  attached mid-recording are included when the transcript dispatches.
- Surface 'No speech detected' when a capture yields no audio instead of
  failing silently.
- Stretch the waveform canvas to the free composer width (dynamic bar
  count, newest samples at the buttons) and freeze it gray while paused.
- Make the transcribe-and-send wiring test assert the contiguous branch.
- Drop the orphaned chatInput.stopDictation i18n key.

* fix(dictation): address coderabbit findings — audio context ownership, state-gated finish, enter passthrough

* feat(dictation): play recording before transcribe

Stop enters review with playback instead of sending immediately.
Paused capture keeps a left-side preview play button.

* feat(dictation): fix audio playback lifecycle, timeslice, and replay reset

* fix(dictation): address review findings (timer corruption, audio leak, icon, test honesty)

* fix(dictation): restore setIsRecording(true) on capture start and add behavioral test suite

* fix(dictation): allow blob: media in CSP so preview playback works; mic icon for resume

* fix(dictation): draw recorded bars and playback progress in review-mode waveform

* fix(dictation): stop replaced preview audio; neutral confirm label

* chore(ci): retrigger windows verify after flaky MessageView copy test
2026-09-19 20:05:54 +03:00

117 lines
5.5 KiB
TypeScript

import type { NextConfig } from "next";
import { readFileSync } from "fs";
import { join } from "path";
import { PHASE_DEVELOPMENT_SERVER } from "next/constants";
const { version } = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8")) as { version: string };
// Function form: `phase` is authoritative even when the host environment
// carries a stray NODE_ENV (e.g. NODE_ENV=production inherited by `next dev`
// processes). Relying on process.env.NODE_ENV here would apply the immutable
// chunk rule in development, make the browser cache dev chunks forever, and
// produce stale "module factory is not available" / hydration errors after
// every restart.
const nextConfig = (phase: string): NextConfig => {
const isDev = phase === PHASE_DEVELOPMENT_SERVER;
return {
// Keep standalone/server tracing inside this package. Without this explicit
// root, Next can choose a parent lockfile on Windows and traverse protected
// user-profile junctions while compiling.
outputFileTracingRoot: process.cwd(),
// undici is loaded from a runtime dependency (lib/http-dispatcher.ts) to
// honor HTTP(S)_PROXY for server-side fetch; keep it external so the
// bundler does not inline a second copy next to the global dispatcher.
serverExternalPackages: ["undici"],
webpack(config: Parameters<NonNullable<NextConfig["webpack"]>>[0]) {
// Next's entrypoint tracer does not automatically reject dynamic paths
// outside the project root. Add parent/profile patterns to its ignore list
// so user filesystem discovery remains request-time only during builds.
for (const plugin of config.plugins ?? []) {
const candidate = plugin as unknown as {
constructor?: { name?: string };
traceIgnores?: string[];
};
if (candidate.constructor?.name === "TraceEntryPointsPlugin") {
candidate.traceIgnores ??= [];
candidate.traceIgnores.push(
"**/../**",
"**/Users/**",
"**/Application Data/**",
"**/omp-web-bundled-agents/**",
);
}
}
return config;
},
allowedDevOrigins: ["127.0.0.1", "192.168.*.*"],
// Security: stop advertising the runtime, and surface dev-mode problems
// earlier. Source maps in the browser bundle leak server path layout and
// bloat downloads without helping end users of a published app.
poweredByHeader: false,
reactStrictMode: true,
productionBrowserSourceMaps: false,
// Next.js enables gzip/brotli compression for `next start` by default; no
// custom compression middleware is needed (and would require a custom server).
async headers() {
const securityHeaders = [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Referrer-Policy", value: "no-referrer" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(self), geolocation=()" },
{ key: "Content-Security-Policy", value: "default-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' ws: wss:; font-src 'self' data:; media-src 'self' blob:" },
];
// /api/files streams workspace files whose document policy depends on the
// content type (strict CSP for SVG, the DOCX preview policy, none for
// media the browser renders natively). Config-level headers overwrite
// same-key headers set by route handlers, so the global CSP must not
// match these paths — it would both reopen script execution for SVG
// documents and block the same-origin <iframe> previews (DOCX/PDF) with
// frame-ancestors 'none'. The handler-agnostic protections stay here.
const fileResponseHeaders = [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "SAMEORIGIN" },
{ key: "Referrer-Policy", value: "no-referrer" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
];
const globalRule = {
// Everything except /api/files (negative lookahead, same pattern style
// as the proxy matcher).
source: "/((?!api/files/).*)",
headers: securityHeaders,
};
const fileRule = {
source: "/api/files/:path*",
headers: fileResponseHeaders,
};
const rootNoCacheRule = {
source: "/",
headers: [
{ key: "Cache-Control", value: "private, no-cache, max-age=0, must-revalidate" },
],
};
const staticImmutableRule = {
// Hashed build output never changes, so browsers/proxies may cache it
// immutably for a year and skip revalidation entirely.
// NOTE: scoped to /_next/static/ only — broader /_next/ patterns would
// shadow the HMR WebSocket in development.
source: "/_next/static/:path*",
headers: [
{ key: "Cache-Control", value: "public, max-age=31536000, immutable" },
],
};
// Dev chunks have stable URLs whose content changes in place; caching
// them immutably would serve stale module factories after a restart.
if (isDev) return [globalRule, fileRule, rootNoCacheRule];
return [globalRule, fileRule, staticImmutableRule, rootNoCacheRule];
},
env: {
NEXT_PUBLIC_APP_VERSION: version,
NEXT_PUBLIC_OMP_WEB_VERSION: version,
},
};
};
export default nextConfig;