surge refill
This commit is contained in:
+8
-11
@@ -16,11 +16,9 @@ from .config import (
|
||||
SOCKETIO_CORS_ALLOWED_ORIGINS,
|
||||
)
|
||||
from .db import init_db
|
||||
from .services.frontend_assets import asset_path, bootstrap_css_path, validate_offline_assets
|
||||
from .utils import file_md5
|
||||
from .services.frontend_assets import asset_path, bootstrap_css_path, static_hash, validate_offline_assets
|
||||
|
||||
socketio = SocketIO(cors_allowed_origins=SOCKETIO_CORS_ALLOWED_ORIGINS, ping_timeout=30, async_mode="threading")
|
||||
_static_md5_cache: dict[tuple, str] = {}
|
||||
|
||||
|
||||
def _wants_json_response() -> bool:
|
||||
@@ -78,17 +76,15 @@ def create_app() -> Flask:
|
||||
|
||||
@app.context_processor
|
||||
def static_helpers():
|
||||
def current_static_hash() -> str:
|
||||
return static_hash(Path(app.static_folder or ""))
|
||||
|
||||
def static_url(filename: str) -> str:
|
||||
path = Path(app.static_folder or "") / filename
|
||||
try:
|
||||
stat = path.stat()
|
||||
key = (filename, stat.st_mtime_ns, stat.st_size)
|
||||
version = _static_md5_cache.get(key)
|
||||
if not version:
|
||||
_static_md5_cache.clear()
|
||||
version = file_md5(path)
|
||||
_static_md5_cache[key] = version
|
||||
return url_for("static", filename=filename, v=version)
|
||||
path.stat()
|
||||
# Note: A single static hash keeps module imports, CSS and local libraries on the same cache version.
|
||||
return url_for("static", filename=filename, v=current_static_hash())
|
||||
except OSError:
|
||||
return url_for("static", filename=filename)
|
||||
|
||||
@@ -104,6 +100,7 @@ def create_app() -> Flask:
|
||||
"static_url": static_url,
|
||||
"frontend_asset_url": frontend_asset_url,
|
||||
"bootstrap_theme_url": bootstrap_theme_url,
|
||||
"static_hash": current_static_hash,
|
||||
}
|
||||
|
||||
@app.after_request
|
||||
|
||||
@@ -1359,6 +1359,9 @@
|
||||
},
|
||||
"settings": {
|
||||
"$ref": "#/components/schemas/SmartQueueSettings"
|
||||
},
|
||||
"surge_refill_remaining_seconds": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -1422,6 +1425,17 @@
|
||||
"stop_batch_size": {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"surge_refill_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"surge_refill_interval_minutes": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"surge_refill_batch_size": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -7479,6 +7493,38 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/static_hash": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"System"
|
||||
],
|
||||
"summary": "Get static asset version hash",
|
||||
"description": "Returns the current hash for app static assets. Clients can compare it with window.PYTORRENT.staticHash and reload when it changes.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Static asset hash",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"static_hash": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ._shared import *
|
||||
from flask import current_app
|
||||
from pathlib import Path
|
||||
from ..services import operation_logs
|
||||
from ..services.frontend_assets import static_hash
|
||||
|
||||
@bp.get("/system/disk")
|
||||
def system_disk():
|
||||
@@ -46,6 +49,13 @@ def system_status():
|
||||
|
||||
|
||||
|
||||
@bp.get("/static_hash")
|
||||
def static_hash_get():
|
||||
# Note: The frontend uses this lightweight version to detect changed static assets on mobile browsers.
|
||||
value = static_hash(Path(current_app.static_folder or ""))
|
||||
return ok({"static_hash": value, "version": value})
|
||||
|
||||
|
||||
@bp.get("/health")
|
||||
def health_check():
|
||||
# Note: Lightweight health endpoint avoids rTorrent calls, making it safe for frequent monitoring.
|
||||
|
||||
@@ -188,3 +188,36 @@ def validate_offline_assets() -> None:
|
||||
"Run: ./scripts/download_frontend_libs.py or ./install.sh\n"
|
||||
f"Missing files:\n{preview}{extra}"
|
||||
)
|
||||
|
||||
|
||||
_STATIC_HASH_CACHE: dict[tuple[str, int], str] = {}
|
||||
|
||||
def static_hash(static_root: Path | None = None) -> str:
|
||||
"""Return one short hash for all app static files.
|
||||
|
||||
Note: This value is used as the shared browser-cache version, so any static file
|
||||
change invalidates app.js imports, CSS and local frontend assets together.
|
||||
"""
|
||||
import hashlib
|
||||
|
||||
root = static_root or (BASE_DIR / "pytorrent" / "static")
|
||||
files = [path for path in root.rglob("*") if path.is_file() and "tracker_favicons" not in path.parts]
|
||||
fingerprint = f"{root}:{sum(path.stat().st_mtime_ns for path in files)}:{sum(path.stat().st_size for path in files)}"
|
||||
cached = _STATIC_HASH_CACHE.get((fingerprint, len(files)))
|
||||
if cached:
|
||||
return cached
|
||||
digest = hashlib.sha256()
|
||||
for path in sorted(files):
|
||||
rel = path.relative_to(root).as_posix()
|
||||
stat = path.stat()
|
||||
digest.update(rel.encode("utf-8"))
|
||||
digest.update(str(stat.st_size).encode("ascii"))
|
||||
digest.update(str(stat.st_mtime_ns).encode("ascii"))
|
||||
try:
|
||||
digest.update(path.read_bytes())
|
||||
except OSError:
|
||||
continue
|
||||
value = digest.hexdigest()[:16]
|
||||
_STATIC_HASH_CACHE.clear()
|
||||
_STATIC_HASH_CACHE[(fingerprint, len(files))] = value
|
||||
return value
|
||||
|
||||
+109
-175
@@ -1,186 +1,120 @@
|
||||
import { stateCoreSource } from './stateCore.js';
|
||||
import { columnStateSource } from './columnState.js';
|
||||
import { runtimeStateSource } from './runtimeState.js';
|
||||
import { sharedUiSource } from './sharedUi.js';
|
||||
import { torrentFilterHelpersSource } from './torrentFilterHelpers.js';
|
||||
import { torrentFilterUiSource } from './torrentFilterUi.js';
|
||||
import { torrentTrackerFiltersSource } from './torrentTrackerFilters.js';
|
||||
import { torrentTableStateSource } from './torrentTableState.js';
|
||||
import { torrentActionStateSource } from './torrentActionState.js';
|
||||
import { torrentRowRendererSource } from './torrentRowRenderer.js';
|
||||
import { torrentTableRendererSource } from './torrentTableRenderer.js';
|
||||
import { mobileSource } from './mobile.js';
|
||||
import { messagesSource } from './messages.js';
|
||||
import { torrentAddSource } from './torrentAdd.js';
|
||||
import { apiSource } from './api.js';
|
||||
import { createTorrentSource } from './createTorrent.js';
|
||||
import { torrentGeneralDetailsSource } from './torrentGeneralDetails.js';
|
||||
import { torrentFileDetailsSource } from './torrentFileDetails.js';
|
||||
import { torrentChunkDetailsSource } from './torrentChunkDetails.js';
|
||||
import { torrentPeerDetailsSource } from './torrentPeerDetails.js';
|
||||
import { torrentTrackerDetailsSource } from './torrentTrackerDetails.js';
|
||||
import { mobileTorrentDetailsSource } from './mobileTorrentDetails.js';
|
||||
import { torrentDetailsLoaderSource } from './torrentDetailsLoader.js';
|
||||
import { pathPickerToolsSource } from './pathPickerTools.js';
|
||||
import { columnManagerSource } from './columnManager.js';
|
||||
import { jobToolsSource } from './jobTools.js';
|
||||
import { labelToolsSource } from './labelTools.js';
|
||||
import { ratioToolsSource } from './ratioTools.js';
|
||||
import { rssToolsSource } from './rssTools.js';
|
||||
import { backupToolsSource } from './backupTools.js';
|
||||
import { smartQueueSource } from './smartQueue.js';
|
||||
import { rtorrentConfigSource } from './rtorrentConfig.js';
|
||||
import { appearancePreferencesSource } from './appearancePreferences.js';
|
||||
import { peerRefreshSource } from './peerRefresh.js';
|
||||
import { automationRulesSource } from './automationRules.js';
|
||||
import { cleanupToolsSource } from './cleanupTools.js';
|
||||
import { appDiagnosticsSource } from './appDiagnostics.js';
|
||||
import { footerPreferencesSource } from './footerPreferences.js';
|
||||
import { liveSpeedStatsSource } from './liveSpeedStats.js';
|
||||
import { statusBarSource } from './statusBar.js';
|
||||
import { preferencesToolsSource } from './preferencesTools.js';
|
||||
import { diskMonitorSource } from './diskMonitor.js';
|
||||
import { portCheckActionsSource } from './portCheckActions.js';
|
||||
import { appStatusSource } from './appStatus.js';
|
||||
import { torrentStatsSource } from './torrentStats.js';
|
||||
import { toolUiHelpersSource } from './toolUiHelpers.js';
|
||||
import { authUsersSource } from './authUsers.js';
|
||||
import { plannerToolsUiSource } from './plannerToolsUi.js';
|
||||
import { plannerSpeedControlsSource } from './plannerSpeedControls.js';
|
||||
import { plannerSettingsSource } from './plannerSettings.js';
|
||||
import { plannerPreviewHistorySource } from './plannerPreviewHistory.js';
|
||||
import { plannerActionsSource } from './plannerActions.js';
|
||||
import { smartViewsSource } from './smartViews.js';
|
||||
import { notificationCenterSource } from './notificationCenter.js';
|
||||
import { diagnosticsDashboardSource } from './diagnosticsDashboard.js';
|
||||
import { dashboardToolsSource } from './dashboardTools.js';
|
||||
import { operationLogsSource } from './operationLogs.js';
|
||||
import { pollerSettingsSource } from './pollerSettings.js';
|
||||
import { toolsModalSource } from './toolsModal.js';
|
||||
import { toolPaneEventsSource } from './toolPaneEvents.js';
|
||||
import { rssEventsSource } from './rssEvents.js';
|
||||
import { smartQueueEventsSource } from './smartQueueEvents.js';
|
||||
import { backupCleanupRtconfigEventsSource } from './backupCleanupRtconfigEvents.js';
|
||||
import { automationEventsSource } from './automationEvents.js';
|
||||
import { labelSmartEventsSource } from './labelSmartEvents.js';
|
||||
import { torrentSelectionEventsSource } from './torrentSelectionEvents.js';
|
||||
import { torrentTableEventsSource } from './torrentTableEvents.js';
|
||||
import { preferenceEventsSource } from './preferenceEvents.js';
|
||||
import { keyboardEventsSource } from './keyboardEvents.js';
|
||||
import { speedLimitControlsSource } from './speedLimitControls.js';
|
||||
import { themeMobileControlsSource } from './themeMobileControls.js';
|
||||
import { jobSettingsSource } from './jobSettings.js';
|
||||
import { profileListSource } from './profileList.js';
|
||||
import { profileFormSource } from './profileForm.js';
|
||||
import { profileActionsSource } from './profileActions.js';
|
||||
import { profileSelectionSource } from './profileSelection.js';
|
||||
import { realtimeChartsSource } from './realtimeCharts.js';
|
||||
import { trafficHistoryDataSource } from './trafficHistoryData.js';
|
||||
import { trafficChartRendererSource } from './trafficChartRenderer.js';
|
||||
import { initialSnapshotSource } from './initialSnapshot.js';
|
||||
import { footerStatusRefreshSource } from './footerStatusRefresh.js';
|
||||
import { systemStatsSocketSource } from './systemStatsSocket.js';
|
||||
import { mobileSelectEventsSource } from './mobileSelectEvents.js';
|
||||
import { bootstrapRuntimeSource } from './bootstrapRuntime.js';
|
||||
|
||||
export const moduleSources = [
|
||||
stateCoreSource,
|
||||
columnStateSource,
|
||||
runtimeStateSource,
|
||||
sharedUiSource,
|
||||
torrentFilterHelpersSource,
|
||||
torrentFilterUiSource,
|
||||
torrentTrackerFiltersSource,
|
||||
torrentTableStateSource,
|
||||
torrentActionStateSource,
|
||||
torrentRowRendererSource,
|
||||
torrentTableRendererSource,
|
||||
mobileSource,
|
||||
messagesSource,
|
||||
torrentAddSource,
|
||||
apiSource,
|
||||
createTorrentSource,
|
||||
torrentGeneralDetailsSource,
|
||||
torrentFileDetailsSource,
|
||||
torrentChunkDetailsSource,
|
||||
torrentPeerDetailsSource,
|
||||
torrentTrackerDetailsSource,
|
||||
mobileTorrentDetailsSource,
|
||||
torrentDetailsLoaderSource,
|
||||
pathPickerToolsSource,
|
||||
columnManagerSource,
|
||||
jobToolsSource,
|
||||
labelToolsSource,
|
||||
ratioToolsSource,
|
||||
rssToolsSource,
|
||||
backupToolsSource,
|
||||
smartQueueSource,
|
||||
rtorrentConfigSource,
|
||||
appearancePreferencesSource,
|
||||
peerRefreshSource,
|
||||
automationRulesSource,
|
||||
cleanupToolsSource,
|
||||
appDiagnosticsSource,
|
||||
footerPreferencesSource,
|
||||
liveSpeedStatsSource,
|
||||
statusBarSource,
|
||||
preferencesToolsSource,
|
||||
diskMonitorSource,
|
||||
portCheckActionsSource,
|
||||
appStatusSource,
|
||||
torrentStatsSource,
|
||||
toolUiHelpersSource,
|
||||
authUsersSource,
|
||||
plannerToolsUiSource,
|
||||
plannerSpeedControlsSource,
|
||||
plannerSettingsSource,
|
||||
plannerPreviewHistorySource,
|
||||
plannerActionsSource,
|
||||
smartViewsSource,
|
||||
notificationCenterSource,
|
||||
diagnosticsDashboardSource,
|
||||
dashboardToolsSource,
|
||||
operationLogsSource,
|
||||
pollerSettingsSource,
|
||||
toolsModalSource,
|
||||
toolPaneEventsSource,
|
||||
rssEventsSource,
|
||||
smartQueueEventsSource,
|
||||
backupCleanupRtconfigEventsSource,
|
||||
automationEventsSource,
|
||||
labelSmartEventsSource,
|
||||
torrentSelectionEventsSource,
|
||||
torrentTableEventsSource,
|
||||
preferenceEventsSource,
|
||||
keyboardEventsSource,
|
||||
speedLimitControlsSource,
|
||||
themeMobileControlsSource,
|
||||
jobSettingsSource,
|
||||
profileListSource,
|
||||
profileFormSource,
|
||||
profileActionsSource,
|
||||
profileSelectionSource,
|
||||
realtimeChartsSource,
|
||||
trafficHistoryDataSource,
|
||||
trafficChartRendererSource,
|
||||
initialSnapshotSource,
|
||||
footerStatusRefreshSource,
|
||||
systemStatsSocketSource,
|
||||
mobileSelectEventsSource,
|
||||
bootstrapRuntimeSource,
|
||||
const staticImportVersion = encodeURIComponent(String(window.PYTORRENT?.staticHash || 'dev'));
|
||||
const versionedImport = (path) => import(`${path}?v=${staticImportVersion}`);
|
||||
const moduleImportSpecs = [
|
||||
['./stateCore.js', 'stateCoreSource'],
|
||||
['./columnState.js', 'columnStateSource'],
|
||||
['./runtimeState.js', 'runtimeStateSource'],
|
||||
['./sharedUi.js', 'sharedUiSource'],
|
||||
['./torrentFilterHelpers.js', 'torrentFilterHelpersSource'],
|
||||
['./torrentFilterUi.js', 'torrentFilterUiSource'],
|
||||
['./torrentTrackerFilters.js', 'torrentTrackerFiltersSource'],
|
||||
['./torrentTableState.js', 'torrentTableStateSource'],
|
||||
['./torrentActionState.js', 'torrentActionStateSource'],
|
||||
['./torrentRowRenderer.js', 'torrentRowRendererSource'],
|
||||
['./torrentTableRenderer.js', 'torrentTableRendererSource'],
|
||||
['./mobile.js', 'mobileSource'],
|
||||
['./messages.js', 'messagesSource'],
|
||||
['./torrentAdd.js', 'torrentAddSource'],
|
||||
['./api.js', 'apiSource'],
|
||||
['./createTorrent.js', 'createTorrentSource'],
|
||||
['./torrentGeneralDetails.js', 'torrentGeneralDetailsSource'],
|
||||
['./torrentFileDetails.js', 'torrentFileDetailsSource'],
|
||||
['./torrentChunkDetails.js', 'torrentChunkDetailsSource'],
|
||||
['./torrentPeerDetails.js', 'torrentPeerDetailsSource'],
|
||||
['./torrentTrackerDetails.js', 'torrentTrackerDetailsSource'],
|
||||
['./mobileTorrentDetails.js', 'mobileTorrentDetailsSource'],
|
||||
['./torrentDetailsLoader.js', 'torrentDetailsLoaderSource'],
|
||||
['./pathPickerTools.js', 'pathPickerToolsSource'],
|
||||
['./columnManager.js', 'columnManagerSource'],
|
||||
['./jobTools.js', 'jobToolsSource'],
|
||||
['./labelTools.js', 'labelToolsSource'],
|
||||
['./ratioTools.js', 'ratioToolsSource'],
|
||||
['./rssTools.js', 'rssToolsSource'],
|
||||
['./backupTools.js', 'backupToolsSource'],
|
||||
['./smartQueue.js', 'smartQueueSource'],
|
||||
['./rtorrentConfig.js', 'rtorrentConfigSource'],
|
||||
['./appearancePreferences.js', 'appearancePreferencesSource'],
|
||||
['./peerRefresh.js', 'peerRefreshSource'],
|
||||
['./automationRules.js', 'automationRulesSource'],
|
||||
['./cleanupTools.js', 'cleanupToolsSource'],
|
||||
['./appDiagnostics.js', 'appDiagnosticsSource'],
|
||||
['./footerPreferences.js', 'footerPreferencesSource'],
|
||||
['./liveSpeedStats.js', 'liveSpeedStatsSource'],
|
||||
['./statusBar.js', 'statusBarSource'],
|
||||
['./preferencesTools.js', 'preferencesToolsSource'],
|
||||
['./diskMonitor.js', 'diskMonitorSource'],
|
||||
['./portCheckActions.js', 'portCheckActionsSource'],
|
||||
['./appStatus.js', 'appStatusSource'],
|
||||
['./torrentStats.js', 'torrentStatsSource'],
|
||||
['./toolUiHelpers.js', 'toolUiHelpersSource'],
|
||||
['./authUsers.js', 'authUsersSource'],
|
||||
['./plannerToolsUi.js', 'plannerToolsUiSource'],
|
||||
['./plannerSpeedControls.js', 'plannerSpeedControlsSource'],
|
||||
['./plannerSettings.js', 'plannerSettingsSource'],
|
||||
['./plannerPreviewHistory.js', 'plannerPreviewHistorySource'],
|
||||
['./plannerActions.js', 'plannerActionsSource'],
|
||||
['./smartViews.js', 'smartViewsSource'],
|
||||
['./notificationCenter.js', 'notificationCenterSource'],
|
||||
['./diagnosticsDashboard.js', 'diagnosticsDashboardSource'],
|
||||
['./dashboardTools.js', 'dashboardToolsSource'],
|
||||
['./operationLogs.js', 'operationLogsSource'],
|
||||
['./pollerSettings.js', 'pollerSettingsSource'],
|
||||
['./toolsModal.js', 'toolsModalSource'],
|
||||
['./toolPaneEvents.js', 'toolPaneEventsSource'],
|
||||
['./rssEvents.js', 'rssEventsSource'],
|
||||
['./smartQueueEvents.js', 'smartQueueEventsSource'],
|
||||
['./backupCleanupRtconfigEvents.js', 'backupCleanupRtconfigEventsSource'],
|
||||
['./automationEvents.js', 'automationEventsSource'],
|
||||
['./labelSmartEvents.js', 'labelSmartEventsSource'],
|
||||
['./torrentSelectionEvents.js', 'torrentSelectionEventsSource'],
|
||||
['./torrentTableEvents.js', 'torrentTableEventsSource'],
|
||||
['./preferenceEvents.js', 'preferenceEventsSource'],
|
||||
['./keyboardEvents.js', 'keyboardEventsSource'],
|
||||
['./speedLimitControls.js', 'speedLimitControlsSource'],
|
||||
['./themeMobileControls.js', 'themeMobileControlsSource'],
|
||||
['./jobSettings.js', 'jobSettingsSource'],
|
||||
['./profileList.js', 'profileListSource'],
|
||||
['./profileForm.js', 'profileFormSource'],
|
||||
['./profileActions.js', 'profileActionsSource'],
|
||||
['./profileSelection.js', 'profileSelectionSource'],
|
||||
['./realtimeCharts.js', 'realtimeChartsSource'],
|
||||
['./trafficHistoryData.js', 'trafficHistoryDataSource'],
|
||||
['./trafficChartRenderer.js', 'trafficChartRendererSource'],
|
||||
['./initialSnapshot.js', 'initialSnapshotSource'],
|
||||
['./footerStatusRefresh.js', 'footerStatusRefreshSource'],
|
||||
['./systemStatsSocket.js', 'systemStatsSocketSource'],
|
||||
['./mobileSelectEvents.js', 'mobileSelectEventsSource'],
|
||||
['./bootstrapRuntime.js', 'bootstrapRuntimeSource'],
|
||||
];
|
||||
|
||||
export function buildRuntimeSource(){
|
||||
return `(() => {\n${moduleSources.join('\n')}\n})();\n`;
|
||||
export let moduleSources = [];
|
||||
let moduleSourcesPromise = null;
|
||||
|
||||
async function loadModuleSources(){
|
||||
if(moduleSourcesPromise) return moduleSourcesPromise;
|
||||
moduleSourcesPromise = Promise.all(moduleImportSpecs.map(([path]) => versionedImport(path))).then((modules) => {
|
||||
moduleSources = modules.map((mod, index) => mod[moduleImportSpecs[index][1]]);
|
||||
return moduleSources;
|
||||
});
|
||||
return moduleSourcesPromise;
|
||||
}
|
||||
|
||||
export function startApp(){
|
||||
const runtimeSource = buildRuntimeSource();
|
||||
export async function buildRuntimeSource(){
|
||||
const sources = await loadModuleSources();
|
||||
return `(() => {\n${sources.join('\n')}\n})();\n`;
|
||||
}
|
||||
|
||||
export async function startApp(){
|
||||
const runtimeSource = await buildRuntimeSource();
|
||||
// Keep the original shared lexical scope while loading the source from smaller ES modules.
|
||||
// `io` is passed explicitly so Socket.IO remains available inside the generated runtime.
|
||||
return Function('io', runtimeSource)(window.io);
|
||||
}
|
||||
|
||||
if(typeof window !== 'undefined' && !window.PYTORRENT_DISABLE_AUTOSTART){
|
||||
startApp();
|
||||
startApp().catch((error) => {
|
||||
console.error('pyTorrent frontend failed to start', error);
|
||||
const loaderText = document.getElementById('initialLoaderText');
|
||||
if(loaderText) loaderText.textContent = 'Frontend failed to start. Reload the page or clear browser cache.';
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const bootstrapRuntimeSource = " updateSortHeaders(); setupColumnResizers(); applyColumnVisibility(); renderColumnManager(); restoreFooterStatusCache(); refreshFooterStatusNow(); renderFooterPreferences(); applyFooterPreferences(); updateFooterClock(); updateBrowserSpeedTitle(); setupTorrentDropZone(); setInterval(updateFooterClock,1000); scheduleRender(true); if(!hasActiveProfile) renderNoProfileState(); loadLabels().catch(()=>{}); loadRatios().catch(()=>{}); loadSmartQueue().catch(()=>{}); loadAutomations().catch(()=>{}); ensureDashboardToolsUI(); if(portCheckEnabled) loadPortCheck(false); else renderPortCheck({status:'disabled',enabled:false}); if(hasActiveProfile) applyDefaultDownloadPath(false).catch(()=>{}); if(hasActiveProfile) refreshUserDiskUsage(true).catch(()=>{}); scheduleTrackerSummary(true);\n";
|
||||
export const bootstrapRuntimeSource = " async function checkStaticAssetVersion(){ try{ const r=await fetch('/api/static_hash',{cache:'no-store'}); const j=await r.json(); const current=String(window.PYTORRENT?.staticHash||''); const next=String(j.static_hash||j.version||''); if(current && next && current!==next){ window.PYTORRENT.staticHash=next; toast('A new frontend version is available. Reloading...','info'); setTimeout(()=>window.location.reload(), 600); } }catch(e){} }\n setInterval(checkStaticAssetVersion, 120000);\n window.addEventListener('focus', checkStaticAssetVersion);\n updateSortHeaders(); setupColumnResizers(); applyColumnVisibility(); renderColumnManager(); restoreFooterStatusCache(); refreshFooterStatusNow(); renderFooterPreferences(); applyFooterPreferences(); updateFooterClock(); updateBrowserSpeedTitle(); setupTorrentDropZone(); setInterval(updateFooterClock,1000); scheduleRender(true); if(!hasActiveProfile) renderNoProfileState(); loadLabels().catch(()=>{}); loadRatios().catch(()=>{}); loadSmartQueue().catch(()=>{}); loadAutomations().catch(()=>{}); ensureDashboardToolsUI(); if(portCheckEnabled) loadPortCheck(false); else renderPortCheck({status:'disabled',enabled:false}); if(hasActiveProfile) applyDefaultDownloadPath(false).catch(()=>{}); if(hasActiveProfile) refreshUserDiskUsage(true).catch(()=>{}); scheduleTrackerSummary(true);\\n";
|
||||
|
||||
@@ -2979,17 +2979,15 @@ body.mobile-mode .mobile-filter-bar {
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.smart-surge-refill-card .smart-refill-controls {
|
||||
grid-template-columns: minmax(84px, 0.6fr) minmax(110px, 1fr) minmax(110px, 1fr);
|
||||
width: min(450px, 100%);
|
||||
.smart-surge-cooldown-card {
|
||||
background: rgba(var(--bs-info-rgb), 0.08);
|
||||
}
|
||||
|
||||
.smart-refill-switch {
|
||||
align-content: end;
|
||||
}
|
||||
|
||||
.smart-refill-switch .form-check-input {
|
||||
margin: 0;
|
||||
.smart-surge-refill-controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(110px, 1fr));
|
||||
gap: 0.55rem;
|
||||
width: min(330px, 100%);
|
||||
}
|
||||
.disk-monitor-shell {
|
||||
display: grid;
|
||||
@@ -3065,6 +3063,7 @@ body.mobile-mode .mobile-filter-bar {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.smart-surge-refill-controls,
|
||||
.disk-monitor-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user