mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-06 16:53:38 +02:00
Move to server side preference and read indicator management
This commit is contained in:
+24
-3
@@ -47,12 +47,18 @@ app/
|
||||
All database operations go through repository classes in `repository.py`:
|
||||
|
||||
```python
|
||||
from app.repository import ContactRepository, ChannelRepository, MessageRepository, RawPacketRepository
|
||||
from app.repository import ContactRepository, ChannelRepository, MessageRepository, RawPacketRepository, AppSettingsRepository
|
||||
|
||||
# Examples
|
||||
contact = await ContactRepository.get_by_key_prefix("abc123")
|
||||
await MessageRepository.create(msg_type="PRIV", text="Hello", received_at=timestamp)
|
||||
await RawPacketRepository.mark_decrypted(packet_id, message_id)
|
||||
|
||||
# App settings (single-row pattern)
|
||||
settings = await AppSettingsRepository.get()
|
||||
await AppSettingsRepository.update(auto_decrypt_dm_on_advert=True)
|
||||
await AppSettingsRepository.add_favorite("contact", public_key)
|
||||
await AppSettingsRepository.update_last_message_time("channel-KEY", timestamp)
|
||||
```
|
||||
|
||||
### Radio Connection
|
||||
@@ -206,6 +212,16 @@ raw_packets (
|
||||
last_attempt INTEGER,
|
||||
FOREIGN KEY (message_id) REFERENCES messages(id)
|
||||
)
|
||||
|
||||
app_settings (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1), -- Single-row pattern
|
||||
max_radio_contacts INTEGER DEFAULT 200,
|
||||
favorites TEXT DEFAULT '[]', -- JSON array of {type, id}
|
||||
auto_decrypt_dm_on_advert INTEGER DEFAULT 0,
|
||||
sidebar_sort_order TEXT DEFAULT 'recent', -- 'recent' or 'alpha'
|
||||
last_message_times TEXT DEFAULT '{}', -- JSON object of state_key -> timestamp
|
||||
preferences_migrated INTEGER DEFAULT 0 -- One-time migration flag
|
||||
)
|
||||
```
|
||||
|
||||
## Database Migrations (`migrations.py`)
|
||||
@@ -453,8 +469,13 @@ All endpoints are prefixed with `/api`.
|
||||
- `POST /api/packets/decrypt/historical` - Try decrypting old packets with new key
|
||||
|
||||
### Settings
|
||||
- `GET /api/settings` - Get app settings (max_radio_contacts)
|
||||
- `PATCH /api/settings` - Update app settings
|
||||
- `GET /api/settings` - Get all app settings
|
||||
- `PATCH /api/settings` - Update settings (max_radio_contacts, auto_decrypt_dm_on_advert, sidebar_sort_order)
|
||||
- `POST /api/settings/favorites` - Add a favorite
|
||||
- `DELETE /api/settings/favorites` - Remove a favorite
|
||||
- `POST /api/settings/favorites/toggle` - Toggle favorite status
|
||||
- `POST /api/settings/last-message-time` - Update last message time for a conversation
|
||||
- `POST /api/settings/migrate` - One-time migration from frontend localStorage
|
||||
|
||||
### WebSocket
|
||||
- `WS /api/ws` - Real-time updates (health, contacts, channels, messages, raw packets)
|
||||
|
||||
@@ -93,6 +93,13 @@ async def run_migrations(conn: aiosqlite.Connection) -> int:
|
||||
await set_version(conn, 8)
|
||||
applied += 1
|
||||
|
||||
# Migration 9: Create app_settings table for persistent preferences
|
||||
if version < 9:
|
||||
logger.info("Applying migration 9: create app_settings table")
|
||||
await _migrate_009_create_app_settings_table(conn)
|
||||
await set_version(conn, 9)
|
||||
applied += 1
|
||||
|
||||
if applied > 0:
|
||||
logger.info(
|
||||
"Applied %d migration(s), schema now at version %d", applied, await get_version(conn)
|
||||
@@ -582,3 +589,43 @@ async def _migrate_008_convert_path_to_paths_array(conn: aiosqlite.Connection) -
|
||||
raise
|
||||
|
||||
await conn.commit()
|
||||
|
||||
|
||||
async def _migrate_009_create_app_settings_table(conn: aiosqlite.Connection) -> None:
|
||||
"""
|
||||
Create app_settings table for persistent application preferences.
|
||||
|
||||
This table stores:
|
||||
- max_radio_contacts: Max non-repeater contacts to keep on radio for DM ACKs
|
||||
- favorites: JSON array of favorite conversations [{type, id}, ...]
|
||||
- auto_decrypt_dm_on_advert: Whether to attempt historical DM decryption on new contact
|
||||
- sidebar_sort_order: 'recent' or 'alpha' for sidebar sorting
|
||||
- last_message_times: JSON object mapping conversation keys to timestamps
|
||||
- preferences_migrated: Flag to track if localStorage has been migrated
|
||||
|
||||
The table uses a single-row pattern (id=1) for simplicity.
|
||||
"""
|
||||
await conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS app_settings (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
max_radio_contacts INTEGER DEFAULT 200,
|
||||
favorites TEXT DEFAULT '[]',
|
||||
auto_decrypt_dm_on_advert INTEGER DEFAULT 0,
|
||||
sidebar_sort_order TEXT DEFAULT 'recent',
|
||||
last_message_times TEXT DEFAULT '{}',
|
||||
preferences_migrated INTEGER DEFAULT 0
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
# Initialize with default row
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO app_settings (id, max_radio_contacts, favorites, auto_decrypt_dm_on_advert, sidebar_sort_order, last_message_times, preferences_migrated)
|
||||
VALUES (1, 200, '[]', 0, 'recent', '{}', 0)
|
||||
"""
|
||||
)
|
||||
|
||||
await conn.commit()
|
||||
logger.debug("Created app_settings table with default values")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
@@ -218,3 +220,38 @@ class CommandResponse(BaseModel):
|
||||
sender_timestamp: int | None = Field(
|
||||
default=None, description="Timestamp from the repeater's response"
|
||||
)
|
||||
|
||||
|
||||
class Favorite(BaseModel):
|
||||
"""A favorite conversation."""
|
||||
|
||||
type: Literal["channel", "contact"] = Field(description="'channel' or 'contact'")
|
||||
id: str = Field(description="Channel key or contact public key")
|
||||
|
||||
|
||||
class AppSettings(BaseModel):
|
||||
"""Application settings stored in the database."""
|
||||
|
||||
max_radio_contacts: int = Field(
|
||||
default=200,
|
||||
description="Maximum non-repeater contacts to keep on radio for DM ACKs",
|
||||
)
|
||||
favorites: list[Favorite] = Field(
|
||||
default_factory=list, description="List of favorited conversations"
|
||||
)
|
||||
auto_decrypt_dm_on_advert: bool = Field(
|
||||
default=False,
|
||||
description="Whether to attempt historical DM decryption on new contact advertisement",
|
||||
)
|
||||
sidebar_sort_order: Literal["recent", "alpha"] = Field(
|
||||
default="recent",
|
||||
description="Sidebar sort order: 'recent' or 'alpha'",
|
||||
)
|
||||
last_message_times: dict[str, int] = Field(
|
||||
default_factory=dict,
|
||||
description="Map of conversation state keys to last message timestamps",
|
||||
)
|
||||
preferences_migrated: bool = Field(
|
||||
default=False,
|
||||
description="Whether preferences have been migrated from localStorage",
|
||||
)
|
||||
|
||||
@@ -659,9 +659,14 @@ async def _process_advertisement(
|
||||
},
|
||||
)
|
||||
|
||||
# For new contacts, attempt to decrypt any historical DMs we may have stored
|
||||
# For new contacts, optionally attempt to decrypt any historical DMs we may have stored
|
||||
# This is controlled by the auto_decrypt_dm_on_advert setting
|
||||
if existing is None:
|
||||
await start_historical_dm_decryption(None, advert.public_key, advert.name)
|
||||
from app.repository import AppSettingsRepository
|
||||
|
||||
settings = await AppSettingsRepository.get()
|
||||
if settings.auto_decrypt_dm_on_advert:
|
||||
await start_historical_dm_decryption(None, advert.public_key, advert.name)
|
||||
|
||||
# If this is not a repeater, trigger recent contacts sync to radio
|
||||
# This ensures we can auto-ACK DMs from recent contacts
|
||||
|
||||
+191
-2
@@ -3,11 +3,11 @@ import logging
|
||||
import sqlite3
|
||||
import time
|
||||
from hashlib import sha256
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
from app.database import db
|
||||
from app.decoder import PayloadType, extract_payload, get_packet_payload_type
|
||||
from app.models import Channel, Contact, Message, MessagePath, RawPacket
|
||||
from app.models import AppSettings, Channel, Contact, Favorite, Message, MessagePath, RawPacket
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -703,3 +703,192 @@ class RawPacketRepository:
|
||||
result.append((row["id"], data, row["timestamp"]))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class AppSettingsRepository:
|
||||
"""Repository for app_settings table (single-row pattern)."""
|
||||
|
||||
@staticmethod
|
||||
async def get() -> AppSettings:
|
||||
"""Get the current app settings.
|
||||
|
||||
Always returns settings - creates default row if needed (migration handles initial row).
|
||||
"""
|
||||
cursor = await db.conn.execute(
|
||||
"""
|
||||
SELECT max_radio_contacts, favorites, auto_decrypt_dm_on_advert,
|
||||
sidebar_sort_order, last_message_times, preferences_migrated
|
||||
FROM app_settings WHERE id = 1
|
||||
"""
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
|
||||
if not row:
|
||||
# Should not happen after migration, but handle gracefully
|
||||
return AppSettings()
|
||||
|
||||
# Parse favorites JSON
|
||||
favorites = []
|
||||
if row["favorites"]:
|
||||
try:
|
||||
favorites_data = json.loads(row["favorites"])
|
||||
favorites = [Favorite(**f) for f in favorites_data]
|
||||
except (json.JSONDecodeError, TypeError, KeyError) as e:
|
||||
logger.warning(
|
||||
"Failed to parse favorites JSON, using empty list: %s (data=%r)",
|
||||
e,
|
||||
row["favorites"][:100] if row["favorites"] else None,
|
||||
)
|
||||
favorites = []
|
||||
|
||||
# Parse last_message_times JSON
|
||||
last_message_times: dict[str, int] = {}
|
||||
if row["last_message_times"]:
|
||||
try:
|
||||
last_message_times = json.loads(row["last_message_times"])
|
||||
except (json.JSONDecodeError, TypeError) as e:
|
||||
logger.warning(
|
||||
"Failed to parse last_message_times JSON, using empty dict: %s",
|
||||
e,
|
||||
)
|
||||
last_message_times = {}
|
||||
|
||||
# Validate sidebar_sort_order (fallback to "recent" if invalid)
|
||||
sort_order = row["sidebar_sort_order"]
|
||||
if sort_order not in ("recent", "alpha"):
|
||||
sort_order = "recent"
|
||||
|
||||
return AppSettings(
|
||||
max_radio_contacts=row["max_radio_contacts"],
|
||||
favorites=favorites,
|
||||
auto_decrypt_dm_on_advert=bool(row["auto_decrypt_dm_on_advert"]),
|
||||
sidebar_sort_order=sort_order,
|
||||
last_message_times=last_message_times,
|
||||
preferences_migrated=bool(row["preferences_migrated"]),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def update(
|
||||
max_radio_contacts: int | None = None,
|
||||
favorites: list[Favorite] | None = None,
|
||||
auto_decrypt_dm_on_advert: bool | None = None,
|
||||
sidebar_sort_order: str | None = None,
|
||||
last_message_times: dict[str, int] | None = None,
|
||||
preferences_migrated: bool | None = None,
|
||||
) -> AppSettings:
|
||||
"""Update app settings. Only provided fields are updated."""
|
||||
updates = []
|
||||
params: list[Any] = []
|
||||
|
||||
if max_radio_contacts is not None:
|
||||
updates.append("max_radio_contacts = ?")
|
||||
params.append(max_radio_contacts)
|
||||
|
||||
if favorites is not None:
|
||||
updates.append("favorites = ?")
|
||||
favorites_json = json.dumps([f.model_dump() for f in favorites])
|
||||
params.append(favorites_json)
|
||||
|
||||
if auto_decrypt_dm_on_advert is not None:
|
||||
updates.append("auto_decrypt_dm_on_advert = ?")
|
||||
params.append(1 if auto_decrypt_dm_on_advert else 0)
|
||||
|
||||
if sidebar_sort_order is not None:
|
||||
updates.append("sidebar_sort_order = ?")
|
||||
params.append(sidebar_sort_order)
|
||||
|
||||
if last_message_times is not None:
|
||||
updates.append("last_message_times = ?")
|
||||
params.append(json.dumps(last_message_times))
|
||||
|
||||
if preferences_migrated is not None:
|
||||
updates.append("preferences_migrated = ?")
|
||||
params.append(1 if preferences_migrated else 0)
|
||||
|
||||
if updates:
|
||||
query = f"UPDATE app_settings SET {', '.join(updates)} WHERE id = 1"
|
||||
await db.conn.execute(query, params)
|
||||
await db.conn.commit()
|
||||
|
||||
return await AppSettingsRepository.get()
|
||||
|
||||
@staticmethod
|
||||
async def add_favorite(fav_type: Literal["channel", "contact"], fav_id: str) -> AppSettings:
|
||||
"""Add a favorite, avoiding duplicates."""
|
||||
settings = await AppSettingsRepository.get()
|
||||
|
||||
# Check if already favorited
|
||||
if any(f.type == fav_type and f.id == fav_id for f in settings.favorites):
|
||||
return settings
|
||||
|
||||
new_favorites = settings.favorites + [Favorite(type=fav_type, id=fav_id)]
|
||||
return await AppSettingsRepository.update(favorites=new_favorites)
|
||||
|
||||
@staticmethod
|
||||
async def remove_favorite(fav_type: Literal["channel", "contact"], fav_id: str) -> AppSettings:
|
||||
"""Remove a favorite."""
|
||||
settings = await AppSettingsRepository.get()
|
||||
new_favorites = [
|
||||
f for f in settings.favorites if not (f.type == fav_type and f.id == fav_id)
|
||||
]
|
||||
return await AppSettingsRepository.update(favorites=new_favorites)
|
||||
|
||||
@staticmethod
|
||||
async def update_last_message_time(state_key: str, timestamp: int) -> None:
|
||||
"""Update the last message time for a conversation atomically.
|
||||
|
||||
Only updates if the new timestamp is greater than the existing one.
|
||||
Uses SQLite's json_set for atomic update to avoid race conditions.
|
||||
"""
|
||||
# Use COALESCE to handle NULL or missing keys, json_set for atomic update
|
||||
# Only update if new timestamp > existing (or key doesn't exist)
|
||||
await db.conn.execute(
|
||||
"""
|
||||
UPDATE app_settings
|
||||
SET last_message_times = json_set(
|
||||
COALESCE(last_message_times, '{}'),
|
||||
'$.' || ?,
|
||||
?
|
||||
)
|
||||
WHERE id = 1
|
||||
AND (
|
||||
json_extract(last_message_times, '$.' || ?) IS NULL
|
||||
OR json_extract(last_message_times, '$.' || ?) < ?
|
||||
)
|
||||
""",
|
||||
(state_key, timestamp, state_key, state_key, timestamp),
|
||||
)
|
||||
await db.conn.commit()
|
||||
|
||||
@staticmethod
|
||||
async def migrate_preferences_from_frontend(
|
||||
favorites: list[dict],
|
||||
sort_order: str,
|
||||
last_message_times: dict[str, int],
|
||||
) -> tuple[AppSettings, bool]:
|
||||
"""Migrate all preferences from frontend localStorage.
|
||||
|
||||
This is a one-time migration. If already migrated, returns current settings
|
||||
without overwriting. Returns (settings, did_migrate) tuple.
|
||||
"""
|
||||
settings = await AppSettingsRepository.get()
|
||||
|
||||
if settings.preferences_migrated:
|
||||
# Already migrated, don't overwrite
|
||||
return settings, False
|
||||
|
||||
# Convert frontend favorites format to Favorite objects
|
||||
new_favorites = []
|
||||
for f in favorites:
|
||||
if f.get("type") in ("channel", "contact") and f.get("id"):
|
||||
new_favorites.append(Favorite(type=f["type"], id=f["id"]))
|
||||
|
||||
# Update with migrated preferences and mark as migrated
|
||||
settings = await AppSettingsRepository.update(
|
||||
favorites=new_favorites,
|
||||
sidebar_sort_order=sort_order if sort_order in ("recent", "alpha") else "recent",
|
||||
last_message_times=last_message_times,
|
||||
preferences_migrated=True,
|
||||
)
|
||||
|
||||
return settings, True
|
||||
|
||||
+144
-27
@@ -1,20 +1,16 @@
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.config import settings
|
||||
from app.models import AppSettings
|
||||
from app.repository import AppSettingsRepository
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/settings", tags=["settings"])
|
||||
|
||||
|
||||
class AppSettingsResponse(BaseModel):
|
||||
max_radio_contacts: int = Field(
|
||||
description="Maximum non-repeater contacts to keep on radio for DM ACKs"
|
||||
)
|
||||
|
||||
|
||||
class AppSettingsUpdate(BaseModel):
|
||||
max_radio_contacts: int | None = Field(
|
||||
default=None,
|
||||
@@ -22,32 +18,153 @@ class AppSettingsUpdate(BaseModel):
|
||||
le=1000,
|
||||
description="Maximum non-repeater contacts to keep on radio (1-1000)",
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=AppSettingsResponse)
|
||||
async def get_settings() -> AppSettingsResponse:
|
||||
"""Get current application settings."""
|
||||
return AppSettingsResponse(
|
||||
max_radio_contacts=settings.max_radio_contacts,
|
||||
auto_decrypt_dm_on_advert: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to attempt historical DM decryption on new contact advertisement",
|
||||
)
|
||||
sidebar_sort_order: Literal["recent", "alpha"] | None = Field(
|
||||
default=None,
|
||||
description="Sidebar sort order: 'recent' or 'alpha'",
|
||||
)
|
||||
|
||||
|
||||
@router.patch("", response_model=AppSettingsResponse)
|
||||
async def update_settings(update: AppSettingsUpdate) -> AppSettingsResponse:
|
||||
class FavoriteRequest(BaseModel):
|
||||
type: Literal["channel", "contact"] = Field(description="'channel' or 'contact'")
|
||||
id: str = Field(description="Channel key or contact public key")
|
||||
|
||||
|
||||
class LastMessageTimeUpdate(BaseModel):
|
||||
state_key: str = Field(
|
||||
description="Conversation state key (e.g., 'channel-KEY' or 'contact-PREFIX')"
|
||||
)
|
||||
timestamp: int = Field(description="Unix timestamp of the last message")
|
||||
|
||||
|
||||
class MigratePreferencesRequest(BaseModel):
|
||||
favorites: list[FavoriteRequest] = Field(
|
||||
default_factory=list,
|
||||
description="List of favorites from localStorage",
|
||||
)
|
||||
sort_order: str = Field(
|
||||
default="recent",
|
||||
description="Sort order preference from localStorage",
|
||||
)
|
||||
last_message_times: dict[str, int] = Field(
|
||||
default_factory=dict,
|
||||
description="Map of conversation state keys to timestamps from localStorage",
|
||||
)
|
||||
|
||||
|
||||
class MigratePreferencesResponse(BaseModel):
|
||||
migrated: bool = Field(description="Whether migration occurred (false if already migrated)")
|
||||
settings: AppSettings = Field(description="Current settings after migration attempt")
|
||||
|
||||
|
||||
@router.get("", response_model=AppSettings)
|
||||
async def get_settings() -> AppSettings:
|
||||
"""Get current application settings."""
|
||||
return await AppSettingsRepository.get()
|
||||
|
||||
|
||||
@router.patch("", response_model=AppSettings)
|
||||
async def update_settings(update: AppSettingsUpdate) -> AppSettings:
|
||||
"""Update application settings.
|
||||
|
||||
Note: Changes are applied immediately but not persisted across restarts.
|
||||
Set MESHCORE_MAX_RADIO_CONTACTS environment variable for persistent changes.
|
||||
Settings are persisted to the database and survive restarts.
|
||||
"""
|
||||
kwargs = {}
|
||||
if update.max_radio_contacts is not None:
|
||||
logger.info(
|
||||
"Updating max_radio_contacts from %d to %d",
|
||||
settings.max_radio_contacts,
|
||||
update.max_radio_contacts,
|
||||
)
|
||||
# Pydantic settings are mutable, we can update them directly
|
||||
object.__setattr__(settings, "max_radio_contacts", update.max_radio_contacts)
|
||||
logger.info("Updating max_radio_contacts to %d", update.max_radio_contacts)
|
||||
kwargs["max_radio_contacts"] = update.max_radio_contacts
|
||||
|
||||
return AppSettingsResponse(
|
||||
max_radio_contacts=settings.max_radio_contacts,
|
||||
if update.auto_decrypt_dm_on_advert is not None:
|
||||
logger.info("Updating auto_decrypt_dm_on_advert to %s", update.auto_decrypt_dm_on_advert)
|
||||
kwargs["auto_decrypt_dm_on_advert"] = update.auto_decrypt_dm_on_advert
|
||||
|
||||
if update.sidebar_sort_order is not None:
|
||||
logger.info("Updating sidebar_sort_order to %s", update.sidebar_sort_order)
|
||||
kwargs["sidebar_sort_order"] = update.sidebar_sort_order
|
||||
|
||||
if kwargs:
|
||||
return await AppSettingsRepository.update(**kwargs)
|
||||
|
||||
return await AppSettingsRepository.get()
|
||||
|
||||
|
||||
@router.post("/favorites", response_model=AppSettings)
|
||||
async def add_favorite(request: FavoriteRequest) -> AppSettings:
|
||||
"""Add a conversation to favorites."""
|
||||
logger.info("Adding favorite: %s %s", request.type, request.id[:12])
|
||||
return await AppSettingsRepository.add_favorite(request.type, request.id)
|
||||
|
||||
|
||||
@router.delete("/favorites", response_model=AppSettings)
|
||||
async def remove_favorite(request: FavoriteRequest) -> AppSettings:
|
||||
"""Remove a conversation from favorites."""
|
||||
logger.info("Removing favorite: %s %s", request.type, request.id[:12])
|
||||
return await AppSettingsRepository.remove_favorite(request.type, request.id)
|
||||
|
||||
|
||||
@router.post("/favorites/toggle", response_model=AppSettings)
|
||||
async def toggle_favorite(request: FavoriteRequest) -> AppSettings:
|
||||
"""Toggle a conversation's favorite status."""
|
||||
settings = await AppSettingsRepository.get()
|
||||
is_favorited = any(f.type == request.type and f.id == request.id for f in settings.favorites)
|
||||
|
||||
if is_favorited:
|
||||
logger.info("Removing favorite: %s %s", request.type, request.id[:12])
|
||||
return await AppSettingsRepository.remove_favorite(request.type, request.id)
|
||||
else:
|
||||
logger.info("Adding favorite: %s %s", request.type, request.id[:12])
|
||||
return await AppSettingsRepository.add_favorite(request.type, request.id)
|
||||
|
||||
|
||||
@router.post("/last-message-time")
|
||||
async def update_last_message_time(request: LastMessageTimeUpdate) -> dict:
|
||||
"""Update the last message time for a conversation.
|
||||
|
||||
Used to track when conversations last received messages for sidebar sorting.
|
||||
Only updates if the new timestamp is greater than the existing one.
|
||||
"""
|
||||
await AppSettingsRepository.update_last_message_time(request.state_key, request.timestamp)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/migrate", response_model=MigratePreferencesResponse)
|
||||
async def migrate_preferences(request: MigratePreferencesRequest) -> MigratePreferencesResponse:
|
||||
"""Migrate all preferences from frontend localStorage to database.
|
||||
|
||||
This is a one-time migration. If preferences have already been migrated,
|
||||
this endpoint will not overwrite them and will return migrated=false.
|
||||
|
||||
Call this on frontend startup to ensure preferences are moved to the database.
|
||||
After successful migration, the frontend should clear localStorage preferences.
|
||||
|
||||
Migrates:
|
||||
- favorites (remoteterm-favorites)
|
||||
- sort_order (remoteterm-sortOrder)
|
||||
- last_message_times (remoteterm-lastMessageTime)
|
||||
"""
|
||||
# Convert to dict format for the repository method
|
||||
frontend_favorites = [{"type": f.type, "id": f.id} for f in request.favorites]
|
||||
|
||||
settings, did_migrate = await AppSettingsRepository.migrate_preferences_from_frontend(
|
||||
favorites=frontend_favorites,
|
||||
sort_order=request.sort_order,
|
||||
last_message_times=request.last_message_times,
|
||||
)
|
||||
|
||||
if did_migrate:
|
||||
logger.info(
|
||||
"Migrated preferences from frontend: %d favorites, sort_order=%s, %d message times",
|
||||
len(frontend_favorites),
|
||||
request.sort_order,
|
||||
len(request.last_message_times),
|
||||
)
|
||||
else:
|
||||
logger.debug("Preferences already migrated, skipping")
|
||||
|
||||
return MigratePreferencesResponse(
|
||||
migrated=did_migrate,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user