Compare commits
No commits in common. "cafe777377fdcdcf075034b9a1d74b98a765fddd" and "221eb55380828ebfd2949c5632bd0b5f512ce691" have entirely different histories.
cafe777377
...
221eb55380
@ -63,7 +63,6 @@ const DEFAULTS = {
|
|||||||
webhookMention: '', // optional Discord ping target: user-id, role:id, @here, @everyone
|
webhookMention: '', // optional Discord ping target: user-id, role:id, @here, @everyone
|
||||||
autoRetryRounds: 0, // 0 = off; 1-5 automatic retry rounds for transient failures after batch end
|
autoRetryRounds: 0, // 0 = off; 1-5 automatic retry rounds for transient failures after batch end
|
||||||
autoRetryDelayMin: 5, // base delay in minutes between auto-retry rounds (linear backoff: round N waits N*delay)
|
autoRetryDelayMin: 5, // base delay in minutes between auto-retry rounds (linear backoff: round N waits N*delay)
|
||||||
historyRetention: 'all', // 'all' | '7d' | '30d' | '90d' | '1000' | '100' — storage cap for upload history
|
|
||||||
// NOTE: logMode is intentionally NOT in DEFAULTS. If it were, the deep-merge
|
// NOTE: logMode is intentionally NOT in DEFAULTS. If it were, the deep-merge
|
||||||
// would seed logMode='single' for every load, which would beat (and silently
|
// would seed logMode='single' for every load, which would beat (and silently
|
||||||
// erase) the legacy sessionLog:true → "daily" migration. normalizeLogMode in
|
// erase) the legacy sessionLog:true → "daily" migration. normalizeLogMode in
|
||||||
@ -103,67 +102,6 @@ const DEFAULTS = {
|
|||||||
history: []
|
history: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const HISTORY_RETENTION_OPTIONS = [
|
|
||||||
{ value: 'all', label: 'Alles behalten' },
|
|
||||||
{ value: '7d', label: 'Letzte 7 Tage' },
|
|
||||||
{ value: '30d', label: 'Letzte 30 Tage' },
|
|
||||||
{ value: '90d', label: 'Letzte 90 Tage' },
|
|
||||||
{ value: '1000', label: 'Letzte 1000 Uploads' },
|
|
||||||
{ value: '100', label: 'Letzte 100 Uploads' }
|
|
||||||
];
|
|
||||||
|
|
||||||
function batchTimestampMs(batch) {
|
|
||||||
const raw = batch && batch.timestamp;
|
|
||||||
if (raw === null || raw === undefined || raw === '') return null;
|
|
||||||
const ms = typeof raw === 'number' ? raw : Date.parse(raw);
|
|
||||||
return Number.isFinite(ms) ? ms : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function batchRowCount(batch) {
|
|
||||||
let n = 0;
|
|
||||||
const files = (batch && batch.files) || [];
|
|
||||||
for (const file of files) {
|
|
||||||
for (const result of (file.results || [])) {
|
|
||||||
if (result.status === 'aborted' || result.status === 'error') continue;
|
|
||||||
n++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
function countHistoryRows(history) {
|
|
||||||
let n = 0;
|
|
||||||
for (const batch of (history || [])) n += batchRowCount(batch);
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyHistoryRetention(history, retention, nowMs) {
|
|
||||||
if (!Array.isArray(history) || history.length === 0) return history;
|
|
||||||
const policy = String(retention || 'all');
|
|
||||||
if (policy === 'all') return history;
|
|
||||||
|
|
||||||
if (/^\d+d$/.test(policy)) {
|
|
||||||
const days = parseInt(policy, 10);
|
|
||||||
if (!Number.isFinite(days) || days <= 0) return history;
|
|
||||||
const cutoff = nowMs - days * 86400000;
|
|
||||||
return history.filter(b => {
|
|
||||||
const ts = batchTimestampMs(b);
|
|
||||||
return ts === null || ts >= cutoff;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxRows = parseInt(policy, 10);
|
|
||||||
if (!Number.isFinite(maxRows) || maxRows <= 0) return history;
|
|
||||||
const keptReversed = [];
|
|
||||||
let acc = 0;
|
|
||||||
for (let i = history.length - 1; i >= 0; i--) {
|
|
||||||
keptReversed.push(history[i]);
|
|
||||||
acc += batchRowCount(history[i]);
|
|
||||||
if (acc >= maxRows) break;
|
|
||||||
}
|
|
||||||
return keptReversed.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConfigStore {
|
class ConfigStore {
|
||||||
constructor(app) {
|
constructor(app) {
|
||||||
const dir = app && app.isPackaged
|
const dir = app && app.isPackaged
|
||||||
@ -363,32 +301,10 @@ class ConfigStore {
|
|||||||
return this._enqueueWrite(() => {
|
return this._enqueueWrite(() => {
|
||||||
const config = this.load();
|
const config = this.load();
|
||||||
config.history.push(entry);
|
config.history.push(entry);
|
||||||
const retention = (config.globalSettings && config.globalSettings.historyRetention) || 'all';
|
|
||||||
config.history = applyHistoryRetention(config.history, retention, Date.now());
|
|
||||||
return this._atomicWrite(this._serializeForDisk(config));
|
return this._atomicWrite(this._serializeForDisk(config));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pruneHistory(retention, opts = {}) {
|
|
||||||
const dryRun = !!opts.dryRun;
|
|
||||||
return this._enqueueWrite(() => {
|
|
||||||
const config = this.load();
|
|
||||||
const beforeBatches = config.history.length;
|
|
||||||
const beforeRows = countHistoryRows(config.history);
|
|
||||||
const pruned = applyHistoryRetention(config.history, retention, Date.now());
|
|
||||||
const result = {
|
|
||||||
removedBatches: beforeBatches - pruned.length,
|
|
||||||
removedRows: beforeRows - countHistoryRows(pruned),
|
|
||||||
keptBatches: pruned.length,
|
|
||||||
keptRows: countHistoryRows(pruned)
|
|
||||||
};
|
|
||||||
if (dryRun) return result;
|
|
||||||
config.history = pruned;
|
|
||||||
if (config.globalSettings) config.globalSettings.historyRetention = String(retention || 'all');
|
|
||||||
return this._atomicWrite(this._serializeForDisk(config)).then(() => result);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
clearHistory() {
|
clearHistory() {
|
||||||
return this._enqueueWrite(() => {
|
return this._enqueueWrite(() => {
|
||||||
const config = this.load();
|
const config = this.load();
|
||||||
@ -402,6 +318,3 @@ module.exports = ConfigStore;
|
|||||||
module.exports.HOSTER_ACCOUNT_TEMPLATES = HOSTER_ACCOUNT_TEMPLATES;
|
module.exports.HOSTER_ACCOUNT_TEMPLATES = HOSTER_ACCOUNT_TEMPLATES;
|
||||||
module.exports.HOSTER_NAMES = HOSTER_NAMES;
|
module.exports.HOSTER_NAMES = HOSTER_NAMES;
|
||||||
module.exports.HOSTER_ADD_OPTIONS = HOSTER_ADD_OPTIONS;
|
module.exports.HOSTER_ADD_OPTIONS = HOSTER_ADD_OPTIONS;
|
||||||
module.exports.HISTORY_RETENTION_OPTIONS = HISTORY_RETENTION_OPTIONS;
|
|
||||||
module.exports.applyHistoryRetention = applyHistoryRetention;
|
|
||||||
module.exports.countHistoryRows = countHistoryRows;
|
|
||||||
|
|||||||
6
main.js
6
main.js
@ -1297,12 +1297,6 @@ ipcMain.handle('get-history', () => {
|
|||||||
return configStore.loadHistory();
|
return configStore.loadHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('prune-history', async (_event, payload) => {
|
|
||||||
const retention = payload && payload.retention;
|
|
||||||
const dryRun = !!(payload && payload.dryRun);
|
|
||||||
return configStore.pruneHistory(retention, { dryRun });
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('save-text-file', async (_event, defaultName, content, filters) => {
|
ipcMain.handle('save-text-file', async (_event, defaultName, content, filters) => {
|
||||||
const safeName = String(defaultName || `export-${new Date().toISOString().slice(0, 10)}.txt`);
|
const safeName = String(defaultName || `export-${new Date().toISOString().slice(0, 10)}.txt`);
|
||||||
const safeFilters = Array.isArray(filters) && filters.length
|
const safeFilters = Array.isArray(filters) && filters.length
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "3.3.67",
|
"version": "3.3.65",
|
||||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -6,7 +6,6 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
saveConfig: (config) => ipcRenderer.invoke('save-config', config),
|
saveConfig: (config) => ipcRenderer.invoke('save-config', config),
|
||||||
getHistory: () => ipcRenderer.invoke('get-history'),
|
getHistory: () => ipcRenderer.invoke('get-history'),
|
||||||
clearHistory: () => ipcRenderer.invoke('clear-history'),
|
clearHistory: () => ipcRenderer.invoke('clear-history'),
|
||||||
pruneHistory: (retention, opts) => ipcRenderer.invoke('prune-history', { retention, dryRun: !!(opts && opts.dryRun) }),
|
|
||||||
exportHistory: (format) => ipcRenderer.invoke('export-history', format),
|
exportHistory: (format) => ipcRenderer.invoke('export-history', format),
|
||||||
saveTextFile: (defaultName, content, filters) => ipcRenderer.invoke('save-text-file', defaultName, content, filters),
|
saveTextFile: (defaultName, content, filters) => ipcRenderer.invoke('save-text-file', defaultName, content, filters),
|
||||||
|
|
||||||
|
|||||||
@ -3485,15 +3485,12 @@ function renderAccounts() {
|
|||||||
const runCheckBtn = document.getElementById('accountsRunHealthCheckBtn');
|
const runCheckBtn = document.getElementById('accountsRunHealthCheckBtn');
|
||||||
if (runCheckBtn) runCheckBtn.disabled = healthCheckRunning;
|
if (runCheckBtn) runCheckBtn.disabled = healthCheckRunning;
|
||||||
|
|
||||||
const footer = document.getElementById('accountsListFooter');
|
|
||||||
|
|
||||||
if (allAccounts.length === 0) {
|
if (allAccounts.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="accounts-empty">
|
<div class="accounts-empty">
|
||||||
<p>Keine Accounts vorhanden</p>
|
<p>Keine Accounts vorhanden</p>
|
||||||
<span class="hint">Klicke auf "Account hinzufügen", um einen Hoster einzurichten.</span>
|
<span class="hint">Klicke auf "Account hinzufügen", um einen Hoster einzurichten.</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
if (footer) footer.style.display = 'none';
|
|
||||||
if (!_accountListenersBound) bindAccountListeners(container);
|
if (!_accountListenersBound) bindAccountListeners(container);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -3512,9 +3509,6 @@ function renderAccounts() {
|
|||||||
}
|
}
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
|
||||||
if (footer) footer.style.display = '';
|
|
||||||
_updateToggleAllAccountsBtn();
|
|
||||||
|
|
||||||
if (!_accountListenersBound) bindAccountListeners(container);
|
if (!_accountListenersBound) bindAccountListeners(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3584,35 +3578,6 @@ function _hosterLifetimeStat(name) {
|
|||||||
}
|
}
|
||||||
function _invalidateHosterLifetimeCache() { _hosterLifetimeCache = null; }
|
function _invalidateHosterLifetimeCache() { _hosterLifetimeCache = null; }
|
||||||
|
|
||||||
function _allAccountGroupsOpen() {
|
|
||||||
const bodies = document.querySelectorAll('#accountsList .account-hoster-group-body');
|
|
||||||
if (!bodies.length) return false;
|
|
||||||
for (const b of bodies) if (b.style.display === 'none') return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _setAllAccountGroupsOpen(open) {
|
|
||||||
const groups = document.querySelectorAll('#accountsList .account-hoster-group');
|
|
||||||
groups.forEach(group => {
|
|
||||||
const name = group.dataset.hosterGroup;
|
|
||||||
const body = group.querySelector('.account-hoster-group-body');
|
|
||||||
const arrow = group.querySelector('.panel-arrow');
|
|
||||||
if (body) body.style.display = open ? '' : 'none';
|
|
||||||
if (arrow) arrow.innerHTML = open ? '▼' : '▶';
|
|
||||||
if (name) {
|
|
||||||
const summary = _summarizeHosterGroup(config.hosters[name] || []);
|
|
||||||
_hosterGroupOpenMemory.set(name, { state: open ? 'open' : 'closed', errorsAtClose: summary.error });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
_updateToggleAllAccountsBtn();
|
|
||||||
}
|
|
||||||
|
|
||||||
function _updateToggleAllAccountsBtn() {
|
|
||||||
const btn = document.getElementById('toggleAllAccountsBtn');
|
|
||||||
if (!btn) return;
|
|
||||||
btn.textContent = _allAccountGroupsOpen() ? 'Alle einklappen' : 'Alle ausklappen';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single set of delegated listeners on the accounts container. Bound once on
|
// Single set of delegated listeners on the accounts container. Bound once on
|
||||||
// the first render and reused for every subsequent in-place update / card
|
// the first render and reused for every subsequent in-place update / card
|
||||||
// swap. Previously we rebound 4 × N button listeners + 5 × N drag listeners
|
// swap. Previously we rebound 4 × N button listeners + 5 × N drag listeners
|
||||||
@ -4157,8 +4122,6 @@ async function loadHistory() {
|
|||||||
const history = await window.api.getHistory();
|
const history = await window.api.getHistory();
|
||||||
window._historyForStats = history || [];
|
window._historyForStats = history || [];
|
||||||
_invalidateHosterLifetimeCache();
|
_invalidateHosterLifetimeCache();
|
||||||
const retSel = document.getElementById('historyRetentionSelect');
|
|
||||||
if (retSel) retSel.value = (config.globalSettings && config.globalSettings.historyRetention) || 'all';
|
|
||||||
const container = document.getElementById('historyContainer');
|
const container = document.getElementById('historyContainer');
|
||||||
|
|
||||||
if (!history || history.length === 0) {
|
if (!history || history.length === 0) {
|
||||||
@ -4341,29 +4304,13 @@ function renderRecentUploadsPanel() {
|
|||||||
if (!wasAppendOnly) updateRecentSortHeaders();
|
if (!wasAppendOnly) updateRecentSortHeaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
const HISTORY_RENDER_CAP = 2000;
|
|
||||||
|
|
||||||
function renderHistoryTable(container) {
|
function renderHistoryTable(container) {
|
||||||
if (!container || !historyRowsData.length) {
|
if (!container || !historyRowsData.length) {
|
||||||
if (container) container.innerHTML = '<p class="empty-state">Noch keine Uploads.</p>';
|
if (container) container.innerHTML = '<p class="empty-state">Noch keine Uploads.</p>';
|
||||||
const emptyNotice = document.getElementById('historyCapNotice');
|
|
||||||
if (emptyNotice) emptyNotice.style.display = 'none';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const total = historyRowsData.length;
|
const rows = sortHistoryRows(historyRowsData);
|
||||||
const working = total > HISTORY_RENDER_CAP ? historyRowsData.slice(-HISTORY_RENDER_CAP) : historyRowsData;
|
|
||||||
const notice = document.getElementById('historyCapNotice');
|
|
||||||
if (notice) {
|
|
||||||
if (total > HISTORY_RENDER_CAP) {
|
|
||||||
notice.style.display = '';
|
|
||||||
notice.textContent = `Zeige neueste ${HISTORY_RENDER_CAP.toLocaleString('de-DE')} von ${total.toLocaleString('de-DE')} Einträgen. Der vollständige Verlauf bleibt gespeichert und ist über „Verlauf exportieren“ verfügbar.`;
|
|
||||||
} else {
|
|
||||||
notice.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = sortHistoryRows(working);
|
|
||||||
const headerCell = (key, label) => {
|
const headerCell = (key, label) => {
|
||||||
const active = historySortState.key === key;
|
const active = historySortState.key === key;
|
||||||
const dir = active ? (historySortState.direction === 'asc' ? '▲' : '▼') : '↕';
|
const dir = active ? (historySortState.direction === 'asc' ? '▲' : '▼') : '↕';
|
||||||
@ -4519,7 +4466,6 @@ function setupListeners() {
|
|||||||
document.getElementById('moveDownBtn').addEventListener('click', () => moveSelectedJobs('down'));
|
document.getElementById('moveDownBtn').addEventListener('click', () => moveSelectedJobs('down'));
|
||||||
document.getElementById('moveBottomBtn').addEventListener('click', () => moveSelectedJobs('bottom'));
|
document.getElementById('moveBottomBtn').addEventListener('click', () => moveSelectedJobs('bottom'));
|
||||||
document.getElementById('accountsRunHealthCheckBtn').addEventListener('click', () => runHealthCheck('manual'));
|
document.getElementById('accountsRunHealthCheckBtn').addEventListener('click', () => runHealthCheck('manual'));
|
||||||
document.getElementById('toggleAllAccountsBtn').addEventListener('click', () => _setAllAccountGroupsOpen(!_allAccountGroupsOpen()));
|
|
||||||
document.getElementById('copyAllLinksBtn').addEventListener('click', copyAllLinks);
|
document.getElementById('copyAllLinksBtn').addEventListener('click', copyAllLinks);
|
||||||
document.getElementById('clearRecentFilesBtn').addEventListener('click', clearAllRecentFiles);
|
document.getElementById('clearRecentFilesBtn').addEventListener('click', clearAllRecentFiles);
|
||||||
document.getElementById('exportRecentFilesBtn').addEventListener('click', exportAllRecentFiles);
|
document.getElementById('exportRecentFilesBtn').addEventListener('click', exportAllRecentFiles);
|
||||||
@ -4552,29 +4498,6 @@ function setupListeners() {
|
|||||||
});
|
});
|
||||||
document.getElementById('exportHistoryBtn').addEventListener('click', exportHistory);
|
document.getElementById('exportHistoryBtn').addEventListener('click', exportHistory);
|
||||||
|
|
||||||
const historyRetentionSelect = document.getElementById('historyRetentionSelect');
|
|
||||||
if (historyRetentionSelect) {
|
|
||||||
historyRetentionSelect.addEventListener('change', async () => {
|
|
||||||
const value = historyRetentionSelect.value;
|
|
||||||
const prev = (config.globalSettings && config.globalSettings.historyRetention) || 'all';
|
|
||||||
if (value !== 'all') {
|
|
||||||
const preview = await window.api.pruneHistory(value, { dryRun: true });
|
|
||||||
if (preview && preview.removedRows > 0) {
|
|
||||||
const ok = confirm(`${preview.removedRows.toLocaleString('de-DE')} Verlaufseinträge werden dauerhaft entfernt.\n\nFortfahren?`);
|
|
||||||
if (!ok) { historyRetentionSelect.value = prev; return; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const globalSettings = { ...(config.globalSettings || {}), historyRetention: value };
|
|
||||||
config.globalSettings = globalSettings;
|
|
||||||
await window.api.saveGlobalSettings(globalSettings).catch(() => {});
|
|
||||||
if (value !== 'all') {
|
|
||||||
const res = await window.api.pruneHistory(value);
|
|
||||||
if (res && res.removedRows > 0) showCopyToast(`Verlauf gekürzt: ${res.removedRows.toLocaleString('de-DE')} entfernt`);
|
|
||||||
}
|
|
||||||
loadHistory();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto health check toggle
|
// Auto health check toggle
|
||||||
const autoToggle = document.getElementById('autoHealthCheckToggle');
|
const autoToggle = document.getElementById('autoHealthCheckToggle');
|
||||||
if (autoToggle) {
|
if (autoToggle) {
|
||||||
|
|||||||
@ -178,9 +178,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="health-check-results account-health-results" id="healthCheckResults"></div>
|
<div class="health-check-results account-health-results" id="healthCheckResults"></div>
|
||||||
<div class="accounts-list" id="accountsList"></div>
|
<div class="accounts-list" id="accountsList"></div>
|
||||||
<div class="accounts-list-footer" id="accountsListFooter" style="display:none">
|
|
||||||
<button class="btn btn-secondary" id="toggleAllAccountsBtn">Alle ausklappen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -260,21 +257,11 @@
|
|||||||
<div class="history-container">
|
<div class="history-container">
|
||||||
<div class="history-header">
|
<div class="history-header">
|
||||||
<h2>Upload-Verlauf</h2>
|
<h2>Upload-Verlauf</h2>
|
||||||
<div style="display:flex; gap:8px; align-items:center">
|
<div style="display:flex; gap:8px">
|
||||||
<label for="historyRetentionSelect" class="history-retention-label">Aufbewahrung</label>
|
|
||||||
<select id="historyRetentionSelect" class="key-input history-retention-select">
|
|
||||||
<option value="all">Alles behalten</option>
|
|
||||||
<option value="7d">Letzte 7 Tage</option>
|
|
||||||
<option value="30d">Letzte 30 Tage</option>
|
|
||||||
<option value="90d">Letzte 90 Tage</option>
|
|
||||||
<option value="1000">Letzte 1000 Uploads</option>
|
|
||||||
<option value="100">Letzte 100 Uploads</option>
|
|
||||||
</select>
|
|
||||||
<button class="btn btn-secondary" id="exportHistoryBtn">Verlauf exportieren</button>
|
<button class="btn btn-secondary" id="exportHistoryBtn">Verlauf exportieren</button>
|
||||||
<button class="btn btn-secondary" id="clearHistoryBtn">Verlauf löschen</button>
|
<button class="btn btn-secondary" id="clearHistoryBtn">Verlauf löschen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="historyCapNotice" class="history-cap-notice" style="display:none"></div>
|
|
||||||
<div id="historyContainer"></div>
|
<div id="historyContainer"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -793,7 +793,6 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
|||||||
padding: 0 0 12px;
|
padding: 0 0 12px;
|
||||||
}
|
}
|
||||||
.accounts-list { display: grid; gap: 8px; }
|
.accounts-list { display: grid; gap: 8px; }
|
||||||
.accounts-list-footer { display: flex; justify-content: flex-end; margin-top: 12px; }
|
|
||||||
|
|
||||||
.account-card {
|
.account-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -980,17 +979,6 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
|||||||
.history-container { padding: 16px; overflow: auto; flex: 1; background: linear-gradient(180deg, rgba(255,255,255,0.015), transparent 24%); }
|
.history-container { padding: 16px; overflow: auto; flex: 1; background: linear-gradient(180deg, rgba(255,255,255,0.015), transparent 24%); }
|
||||||
.history-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
.history-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||||||
.history-header h2 { font-size: 18px; }
|
.history-header h2 { font-size: 18px; }
|
||||||
.history-retention-label { font-size: 12px; color: var(--text-dim); margin-right: 2px; }
|
|
||||||
.history-retention-select { width: auto; min-width: 150px; padding: 6px 8px; }
|
|
||||||
.history-cap-notice {
|
|
||||||
margin: 0 0 10px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-dim);
|
|
||||||
background: rgba(255,255,255,0.03);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.results-table, .history-table {
|
.results-table, .history-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@ -1,84 +0,0 @@
|
|||||||
const test = require('node:test');
|
|
||||||
const assert = require('node:assert');
|
|
||||||
const { applyHistoryRetention, countHistoryRows } = require('../lib/config-store');
|
|
||||||
|
|
||||||
function batch(timestamp, okRows, extras = {}) {
|
|
||||||
const results = [];
|
|
||||||
for (let i = 0; i < okRows; i++) results.push({ status: 'success', hoster: 'voe.sx', download_url: `https://voe.sx/${i}` });
|
|
||||||
if (extras.aborted) for (let i = 0; i < extras.aborted; i++) results.push({ status: 'aborted', hoster: 'voe.sx' });
|
|
||||||
if (extras.error) for (let i = 0; i < extras.error; i++) results.push({ status: 'error', hoster: 'voe.sx' });
|
|
||||||
return { timestamp, files: [{ name: 'clip.mp4', results }] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const DAY = 86400000;
|
|
||||||
|
|
||||||
test('countHistoryRows counts only non-aborted, non-error results', () => {
|
|
||||||
const h = [batch('2026-01-01', 3, { aborted: 2, error: 1 })];
|
|
||||||
assert.strictEqual(countHistoryRows(h), 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('retention "all" returns the array unchanged', () => {
|
|
||||||
const h = [batch('2026-01-01', 5), batch('2026-01-02', 5)];
|
|
||||||
assert.strictEqual(applyHistoryRetention(h, 'all', Date.parse('2026-06-01')), h);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('count policy keeps newest whole batches up to the row target', () => {
|
|
||||||
const h = [batch('2026-01-01', 400), batch('2026-01-02', 400), batch('2026-01-03', 400)];
|
|
||||||
const pruned = applyHistoryRetention(h, '1000', Date.parse('2026-06-01'));
|
|
||||||
assert.strictEqual(pruned.length, 3);
|
|
||||||
assert.strictEqual(countHistoryRows(pruned), 1200);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('count policy drops older batches once target reached (newest first)', () => {
|
|
||||||
const h = [batch('2026-01-01', 600), batch('2026-01-02', 600), batch('2026-01-03', 600)];
|
|
||||||
const pruned = applyHistoryRetention(h, '1000', Date.parse('2026-06-01'));
|
|
||||||
assert.strictEqual(pruned.length, 2);
|
|
||||||
assert.deepStrictEqual(pruned.map(b => b.timestamp), ['2026-01-02', '2026-01-03']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('count policy always keeps the newest batch even if it alone exceeds N', () => {
|
|
||||||
const h = [batch('2026-01-01', 50), batch('2026-01-02', 5000)];
|
|
||||||
const pruned = applyHistoryRetention(h, '100', Date.parse('2026-06-01'));
|
|
||||||
assert.strictEqual(pruned.length, 1);
|
|
||||||
assert.strictEqual(pruned[0].timestamp, '2026-01-02');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('time policy drops batches older than the cutoff', () => {
|
|
||||||
const now = Date.parse('2026-06-15T00:00:00Z');
|
|
||||||
const h = [
|
|
||||||
batch(new Date(now - 10 * DAY).toISOString(), 5),
|
|
||||||
batch(new Date(now - 3 * DAY).toISOString(), 5),
|
|
||||||
batch(new Date(now - 1 * DAY).toISOString(), 5)
|
|
||||||
];
|
|
||||||
const pruned = applyHistoryRetention(h, '7d', now);
|
|
||||||
assert.strictEqual(pruned.length, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('time policy keeps batches with missing or invalid timestamp', () => {
|
|
||||||
const now = Date.parse('2026-06-15T00:00:00Z');
|
|
||||||
const h = [
|
|
||||||
batch(undefined, 5),
|
|
||||||
batch('not-a-date', 5),
|
|
||||||
batch(new Date(now - 99 * DAY).toISOString(), 5),
|
|
||||||
batch(new Date(now - 1 * DAY).toISOString(), 5)
|
|
||||||
];
|
|
||||||
const pruned = applyHistoryRetention(h, '30d', now);
|
|
||||||
assert.strictEqual(pruned.length, 3);
|
|
||||||
assert.ok(pruned.includes(h[0]));
|
|
||||||
assert.ok(pruned.includes(h[1]));
|
|
||||||
assert.ok(!pruned.includes(h[2]));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('count policy shrinks a realistic 41-batch / >1000-row history', () => {
|
|
||||||
const h = [];
|
|
||||||
for (let i = 0; i < 41; i++) h.push(batch(`2026-04-${String((i % 28) + 1).padStart(2, '0')}`, 1300));
|
|
||||||
assert.strictEqual(countHistoryRows(h), 41 * 1300);
|
|
||||||
const pruned = applyHistoryRetention(h, '1000', Date.parse('2026-06-01'));
|
|
||||||
assert.strictEqual(pruned.length, 1);
|
|
||||||
assert.strictEqual(countHistoryRows(pruned), 1300);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('empty history is returned as-is for any policy', () => {
|
|
||||||
assert.deepStrictEqual(applyHistoryRetention([], '7d', Date.now()), []);
|
|
||||||
assert.deepStrictEqual(applyHistoryRetention([], '100', Date.now()), []);
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue
Block a user