diff --git a/packages/web/src/routeTree.gen.ts b/packages/web/src/routeTree.gen.ts index 973181c0..9fb5eb92 100644 --- a/packages/web/src/routeTree.gen.ts +++ b/packages/web/src/routeTree.gen.ts @@ -33,6 +33,7 @@ import { Route as ApiUsageEventsRouteImport } from './routes/api/usage-events' import { Route as ApiStreamStatusRouteImport } from './routes/api/stream-status' import { Route as ApiStreamReplaysRouteImport } from './routes/api/stream-replays' import { Route as ApiStreamRecordingRouteImport } from './routes/api/stream-recording' +import { Route as ApiStreamFilterRouteImport } from './routes/api/stream-filter' import { Route as ApiStreamCommentsRouteImport } from './routes/api/stream-comments' import { Route as ApiStreamRouteImport } from './routes/api/stream' import { Route as ApiProfileRouteImport } from './routes/api/profile' @@ -199,6 +200,11 @@ const ApiStreamRecordingRoute = ApiStreamRecordingRouteImport.update({ path: '/api/stream-recording', getParentRoute: () => rootRouteImport, } as any) +const ApiStreamFilterRoute = ApiStreamFilterRouteImport.update({ + id: '/api/stream-filter', + path: '/api/stream-filter', + getParentRoute: () => rootRouteImport, +} as any) const ApiStreamCommentsRoute = ApiStreamCommentsRouteImport.update({ id: '/api/stream-comments', path: '/api/stream-comments', @@ -459,6 +465,7 @@ export interface FileRoutesByFullPath { '/api/profile': typeof ApiProfileRoute '/api/stream': typeof ApiStreamRouteWithChildren '/api/stream-comments': typeof ApiStreamCommentsRoute + '/api/stream-filter': typeof ApiStreamFilterRoute '/api/stream-recording': typeof ApiStreamRecordingRoute '/api/stream-replays': typeof ApiStreamReplaysRouteWithChildren '/api/stream-status': typeof ApiStreamStatusRoute @@ -529,6 +536,7 @@ export interface FileRoutesByTo { '/api/profile': typeof ApiProfileRoute '/api/stream': typeof ApiStreamRouteWithChildren '/api/stream-comments': typeof ApiStreamCommentsRoute + '/api/stream-filter': typeof ApiStreamFilterRoute '/api/stream-recording': typeof ApiStreamRecordingRoute '/api/stream-replays': typeof ApiStreamReplaysRouteWithChildren '/api/stream-status': typeof ApiStreamStatusRoute @@ -601,6 +609,7 @@ export interface FileRoutesById { '/api/profile': typeof ApiProfileRoute '/api/stream': typeof ApiStreamRouteWithChildren '/api/stream-comments': typeof ApiStreamCommentsRoute + '/api/stream-filter': typeof ApiStreamFilterRoute '/api/stream-recording': typeof ApiStreamRecordingRoute '/api/stream-replays': typeof ApiStreamReplaysRouteWithChildren '/api/stream-status': typeof ApiStreamStatusRoute @@ -674,6 +683,7 @@ export interface FileRouteTypes { | '/api/profile' | '/api/stream' | '/api/stream-comments' + | '/api/stream-filter' | '/api/stream-recording' | '/api/stream-replays' | '/api/stream-status' @@ -744,6 +754,7 @@ export interface FileRouteTypes { | '/api/profile' | '/api/stream' | '/api/stream-comments' + | '/api/stream-filter' | '/api/stream-recording' | '/api/stream-replays' | '/api/stream-status' @@ -815,6 +826,7 @@ export interface FileRouteTypes { | '/api/profile' | '/api/stream' | '/api/stream-comments' + | '/api/stream-filter' | '/api/stream-recording' | '/api/stream-replays' | '/api/stream-status' @@ -887,6 +899,7 @@ export interface RootRouteChildren { ApiProfileRoute: typeof ApiProfileRoute ApiStreamRoute: typeof ApiStreamRouteWithChildren ApiStreamCommentsRoute: typeof ApiStreamCommentsRoute + ApiStreamFilterRoute: typeof ApiStreamFilterRoute ApiStreamRecordingRoute: typeof ApiStreamRecordingRoute ApiStreamReplaysRoute: typeof ApiStreamReplaysRouteWithChildren ApiStreamStatusRoute: typeof ApiStreamStatusRoute @@ -1086,6 +1099,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiStreamRecordingRouteImport parentRoute: typeof rootRouteImport } + '/api/stream-filter': { + id: '/api/stream-filter' + path: '/api/stream-filter' + fullPath: '/api/stream-filter' + preLoaderRoute: typeof ApiStreamFilterRouteImport + parentRoute: typeof rootRouteImport + } '/api/stream-comments': { id: '/api/stream-comments' path: '/api/stream-comments' @@ -1580,6 +1600,7 @@ const rootRouteChildren: RootRouteChildren = { ApiProfileRoute: ApiProfileRoute, ApiStreamRoute: ApiStreamRouteWithChildren, ApiStreamCommentsRoute: ApiStreamCommentsRoute, + ApiStreamFilterRoute: ApiStreamFilterRoute, ApiStreamRecordingRoute: ApiStreamRecordingRoute, ApiStreamReplaysRoute: ApiStreamReplaysRouteWithChildren, ApiStreamStatusRoute: ApiStreamStatusRoute, diff --git a/packages/web/src/routes/api/stream-filter.ts b/packages/web/src/routes/api/stream-filter.ts index e919df02..dfe67c73 100644 --- a/packages/web/src/routes/api/stream-filter.ts +++ b/packages/web/src/routes/api/stream-filter.ts @@ -1,47 +1,60 @@ -import { json } from "@tanstack/react-start" -import type { APIContext } from "@tanstack/react-router" +import { createFileRoute } from "@tanstack/react-router" + +const json = (data: unknown, status = 200) => + new Response(JSON.stringify(data), { + status, + headers: { + "content-type": "application/json", + "access-control-allow-origin": "*", + "cache-control": "no-store", + }, + }) /** - * Get or update stream filter configuration (allowed/blocked apps) - * - * GET: Returns current filter config from Jazz (or hardcoded default) - * PUT: Updates filter config in Jazz + * Stream filter configuration for nikiv + * Stores in-memory (will connect to Jazz later) */ -// Hardcoded default for nikiv (will be in Jazz later) -const DEFAULT_FILTER = { +// In-memory store (will be replaced with Jazz) +let currentFilter = { allowedApps: ["zed", "cursor", "xcode", "safari", "warp", "warpPreview"], - blockedApps: ["1password", "keychain", "telegram"], + blockedApps: ["1password", "keychain"], audioApps: ["spotify", "arc"], version: 1, updatedAt: Date.now(), } -export async function GET({ request }: APIContext) { - try { - // TODO: Read from Jazz when worker is set up - return json(DEFAULT_FILTER) - } catch (error) { - return json({ error: "Failed to fetch filter config" }, { status: 500 }) - } -} +export const Route = createFileRoute("/api/stream-filter")({ + server: { + handlers: { + GET: async () => { + return json(currentFilter) + }, + POST: async ({ request }) => { + try { + const body = await request.json() + const { allowedApps, blockedApps, audioApps } = body -export async function PUT({ request }: APIContext) { - try { - const body = await request.json() - const { allowedApps, blockedApps, audioApps } = body + // Update the filter + currentFilter = { + allowedApps: allowedApps || [], + blockedApps: blockedApps || [], + audioApps: audioApps || [], + version: currentFilter.version + 1, + updatedAt: Date.now(), + } - // TODO: Write to Jazz when worker is set up - // For now, return the updated config - return json({ - success: true, - allowedApps: allowedApps || [], - blockedApps: blockedApps || [], - audioApps: audioApps || [], - version: DEFAULT_FILTER.version + 1, - updatedAt: Date.now(), - }) - } catch (error) { - return json({ error: "Failed to update filter config" }, { status: 500 }) - } -} + console.log(`[stream-filter] Updated config v${currentFilter.version}:`, currentFilter) + + return json({ + success: true, + ...currentFilter, + }) + } catch (error) { + console.error("[stream-filter] Update failed:", error) + return json({ error: "Failed to update filter config" }, 500) + } + }, + }, + }, +}) diff --git a/packages/web/src/routes/settings.tsx b/packages/web/src/routes/settings.tsx index 365fb83c..93f16909 100644 --- a/packages/web/src/routes/settings.tsx +++ b/packages/web/src/routes/settings.tsx @@ -525,6 +525,14 @@ function StreamingSection({ username }: { username: string | null | undefined }) const [customerCode, setCustomerCode] = useState("") const [streamKey, setStreamKey] = useState("") + // Filter settings + const [allowedApps, setAllowedApps] = useState([]) + const [blockedApps, setBlockedApps] = useState([]) + const [audioApps, setAudioApps] = useState([]) + const [filterVersion, setFilterVersion] = useState(0) + const [filterSaving, setFilterSaving] = useState(false) + const [filterSaved, setFilterSaved] = useState(false) + useEffect(() => { const fetchSettings = async () => { try { @@ -543,7 +551,24 @@ function StreamingSection({ username }: { username: string | null | undefined }) setLoading(false) } } + + const fetchFilterConfig = async () => { + try { + const res = await fetch("/api/stream-filter") + if (res.ok) { + const data = await res.json() + setAllowedApps(data.allowedApps || []) + setBlockedApps(data.blockedApps || []) + setAudioApps(data.audioApps || []) + setFilterVersion(data.version || 0) + } + } catch { + // Ignore errors + } + } + fetchSettings() + fetchFilterConfig() }, []) const handleSave = async () => { @@ -582,6 +607,32 @@ function StreamingSection({ username }: { username: string | null | undefined }) setTimeout(() => setCopied(false), 2000) } + const handleFilterSave = async () => { + setFilterSaving(true) + setFilterSaved(false) + try { + const res = await fetch("/api/stream-filter", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + allowedApps, + blockedApps, + audioApps, + }), + }) + if (res.ok) { + const data = await res.json() + setFilterVersion(data.version) + setFilterSaved(true) + setTimeout(() => setFilterSaved(false), 2000) + } + } catch { + // Ignore errors + } finally { + setFilterSaving(false) + } + } + const streamUrl = username ? `https://linsa.io/${username}` : null return ( @@ -657,6 +708,69 @@ function StreamingSection({ username }: { username: string | null | undefined }) + +
+
+

+ Control which apps appear in your stream. Changes apply live without restart. +

+
+
+ + setAllowedApps(e.target.value.split(",").map(s => s.trim()).filter(Boolean))} + placeholder="e.g., zed, cursor, safari, warp" + className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-teal-500" + /> +

+ Only these apps will be visible in the stream. Leave empty to allow all. +

+
+
+ + setBlockedApps(e.target.value.split(",").map(s => s.trim()).filter(Boolean))} + placeholder="e.g., 1password, telegram, keychain" + className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-teal-500" + /> +

+ These apps will be hidden from the stream even if allowed. +

+
+
+ + setAudioApps(e.target.value.split(",").map(s => s.trim()).filter(Boolean))} + placeholder="e.g., spotify, arc" + className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-teal-500" + /> +

+ Apps to capture audio from. +

+
+
+ Config version: {filterVersion} +
+ {filterSaved && Saved} + +
+
+
+
+
{streamUrl && (