fix: cross-machine access + center filter bar + floating hints

- api.ts: switch baseURL from http://localhost:8001/api/v1 to relative
  /api/v1. Both nginx (prod) and vite (dev) already proxy /api/ to the
  backend, so requests become same-origin and the app works from any
  host (LAN IP, reverse proxy, another machine) with no CORS dance.
- backend CORS: open to "*" as a fallback for the rare direct-hit case;
  the normal flow is same-origin via the proxy and never touches CORS.
- App layout: move FilterBar and DiscardActionBar inside the main
  content column (right of the left sidebar) so the filter row no
  longer bleeds across the sidebar.
- FilterBar: justify-center the pills so they sit centered above the
  timeline. Clear-all uses ml-2 instead of ml-auto.
- KeyboardHints: convert to a floating, glassy pill pinned bottom-
  center (fixed positioning + backdrop-blur + ring) instead of a flat
  toolbar row. Removed from the column layout — now mounted as an
  overlay sibling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 20:28:04 +02:00
parent a5b4054a71
commit ac5b18b60c
5 changed files with 34 additions and 17 deletions

View File

@@ -62,11 +62,16 @@ app = FastAPI(
lifespan=lifespan lifespan=lifespan
) )
# Configure CORS # Configure CORS. The frontend normally talks to the backend through the
# nginx (prod) or vite (dev) proxy, so requests are same-origin and never
# trip CORS. The wildcard here is a fallback for the rare case where a
# user / script hits the backend directly from a browser at some other
# origin (LAN IP, reverse proxy under a different host, etc.). This is a
# single-user homelab tool, so a permissive CORS policy is fine.
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["http://localhost:3000", "http://localhost:5173"], # Frontend URLs allow_origins=["*"],
allow_credentials=True, allow_credentials=False,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )

View File

@@ -53,9 +53,6 @@ function App() {
return ( return (
<div className="flex flex-col h-screen bg-bg text-text"> <div className="flex flex-col h-screen bg-bg text-text">
<TopBar /> <TopBar />
<FilterBar />
<DiscardActionBar />
<KeyboardHints />
<div className="flex flex-1 overflow-hidden"> <div className="flex flex-1 overflow-hidden">
{/* Left Sidebar */} {/* Left Sidebar */}
@@ -67,10 +64,16 @@ function App() {
<LeftSidebar /> <LeftSidebar />
</div> </div>
{/* Main Content - Timeline */} {/* Main column — filter bar, discard bar, timeline. Lives to the
* right of the left sidebar so the filter row doesn't bleed
* across the sidebar. */}
<div className="flex min-w-0 flex-1 flex-col">
<FilterBar />
<DiscardActionBar />
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<Timeline /> <Timeline />
</div> </div>
</div>
{/* Right Sidebar */} {/* Right Sidebar */}
<div <div
@@ -82,6 +85,10 @@ function App() {
</div> </div>
</div> </div>
{/* Floating keyboard hints — pinned bottom-center, glassy. Sits
* above the timeline and below the toast layer. */}
<KeyboardHints />
{/* Scan Progress Indicator */} {/* Scan Progress Indicator */}
<ScanProgress /> <ScanProgress />

View File

@@ -25,22 +25,22 @@ export function KeyboardHints() {
] ]
return ( return (
<div className="flex justify-center border-b border-border bg-surface/60 px-4 py-1.5"> <div className="pointer-events-none fixed bottom-4 left-1/2 z-30 -translate-x-1/2">
<div className="flex items-center gap-3"> <div className="pointer-events-auto flex items-center gap-3 rounded-full border border-border/60 bg-surface/40 px-4 py-1.5 shadow-lg ring-1 ring-white/5 backdrop-blur-md">
{hints.map((hint, i) => ( {hints.map((hint, i) => (
<div key={i} className="flex items-center gap-1.5"> <div key={i} className="flex items-center gap-1.5">
<kbd className="rounded bg-surface-offset px-2 py-0.5 text-[11px] font-medium text-text"> <kbd className="rounded bg-surface-offset/80 px-1.5 py-0.5 text-[11px] font-medium text-text">
{hint.key} {hint.key}
</kbd> </kbd>
<span className="text-xs text-text-muted">{hint.action}</span> <span className="text-xs text-text-muted">{hint.action}</span>
{i < hints.length - 1 && ( {i < hints.length - 1 && (
<span className="ml-2 text-text-faint"></span> <span className="ml-1 text-text-faint"></span>
)} )}
</div> </div>
))} ))}
{selectedCount > 0 && ( {selectedCount > 0 && (
<> <>
<span className="ml-2 text-text-faint"></span> <span className="text-text-faint"></span>
<span className="text-xs font-medium text-primary"> <span className="text-xs font-medium text-primary">
{selectedCount} selected {selectedCount} selected
</span> </span>

View File

@@ -93,7 +93,7 @@ export function FilterBar() {
const anyActive = hasActiveFilters(filterState) const anyActive = hasActiveFilters(filterState)
return ( return (
<div className="flex items-center gap-1.5 overflow-x-auto border-b border-border bg-surface px-3 py-1.5"> <div className="flex items-center justify-center gap-1.5 overflow-x-auto border-b border-border bg-surface px-3 py-1.5">
{/* Date */} {/* Date */}
<FilterPill <FilterPill
label="Date" label="Date"
@@ -320,7 +320,7 @@ export function FilterBar() {
{anyActive && ( {anyActive && (
<button <button
onClick={clearAll} onClick={clearAll}
className="ml-auto whitespace-nowrap rounded-full border border-border px-2.5 py-1 text-xs text-text-muted hover:bg-surface-2 hover:text-text" className="ml-2 whitespace-nowrap rounded-full border border-border px-2.5 py-1 text-xs text-text-muted hover:bg-surface-2 hover:text-text"
title="Clear all filters in this section" title="Clear all filters in this section"
> >
Clear all Clear all

View File

@@ -1,6 +1,11 @@
import axios from 'axios' import axios from 'axios'
const API_BASE_URL = 'http://localhost:8001/api/v1' // Relative API base. In production the nginx in front of the SPA proxies
// /api/ to the backend container; in dev the vite server has the same
// proxy in vite.config.ts. Using a relative URL means requests are
// always same-origin, so the app works whether you hit it from
// localhost, a LAN IP, or a reverse proxy without any CORS dance.
const API_BASE_URL = '/api/v1'
const api = axios.create({ const api = axios.create({
baseURL: API_BASE_URL, baseURL: API_BASE_URL,