Преглед изворни кода

Persist added tables in backend for multi-device access

Backend:
- Add known_tables field to AppState for persistent storage
- Add GET/POST/DELETE/PATCH /api/known-tables endpoints
- Tables stored in state.json alongside other settings

Frontend:
- Fetch known tables from backend on discovery
- Persist added tables to backend via POST
- Remove tables from backend via DELETE
- Update known table names in backend for remote tables
- Keep localStorage for active table selection (client-specific)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris пре 2 недеља
родитељ
комит
4292f0bf1f
3 измењених фајлова са 155 додато и 9 уклоњено
  1. 61 9
      frontend/src/contexts/TableContext.tsx
  2. 88 0
      main.py
  3. 6 0
      modules/core/state.py

+ 61 - 9
frontend/src/contexts/TableContext.tsx

@@ -142,10 +142,11 @@ export function TableProvider({ children }: { children: React.ReactNode }) {
     setIsDiscovering(true)
 
     try {
-      // Fetch table info and settings in parallel
-      const [infoResponse, settingsResponse] = await Promise.all([
+      // Fetch table info, settings, and known tables in parallel
+      const [infoResponse, settingsResponse, knownTablesResponse] = await Promise.all([
         fetch('/api/table-info'),
         fetch('/api/settings').catch(() => null),
+        fetch('/api/known-tables').catch(() => null),
       ])
 
       if (!infoResponse.ok) {
@@ -154,6 +155,8 @@ export function TableProvider({ children }: { children: React.ReactNode }) {
 
       const info = await infoResponse.json()
       const settings = settingsResponse?.ok ? await settingsResponse.json() : null
+      const knownTablesData = knownTablesResponse?.ok ? await knownTablesResponse.json() : null
+      const knownTables: Array<{ id: string; name: string; url: string; host?: string; port?: number; version?: string }> = knownTablesData?.tables || []
 
       const currentTable: Table = {
         id: info.id,
@@ -165,15 +168,24 @@ export function TableProvider({ children }: { children: React.ReactNode }) {
         customLogo: settings?.app?.custom_logo || undefined,
       }
 
-      // Merge with existing tables
-      setTables(prev => {
+      // Merge current table with known tables from backend
+      setTables(() => {
         // Start with current table
         const merged: Table[] = [currentTable]
 
-        // Add any other tables (manual additions), mark them for status check
-        prev.forEach(existing => {
-          if (existing.id !== currentTable.id && !existing.isCurrent) {
-            merged.push({ ...existing, isOnline: existing.isOnline ?? false })
+        // Add known tables from backend (these are persisted remote tables)
+        knownTables.forEach(known => {
+          if (known.id !== currentTable.id) {
+            merged.push({
+              id: known.id,
+              name: known.name,
+              url: known.url,
+              host: known.host,
+              port: known.port,
+              version: known.version,
+              isOnline: false, // Will be updated by background refresh
+              isCurrent: false,
+            })
           }
         })
 
@@ -274,6 +286,25 @@ export function TableProvider({ children }: { children: React.ReactNode }) {
         customLogo: settings?.app?.custom_logo || undefined,
       }
 
+      // Persist to backend
+      try {
+        const hostname = new URL(normalizedUrl).hostname
+        await fetch('/api/known-tables', {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify({
+            id: newTable.id,
+            name: newTable.name,
+            url: newTable.url,
+            host: hostname,
+            version: newTable.version,
+          }),
+        })
+      } catch (e) {
+        console.error('Failed to persist table to backend:', e)
+        // Continue anyway - table will still work for this session
+      }
+
       setTables(prev => [...prev, newTable])
       return newTable
     } catch (e) {
@@ -283,7 +314,15 @@ export function TableProvider({ children }: { children: React.ReactNode }) {
   }, [tables])
 
   // Remove a table
-  const removeTable = useCallback((id: string) => {
+  const removeTable = useCallback(async (id: string) => {
+    // Remove from backend
+    try {
+      await fetch(`/api/known-tables/${id}`, { method: 'DELETE' })
+    } catch (e) {
+      console.error('Failed to remove table from backend:', e)
+      // Continue anyway - remove from local state
+    }
+
     setTables(prev => prev.filter(t => t.id !== id))
 
     // If removing active table, switch to another
@@ -312,6 +351,19 @@ export function TableProvider({ children }: { children: React.ReactNode }) {
       })
 
       if (response.ok) {
+        // Also update the known table name in the current backend (for remote tables)
+        if (!table.isCurrent) {
+          try {
+            await fetch(`/api/known-tables/${id}`, {
+              method: 'PATCH',
+              headers: { 'Content-Type': 'application/json' },
+              body: JSON.stringify({ name }),
+            })
+          } catch (e) {
+            console.error('Failed to update known table name:', e)
+          }
+        }
+
         setTables(prev =>
           prev.map(t => (t.id === id ? { ...t, name } : t))
         )

+ 88 - 0
main.py

@@ -965,6 +965,17 @@ async def update_settings(settings_update: SettingsUpdate):
 class TableInfoUpdate(BaseModel):
     name: Optional[str] = None
 
+class KnownTableAdd(BaseModel):
+    id: str
+    name: str
+    url: str
+    host: Optional[str] = None
+    port: Optional[int] = None
+    version: Optional[str] = None
+
+class KnownTableUpdate(BaseModel):
+    name: Optional[str] = None
+
 @app.get("/api/table-info", tags=["multi-table"])
 async def get_table_info():
     """
@@ -997,6 +1008,83 @@ async def update_table_info(update: TableInfoUpdate):
         "name": state.table_name
     }
 
+@app.get("/api/known-tables", tags=["multi-table"])
+async def get_known_tables():
+    """
+    Get list of known remote tables.
+
+    These are tables that have been manually added and are persisted
+    for multi-table management.
+    """
+    return {"tables": state.known_tables}
+
+@app.post("/api/known-tables", tags=["multi-table"])
+async def add_known_table(table: KnownTableAdd):
+    """
+    Add a known remote table.
+
+    This persists the table information so it's available across
+    browser sessions and devices.
+    """
+    # Check if table with same ID already exists
+    existing_ids = [t.get("id") for t in state.known_tables]
+    if table.id in existing_ids:
+        raise HTTPException(status_code=400, detail="Table with this ID already exists")
+
+    # Check if table with same URL already exists
+    existing_urls = [t.get("url") for t in state.known_tables]
+    if table.url in existing_urls:
+        raise HTTPException(status_code=400, detail="Table with this URL already exists")
+
+    new_table = {
+        "id": table.id,
+        "name": table.name,
+        "url": table.url,
+    }
+    if table.host:
+        new_table["host"] = table.host
+    if table.port:
+        new_table["port"] = table.port
+    if table.version:
+        new_table["version"] = table.version
+
+    state.known_tables.append(new_table)
+    state.save()
+    logger.info(f"Added known table: {table.name} ({table.url})")
+
+    return {"success": True, "table": new_table}
+
+@app.delete("/api/known-tables/{table_id}", tags=["multi-table"])
+async def remove_known_table(table_id: str):
+    """
+    Remove a known remote table by ID.
+    """
+    original_count = len(state.known_tables)
+    state.known_tables = [t for t in state.known_tables if t.get("id") != table_id]
+
+    if len(state.known_tables) == original_count:
+        raise HTTPException(status_code=404, detail="Table not found")
+
+    state.save()
+    logger.info(f"Removed known table: {table_id}")
+
+    return {"success": True}
+
+@app.patch("/api/known-tables/{table_id}", tags=["multi-table"])
+async def update_known_table(table_id: str, update: KnownTableUpdate):
+    """
+    Update a known remote table's name.
+    """
+    for table in state.known_tables:
+        if table.get("id") == table_id:
+            if update.name is not None:
+                table["name"] = update.name.strip()
+            state.save()
+            logger.info(f"Updated known table {table_id}: name={update.name}")
+            return {"success": True, "table": table}
+
+    raise HTTPException(status_code=404, detail="Table not found")
+
 # ============================================================================
 # Individual Settings Endpoints (Deprecated - use /api/settings instead)
 # ============================================================================

+ 6 - 0
modules/core/state.py

@@ -117,6 +117,10 @@ class AppState:
         self.table_id = str(uuid.uuid4())  # UUID generated on first run, persistent across restarts
         self.table_name = "Dune Weaver"  # User-customizable table name
 
+        # Known remote tables (for multi-table management)
+        # List of dicts: [{id, name, url, host?, port?, version?}, ...]
+        self.known_tables = []
+
         # Custom branding settings (filenames only, files stored in static/custom/)
         # Favicon is auto-generated from logo as logo-favicon.ico
         self.custom_logo = None  # Custom logo filename (e.g., "logo-abc123.png")
@@ -428,6 +432,7 @@ class AppState:
             "app_name": self.app_name,
             "table_id": self.table_id,
             "table_name": self.table_name,
+            "known_tables": self.known_tables,
             "custom_logo": self.custom_logo,
             "auto_play_enabled": self.auto_play_enabled,
             "auto_play_playlist": self.auto_play_playlist,
@@ -520,6 +525,7 @@ class AppState:
         if self.table_id is None:
             self.table_id = str(uuid.uuid4())
         self.table_name = data.get("table_name", "Dune Weaver")
+        self.known_tables = data.get("known_tables", [])
         self.custom_logo = data.get("custom_logo", None)
         self.auto_play_enabled = data.get("auto_play_enabled", False)
         self.auto_play_playlist = data.get("auto_play_playlist", None)