diff --git a/AGENTS.md b/AGENTS.md index 69b402a..481468f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -477,7 +477,7 @@ mc.subscribe(EventType.ACK, handler) | `MESHCORE_ENABLE_MESSAGE_POLL_FALLBACK` | `false` | Switch the always-on radio audit task from hourly checks to aggressive 10-second polling; the audit checks both missed message drift and channel-slot cache drift | | `MESHCORE_FORCE_CHANNEL_SLOT_RECONFIGURE` | `false` | Disable channel-slot reuse and force `set_channel(...)` before every channel send, even on serial/BLE | -**Note:** Runtime app settings are stored in the database (`app_settings` table), not environment variables. These include `max_radio_contacts`, `auto_decrypt_dm_on_advert`, `sidebar_sort_order`, `advert_interval`, `last_advert_time`, `favorites`, `last_message_times`, `flood_scope`, `blocked_keys`, `blocked_names`, and `discovery_blocked_types`. `max_radio_contacts` is the configured radio contact capacity baseline used by background maintenance: favorites reload first, non-favorite fill targets about 80% of that value, and full offload/reload triggers around 95% occupancy. They are configured via `GET/PATCH /api/settings`. The backend still carries `sidebar_sort_order` for compatibility and migration, but the current frontend sidebar stores sort order per section (`Channels`, `Contacts`, `Repeaters`) in localStorage rather than treating it as one shared server-backed preference. MQTT, bot, webhook, Apprise, and SQS configs are stored in the `fanout_configs` table, managed via `/api/fanout`. If the radio's channel slots appear unstable or another client is mutating them underneath this app, operators can force the old always-reconfigure send path with `MESHCORE_FORCE_CHANNEL_SLOT_RECONFIGURE=true`. +**Note:** Runtime app settings are stored in the database (`app_settings` table), not environment variables. These include `max_radio_contacts`, `auto_decrypt_dm_on_advert`, `advert_interval`, `last_advert_time`, `favorites`, `last_message_times`, `flood_scope`, `blocked_keys`, `blocked_names`, and `discovery_blocked_types`. `max_radio_contacts` is the configured radio contact capacity baseline used by background maintenance: favorites reload first, non-favorite fill targets about 80% of that value, and full offload/reload triggers around 95% occupancy. They are configured via `GET/PATCH /api/settings`. MQTT, bot, webhook, Apprise, and SQS configs are stored in the `fanout_configs` table, managed via `/api/fanout`. If the radio's channel slots appear unstable or another client is mutating them underneath this app, operators can force the old always-reconfigure send path with `MESHCORE_FORCE_CHANNEL_SLOT_RECONFIGURE=true`. Byte-perfect channel retries are user-triggered via `POST /api/messages/channel/{message_id}/resend` and are allowed for 30 seconds after the original send. diff --git a/app/AGENTS.md b/app/AGENTS.md index b4c1e49..ba7136d 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -302,7 +302,6 @@ Repository writes should prefer typed models such as `ContactUpsert` over ad hoc - `max_radio_contacts` - `favorites` - `auto_decrypt_dm_on_advert` -- `sidebar_sort_order` - `last_message_times` - `preferences_migrated` - `advert_interval` @@ -310,8 +309,6 @@ Repository writes should prefer typed models such as `ContactUpsert` over ad hoc - `flood_scope` - `blocked_keys`, `blocked_names`, `discovery_blocked_types` -Note: `sidebar_sort_order` remains in the backend model for compatibility and migration, but the current frontend sidebar uses per-section localStorage sort preferences instead of a single shared server-backed sort mode. - Note: MQTT, community MQTT, and bot configs were migrated to the `fanout_configs` table (migrations 36-38). ## Security Posture (intentional) diff --git a/app/database.py b/app/database.py index 130c129..b81e819 100644 --- a/app/database.py +++ b/app/database.py @@ -96,7 +96,6 @@ CREATE TABLE IF NOT EXISTS app_settings ( max_radio_contacts INTEGER DEFAULT 200, favorites TEXT DEFAULT '[]', auto_decrypt_dm_on_advert INTEGER DEFAULT 1, - sidebar_sort_order TEXT DEFAULT 'recent', last_message_times TEXT DEFAULT '{}', preferences_migrated INTEGER DEFAULT 0, advert_interval INTEGER DEFAULT 0, diff --git a/app/migrations.py b/app/migrations.py index 9a0ce70..52b6386 100644 --- a/app/migrations.py +++ b/app/migrations.py @@ -389,6 +389,12 @@ async def run_migrations(conn: aiosqlite.Connection) -> int: await set_version(conn, 50) applied += 1 + if version < 51: + logger.info("Applying migration 51: drop sidebar_sort_order from app_settings") + await _migrate_051_drop_sidebar_sort_order(conn) + await set_version(conn, 51) + applied += 1 + if applied > 0: logger.info( "Applied %d migration(s), schema now at version %d", applied, await get_version(conn) @@ -859,13 +865,9 @@ async def _migrate_009_create_app_settings_table(conn: aiosqlite.Connection) -> """ ) - # 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, '[]', 1, 'recent', '{}', 0) - """ - ) + # Initialize with default row (use only the id column so this works + # regardless of which columns exist — defaults fill the rest). + await conn.execute("INSERT OR IGNORE INTO app_settings (id) VALUES (1)") await conn.commit() logger.debug("Created app_settings table with default values") @@ -3128,3 +3130,22 @@ async def _migrate_050_repeater_telemetry_history(conn: aiosqlite.Connection) -> """ ) await conn.commit() + + +async def _migrate_051_drop_sidebar_sort_order(conn: aiosqlite.Connection) -> None: + """Remove vestigial sidebar_sort_order column from app_settings.""" + col_cursor = await conn.execute("PRAGMA table_info(app_settings)") + columns = {row[1] for row in await col_cursor.fetchall()} + if "sidebar_sort_order" in columns: + try: + await conn.execute("ALTER TABLE app_settings DROP COLUMN sidebar_sort_order") + await conn.commit() + except Exception as e: + error_msg = str(e).lower() + if "syntax error" in error_msg or "drop column" in error_msg: + logger.debug( + "SQLite doesn't support DROP COLUMN, sidebar_sort_order column will remain" + ) + await conn.commit() + else: + raise diff --git a/app/models.py b/app/models.py index 1a49db3..17fca18 100644 --- a/app/models.py +++ b/app/models.py @@ -787,10 +787,6 @@ class AppSettings(BaseModel): default=True, 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", diff --git a/app/repository/contacts.py b/app/repository/contacts.py index 8ca2d9b..3a6a343 100644 --- a/app/repository/contacts.py +++ b/app/repository/contacts.py @@ -395,12 +395,9 @@ class ContactRepository: @staticmethod async def delete(public_key: str) -> None: normalized = public_key.lower() - # contact_name_history and contact_advert_paths cascade via FK, but - # messages has no FK to contacts — clean up DMs explicitly. - await db.conn.execute( - "DELETE FROM messages WHERE type = 'PRIV' AND conversation_key = ?", - (normalized,), - ) + # contact_name_history and contact_advert_paths cascade via FK. + # Messages are intentionally preserved so history re-surfaces + # if the contact is re-added later. await db.conn.execute("DELETE FROM contacts WHERE public_key = ?", (normalized,)) await db.conn.commit() diff --git a/app/repository/messages.py b/app/repository/messages.py index 93e4ed2..12adc96 100644 --- a/app/repository/messages.py +++ b/app/repository/messages.py @@ -675,7 +675,7 @@ class MessageRepository: ELSE 0 END) > 0 as has_mention FROM messages m - JOIN contacts ct ON m.conversation_key = ct.public_key + LEFT JOIN contacts ct ON m.conversation_key = ct.public_key WHERE m.type = 'PRIV' AND m.outgoing = 0 AND m.received_at > COALESCE(ct.last_read_at, 0) {blocked_sql} diff --git a/app/repository/settings.py b/app/repository/settings.py index 6b91148..1fba7b4 100644 --- a/app/repository/settings.py +++ b/app/repository/settings.py @@ -27,7 +27,7 @@ class AppSettingsRepository: cursor = await db.conn.execute( """ SELECT max_radio_contacts, favorites, auto_decrypt_dm_on_advert, - sidebar_sort_order, last_message_times, preferences_migrated, + last_message_times, preferences_migrated, advert_interval, last_advert_time, flood_scope, blocked_keys, blocked_names, discovery_blocked_types FROM app_settings WHERE id = 1 @@ -89,16 +89,10 @@ class AppSettingsRepository: except (json.JSONDecodeError, TypeError): discovery_blocked_types = [] - # 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"]), advert_interval=row["advert_interval"] or 0, @@ -114,7 +108,6 @@ class AppSettingsRepository: 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, advert_interval: int | None = None, @@ -141,10 +134,6 @@ class AppSettingsRepository: 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)) @@ -252,7 +241,6 @@ class AppSettingsRepository: # 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, ) diff --git a/app/routers/server_control.py b/app/routers/server_control.py index 425978a..c348638 100644 --- a/app/routers/server_control.py +++ b/app/routers/server_control.py @@ -250,6 +250,7 @@ async def batch_cli_fetch( # Re-ensure contact is loaded each iteration; another operation # may have evicted it while we didn't hold the lock. await _ensure_on_radio(mc, contact) + await asyncio.sleep(1.0) # settle after add_contact send_result = await mc.commands.send_cmd(contact.public_key, cmd) if send_result.type == EventType.ERROR: diff --git a/app/routers/settings.py b/app/routers/settings.py index 3625584..6a5687a 100644 --- a/app/routers/settings.py +++ b/app/routers/settings.py @@ -27,10 +27,6 @@ class AppSettingsUpdate(BaseModel): 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'", - ) advert_interval: int | None = Field( default=None, ge=0, @@ -111,10 +107,6 @@ async def update_settings(update: AppSettingsUpdate) -> AppSettings: 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 update.advert_interval is not None: # Enforce minimum 1-hour interval; 0 means disabled interval = update.advert_interval diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index b1ee3e5..62ac406 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -350,7 +350,6 @@ LocalStorage migration helpers for favorites; canonical favorites are server-sid - `max_radio_contacts` - `favorites` - `auto_decrypt_dm_on_advert` -- `sidebar_sort_order` - `last_message_times` - `preferences_migrated` - `advert_interval` @@ -358,7 +357,6 @@ LocalStorage migration helpers for favorites; canonical favorites are server-sid - `flood_scope` - `blocked_keys`, `blocked_names`, `discovery_blocked_types` -The backend still carries `sidebar_sort_order` for compatibility and old preference migration, but the current sidebar UI stores sort order per section (`Channels`, `Contacts`, `Repeaters`) in frontend localStorage rather than treating it as one global server-backed setting. Note: MQTT, bot, and community MQTT settings were migrated to the `fanout_configs` table (managed via `/api/fanout`). They are no longer part of `AppSettings`. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8f8b3aa..e172f5f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -490,7 +490,6 @@ export function App() { void markAllRead(); }, favorites, - legacySortOrder: appSettings?.sidebar_sort_order, isConversationNotificationsEnabled, blockedKeys: appSettings?.blocked_keys ?? [], blockedNames: appSettings?.blocked_names ?? [], diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 1a07a24..e6e4fea 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -107,36 +107,19 @@ interface SidebarProps { onToggleCracker: () => void; onMarkAllRead: () => void; favorites: Favorite[]; - /** Legacy global sort order, used only to seed per-section local preferences. */ - legacySortOrder?: SortOrder; isConversationNotificationsEnabled?: (type: 'channel' | 'contact', id: string) => boolean; blockedKeys?: string[]; blockedNames?: string[]; } -type InitialSectionSortState = { - orders: SidebarSectionSortOrders; - source: 'section' | 'legacy' | 'none'; -}; - -function loadInitialSectionSortOrders(): InitialSectionSortState { +function loadInitialSectionSortOrders(): SidebarSectionSortOrders { const storedOrders = loadLocalStorageSidebarSectionSortOrders(); - if (storedOrders) { - return { orders: storedOrders, source: 'section' }; - } + if (storedOrders) return storedOrders; const legacyOrder = loadLegacyLocalStorageSortOrder(); - if (legacyOrder) { - return { - orders: buildSidebarSectionSortOrders(legacyOrder), - source: 'legacy', - }; - } - - return { - orders: buildSidebarSectionSortOrders(), - source: 'none', - }; + const orders = buildSidebarSectionSortOrders(legacyOrder ?? undefined); + saveLocalStorageSidebarSectionSortOrders(orders); + return orders; } export function Sidebar({ @@ -153,7 +136,6 @@ export function Sidebar({ onToggleCracker, onMarkAllRead, favorites, - legacySortOrder, isConversationNotificationsEnabled, blockedKeys = [], blockedNames = [], @@ -166,8 +148,8 @@ export function Sidebar({ ); const [searchQuery, setSearchQuery] = useState(''); - const initialSectionSortState = useMemo(loadInitialSectionSortOrders, []); - const [sectionSortOrders, setSectionSortOrders] = useState(initialSectionSortState.orders); + const initialSectionSortOrders = useMemo(loadInitialSectionSortOrders, []); + const [sectionSortOrders, setSectionSortOrders] = useState(initialSectionSortOrders); const initialCollapsedState = useMemo(loadCollapsedState, []); const [toolsCollapsed, setToolsCollapsed] = useState(initialCollapsedState.tools); const [favoritesCollapsed, setFavoritesCollapsed] = useState(initialCollapsedState.favorites); @@ -176,29 +158,12 @@ export function Sidebar({ const [roomsCollapsed, setRoomsCollapsed] = useState(initialCollapsedState.rooms); const [repeatersCollapsed, setRepeatersCollapsed] = useState(initialCollapsedState.repeaters); const collapseSnapshotRef = useRef(null); - const sectionSortSourceRef = useRef(initialSectionSortState.source); - - useEffect(() => { - if (sectionSortSourceRef.current === 'legacy') { - saveLocalStorageSidebarSectionSortOrders(sectionSortOrders); - sectionSortSourceRef.current = 'section'; - return; - } - - if (sectionSortSourceRef.current !== 'none' || legacySortOrder === undefined) return; - - const seededOrders = buildSidebarSectionSortOrders(legacySortOrder); - setSectionSortOrders(seededOrders); - saveLocalStorageSidebarSectionSortOrders(seededOrders); - sectionSortSourceRef.current = 'section'; - }, [legacySortOrder, sectionSortOrders]); const handleSortToggle = (section: SidebarSortableSection) => { setSectionSortOrders((prev) => { const nextOrder = prev[section] === 'alpha' ? 'recent' : 'alpha'; const updated = { ...prev, [section]: nextOrder }; saveLocalStorageSidebarSectionSortOrders(updated); - sectionSortSourceRef.current = 'section'; return updated; }); }; diff --git a/frontend/src/test/appFavorites.test.tsx b/frontend/src/test/appFavorites.test.tsx index 48781d0..428d87a 100644 --- a/frontend/src/test/appFavorites.test.tsx +++ b/frontend/src/test/appFavorites.test.tsx @@ -190,7 +190,6 @@ const baseSettings = { max_radio_contacts: 200, favorites: [] as Array<{ type: 'channel' | 'contact'; id: string }>, auto_decrypt_dm_on_advert: false, - sidebar_sort_order: 'recent' as const, last_message_times: {}, preferences_migrated: false, advert_interval: 0, diff --git a/frontend/src/test/appSearchJump.test.tsx b/frontend/src/test/appSearchJump.test.tsx index f871b84..3855527 100644 --- a/frontend/src/test/appSearchJump.test.tsx +++ b/frontend/src/test/appSearchJump.test.tsx @@ -218,7 +218,6 @@ describe('App search jump target handling', () => { max_radio_contacts: 200, favorites: [], auto_decrypt_dm_on_advert: false, - sidebar_sort_order: 'recent', last_message_times: {}, preferences_migrated: true, advert_interval: 0, diff --git a/frontend/src/test/appStartupHash.test.tsx b/frontend/src/test/appStartupHash.test.tsx index d0170af..f207419 100644 --- a/frontend/src/test/appStartupHash.test.tsx +++ b/frontend/src/test/appStartupHash.test.tsx @@ -169,7 +169,6 @@ describe('App startup hash resolution', () => { max_radio_contacts: 200, favorites: [], auto_decrypt_dm_on_advert: false, - sidebar_sort_order: 'recent', last_message_times: {}, preferences_migrated: true, advert_interval: 0, diff --git a/frontend/src/test/settingsModal.test.tsx b/frontend/src/test/settingsModal.test.tsx index 6bff4cb..3d27ff7 100644 --- a/frontend/src/test/settingsModal.test.tsx +++ b/frontend/src/test/settingsModal.test.tsx @@ -61,7 +61,6 @@ const baseSettings: AppSettings = { max_radio_contacts: 200, favorites: [], auto_decrypt_dm_on_advert: false, - sidebar_sort_order: 'recent', last_message_times: {}, preferences_migrated: false, advert_interval: 0, diff --git a/frontend/src/test/sidebar.test.tsx b/frontend/src/test/sidebar.test.tsx index a2a75ab..107d923 100644 --- a/frontend/src/test/sidebar.test.tsx +++ b/frontend/src/test/sidebar.test.tsx @@ -92,7 +92,6 @@ function renderSidebar(overrides?: { onToggleCracker={vi.fn()} onMarkAllRead={vi.fn()} favorites={favorites} - legacySortOrder="recent" isConversationNotificationsEnabled={overrides?.isConversationNotificationsEnabled} /> ); @@ -140,7 +139,6 @@ describe('Sidebar section summaries', () => { onToggleCracker={vi.fn()} onMarkAllRead={vi.fn()} favorites={[]} - legacySortOrder="recent" /> ); @@ -300,7 +298,6 @@ describe('Sidebar section summaries', () => { onToggleCracker={vi.fn()} onMarkAllRead={vi.fn()} favorites={[]} - legacySortOrder="recent" /> ); @@ -397,7 +394,6 @@ describe('Sidebar section summaries', () => { onToggleCracker: vi.fn(), onMarkAllRead: vi.fn(), favorites: [], - legacySortOrder: 'recent' as const, }; const getChannelsOrder = () => screen.getAllByText(/^#/).map((node) => node.textContent); @@ -469,7 +465,6 @@ describe('Sidebar section summaries', () => { onToggleCracker={vi.fn()} onMarkAllRead={vi.fn()} favorites={[]} - legacySortOrder="recent" /> ); @@ -504,7 +499,6 @@ describe('Sidebar section summaries', () => { onToggleCracker={vi.fn()} onMarkAllRead={vi.fn()} favorites={[]} - legacySortOrder="recent" /> ); @@ -553,7 +547,6 @@ describe('Sidebar section summaries', () => { onToggleCracker={vi.fn()} onMarkAllRead={vi.fn()} favorites={[]} - legacySortOrder="recent" /> ); @@ -586,7 +579,6 @@ describe('Sidebar section summaries', () => { onToggleCracker={vi.fn()} onMarkAllRead={vi.fn()} favorites={[]} - legacySortOrder="alpha" /> ); @@ -623,7 +615,6 @@ describe('Sidebar section summaries', () => { { type: 'contact', id: zed.public_key }, { type: 'contact', id: amy.public_key }, ] satisfies Favorite[], - legacySortOrder: 'recent' as const, }; const getFavoritesOrder = () => diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 26ec9f3..e0ae2d1 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -316,7 +316,6 @@ export interface AppSettings { max_radio_contacts: number; favorites: Favorite[]; auto_decrypt_dm_on_advert: boolean; - sidebar_sort_order: 'recent' | 'alpha'; last_message_times: Record; preferences_migrated: boolean; advert_interval: number; @@ -330,7 +329,6 @@ export interface AppSettings { export interface AppSettingsUpdate { max_radio_contacts?: number; auto_decrypt_dm_on_advert?: boolean; - sidebar_sort_order?: 'recent' | 'alpha'; advert_interval?: number; flood_scope?: string; blocked_keys?: string[]; diff --git a/tests/e2e/helpers/api.ts b/tests/e2e/helpers/api.ts index 36d25f7..816c5fa 100644 --- a/tests/e2e/helpers/api.ts +++ b/tests/e2e/helpers/api.ts @@ -222,7 +222,6 @@ export interface AppSettings { max_radio_contacts: number; favorites: Favorite[]; auto_decrypt_dm_on_advert: boolean; - sidebar_sort_order: string; last_message_times: Record; preferences_migrated: boolean; advert_interval: number; diff --git a/tests/e2e/specs/apprise.spec.ts b/tests/e2e/specs/apprise.spec.ts index 3da6a09..abb6817 100644 --- a/tests/e2e/specs/apprise.spec.ts +++ b/tests/e2e/specs/apprise.spec.ts @@ -25,6 +25,16 @@ test.describe('Apprise integration settings', () => { receiver.close(); }); + test.beforeEach(async () => { + // Clean up any stale configs from previous failed runs + const configs = await getFanoutConfigs(); + for (const c of configs.filter((c) => c.name === 'E2E Apprise')) { + try { + await deleteFanoutConfig(c.id); + } catch { /* ignore */ } + } + }); + test.afterEach(async () => { if (createdAppriseId) { try { @@ -66,16 +76,15 @@ test.describe('Apprise integration settings', () => { await page.getByRole('button', { name: /Save as Enabled/i }).click(); await expect(page.getByText('Integration saved and enabled')).toBeVisible(); - // Should be back on list view with our apprise config visible - await expect(page.getByText('E2E Apprise')).toBeVisible(); - await expect(page.getByText(appriseUrl)).toBeVisible(); - - // Clean up via API + // Capture ID for cleanup before assertions that might fail const configs = await getFanoutConfigs(); const apprise = configs.find((c) => c.name === 'E2E Apprise'); if (apprise) { createdAppriseId = apprise.id; } + + // Should be back on list view with our apprise config visible + await expect(fanoutHeader(page, 'E2E Apprise')).toBeVisible(); }); test('create apprise via API, verify options persist after edit', async ({ page }) => { diff --git a/tests/test_api.py b/tests/test_api.py index 4b82358..cc8aeaf 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -293,7 +293,6 @@ class TestDebugEndpoint: json={ "max_radio_contacts": 321, "auto_decrypt_dm_on_advert": True, - "sidebar_sort_order": "alpha", "advert_interval": 7200, "flood_scope": "US-CA", "blocked_keys": [pub_key], diff --git a/tests/test_contacts_router.py b/tests/test_contacts_router.py index 2161ab1..633b762 100644 --- a/tests/test_contacts_router.py +++ b/tests/test_contacts_router.py @@ -364,7 +364,7 @@ class TestDeleteContactCascade: assert len(await ContactAdvertPathRepository.get_recent_for_contact(KEY_A)) == 0 @pytest.mark.asyncio - async def test_delete_removes_direct_messages(self, test_db, client): + async def test_delete_preserves_direct_messages(self, test_db, client): await _insert_contact(KEY_A, "Alice") # Create a DM for this contact @@ -375,8 +375,6 @@ class TestDeleteContactCascade: sender_timestamp=1000, received_at=1000, ) - msgs = await MessageRepository.get_all(msg_type="PRIV", conversation_key=KEY_A) - assert len(msgs) == 1 with patch("app.routers.contacts.radio_manager") as mock_rm: mock_rm.is_connected = False @@ -387,9 +385,9 @@ class TestDeleteContactCascade: assert response.status_code == 200 - # DMs for the deleted contact should be gone + # DMs are preserved so they re-surface if the contact is re-added msgs = await MessageRepository.get_all(msg_type="PRIV", conversation_key=KEY_A) - assert len(msgs) == 0 + assert len(msgs) == 1 class TestMarkRead: diff --git a/tests/test_migrations.py b/tests/test_migrations.py index 6c3044e..d5d48ab 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -1249,8 +1249,8 @@ class TestMigration039: applied = await run_migrations(conn) - assert applied == 12 - assert await get_version(conn) == 50 + assert applied == 13 + assert await get_version(conn) == 51 cursor = await conn.execute( """ @@ -1321,8 +1321,8 @@ class TestMigration039: applied = await run_migrations(conn) - assert applied == 12 - assert await get_version(conn) == 50 + assert applied == 13 + assert await get_version(conn) == 51 cursor = await conn.execute( """ @@ -1388,8 +1388,8 @@ class TestMigration039: applied = await run_migrations(conn) - assert applied == 6 - assert await get_version(conn) == 50 + assert applied == 7 + assert await get_version(conn) == 51 cursor = await conn.execute( """ @@ -1441,8 +1441,8 @@ class TestMigration040: applied = await run_migrations(conn) - assert applied == 11 - assert await get_version(conn) == 50 + assert applied == 12 + assert await get_version(conn) == 51 await conn.execute( """ @@ -1503,8 +1503,8 @@ class TestMigration041: applied = await run_migrations(conn) - assert applied == 10 - assert await get_version(conn) == 50 + assert applied == 11 + assert await get_version(conn) == 51 await conn.execute( """ @@ -1556,8 +1556,8 @@ class TestMigration042: applied = await run_migrations(conn) - assert applied == 9 - assert await get_version(conn) == 50 + assert applied == 10 + assert await get_version(conn) == 51 await conn.execute( """ @@ -1696,8 +1696,8 @@ class TestMigration046: applied = await run_migrations(conn) - assert applied == 5 - assert await get_version(conn) == 50 + assert applied == 6 + assert await get_version(conn) == 51 cursor = await conn.execute( """ @@ -1790,8 +1790,8 @@ class TestMigration047: applied = await run_migrations(conn) - assert applied == 4 - assert await get_version(conn) == 50 + assert applied == 5 + assert await get_version(conn) == 51 cursor = await conn.execute( """ diff --git a/tests/test_repository.py b/tests/test_repository.py index a213113..cf72ea0 100644 --- a/tests/test_repository.py +++ b/tests/test_repository.py @@ -622,7 +622,6 @@ class TestAppSettingsRepository: "max_radio_contacts": 250, "favorites": "{not-json", "auto_decrypt_dm_on_advert": 1, - "sidebar_sort_order": "invalid", "last_message_times": "{also-not-json", "preferences_migrated": 0, "advert_interval": None, @@ -645,7 +644,6 @@ class TestAppSettingsRepository: assert settings.max_radio_contacts == 250 assert settings.favorites == [] assert settings.last_message_times == {} - assert settings.sidebar_sort_order == "recent" assert settings.advert_interval == 0 assert settings.last_advert_time == 0 @@ -680,7 +678,7 @@ class TestAppSettingsRepository: from app.models import AppSettings current = AppSettings(preferences_migrated=False) - migrated = AppSettings(preferences_migrated=True, sidebar_sort_order="recent") + migrated = AppSettings(preferences_migrated=True) with ( patch( @@ -704,7 +702,7 @@ class TestAppSettingsRepository: assert did_migrate is True assert result.preferences_migrated is True - assert mock_update.call_args.kwargs["sidebar_sort_order"] == "recent" + assert "sidebar_sort_order" not in mock_update.call_args.kwargs assert mock_update.call_args.kwargs["preferences_migrated"] is True diff --git a/tests/test_settings_router.py b/tests/test_settings_router.py index 36bc1b8..1c73d97 100644 --- a/tests/test_settings_router.py +++ b/tests/test_settings_router.py @@ -177,7 +177,6 @@ class TestMigratePreferences: assert response.migrated is True assert response.settings.preferences_migrated is True - assert response.settings.sidebar_sort_order == "alpha" assert len(response.settings.favorites) == 1 assert response.settings.favorites[0].type == "contact" assert response.settings.favorites[0].id == "aa" * 32