mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Move to hour-resolution adverts
This commit is contained in:
@@ -177,6 +177,13 @@ async def run_migrations(conn: aiosqlite.Connection) -> int:
|
||||
await set_version(conn, 20)
|
||||
applied += 1
|
||||
|
||||
# Migration 21: Enforce minimum 1-hour advert interval
|
||||
if version < 21:
|
||||
logger.info("Applying migration 21: enforce minimum 1-hour advert interval")
|
||||
await _migrate_021_enforce_min_advert_interval(conn)
|
||||
await set_version(conn, 21)
|
||||
applied += 1
|
||||
|
||||
if applied > 0:
|
||||
logger.info(
|
||||
"Applied %d migration(s), schema now at version %d", applied, await get_version(conn)
|
||||
@@ -1252,3 +1259,27 @@ async def _migrate_020_enable_wal_and_auto_vacuum(conn: aiosqlite.Connection) ->
|
||||
logger.info("Journal mode set to %s", mode)
|
||||
|
||||
await conn.commit()
|
||||
|
||||
|
||||
async def _migrate_021_enforce_min_advert_interval(conn: aiosqlite.Connection) -> None:
|
||||
"""
|
||||
Enforce minimum 1-hour advert interval.
|
||||
|
||||
Any advert_interval between 1 and 3599 is clamped up to 3600 (1 hour).
|
||||
Zero (disabled) is left unchanged.
|
||||
"""
|
||||
# Guard: app_settings table may not exist if running against a very old schema
|
||||
# (it's created in migration 9). The UPDATE is harmless if the table exists
|
||||
# but has no rows, but will error if the table itself is missing.
|
||||
cursor = await conn.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='app_settings'"
|
||||
)
|
||||
if await cursor.fetchone() is None:
|
||||
logger.debug("app_settings table does not exist yet, skipping advert_interval clamp")
|
||||
return
|
||||
|
||||
await conn.execute(
|
||||
"UPDATE app_settings SET advert_interval = 3600 WHERE advert_interval > 0 AND advert_interval < 3600"
|
||||
)
|
||||
await conn.commit()
|
||||
logger.debug("Clamped advert_interval to minimum 3600 seconds")
|
||||
|
||||
@@ -41,6 +41,11 @@ _advert_task: asyncio.Task | None = None
|
||||
# We still need to periodically check if it's been enabled
|
||||
ADVERT_CHECK_INTERVAL = 60
|
||||
|
||||
# Minimum allowed advertisement interval (1 hour).
|
||||
# Even if the database has a shorter value, we silently refuse to advertise
|
||||
# more frequently than this.
|
||||
MIN_ADVERT_INTERVAL = 3600
|
||||
|
||||
# Counter to pause polling during repeater operations (supports nested pauses)
|
||||
_polling_pause_count: int = 0
|
||||
|
||||
@@ -384,6 +389,9 @@ async def send_advertisement(force: bool = False) -> bool:
|
||||
logger.debug("Advertisement skipped: periodic advertising is disabled")
|
||||
return False
|
||||
|
||||
# Enforce minimum interval floor
|
||||
interval = max(interval, MIN_ADVERT_INTERVAL)
|
||||
|
||||
# Check if enough time has passed
|
||||
elapsed = now - last_time
|
||||
if elapsed < interval:
|
||||
|
||||
@@ -52,7 +52,7 @@ class AppSettingsUpdate(BaseModel):
|
||||
advert_interval: int | None = Field(
|
||||
default=None,
|
||||
ge=0,
|
||||
description="Periodic advertisement interval in seconds (0 = disabled)",
|
||||
description="Periodic advertisement interval in seconds (0 = disabled, minimum 3600)",
|
||||
)
|
||||
bots: list[BotConfig] | None = Field(
|
||||
default=None,
|
||||
@@ -111,8 +111,12 @@ async def update_settings(update: AppSettingsUpdate) -> AppSettings:
|
||||
kwargs["sidebar_sort_order"] = update.sidebar_sort_order
|
||||
|
||||
if update.advert_interval is not None:
|
||||
logger.info("Updating advert_interval to %d", update.advert_interval)
|
||||
kwargs["advert_interval"] = update.advert_interval
|
||||
# Enforce minimum 1-hour interval; 0 means disabled
|
||||
interval = update.advert_interval
|
||||
if 0 < interval < 3600:
|
||||
interval = 3600
|
||||
logger.info("Updating advert_interval to %d", interval)
|
||||
kwargs["advert_interval"] = interval
|
||||
|
||||
if update.bots is not None:
|
||||
validate_all_bots(update.bots)
|
||||
|
||||
@@ -151,8 +151,8 @@ export function SettingsModal(props: SettingsModalProps) {
|
||||
getReopenLastConversationEnabled
|
||||
);
|
||||
|
||||
// Advertisement interval state
|
||||
const [advertInterval, setAdvertInterval] = useState('0');
|
||||
// Advertisement interval state (displayed in hours, stored as seconds in DB)
|
||||
const [advertIntervalHours, setAdvertIntervalHours] = useState('0');
|
||||
|
||||
// Bot state
|
||||
const DEFAULT_BOT_CODE = `def bot(
|
||||
@@ -218,7 +218,7 @@ export function SettingsModal(props: SettingsModalProps) {
|
||||
if (appSettings) {
|
||||
setMaxRadioContacts(String(appSettings.max_radio_contacts));
|
||||
setAutoDecryptOnAdvert(appSettings.auto_decrypt_dm_on_advert);
|
||||
setAdvertInterval(String(appSettings.advert_interval));
|
||||
setAdvertIntervalHours(String(Math.round(appSettings.advert_interval / 3600)));
|
||||
setBots(appSettings.bots || []);
|
||||
}
|
||||
}, [appSettings]);
|
||||
@@ -394,9 +394,10 @@ export function SettingsModal(props: SettingsModalProps) {
|
||||
const update: RadioConfigUpdate = { name };
|
||||
await onSave(update);
|
||||
|
||||
// Save advert interval to app settings
|
||||
const newAdvertInterval = parseInt(advertInterval, 10);
|
||||
if (!isNaN(newAdvertInterval) && newAdvertInterval !== appSettings?.advert_interval) {
|
||||
// Save advert interval to app settings (convert hours to seconds)
|
||||
const hours = parseInt(advertIntervalHours, 10);
|
||||
const newAdvertInterval = isNaN(hours) ? 0 : hours * 3600;
|
||||
if (newAdvertInterval !== appSettings?.advert_interval) {
|
||||
await onSaveAppSettings({ advert_interval: newAdvertInterval });
|
||||
}
|
||||
|
||||
@@ -865,15 +866,15 @@ export function SettingsModal(props: SettingsModalProps) {
|
||||
id="advert-interval"
|
||||
type="number"
|
||||
min="0"
|
||||
value={advertInterval}
|
||||
onChange={(e) => setAdvertInterval(e.target.value)}
|
||||
value={advertIntervalHours}
|
||||
onChange={(e) => setAdvertIntervalHours(e.target.value)}
|
||||
className="w-28"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">seconds (0 = off)</span>
|
||||
<span className="text-sm text-muted-foreground">hours (0 = off)</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
How often to automatically advertise presence. Set to 0 to disable. Recommended:
|
||||
86400 (24 hours) or higher.
|
||||
How often to automatically advertise presence. Set to 0 to disable. Minimum: 1
|
||||
hour. Recommended: 24 hours or higher.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -100,8 +100,8 @@ class TestMigration001:
|
||||
# Run migrations
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
assert applied == 20 # All 17 migrations run
|
||||
assert await get_version(conn) == 20
|
||||
assert applied == 21 # All migrations run
|
||||
assert await get_version(conn) == 21
|
||||
|
||||
# Verify columns exist by inserting and selecting
|
||||
await conn.execute(
|
||||
@@ -183,9 +183,9 @@ class TestMigration001:
|
||||
applied1 = await run_migrations(conn)
|
||||
applied2 = await run_migrations(conn)
|
||||
|
||||
assert applied1 == 20 # All 20 migrations run
|
||||
assert applied1 == 21 # All 21 migrations run
|
||||
assert applied2 == 0 # No migrations on second run
|
||||
assert await get_version(conn) == 20
|
||||
assert await get_version(conn) == 21
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -245,9 +245,9 @@ class TestMigration001:
|
||||
# Run migrations - should not fail
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
# All 17 migrations applied (version incremented) but no error
|
||||
assert applied == 20
|
||||
assert await get_version(conn) == 20
|
||||
# All migrations applied (version incremented) but no error
|
||||
assert applied == 21
|
||||
assert await get_version(conn) == 21
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -374,10 +374,10 @@ class TestMigration013:
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
# Run migration 13 (plus 14-20 which also run)
|
||||
# Run migration 13 (plus 14-21 which also run)
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 8
|
||||
assert await get_version(conn) == 20
|
||||
assert applied == 9
|
||||
assert await get_version(conn) == 21
|
||||
|
||||
# Verify bots array was created with migrated data
|
||||
cursor = await conn.execute("SELECT bots FROM app_settings WHERE id = 1")
|
||||
@@ -497,7 +497,7 @@ class TestMigration018:
|
||||
assert await cursor.fetchone() is not None
|
||||
|
||||
await run_migrations(conn)
|
||||
assert await get_version(conn) == 20
|
||||
assert await get_version(conn) == 21
|
||||
|
||||
# Verify autoindex is gone
|
||||
cursor = await conn.execute(
|
||||
@@ -571,8 +571,8 @@ class TestMigration018:
|
||||
await conn.commit()
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 3 # Migrations 18+19+20 run (18+19 skip internally)
|
||||
assert await get_version(conn) == 20
|
||||
assert applied == 4 # Migrations 18+19+20+21 run (18+19 skip internally)
|
||||
assert await get_version(conn) == 21
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -644,7 +644,7 @@ class TestMigration019:
|
||||
assert await cursor.fetchone() is not None
|
||||
|
||||
await run_migrations(conn)
|
||||
assert await get_version(conn) == 20
|
||||
assert await get_version(conn) == 21
|
||||
|
||||
# Verify autoindex is gone
|
||||
cursor = await conn.execute(
|
||||
@@ -710,8 +710,8 @@ class TestMigration020:
|
||||
assert (await cursor.fetchone())[0] == "delete"
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 1
|
||||
assert await get_version(conn) == 20
|
||||
assert applied == 2 # Migrations 20+21
|
||||
assert await get_version(conn) == 21
|
||||
|
||||
# Verify WAL mode
|
||||
cursor = await conn.execute("PRAGMA journal_mode")
|
||||
@@ -741,7 +741,7 @@ class TestMigration020:
|
||||
await set_version(conn, 20)
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 0 # Already at version 20
|
||||
assert applied == 1 # Migration 21 still runs
|
||||
|
||||
# Still WAL + INCREMENTAL
|
||||
cursor = await conn.execute("PRAGMA journal_mode")
|
||||
|
||||
@@ -47,6 +47,21 @@ class TestUpdateSettings:
|
||||
assert result.max_radio_contacts == 321
|
||||
assert result.advert_interval == 3600
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_advert_interval_below_minimum_is_clamped_to_one_hour(self, test_db):
|
||||
result = await update_settings(AppSettingsUpdate(advert_interval=600))
|
||||
assert result.advert_interval == 3600
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_advert_interval_zero_stays_disabled(self, test_db):
|
||||
result = await update_settings(AppSettingsUpdate(advert_interval=0))
|
||||
assert result.advert_interval == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_advert_interval_above_minimum_is_preserved(self, test_db):
|
||||
result = await update_settings(AppSettingsUpdate(advert_interval=86400))
|
||||
assert result.advert_interval == 86400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_patch_returns_current_settings(self, test_db):
|
||||
result = await update_settings(AppSettingsUpdate())
|
||||
|
||||
Reference in New Issue
Block a user