Wrap up 1.1.0

This commit is contained in:
Mukhtar Akere
2025-08-09 10:55:10 +01:00
parent 7c8156eacf
commit 3aeb806033
54 changed files with 1592 additions and 1523 deletions

View File

@@ -1,11 +1,8 @@
{{ define "config" }}
<div class="space-y-6">
<!-- Configuration Form -->
<form id="configForm" class="space-y-6">
<!-- Tab Navigation Card -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<!-- Modern Tab Navigation -->
<div class="border-b border-base-300 mb-8">
<nav class="flex space-x-8" aria-label="Configuration Tabs">
<button type="button" class="tab-button active flex items-center gap-2 py-3 px-1 border-b-2 border-primary text-primary font-medium text-sm" data-tab="general">
@@ -35,17 +32,14 @@
</nav>
</div>
<!-- Save Button (Sticky) -->
<div class="sticky top-20 z-30 flex justify-end mb-6">
<button type="submit" class="btn btn-success btn-lg shadow-lg">
<i class="bi bi-save mr-2"></i>Save Configuration
</button>
</div>
<!-- Tab Content Container -->
<div class="tab-content-container">
<!-- General Tab Content -->
<div class="tab-content" data-tab-content="general">
<div class="space-y-6">
<h2 class="text-2xl font-bold flex items-center mb-6">
@@ -157,10 +151,123 @@
</div>
</div>
</div>
<!-- Authentication Settings Section -->
<div class="divider">
<span class="text-lg font-semibold">Authentication Settings</span>
</div>
<div class="card bg-base-200">
<div class="card-body">
<div class="space-y-6">
<div class="flex justify-between items-start">
<div class="flex-1">
<h3 class="text-lg font-semibold mb-2">Authentication Settings</h3>
<p class="text-sm text-base-content/70">Configure username/password authentication and API token for programmatic access.</p>
</div>
</div>
<!-- Username/Password Section -->
<div class="space-y-4">
<h4 class="font-semibold text-base">Web Authentication</h4>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Username</span>
</label>
<input type="text"
id="auth-username"
name="auth_username"
class="input input-bordered"
placeholder="Enter username (leave empty to disable auth)">
<div class="label">
<span class="label-text-alt">Leave empty to disable authentication</span>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Password</span>
</label>
<div class="password-toggle-container">
<input type="password"
id="auth-password"
name="auth_password"
class="input input-bordered input-has-toggle"
placeholder="Enter password">
<button type="button" class="password-toggle-btn">
<i class="bi bi-eye"></i>
</button>
</div>
<div class="label">
<span class="label-text-alt">Leave empty to disable authentication</span>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Confirm Password</span>
</label>
<div class="password-toggle-container">
<input type="password"
id="auth-password-confirm"
name="auth_password_confirm"
class="input input-bordered input-has-toggle"
placeholder="Confirm password">
<button type="button" class="password-toggle-btn">
<i class="bi bi-eye"></i>
</button>
</div>
<div class="label">
<span class="label-text-alt" id="password-match-indicator"></span>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Current Token</span>
</label>
<div class="join">
<input type="text"
id="api-token-display"
class="input input-bordered join-item flex-1 font-mono"
placeholder="No token generated"
readonly>
<button type="button"
id="copy-token-btn"
class="btn btn-outline join-item"
onclick="copyAPIToken();">
<i class="bi bi-copy"></i>
</button>
<button type="button"
id="refresh-token-btn"
class="btn btn-outline btn-secondary join-item"
onclick="refreshAPIToken();">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
<div class="label">
<span class="label-text-alt">Click refresh to generate or update your API token</span>
</div>
</div>
</div>
<div class="flex justify-end items-center mt-5">
<button type="button"
id="update-auth-btn"
class="btn btn-primary"
onclick="updateAuthSettings();">
<i class="bi bi-shield-check mr-2"></i>Update Authentication
</button>
</div>
</div>
<div class="space-y-4">
<p class="text-sm text-base-content/70">Use this token for API authentication instead of session cookies. Perfect for automation and scripts.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Debrid Tab Content -->
<div class="tab-content hidden" data-tab-content="debrid">
<div class="space-y-6">
<div class="flex justify-between items-center">
@@ -173,12 +280,10 @@
</div>
<div id="debridConfigs" class="space-y-4">
<!-- Dynamic debrid configurations will be added here -->
</div>
</div>
</div>
<!-- QBittorrent Tab Content -->
<div class="tab-content hidden" data-tab-content="qbittorrent">
<div class="space-y-6">
<h2 class="text-2xl font-bold flex items-center mb-6">
@@ -226,7 +331,6 @@
</div>
</div>
<!-- Arrs Tab Content -->
<div class="tab-content hidden" data-tab-content="arrs">
<div class="space-y-6">
<div class="flex justify-between items-center">
@@ -239,12 +343,10 @@
</div>
<div id="arrConfigs" class="space-y-4">
<!-- Dynamic arr configurations will be added here -->
</div>
</div>
</div>
<!-- Repair Tab Content -->
<div class="tab-content hidden" data-tab-content="repair">
<div class="space-y-6">
<h2 class="text-2xl font-bold flex items-center mb-6">
@@ -332,7 +434,6 @@
</div>
</div>
<!-- Rclone Tab Content -->
<div class="tab-content hidden" data-tab-content="rclone">
<div class="space-y-6">
<h2 class="text-2xl font-bold flex items-center mb-6">
@@ -349,7 +450,6 @@
</label>
</div>
<!-- Mount Path Section -->
<div class="card bg-base-200">
<div class="card-body">
<h3 class="text-lg font-semibold mb-4 flex items-center">
@@ -519,7 +619,6 @@
</div>
</div>
<!-- Advanced Settings Section -->
<div class="card bg-base-200">
<div class="card-body">
<h3 class="text-lg font-semibold mb-4 flex items-center">
@@ -551,13 +650,12 @@
</div>
</div>
</div> <!-- End tab-content-container -->
</div>
</div>
</div>
</form>
</div>
<!-- Loading Overlay -->
<div id="loadingOverlay" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden">
<div class="flex items-center justify-center h-full">
<div class="card bg-base-100 shadow-2xl">
@@ -634,5 +732,124 @@
}
});
});
// Password confirmation validation
document.addEventListener('DOMContentLoaded', function() {
const password = document.getElementById('auth-password');
const confirmPassword = document.getElementById('auth-password-confirm');
const indicator = document.getElementById('password-match-indicator');
function validatePasswords() {
if (!password.value && !confirmPassword.value) {
indicator.textContent = '';
indicator.className = 'label-text-alt';
return;
}
if (password.value === confirmPassword.value) {
indicator.textContent = '✓ Passwords match';
indicator.className = 'label-text-alt text-success';
} else {
indicator.textContent = '✗ Passwords do not match';
indicator.className = 'label-text-alt text-error';
}
}
password?.addEventListener('input', validatePasswords);
confirmPassword?.addEventListener('input', validatePasswords);
});
// API Token Management Functions
async function refreshAPIToken() {
const refreshBtn = document.getElementById('refresh-token-btn');
const tokenDisplay = document.getElementById('api-token-display');
// Show loading state
window.decypharrUtils.setButtonLoading(refreshBtn, true, 'Refresh Token');
try {
const response = await window.decypharrUtils.fetcher('/api/refresh-token', {
method: 'POST'
});
if (!response.ok) {
throw new Error('Failed to refresh token');
}
const data = await response.json();
tokenDisplay.value = data.token;
window.decypharrUtils.createToast(data.message || 'Token refreshed successfully', 'success');
} catch (error) {
console.error('Error refreshing token:', error);
window.decypharrUtils.createToast('Failed to refresh token: ' + error.message, 'error');
} finally {
window.decypharrUtils.setButtonLoading(refreshBtn, false);
}
}
async function copyAPIToken() {
const tokenDisplay = document.getElementById('api-token-display');
const token = tokenDisplay.value;
if (!token || token === 'No token generated') {
window.decypharrUtils.createToast('No token to copy. Please refresh the token first.', 'warning');
return;
}
try {
await window.decypharrUtils.copyToClipboard(token);
} catch (error) {
console.error('Failed to copy token:', error);
window.decypharrUtils.createToast('Failed to copy token to clipboard', 'error');
}
}
async function updateAuthSettings() {
const username = document.getElementById('auth-username').value;
const password = document.getElementById('auth-password').value;
const confirmPassword = document.getElementById('auth-password-confirm').value;
const updateBtn = document.getElementById('update-auth-btn');
if (password !== confirmPassword) {
window.decypharrUtils.createToast('Passwords do not match', 'error');
return false;
}
// Show loading state
window.decypharrUtils.setButtonLoading(updateBtn, true, 'Update Authentication');
try {
const response = await window.decypharrUtils.fetcher('/api/update-auth', {
method: 'POST',
body: JSON.stringify({
username: username,
password: password,
confirm_password: confirmPassword
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || 'Failed to update authentication settings');
}
const data = await response.json();
window.decypharrUtils.createToast(data.message, 'success');
// Clear password fields for security
document.getElementById('auth-password').value = '';
document.getElementById('auth-password-confirm').value = '';
return true;
} catch (error) {
console.error('Error updating auth settings:', error);
window.decypharrUtils.createToast('Failed to update authentication: ' + error.message, 'error');
return false;
} finally {
window.decypharrUtils.setButtonLoading(updateBtn, false);
}
}
</script>
{{ end }}

View File

@@ -1,10 +1,8 @@
{{ define "download" }}
<div class="space-y-6">
<!-- Download Form -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<form id="downloadForm" enctype="multipart/form-data" class="space-y-3">
<!-- Torrent Input Section -->
<div class="space-y-2">
<div class="form-control">
<label class="label" for="magnetURI">
@@ -44,7 +42,6 @@
<div class="divider"></div>
<!-- Configuration Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
<div class="space-y-2">
<h3 class="text-lg font-semibold flex items-center">
@@ -128,7 +125,6 @@
</div>
</div>
<!-- Submit Button -->
<div class="form-control">
<button type="submit" class="btn btn-primary btn-lg" id="submitDownload">
<i class="bi bi-cloud-upload mr-2"></i>Add to Download Queue

View File

@@ -1,11 +1,9 @@
{{ define "index" }}
<div class="space-y-6">
<!-- Controls Section -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4">
<!-- Batch Actions -->
<div class="flex items-center gap-2">
<button class="btn btn-secondary btn-sm hidden" id="batchDeleteBtn">
<i class="bi bi-trash"></i>
@@ -21,7 +19,6 @@
</button>
</div>
<!-- Filters -->
<div class="flex flex-wrap items-center gap-2 w-full lg:w-auto">
<select class="select select-bordered select-sm w-full sm:w-auto min-w-32" id="stateFilter">
<option value="">All States</option>
@@ -47,7 +44,6 @@
</div>
</div>
<!-- Torrents Table -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-0">
<div class="overflow-x-auto">
@@ -87,12 +83,10 @@
</tr>
</thead>
<tbody id="torrentsList">
<!-- Dynamic content will be loaded here -->
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="flex flex-col sm:flex-row justify-between items-center p-6 border-t border-base-200 gap-4">
<div class="text-sm text-base-content/70">
<span id="paginationInfo">Loading torrents...</span>
@@ -102,7 +96,6 @@
</div>
</div>
<!-- Empty State -->
<div class="card bg-base-100 shadow-xl hidden" id="emptyState">
<div class="card-body text-center py-16">
<div class="text-6xl text-base-content/30 mb-4">
@@ -117,7 +110,6 @@
</div>
</div>
<!-- Context Menu -->
<ul class="menu bg-base-100 shadow-lg rounded-box context-menu hidden fixed z-50" id="torrentContextMenu">
<li class="menu-title">
<span class="torrent-name text-sm font-bold truncate max-w-48"></span>

View File

@@ -13,7 +13,6 @@
<link rel="icon" type="image/png" sizes="16x16" href="{{.URLBase}}images/favicon/favicon-16x16.png">
<link rel="manifest" href="{{.URLBase}}images/favicon/site.webmanifest">
<!-- Preload JavaScript -->
<link rel="preload" href="{{.URLBase}}assets/js/common.js" as="script">
<script>
@@ -34,11 +33,9 @@
</script>
</head>
<body class="min-h-screen bg-base-200 flex flex-col">
<!-- Toast Container -->
<div class="toast-container fixed bottom-4 right-4 z-50 space-y-2">
</div>
<!-- Navigation -->
<header class="navbar bg-base-100 shadow-lg sticky top-0 z-40 backdrop-blur-sm">
<div class="navbar-start">
<div class="dropdown">
@@ -72,9 +69,8 @@
</ul>
</div>
<a class="btn btn-ghost text-xl font-bold text-primary group" href="{{.URLBase}}">
<!-- Logo -->
<img src="{{.URLBase}}images/logo.svg" alt="Decypharr Logo" class="w-8 h-8 inline-block mr-2">
<span class="hidden sm:inline bg-clip-text text-transparent">Decypharr</span>
<span class="hidden sm:inline bg-clip-text">Decypharr</span>
</a>
</div>
@@ -100,10 +96,6 @@
<i class="bi bi-cloud"></i>
<span class="hidden xl:inline">WebDAV</span>
</a></li>
<li><a href="{{.URLBase}}stats" class="{{if eq .Page "stats"}}active{{end}} tooltip tooltip-bottom" data-tip="System Statistics">
<i class="bi bi-graph-up"></i>
<span class="hidden xl:inline">Stats</span>
</a></li>
<li><a href="{{.URLBase}}logs" target="_blank" class="tooltip tooltip-bottom" data-tip="System Logs">
<i class="bi bi-journal-text"></i>
<span class="hidden xl:inline">Logs</span>
@@ -113,26 +105,21 @@
<div class="navbar-end">
<div class="flex items-center gap-3">
<!-- Theme Toggle -->
<div class="tooltip tooltip-left">
<label class="swap swap-rotate btn btn-ghost btn-circle hover:bg-base-300 transition-colors">
<input type="checkbox" id="themeToggle" class="theme-controller" />
<!-- Sun icon for light mode -->
<i class="swap-off bi bi-sun text-lg text-warning"></i>
<!-- Moon icon for dark mode -->
<i class="swap-on bi bi-moon-stars text-lg text-info"></i>
</label>
</div>
<!-- Stats Link -->
<div class="tooltip tooltip-left" data-tip="System Statistics">
<a href="{{.URLBase}}debug/stats" class="btn btn-ghost btn-sm hover:bg-base-300 transition-colors">
<a href="{{.URLBase}}stats" class="btn btn-ghost btn-sm hover:bg-base-300 transition-colors">
<i class="bi bi-graph-up text-lg"></i>
<span class="hidden md:inline ml-1">Stats</span>
</a>
</div>
<!-- Version Badge -->
<div class="tooltip tooltip-left" data-tip="Current Version">
<div class="badge badge-primary font-mono text-xs hover:badge-primary-focus transition-colors cursor-pointer" id="version-badge">
Loading...
@@ -142,7 +129,6 @@
</div>
</header>
<!-- Main Content -->
<main class="flex-1 container mx-auto px-4 py-6">
{{ if eq .Page "index" }}
{{ template "index" . }}
@@ -171,7 +157,6 @@
{{ end }}
</main>
<!-- Footer -->
<footer class="footer footer-center p-6 bg-base-300 text-base-content border-t border-base-200">
<aside class="grid-flow-col gap-4">
<a href="https://github.com/sirrobot01/decypharr" target="_blank"
@@ -187,11 +172,9 @@
</aside>
</footer>
<!-- Scripts -->
<script src="{{.URLBase}}assets/js/jquery-3.7.1.min.js"></script>
<script src="{{.URLBase}}assets/js/common.js"></script>
<!-- Page-specific scripts -->
{{ if eq .Page "index" }}
<script src="{{.URLBase}}assets/js/dashboard.js"></script>
{{ else if eq .Page "download" }}

View File

@@ -1,6 +1,5 @@
{{ define "repair" }}
<div class="space-y-6">
<!-- Repair Form -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-6">
@@ -9,7 +8,6 @@
<form id="repairForm" class="space-y-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Left Column -->
<div class="space-y-4">
<div class="form-control">
<label class="label" for="arrSelect">
@@ -44,7 +42,6 @@
</div>
</div>
<!-- Right Column -->
<div class="space-y-4">
<div class="form-control">
<label class="label">
@@ -89,7 +86,6 @@
</div>
</div>
<!-- Jobs Table -->
<div class="card bg-base-100 shadow-xl">
<div class="card-header">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 p-6 border-b border-base-200">
@@ -136,19 +132,15 @@
</tr>
</thead>
<tbody id="jobsTableBody">
<!-- Dynamic content will be loaded here -->
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="flex justify-center p-6 border-t border-base-200">
<div class="join" id="jobsPagination">
<!-- Pagination will be generated here -->
</div>
</div>
<!-- No Jobs Message -->
<div id="noJobsMessage" class="text-center py-16 hidden">
<div class="text-6xl text-base-content/30 mb-4">
<i class="bi bi-clipboard-check"></i>
@@ -160,7 +152,6 @@
</div>
</div>
<!-- Job Details Modal -->
<dialog id="jobDetailsModal" class="modal">
<div class="modal-box max-w-6xl max-h-[90vh]">
<form method="dialog">
@@ -174,7 +165,6 @@
</div>
<div class="space-y-6">
<!-- Job Information -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="card bg-base-200">
<div class="card-body">
@@ -221,7 +211,6 @@
</div>
</div>
<!-- Error Message -->
<div id="errorContainer" class="alert alert-error hidden">
<i class="bi bi-exclamation-triangle"></i>
<div>
@@ -230,7 +219,6 @@
</div>
</div>
<!-- Broken Items Section -->
<div class="card bg-base-200">
<div class="card-body">
<div class="flex justify-between items-center mb-4">
@@ -240,7 +228,6 @@
</h4>
</div>
<!-- Filters -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
<div class="form-control">
<input type="text"
@@ -268,7 +255,6 @@
</div>
</div>
<!-- Items Table -->
<div class="overflow-x-auto max-h-96 border border-base-300 rounded-lg">
<table class="table table-sm table-hover">
<thead class="sticky top-0 bg-base-300">
@@ -280,19 +266,15 @@
</tr>
</thead>
<tbody id="brokenItemsTableBody">
<!-- Broken items will be loaded here -->
</tbody>
</table>
</div>
<!-- Items Pagination -->
<div class="flex justify-center mt-4">
<div class="join" id="itemsPagination">
<!-- Pagination will be generated here -->
</div>
</div>
<!-- No Items Messages -->
<div id="noBrokenItemsMessage" class="text-center py-8 hidden">
<div class="text-4xl text-base-content/30 mb-2">
<i class="bi bi-check-circle"></i>

View File

@@ -8,15 +8,12 @@
</button>
</div>
<!-- Loading State -->
<div id="loading-stats" class="text-center py-12">
<div class="loading loading-spinner loading-lg text-primary"></div>
<p class="mt-4 text-base-content/70">Loading system statistics...</p>
</div>
<!-- Stats Content -->
<div id="stats-content" class="space-y-6" style="display: none;">
<!-- System Overview -->
<div class="card bg-base-100 shadow-xl">
<div class="card-header p-6 pb-3">
<h2 class="card-title text-xl">
@@ -62,7 +59,17 @@
</div>
</div>
<!-- Rclone Stats -->
<div class="card bg-base-100 shadow-xl" id="debrid-card">
<div class="card-header p-6 pb-3">
<h2 class="card-title text-xl">
<i class="bi bi-cloud-download text-secondary"></i>
Debrid Services
</h2>
</div>
<div class="card-body p-6 pt-3" id="debrid-content">
</div>
</div>
<div class="card bg-base-100 shadow-xl" id="rclone-card">
<div class="card-header p-6 pb-3">
<h2 class="card-title text-xl">
@@ -72,44 +79,10 @@
<div class="badge" id="rclone-status">Unknown</div>
</div>
<div class="card-body p-6 pt-3" id="rclone-content">
<!-- Rclone stats will be populated here -->
</div>
</div>
<!-- Debrid Services -->
<div class="card bg-base-100 shadow-xl" id="debrid-card">
<div class="card-header p-6 pb-3">
<h2 class="card-title text-xl">
<i class="bi bi-cloud-download text-secondary"></i>
Debrid Services
</h2>
</div>
<div class="card-body p-6 pt-3" id="debrid-content">
<!-- Debrid stats will be populated here -->
</div>
</div>
<!-- Memory Details -->
<div class="card bg-base-100 shadow-xl">
<div class="card-header p-6 pb-3">
<h2 class="card-title text-xl">
<i class="bi bi-bar-chart text-accent"></i>
Memory Details
</h2>
</div>
<div class="card-body p-6 pt-3">
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<tbody id="memory-details">
<!-- Memory details will be populated here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Error State -->
<div id="error-stats" class="alert alert-error" style="display: none;">
<i class="bi bi-exclamation-triangle-fill"></i>
<span id="error-message">Failed to load statistics</span>
@@ -121,232 +94,310 @@
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const loadingEl = document.getElementById('loading-stats');
const contentEl = document.getElementById('stats-content');
const errorEl = document.getElementById('error-stats');
const refreshBtn = document.getElementById('refresh-stats');
const retryBtn = document.getElementById('retry-stats');
document.addEventListener('DOMContentLoaded', function() {
const loadingEl = document.getElementById('loading-stats');
const contentEl = document.getElementById('stats-content');
const errorEl = document.getElementById('error-stats');
const refreshBtn = document.getElementById('refresh-stats');
const retryBtn = document.getElementById('retry-stats');
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatNumber(num) {
return num.toLocaleString();
}
function showError(message) {
loadingEl.style.display = 'none';
contentEl.style.display = 'none';
errorEl.style.display = 'flex';
document.getElementById('error-message').textContent = message;
}
function showContent() {
loadingEl.style.display = 'none';
errorEl.style.display = 'none';
contentEl.style.display = 'block';
}
function showLoading() {
contentEl.style.display = 'none';
errorEl.style.display = 'none';
loadingEl.style.display = 'block';
}
function updateRcloneStats(rclone) {
const rcloneCard = document.getElementById('rclone-card');
const rcloneStatus = document.getElementById('rclone-status');
const rcloneContent = document.getElementById('rclone-content');
if (!rclone || !rclone.enabled) {
rcloneStatus.textContent = 'Disabled';
rcloneStatus.className = 'badge badge-error';
rcloneContent.innerHTML = '<p class="text-base-content/70">Rclone is not enabled or configured.</p>';
return;
function formatNumber(num) {
return num.toLocaleString();
}
if (!rclone.server_ready) {
rcloneStatus.textContent = 'Not Ready';
rcloneStatus.className = 'badge badge-warning';
rcloneContent.innerHTML = '<p class="text-base-content/70">Rclone server is not ready.</p>';
return;
function showError(message) {
loadingEl.style.display = 'none';
contentEl.style.display = 'none';
errorEl.style.display = 'flex';
document.getElementById('error-message').textContent = message;
}
rcloneStatus.textContent = 'Active';
rcloneStatus.className = 'badge badge-success';
let html = '<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">';
if (rclone.core_stats) {
html += `
<div class="stat">
<div class="stat-title">Core Version</div>
<div class="stat-value text-sm">${rclone.version || 'Unknown'}</div>
</div>
`;
function showContent() {
loadingEl.style.display = 'none';
errorEl.style.display = 'none';
contentEl.style.display = 'block';
}
if (rclone.transfer_stats) {
const ts = rclone.transfer_stats;
html += `
<div class="stat">
<div class="stat-title">Transferred</div>
<div class="stat-value text-primary">${formatBytes(ts.bytes || 0)}</div>
<div class="stat-desc">Speed: ${formatBytes(ts.speed || 0)}/s</div>
</div>
<div class="stat">
<div class="stat-title">Transfers</div>
<div class="stat-value text-info">${ts.transfers || 0}</div>
<div class="stat-desc">Errors: ${ts.errors || 0}</div>
</div>
`;
function showLoading() {
contentEl.style.display = 'none';
errorEl.style.display = 'none';
loadingEl.style.display = 'block';
}
if (rclone.memory_stats) {
const ms = rclone.memory_stats;
html += `
<div class="stat">
<div class="stat-title">Rclone Memory</div>
<div class="stat-value text-accent">${formatBytes(ms.sys || 0)}</div>
<div class="stat-desc">Heap: ${formatBytes(ms.heap_alloc || 0)}</div>
</div>
`;
}
function updateRcloneStats(rclone) {
const rcloneCard = document.getElementById('rclone-card');
const rcloneStatus = document.getElementById('rclone-status');
const rcloneContent = document.getElementById('rclone-content');
html += '</div>';
rcloneContent.innerHTML = html;
}
if (!rclone || !rclone.enabled) {
rcloneStatus.textContent = 'Disabled';
rcloneStatus.className = 'badge badge-error';
rcloneContent.innerHTML = '<p class="text-base-content/70">Rclone is not enabled or configured.</p>';
return;
}
function updateDebridStats(debrids) {
const debridContent = document.getElementById('debrid-content');
if (!rclone.server_ready) {
rcloneStatus.textContent = 'Not Ready';
rcloneStatus.className = 'badge badge-warning';
rcloneContent.innerHTML = '<p class="text-base-content/70">Rclone server is not ready.</p>';
return;
}
if (!debrids || debrids.length === 0) {
debridContent.innerHTML = '<p class="text-base-content/70">No debrid services configured.</p>';
return;
}
rcloneStatus.textContent = 'Active';
rcloneStatus.className = 'badge badge-success';
let html = '<div class="space-y-4">';
debrids.forEach(debrid => {
html += `
<div class="card bg-base-200">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h3 class="card-title text-lg">${debrid.name || 'Unknown Service'}</h3>
<p class="text-sm text-base-content/70">${debrid.username || 'No username'}</p>
</div>
<div class="text-right">
<div class="text-sm font-mono">${formatBytes(debrid.points || 0)} points</div>
<div class="text-xs text-base-content/70">Expires: ${debrid.expiration || 'Unknown'}</div>
let html = '<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">';
// Version info
if (rclone.version) {
html += `
<div class="stat">
<div class="stat-title">Rclone Version</div>
<div class="stat-value text-sm">${rclone.version.version || 'Unknown'}</div>
<div class="stat-desc">${rclone.version.arch || ''} ${rclone.version.os || ''}</div>
</div>
`;
}
// Core stats contain transfer information
if (rclone.core) {
const cs = rclone.core;
html += `
<div class="stat">
<div class="stat-title">Transferred</div>
<div class="stat-value text-primary">${window.decypharrUtils.formatBytes(cs.bytes || 0)}</div>
<div class="stat-desc">Speed: ${window.decypharrUtils.formatBytes(cs.speed || 0)}/s</div>
</div>
<div class="stat">
<div class="stat-title">Transfers</div>
<div class="stat-value text-info">${cs.transfers || 0}</div>
<div class="stat-desc">Errors: ${cs.errors || 0}</div>
</div>
<div class="stat">
<div class="stat-title">Checks</div>
<div class="stat-value text-secondary">${cs.checks || 0}</div>
<div class="stat-desc">Total: ${cs.totalChecks || 0}</div>
</div>
<div class="stat">
<div class="stat-title">Elapsed Time</div>
<div class="stat-value text-accent">${((cs.elapsedTime || 0) / 60).toFixed(1)}m</div>
<div class="stat-desc">Transfer: ${((cs.transferTime || 0) / 60).toFixed(1)}m</div>
</div>
`;
}
if (rclone.memory) {
const ms = rclone.memory;
html += `
<div class="stat">
<div class="stat-title">Rclone Memory</div>
<div class="stat-value text-accent">${window.decypharrUtils.formatBytes(ms.Sys || 0)}</div>
<div class="stat-desc">Heap: ${window.decypharrUtils.formatBytes(ms.TotalAlloc || 0)}</div>
</div>
`;
}
html += '</div>';
// Add active transfers if available
if (rclone.core && rclone.core.transferring && rclone.core.transferring.length > 0) {
let transferring = rclone.core.transferring;
html += `
<div class="mt-6">
<h3 class="text-lg font-semibold mb-3">
<i class="bi bi-arrow-down-up text-primary"></i>
Active Transfers (${transferring.length})
</h3>
<div class="space-y-2 max-h-64 overflow-y-auto">
`;
transferring.forEach(transfer => {
const progress = ((transfer.bytes || 0) / (transfer.size || 1)) * 100;
html += `
<div class="card bg-base-200 compact">
<div class="card-body">
<div class="flex justify-between items-start mb-2">
<h4 class="font-medium text-sm truncate flex-1 mr-2">${transfer.name || 'Unknown'}</h4>
<span class="text-xs text-base-content/70">${window.decypharrUtils.formatBytes(transfer.speed || 0)}/s</span>
</div>
<div class="flex items-center space-x-2">
<progress class="progress progress-primary flex-1" value="${progress}" max="100"></progress>
<span class="text-xs font-mono">${progress.toFixed(1)}%</span>
</div>
<div class="flex justify-between text-xs text-base-content/60 mt-1">
<span>${window.decypharrUtils.formatBytes(transfer.bytes || 0)} / ${window.decypharrUtils.formatBytes(transfer.size || 0)}</span>
<span>ETA: ${transfer.eta ? Math.ceil(transfer.eta / 60) + 'm' : 'Unknown'}</span>
</div>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4">
<div class="stat">
<div class="stat-title text-xs">Library Size</div>
<div class="stat-value text-sm">${formatNumber(debrid.library_size || 0)}</div>
`;
});
html += '</div></div>';
}
// Add mount information if available
if (rclone.mounts && Object.keys(rclone.mounts).length > 0) {
html += `
<div class="mt-6">
<h3 class="text-lg font-semibold mb-3">
<i class="bi bi-hdd-stack text-primary"></i>
Mounted Services
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
`;
Object.entries(rclone.mounts).forEach(([name, mount]) => {
const statusBadge = mount.mounted ?
'<span class="badge badge-success badge-sm">Mounted</span>' :
'<span class="badge badge-error badge-sm">Not Mounted</span>';
html += `
<div class="card bg-base-200 compact">
<div class="card-body">
<div class="flex justify-between items-center">
<div>
<h4 class="font-medium">${mount.config_name || name}</h4>
<p class="text-xs text-base-content/70">${mount.provider || 'Unknown Provider'}</p>
${mount.local_path ? `<p class="text-xs text-base-content/60 font-mono">${mount.local_path}</p>` : ''}
</div>
<div class="text-right">
${statusBadge}
${mount.mounted_at ? `<p class="text-xs text-base-content/60">Since: ${new Date(mount.mounted_at).toLocaleTimeString()}</p>` : ''}
</div>
</div>
${mount.error ? `<div class="alert alert-error alert-sm mt-2"><span class="text-xs">${mount.error}</span></div>` : ''}
</div>
<div class="stat">
<div class="stat-title text-xs">Bad Torrents</div>
<div class="stat-value text-sm text-error">${formatNumber(debrid.bad_torrents || 0)}</div>
</div>
`;
});
html += '</div></div>';
}
// Add bandwidth stats if available
if (rclone.bandwidth && rclone.bandwidth.rate !== "off") {
html += `
<div class="mt-6">
<h3 class="text-lg font-semibold mb-3">
<i class="bi bi-speedometer2 text-info"></i>
Bandwidth Limits
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
`;
html += `
<div class="stat bg-base-200 rounded-lg p-3">
<div class="stat-title text-xs">Bytes Per Seconds</div>
<div class="stat-value text-sm">${window.decypharrUtils.formatBytes(rclone.bandwidth.bytesPerSecond)}</div>
</div>
<div class="stat bg-base-200 rounded-lg p-3">
<div class="stat-title text-xs">Rate</div>
<div class="stat-value text-sm">${window.decypharrUtils.formatBytes(rclone.bandwidth.rate)}</div>
</div>
`;
html += '</div></div>';
}
rcloneContent.innerHTML = html;
}
function updateDebridStats(debrids) {
const debridContent = document.getElementById('debrid-content');
if (!debrids || debrids.length === 0) {
debridContent.innerHTML = '<p class="text-base-content/70">No debrid services configured.</p>';
return;
}
let html = '<div class="space-y-4">';
debrids.forEach(debrid => {
html += `
<div class="card bg-base-200">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h3 class="card-title text-lg">${debrid.name || 'Unknown Service'}</h3>
<p class="text-sm text-base-content/70">${debrid.username || 'No username'}</p>
</div>
<div class="text-right">
<div class="text-sm font-mono">${debrid.points} points</div>
<div class="text-xs text-base-content/70">Expires: ${debrid.expiration || 'Unknown'}</div>
</div>
</div>
<div class="stat">
<div class="stat-title text-xs">Active Links</div>
<div class="stat-value text-sm text-success">${formatNumber(debrid.active_links || 0)}</div>
</div>
<div class="stat">
<div class="stat-title text-xs">Type</div>
<div class="stat-value text-sm">${debrid.type || 'Unknown'}</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4">
<div class="stat">
<div class="stat-title text-xs">Library Size</div>
<div class="stat-value text-sm">${formatNumber(debrid.library_size || 0)}</div>
</div>
<div class="stat">
<div class="stat-title text-xs">Bad Torrents</div>
<div class="stat-value text-sm text-error">${formatNumber(debrid.bad_torrents || 0)}</div>
</div>
<div class="stat">
<div class="stat-title text-xs">Active Links</div>
<div class="stat-value text-sm text-success">${formatNumber(debrid.active_links || 0)}</div>
</div>
<div class="stat">
<div class="stat-title text-xs">Type</div>
<div class="stat-value text-sm">${debrid.type || 'Unknown'}</div>
</div>
</div>
</div>
</div>
</div>
`;
});
html += '</div>';
debridContent.innerHTML = html;
}
function updateMemoryDetails(stats) {
const memoryDetails = document.getElementById('memory-details');
const details = [
['Total Allocated', stats.total_alloc_mb || '-'],
['Heap Allocated', stats.heap_alloc_mb || '-'],
['System Memory', stats.memory_used || '-'],
['GC Cycles', formatNumber(stats.gc_cycles || 0)],
['Goroutines', formatNumber(stats.goroutines || 0)],
['CPU Cores', stats.num_cpu || '-'],
['OS/Arch', `${stats.os || 'Unknown'}/${stats.arch || 'Unknown'}`],
['Go Version', stats.go_version || '-']
];
let html = '';
details.forEach(([label, value]) => {
html += `
<tr>
<td class="font-medium">${label}</td>
<td class="font-mono">${value}</td>
</tr>
`;
});
memoryDetails.innerHTML = html;
}
function updateStats(stats) {
// System overview
document.getElementById('memory-used').textContent = stats.memory_used || '-';
document.getElementById('heap-alloc').textContent = `Heap: ${stats.heap_alloc_mb || '-'}`;
document.getElementById('goroutines').textContent = formatNumber(stats.goroutines || 0);
document.getElementById('gc-cycles').textContent = `GC: ${formatNumber(stats.gc_cycles || 0)} cycles`;
document.getElementById('num-cpu').textContent = stats.num_cpu || '-';
document.getElementById('arch').textContent = stats.arch || '-';
document.getElementById('os').textContent = stats.os || '-';
document.getElementById('go-version').textContent = stats.go_version || '-';
// Rclone stats
updateRcloneStats(stats.rclone);
// Debrid stats
updateDebridStats(stats.debrids);
// Memory details
updateMemoryDetails(stats);
}
function loadStats() {
showLoading();
fetch(`${window.urlBase}debug/stats`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(stats => {
updateStats(stats);
showContent();
})
.catch(error => {
console.error('Error loading stats:', error);
showError(error.message || 'Failed to load statistics');
`;
});
}
html += '</div>';
debridContent.innerHTML = html;
}
// Event listeners
refreshBtn.addEventListener('click', loadStats);
retryBtn.addEventListener('click', loadStats);
function updateStats(stats) {
// System overview
document.getElementById('memory-used').textContent = stats.memory_used || '-';
document.getElementById('heap-alloc').textContent = `Heap: ${stats.heap_alloc_mb || '-'}`;
document.getElementById('goroutines').textContent = formatNumber(stats.goroutines || 0);
document.getElementById('gc-cycles').textContent = `GC: ${formatNumber(stats.gc_cycles || 0)} cycles`;
document.getElementById('num-cpu').textContent = stats.num_cpu || '-';
document.getElementById('arch').textContent = stats.arch || '-';
document.getElementById('os').textContent = stats.os || '-';
document.getElementById('go-version').textContent = stats.go_version || '-';
// Auto-refresh every 30 seconds
setInterval(loadStats, 30000);
// Rclone stats
updateRcloneStats(stats.rclone);
// Initial load
loadStats();
});
// Debrid stats
updateDebridStats(stats.debrids);
}
function loadStats() {
showLoading();
fetch(`${window.urlBase}debug/stats`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(stats => {
updateStats(stats);
showContent();
})
.catch(error => {
console.error('Error loading stats:', error);
showError(error.message || 'Failed to load statistics');
});
}
// Event listeners
refreshBtn.addEventListener('click', loadStats);
retryBtn.addEventListener('click', loadStats);
// Auto-refresh every 30 seconds
setInterval(loadStats, 30000);
// Initial load
loadStats();
});
</script>
{{ end }}