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:
@@ -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=["*"],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,9 +64,15 @@ function App() {
|
|||||||
<LeftSidebar />
|
<LeftSidebar />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content - Timeline */}
|
{/* Main column — filter bar, discard bar, timeline. Lives to the
|
||||||
<div className="flex-1 overflow-auto">
|
* right of the left sidebar so the filter row doesn't bleed
|
||||||
<Timeline />
|
* across the sidebar. */}
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
|
<FilterBar />
|
||||||
|
<DiscardActionBar />
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
<Timeline />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Sidebar */}
|
{/* Right Sidebar */}
|
||||||
@@ -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 />
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user