fix localstorage and profile settings
This commit is contained in:
@@ -372,6 +372,15 @@ CREATE TABLE IF NOT EXISTS traffic_history (
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_traffic_history_profile_created ON traffic_history(profile_id, created_at);
|
CREATE INDEX IF NOT EXISTS idx_traffic_history_profile_created ON traffic_history(profile_id, created_at);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS profile_speed_limits (
|
||||||
|
profile_id INTEGER PRIMARY KEY,
|
||||||
|
down_limit INTEGER DEFAULT 0,
|
||||||
|
up_limit INTEGER DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
FOREIGN KEY(profile_id) REFERENCES rtorrent_profiles(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS transfer_speed_peaks (
|
CREATE TABLE IF NOT EXISTS transfer_speed_peaks (
|
||||||
profile_id INTEGER PRIMARY KEY,
|
profile_id INTEGER PRIMARY KEY,
|
||||||
session_started_at TEXT NOT NULL,
|
session_started_at TEXT NOT NULL,
|
||||||
|
|||||||
@@ -133,10 +133,26 @@ def migrate_operation_log_split_retention(conn: sqlite3.Connection) -> bool:
|
|||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_profile_speed_limits_table(conn: sqlite3.Connection) -> bool:
|
||||||
|
existing = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='profile_speed_limits'").fetchone()
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS profile_speed_limits (
|
||||||
|
profile_id INTEGER PRIMARY KEY,
|
||||||
|
down_limit INTEGER DEFAULT 0,
|
||||||
|
up_limit INTEGER DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
FOREIGN KEY(profile_id) REFERENCES rtorrent_profiles(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
return existing is None
|
||||||
|
|
||||||
|
|
||||||
MIGRATIONS: tuple[Migration, ...] = (
|
MIGRATIONS: tuple[Migration, ...] = (
|
||||||
migrate_disk_monitor_preferences_to_profile_scope,
|
migrate_disk_monitor_preferences_to_profile_scope,
|
||||||
migrate_profile_preferences_sidebar_columns,
|
migrate_profile_preferences_sidebar_columns,
|
||||||
migrate_operation_log_split_retention,
|
migrate_operation_log_split_retention,
|
||||||
|
migrate_profile_speed_limits_table,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from ._shared import *
|
from ._shared import *
|
||||||
|
from ..services import profile_speed_limits
|
||||||
from ..services import pdf_preview_links, torrent_creator
|
from ..services import pdf_preview_links, torrent_creator
|
||||||
from ..services.reverse_dns import attach_reverse_dns
|
from ..services.reverse_dns import attach_reverse_dns
|
||||||
|
|
||||||
@@ -667,8 +668,10 @@ def speed_limits():
|
|||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
job_id = enqueue("set_limits", profile["id"], {"down": data.get("down"), "up": data.get("up")})
|
limits = profile_speed_limits.save_limits(profile["id"], data.get("down"), data.get("up"))
|
||||||
return ok({"job_id": job_id})
|
# Note: Manual speed limits are stored once per rTorrent profile, so every user opening this profile sees and applies the same values.
|
||||||
|
job_id = enqueue("set_limits", profile["id"], {"down": limits["down"], "up": limits["up"]})
|
||||||
|
return ok({"job_id": job_id, "limits": limits})
|
||||||
|
|
||||||
|
|
||||||
def _user_disk_status(profile: dict) -> dict:
|
def _user_disk_status(profile: dict) -> dict:
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ..db import connect, utcnow
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_limit(value: object) -> int:
|
||||||
|
try:
|
||||||
|
limit = int(float(value or 0))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0
|
||||||
|
return max(0, limit)
|
||||||
|
|
||||||
|
|
||||||
|
def get_limits(profile_id: int | None) -> dict:
|
||||||
|
profile_id = int(profile_id or 0)
|
||||||
|
if not profile_id:
|
||||||
|
return {"down": 0, "up": 0, "configured": False}
|
||||||
|
with connect() as conn:
|
||||||
|
row = conn.execute("SELECT down_limit, up_limit FROM profile_speed_limits WHERE profile_id=?", (profile_id,)).fetchone()
|
||||||
|
if not row:
|
||||||
|
return {"down": 0, "up": 0, "configured": False}
|
||||||
|
return {"down": int(row.get("down_limit") or 0), "up": int(row.get("up_limit") or 0), "configured": True}
|
||||||
|
|
||||||
|
|
||||||
|
def save_limits(profile_id: int, down: object, up: object) -> dict:
|
||||||
|
profile_id = int(profile_id or 0)
|
||||||
|
if not profile_id:
|
||||||
|
raise ValueError("Missing profile id")
|
||||||
|
clean = {"down": normalize_limit(down), "up": normalize_limit(up), "configured": True}
|
||||||
|
now = utcnow()
|
||||||
|
with connect() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO profile_speed_limits(profile_id, down_limit, up_limit, created_at, updated_at)
|
||||||
|
VALUES(?,?,?,?,?)
|
||||||
|
ON CONFLICT(profile_id) DO UPDATE SET
|
||||||
|
down_limit=excluded.down_limit,
|
||||||
|
up_limit=excluded.up_limit,
|
||||||
|
updated_at=excluded.updated_at
|
||||||
|
""",
|
||||||
|
(profile_id, clean["down"], clean["up"], now, now),
|
||||||
|
)
|
||||||
|
return clean
|
||||||
|
|
||||||
|
|
||||||
|
def delete_limits(profile_id: int) -> None:
|
||||||
|
with connect() as conn:
|
||||||
|
conn.execute("DELETE FROM profile_speed_limits WHERE profile_id=?", (int(profile_id or 0),))
|
||||||
@@ -9,7 +9,7 @@ from .preferences import active_profile, get_profile
|
|||||||
from ..db import default_user_id
|
from ..db import default_user_id
|
||||||
from .torrent_cache import torrent_cache
|
from .torrent_cache import torrent_cache
|
||||||
from .torrent_summary import cached_summary
|
from .torrent_summary import cached_summary
|
||||||
from . import rtorrent, smart_queue, traffic_history, automation_rules, torrent_stats, auth, speed_peaks, poller_control, download_planner
|
from . import rtorrent, smart_queue, traffic_history, automation_rules, torrent_stats, auth, speed_peaks, poller_control, download_planner, profile_speed_limits
|
||||||
|
|
||||||
|
|
||||||
def _profile_room(profile_id: int) -> str:
|
def _profile_room(profile_id: int) -> str:
|
||||||
@@ -37,6 +37,14 @@ def _emit_profile(socketio, event: str, payload: dict, profile_id: int) -> None:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_configured_speed_limits(profile: dict) -> None:
|
||||||
|
limits = profile_speed_limits.get_limits(int(profile.get("id") or 0))
|
||||||
|
if not limits.get("configured"):
|
||||||
|
return
|
||||||
|
# Note: Profile-level speed limits are re-applied when the profile is opened so they are not tied to a specific user session.
|
||||||
|
rtorrent.set_limits(profile, limits.get("down"), limits.get("up"))
|
||||||
|
|
||||||
|
|
||||||
def _run_slow_profile_tasks(socketio, profile: dict, profile_id: int) -> None:
|
def _run_slow_profile_tasks(socketio, profile: dict, profile_id: int) -> None:
|
||||||
state = poller_control.state_for(profile_id)
|
state = poller_control.state_for(profile_id)
|
||||||
# Note: Background checks keep the profile owner so bypass/admin profiles do not enqueue jobs as the fallback user.
|
# Note: Background checks keep the profile owner so bypass/admin profiles do not enqueue jobs as the fallback user.
|
||||||
@@ -282,10 +290,14 @@ def register_socketio_handlers(socketio):
|
|||||||
if not profile:
|
if not profile:
|
||||||
emit("profile_required", {"ok": True, "profiles": []})
|
emit("profile_required", {"ok": True, "profiles": []})
|
||||||
return
|
return
|
||||||
|
try:
|
||||||
|
_apply_configured_speed_limits(profile)
|
||||||
|
except Exception as exc:
|
||||||
|
emit("rtorrent_error", {"profile_id": profile["id"], "error": str(exc)})
|
||||||
rows = torrent_cache.snapshot(profile["id"])
|
rows = torrent_cache.snapshot(profile["id"])
|
||||||
emit("torrent_snapshot", {"profile_id": profile["id"], "torrents": rows, "summary": cached_summary(profile["id"], rows), "speed_status": _speed_status_from_rows(profile["id"], rows)})
|
emit("torrent_snapshot", {"profile_id": profile["id"], "torrents": rows, "summary": cached_summary(profile["id"], rows), "speed_status": _speed_status_from_rows(profile["id"], rows)})
|
||||||
emit("poller_settings", {"settings": poller_control.get_settings(int(profile["id"])), "runtime": poller_control.snapshot(int(profile["id"]))})
|
emit("poller_settings", {"profile_id": int(profile["id"]), "settings": poller_control.get_settings(int(profile["id"])), "runtime": poller_control.snapshot(int(profile["id"]))})
|
||||||
emit("download_plan_update", {"settings": download_planner.get_settings(int(profile["id"]))})
|
emit("download_plan_update", {"profile_id": int(profile["id"]), "settings": download_planner.get_settings(int(profile["id"]))})
|
||||||
|
|
||||||
@socketio.on("select_profile")
|
@socketio.on("select_profile")
|
||||||
def handle_select_profile(data):
|
def handle_select_profile(data):
|
||||||
@@ -304,8 +316,12 @@ def register_socketio_handlers(socketio):
|
|||||||
emit("rtorrent_error", {"error": "Profile access denied or profile does not exist"})
|
emit("rtorrent_error", {"error": "Profile access denied or profile does not exist"})
|
||||||
return
|
return
|
||||||
join_room(_profile_room(profile_id))
|
join_room(_profile_room(profile_id))
|
||||||
|
try:
|
||||||
|
_apply_configured_speed_limits(profile)
|
||||||
|
except Exception as exc:
|
||||||
|
emit("rtorrent_error", {"profile_id": profile_id, "error": str(exc)})
|
||||||
diff = torrent_cache.refresh(profile)
|
diff = torrent_cache.refresh(profile)
|
||||||
rows = torrent_cache.snapshot(profile_id)
|
rows = torrent_cache.snapshot(profile_id)
|
||||||
emit("torrent_snapshot", {"profile_id": profile_id, "torrents": rows, "summary": cached_summary(profile_id, rows, force=True), "speed_status": _speed_status_from_rows(profile_id, rows), "error": diff.get("error", "")})
|
emit("torrent_snapshot", {"profile_id": profile_id, "torrents": rows, "summary": cached_summary(profile_id, rows, force=True), "speed_status": _speed_status_from_rows(profile_id, rows), "error": diff.get("error", "")})
|
||||||
emit("poller_settings", {"settings": poller_control.get_settings(profile_id), "runtime": poller_control.snapshot(profile_id)})
|
emit("poller_settings", {"profile_id": profile_id, "settings": poller_control.get_settings(profile_id), "runtime": poller_control.snapshot(profile_id)})
|
||||||
emit("download_plan_update", {"settings": download_planner.get_settings(profile_id)})
|
emit("download_plan_update", {"profile_id": profile_id, "settings": download_planner.get_settings(profile_id)})
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
export const preferencesToolsSource = " async function loadPreferences(){\n try{\n const j=await (await fetch(`/api/preferences?_=${Date.now()}`, {cache:'no-store'})).json();\n const prefs=j.preferences||{};\n portCheckEnabled=!!Number(prefs.port_check_enabled ?? portCheckEnabled);\n reverseDnsEnabled=!!Number(prefs.reverse_dns_enabled ?? (reverseDnsEnabled?1:0));\n if($('reverseDnsEnabled')) $('reverseDnsEnabled').checked=reverseDnsEnabled;\n automationToastsEnabled=Number(prefs.automation_toasts_enabled ?? (automationToastsEnabled?1:0))!==0;\n smartQueueToastsEnabled=Number(prefs.smart_queue_toasts_enabled ?? (smartQueueToastsEnabled?1:0))!==0;\n easterEggEnabled=Number(prefs.easter_egg_enabled ?? (easterEggEnabled?1:0))!==0;\n easterEggLoadingImageUrl=String(prefs.easter_egg_loading_image_url ?? easterEggLoadingImageUrl ?? '').trim();\n easterEggClickImageUrl=String(prefs.easter_egg_click_image_url ?? easterEggClickImageUrl ?? '').trim();\n diskMonitorMode=prefs.disk_monitor_mode||diskMonitorMode;\n diskMonitorSelectedPath=prefs.disk_monitor_selected_path||'';\n diskMonitorOwnerLabel=String(prefs.disk_monitor_owner_label||'').trim();\n try{ diskMonitorPaths=JSON.parse(prefs.disk_monitor_paths_json||'[]'); }catch(_){ diskMonitorPaths=[]; }\n bootstrapTheme=prefs.bootstrap_theme||bootstrapTheme;\n fontFamily=prefs.font_family||fontFamily;\n interfaceScale=Number(prefs.interface_scale||interfaceScale||100);\n torrentListFontSize=clampTorrentListFontSize(prefs.torrent_list_font_size||torrentListFontSize||13);\n compactTorrentListEnabled=Number(prefs.compact_torrent_list_enabled ?? (compactTorrentListEnabled?1:0))!==0;\n try{ footerItems={...DEFAULT_FOOTER_ITEMS,...JSON.parse(prefs.footer_items_json||'{}')}; }catch(_){ footerItems={...DEFAULT_FOOTER_ITEMS}; }\n }catch(e){ console.warn('Preference load failed', e); }\n if($('portCheckEnabled')) $('portCheckEnabled').checked=portCheckEnabled; if($('automationToastsEnabled')) $('automationToastsEnabled').checked=automationToastsEnabled; if($('smartQueueToastsEnabled')) $('smartQueueToastsEnabled').checked=smartQueueToastsEnabled; if($('easterEggEnabled')) $('easterEggEnabled').checked=easterEggEnabled; if($('easterEggLoadingImageUrl')) $('easterEggLoadingImageUrl').value=easterEggLoadingImageUrl; if($('easterEggClickImageUrl')) $('easterEggClickImageUrl').value=easterEggClickImageUrl; if($('diskMonitorMode')) $('diskMonitorMode').value=diskMonitorMode; if($('diskMonitorSelectedPath')) $('diskMonitorSelectedPath').value=diskMonitorSelectedPath; renderDiskMonitorPaths(); applyInitialLoaderEasterEgg(); scheduleRender(true); applyBootstrapTheme(bootstrapTheme); applyFontFamily(fontFamily); applyInterfaceScale(interfaceScale); applyTorrentListFontSize(torrentListFontSize); applyCompactTorrentList(compactTorrentListEnabled); renderFooterPreferences(); applyFooterPreferences(); await loadPortCheck(false); }";
|
export const preferencesToolsSource = " async function loadPreferences(){\n try{\n const j=await (await fetch(`/api/preferences?_=${Date.now()}`, {cache:'no-store'})).json();\n const prefs=j.preferences||{};\n portCheckEnabled=!!Number(prefs.port_check_enabled ?? portCheckEnabled);\n reverseDnsEnabled=!!Number(prefs.reverse_dns_enabled ?? (reverseDnsEnabled?1:0));\n if($('reverseDnsEnabled')) $('reverseDnsEnabled').checked=reverseDnsEnabled;\n automationToastsEnabled=Number(prefs.automation_toasts_enabled ?? (automationToastsEnabled?1:0))!==0;\n smartQueueToastsEnabled=Number(prefs.smart_queue_toasts_enabled ?? (smartQueueToastsEnabled?1:0))!==0;\n easterEggEnabled=Number(prefs.easter_egg_enabled ?? (easterEggEnabled?1:0))!==0;\n easterEggLoadingImageUrl=String(prefs.easter_egg_loading_image_url ?? easterEggLoadingImageUrl ?? '').trim();\n easterEggClickImageUrl=String(prefs.easter_egg_click_image_url ?? easterEggClickImageUrl ?? '').trim();\n diskMonitorMode=prefs.disk_monitor_mode||diskMonitorMode;\n diskMonitorSelectedPath=prefs.disk_monitor_selected_path||'';\n diskMonitorOwnerLabel=String(prefs.disk_monitor_owner_label||'').trim();\n try{ diskMonitorPaths=JSON.parse(prefs.disk_monitor_paths_json||'[]'); }catch(_){ diskMonitorPaths=[]; }\n bootstrapTheme=prefs.bootstrap_theme||bootstrapTheme;\n fontFamily=prefs.font_family||fontFamily;\n interfaceScale=Number(prefs.interface_scale||interfaceScale||100);\n torrentListFontSize=clampTorrentListFontSize(prefs.torrent_list_font_size||torrentListFontSize||13);\n compactTorrentListEnabled=Number(prefs.compact_torrent_list_enabled ?? (compactTorrentListEnabled?1:0))!==0;\n const nextFilter = String(prefs.active_filter || 'all');\n // Note: Profile switches refresh the in-memory filter/sort immediately, matching the state that a full page reload would load.\n activeTrackerFilter = nextFilter.startsWith('tracker:') ? nextFilter.slice(8) : '';\n activeFilter = nextFilter.startsWith('tracker:') ? 'all' : nextFilter;\n mobileActiveFilterKey = nextFilter || 'all';\n try{\n const nextSort = typeof prefs.torrent_sort_json === 'string' ? JSON.parse(prefs.torrent_sort_json || '{}') : (prefs.torrent_sort_json || {});\n if(SORT_KEYS.has(nextSort.key)) sortState = {key: nextSort.key, dir: Number(nextSort.dir) < 0 ? -1 : 1};\n }catch(_){}\n try{ footerItems={...DEFAULT_FOOTER_ITEMS,...JSON.parse(prefs.footer_items_json||'{}')}; }catch(_){ footerItems={...DEFAULT_FOOTER_ITEMS}; }\n }catch(e){ console.warn('Preference load failed', e); }\n if($('portCheckEnabled')) $('portCheckEnabled').checked=portCheckEnabled; if($('automationToastsEnabled')) $('automationToastsEnabled').checked=automationToastsEnabled; if($('smartQueueToastsEnabled')) $('smartQueueToastsEnabled').checked=smartQueueToastsEnabled; if($('easterEggEnabled')) $('easterEggEnabled').checked=easterEggEnabled; if($('easterEggLoadingImageUrl')) $('easterEggLoadingImageUrl').value=easterEggLoadingImageUrl; if($('easterEggClickImageUrl')) $('easterEggClickImageUrl').value=easterEggClickImageUrl; if($('diskMonitorMode')) $('diskMonitorMode').value=diskMonitorMode; if($('diskMonitorSelectedPath')) $('diskMonitorSelectedPath').value=diskMonitorSelectedPath; renderDiskMonitorPaths(); applyInitialLoaderEasterEgg(); scheduleRender(true); applyBootstrapTheme(bootstrapTheme); applyFontFamily(fontFamily); applyInterfaceScale(interfaceScale); applyTorrentListFontSize(torrentListFontSize); applyCompactTorrentList(compactTorrentListEnabled); renderFooterPreferences(); applyFooterPreferences(); await loadPortCheck(false); }";
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user