Add cleanup tooling

This commit is contained in:
2026-05-11 13:56:30 +02:00
parent c1bd5c38e0
commit 784e603979
6 changed files with 376 additions and 56 deletions

View File

@@ -58,6 +58,8 @@
"enable_userhall_image_upload": true, "enable_userhall_image_upload": true,
"abyss_enabled": true, "abyss_enabled": true,
"meme_creator": true, "meme_creator": true,
"enable_cleanup": false,
"cleanup_timeframe_days": 30,
"web_url_upload": true, "web_url_upload": true,
"enable_youtube_upload": true, "enable_youtube_upload": true,

View File

@@ -1,6 +1,7 @@
import db from "../sql.mjs"; import db from "../sql.mjs";
import audit from "../audit.mjs"; import audit from "../audit.mjs";
import f0cklib from "../routeinc/f0cklib.mjs"; import f0cklib from "../routeinc/f0cklib.mjs";
import { safeDeleteMediaFile } from "../lib_delete.mjs";
import lib from "../lib.mjs"; import lib from "../lib.mjs";
import { setMotd } from "../motd.mjs"; import { setMotd } from "../motd.mjs";
@@ -11,7 +12,7 @@ import cfg from "../config.mjs";
import security from "../security.mjs"; import security from "../security.mjs";
import crypto from "crypto"; import crypto from "crypto";
import path from "path"; import path from "path";
import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getEnablePdf, setEnablePdf, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps } from "../settings.mjs"; import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getEnablePdf, setEnablePdf, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate } from "../settings.mjs";
export default (router, tpl) => { export default (router, tpl) => {
router.get(/^\/login(\/)?$/, async (req, res) => { router.get(/^\/login(\/)?$/, async (req, res) => {
@@ -283,6 +284,8 @@ export default (router, tpl) => {
manual_approval: getManualApproval(), manual_approval: getManualApproval(),
log_user_ips: getLogUserIps(), log_user_ips: getLogUserIps(),
hash_user_ips: getHashUserIps(), hash_user_ips: getHashUserIps(),
enable_cleanup: getEnableCleanup(),
enable_cleanup_config: cfg.websrv.enable_cleanup !== false,
tmp: null tmp: null
}, req) }, req)
}); });
@@ -612,14 +615,12 @@ export default (router, tpl) => {
router.post(/^\/admin\/settings\/?$/, lib.auth, async (req, res) => { router.post(/^\/admin\/settings\/?$/, lib.auth, async (req, res) => {
const manual_approval = req.post.manual_approval === 'on' ? 'true' : 'false'; const manual_approval = req.post.manual_approval === 'on' ? 'true' : 'false';
const registration_open = req.post.registration_open === 'on' ? 'true' : 'false'; const registration_open = req.post.registration_open === 'on' ? 'true' : 'false';
const log_user_ips = req.post.log_user_ips === 'on' ? 'true' : 'false'; const enable_cleanup = req.post.enable_cleanup === 'on' ? 'true' : 'false';
const hash_user_ips = req.post.hash_user_ips === 'on' ? 'true' : 'false';
const min_tags = isNaN(parseInt(req.post.min_tags)) ? 3 : Math.max(0, parseInt(req.post.min_tags)); const min_tags = isNaN(parseInt(req.post.min_tags)) ? 3 : Math.max(0, parseInt(req.post.min_tags));
const trusted_uploads = Math.max(0, parseInt(req.post.trusted_uploads) ?? 3); const trusted_uploads = Math.max(0, parseInt(req.post.trusted_uploads) ?? 3);
await db`INSERT INTO site_settings (key, value) VALUES ('manual_approval', ${manual_approval}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`; await db`INSERT INTO site_settings (key, value) VALUES ('manual_approval', ${manual_approval}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`;
await db`INSERT INTO site_settings (key, value) VALUES ('log_user_ips', ${log_user_ips}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`; await db`INSERT INTO site_settings (key, value) VALUES ('enable_cleanup', ${enable_cleanup}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`;
await db`INSERT INTO site_settings (key, value) VALUES ('hash_user_ips', ${hash_user_ips}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`;
if (cfg.websrv.open_registration_web_toggle !== false) { if (cfg.websrv.open_registration_web_toggle !== false) {
await db`INSERT INTO site_settings (key, value) VALUES ('registration_open', ${registration_open}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`; await db`INSERT INTO site_settings (key, value) VALUES ('registration_open', ${registration_open}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`;
@@ -630,8 +631,7 @@ export default (router, tpl) => {
await db`INSERT INTO site_settings (key, value) VALUES ('trusted_uploads', ${trusted_uploads.toString()}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`; await db`INSERT INTO site_settings (key, value) VALUES ('trusted_uploads', ${trusted_uploads.toString()}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`;
setManualApproval(manual_approval === 'true'); setManualApproval(manual_approval === 'true');
setLogUserIps(log_user_ips === 'true'); setEnableCleanup(enable_cleanup === 'true');
setHashUserIps(hash_user_ips === 'true');
setMinTags(min_tags); setMinTags(min_tags);
setTrustedUploads(trusted_uploads); setTrustedUploads(trusted_uploads);
@@ -651,6 +651,160 @@ export default (router, tpl) => {
return res.writeHead(302, { "Location": "/admin" }).end(); return res.writeHead(302, { "Location": "/admin" }).end();
}); });
router.get(/^\/admin\/cleanup\/?$/, lib.auth, async (req, res) => {
if (!getEnableCleanup()) {
return res.redirect("/admin");
}
const cleanup_payload = {
session: req.session,
enable_cleanup: getEnableCleanup(),
cleanup_start_date: getCleanupStartDate(),
cleanup_end_date: getCleanupEndDate(),
totals: await lib.countf0cks(),
tmp: null
};
res.reply({
body: tpl.render("admin/cleanup", cleanup_payload, req)
});
});
router.post(/^\/admin\/cleanup\/?$/, lib.auth, async (req, res) => {
try {
const enable_cleanup = req.post.enable_cleanup === 'on' ? 'true' : 'false';
const cleanup_start_date = req.post.cleanup_start_date || '';
const cleanup_end_date = req.post.cleanup_end_date || '';
await db`INSERT INTO site_settings (key, value) VALUES ('enable_cleanup', ${enable_cleanup}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`;
await db`INSERT INTO site_settings (key, value) VALUES ('cleanup_start_date', ${cleanup_start_date}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`;
await db`INSERT INTO site_settings (key, value) VALUES ('cleanup_end_date', ${cleanup_end_date}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`;
setEnableCleanup(enable_cleanup === 'true');
setCleanupStartDate(cleanup_start_date);
setCleanupEndDate(cleanup_end_date);
if (req.headers['x-requested-with'] === 'XMLHttpRequest') {
const body = JSON.stringify({ success: true, enable_cleanup: getEnableCleanup(), cleanup_start_date: getCleanupStartDate(), cleanup_end_date: getCleanupEndDate() });
return res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }).end(body);
}
return res.writeHead(302, { "Location": "/admin/cleanup" }).end();
} catch (err) {
console.error('[ADMIN] Cleanup Settings Save failed:', err);
const msg = 'Failed to save Cleanup settings: ' + err.message;
if (req.headers['x-requested-with'] === 'XMLHttpRequest') {
const body = JSON.stringify({ success: false, msg });
return res.writeHead(500, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }).end(body);
}
return res.reply({ code: 500, body: msg });
}
});
router.post(/^\/admin\/cleanup\/run\/?$/, lib.auth, async (req, res) => {
try {
// Ensure settings are synced from DB before execution
const settings = await db`SELECT key, value FROM site_settings WHERE key IN ('enable_cleanup', 'cleanup_start_date', 'cleanup_end_date')`;
const settingsMap = Object.fromEntries(settings.map(s => [s.key, s.value]));
const isEnabled = settingsMap['enable_cleanup'] === 'true';
const startDate = settingsMap['cleanup_start_date'] || '';
const endDate = settingsMap['cleanup_end_date'] || '';
// Update memory state
setEnableCleanup(isEnabled);
setCleanupStartDate(startDate);
setCleanupEndDate(endDate);
if (!isEnabled) {
throw new Error('Cleanup is disabled in settings.');
}
if (!startDate || !endDate) {
throw new Error('Please select both a Start Date and an End Date.');
}
console.log(`[ADMIN] Starting manual cleanup for period ${startDate} to ${endDate}...`);
const start_stamp = ~~(new Date(startDate).getTime() / 1000);
const end_stamp = ~~(new Date(endDate).getTime() / 1000) + 86399; // Include full end day
// Diagnostics: Count candidates
const totalCleanable = await db`SELECT count(*) as c FROM items WHERE is_purged = false AND is_pinned = false`;
const withinRange = await db`SELECT count(*) as c FROM items WHERE is_purged = false AND is_pinned = false AND stamp >= ${start_stamp} AND stamp <= ${end_stamp}`;
const oldItems = await db`
SELECT i.id, i.dest, i.mime
FROM items i
WHERE i.is_purged = false
AND i.is_pinned = false
AND i.stamp >= ${start_stamp}
AND i.stamp <= ${end_stamp}
AND (
-- Case 1: Active posts with no engagement (ignoring automatic subscriptions)
(i.active = true AND i.is_deleted = false AND NOT EXISTS (SELECT 1 FROM comments WHERE item_id = i.id) AND NOT EXISTS (SELECT 1 FROM favorites WHERE item_id = i.id))
OR
-- Case 2: Soft-deleted posts (trash) that are old enough to be purged
(i.is_deleted = true)
OR
-- Case 3: Pending posts (not yet approved) that are old enough
(i.active = false AND i.is_deleted = false)
)
`;
const statsInfo = `(Total items: ${totalCleanable[0].c}, In range: ${withinRange[0].c})`;
console.log(`[ADMIN] Cleanup diagnostic: found ${oldItems.length} targets. ${statsInfo}`);
let count = 0;
if (oldItems.length > 0) {
for (const item of oldItems) {
try {
await safeDeleteMediaFile(item.dest, item.id);
await fs.unlink(path.join(cfg.paths.t, `${item.id}.webp`)).catch(() => { });
await fs.unlink(path.join(cfg.paths.t, `${item.id}_blur.webp`)).catch(() => { });
if (item.mime?.startsWith('audio')) {
await fs.unlink(path.join(cfg.paths.ca, `${item.id}.webp`)).catch(() => { });
}
await db`UPDATE items SET is_deleted = true, is_purged = true, active = false WHERE id = ${item.id}`;
count++;
} catch (e) {
console.error(`[CLEANUP] Failed to delete item ${item.id}:`, e.message);
}
}
}
// Log it in audit
await audit.log(req.session.id, 'run_cleanup_manual', 'system', 0, { count, startDate, endDate });
const response = { success: true, count, msg: `Successfully cleaned up ${count} posts. ${statsInfo}` };
if (req.headers['x-requested-with'] === 'XMLHttpRequest') {
const body = JSON.stringify(response);
return res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }).end(body);
}
const cleanup_payload = {
...response,
session: req.session,
enable_cleanup: getEnableCleanup(),
cleanup_start_date: getCleanupStartDate(),
cleanup_end_date: getCleanupEndDate(),
totals: await lib.countf0cks()
};
return res.reply({ body: tpl.render("admin/cleanup", cleanup_payload, req) });
} catch (err) {
console.error('[ADMIN] Cleanup execution failed:', err);
const msg = 'Cleanup failed: ' + err.message;
if (req.headers['x-requested-with'] === 'XMLHttpRequest') {
const body = JSON.stringify({ success: false, msg });
return res.writeHead(500, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }).end(body);
}
return res.reply({ code: 500, body: msg });
}
});
// User Management Routes // User Management Routes
router.get(/^\/admin\/users\/?$/, lib.auth, async (req, res) => { router.get(/^\/admin\/users\/?$/, lib.auth, async (req, res) => {
const q = req.url.qs?.q || ''; const q = req.url.qs?.q || '';

View File

@@ -9,6 +9,21 @@ let protect_files = false;
let private_messages = true; let private_messages = true;
let default_layout = 'modern'; let default_layout = 'modern';
let enable_pdf = false; let enable_pdf = false;
let enable_cleanup = false;
let cleanup_start_date = '';
let cleanup_end_date = '';
export const getEnableCleanup = () => {
if (cfg.websrv.enable_cleanup === false) return false;
return enable_cleanup;
};
export const setEnableCleanup = (val) => enable_cleanup = !!val;
export const getCleanupStartDate = () => cleanup_start_date;
export const setCleanupStartDate = (val) => cleanup_start_date = val || '';
export const getCleanupEndDate = () => cleanup_end_date;
export const setCleanupEndDate = (val) => cleanup_end_date = val || '';
export const getEnablePdf = () => enable_pdf; export const getEnablePdf = () => enable_pdf;
export const setEnablePdf = (val) => enable_pdf = !!val; export const setEnablePdf = (val) => enable_pdf = !!val;
@@ -45,17 +60,8 @@ export const setPrivateMessages = (val) => private_messages = !!val;
export const getDefaultLayout = () => default_layout; export const getDefaultLayout = () => default_layout;
export const setDefaultLayout = (val) => default_layout = (val === 'legacy' ? 'legacy' : 'modern'); export const setDefaultLayout = (val) => default_layout = (val === 'legacy' ? 'legacy' : 'modern');
let log_user_ips = false; export const getLogUserIps = () => !!cfg.websrv.log_user_ips;
export const getLogUserIps = () => log_user_ips; export const setLogUserIps = (val) => {}; // No-op, strictly config-based
export const setLogUserIps = (val) => {
log_user_ips = !!val;
// Also update the config object for components that read from it directly
cfg.websrv.log_user_ips = log_user_ips;
};
let hash_user_ips = false; export const getHashUserIps = () => !!cfg.websrv.hash_user_ips;
export const getHashUserIps = () => hash_user_ips; export const setHashUserIps = (val) => {}; // No-op, strictly config-based
export const setHashUserIps = (val) => {
hash_user_ips = !!val;
cfg.websrv.hash_user_ips = hash_user_ips;
};

View File

@@ -16,7 +16,7 @@ import { handleEmojiUpload } from "./emoji_upload_handler.mjs";
import { handleHallImageUpload, handleHallImageDelete, handleHallDelete, handleHallUpdate, handleHallCreate } from "./hall_image_handler.mjs"; import { handleHallImageUpload, handleHallImageDelete, handleHallDelete, handleHallUpdate, handleHallCreate } from "./hall_image_handler.mjs";
import { handleMetaExtract } from "./meta_extract_handler.mjs"; import { handleMetaExtract } from "./meta_extract_handler.mjs";
import { handleMetaStrip } from "./meta_strip_handler.mjs"; import { handleMetaStrip } from "./meta_strip_handler.mjs";
import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getBypassDuplicateCheck, setBypassDuplicateCheck, getProtectFiles, setProtectFiles, getPrivateMessages, setPrivateMessages, getDefaultLayout, setDefaultLayout, getEnablePdf, setEnablePdf } from "./inc/settings.mjs"; import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getBypassDuplicateCheck, setBypassDuplicateCheck, getProtectFiles, setProtectFiles, getPrivateMessages, setPrivateMessages, getDefaultLayout, setDefaultLayout, getEnablePdf, setEnablePdf, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps } from "./inc/settings.mjs";
import { updateHallsCache, getHalls } from "./inc/halls_cache.mjs"; import { updateHallsCache, getHalls } from "./inc/halls_cache.mjs";
import { createI18n } from "./inc/i18n.mjs"; import { createI18n } from "./inc/i18n.mjs";
import security from "./inc/security.mjs"; import security from "./inc/security.mjs";
@@ -664,28 +664,34 @@ process.on('uncaughtException', err => {
setEnablePdf(!!cfg.enable_pdf); setEnablePdf(!!cfg.enable_pdf);
console.log(`[BOOT] Enable PDF setting: ${getEnablePdf()}`); console.log(`[BOOT] Enable PDF setting: ${getEnablePdf()}`);
// Fetch log_user_ips and hash_user_ips setting // IP logging settings are strictly config-based
const { getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps } = await import("./inc/settings.mjs"); console.log(`[BOOT] Log User IPs: ${getLogUserIps()}`);
try { console.log(`[BOOT] Hash User IPs: ${getHashUserIps()}`);
const lipSetting = await db`SELECT value FROM site_settings WHERE key = 'log_user_ips' LIMIT 1`;
if (lipSetting.length > 0) {
setLogUserIps(lipSetting[0].value === 'true');
} else {
setLogUserIps(!!cfg.websrv.log_user_ips);
}
console.log(`[BOOT] Log User IPs: ${getLogUserIps()}`);
const hipSetting = await db`SELECT value FROM site_settings WHERE key = 'hash_user_ips' LIMIT 1`; // Fetch enable_cleanup, cleanup_start_date, and cleanup_end_date setting
if (hipSetting.length > 0) { try {
setHashUserIps(hipSetting[0].value === 'true'); const ecSetting = await db`SELECT value FROM site_settings WHERE key = 'enable_cleanup' LIMIT 1`;
if (ecSetting.length > 0) {
setEnableCleanup(ecSetting[0].value === 'true');
} else { } else {
setHashUserIps(!!cfg.websrv.hash_user_ips); setEnableCleanup(!!cfg.websrv.enable_cleanup);
} }
console.log(`[BOOT] Hash User IPs: ${getHashUserIps()}`); console.log(`[BOOT] Enable Cleanup: ${getEnableCleanup()}`);
const startSetting = await db`SELECT value FROM site_settings WHERE key = 'cleanup_start_date' LIMIT 1`;
if (startSetting.length > 0) {
setCleanupStartDate(startSetting[0].value);
}
console.log(`[BOOT] Cleanup Start Date: ${getCleanupStartDate()}`);
const endSetting = await db`SELECT value FROM site_settings WHERE key = 'cleanup_end_date' LIMIT 1`;
if (endSetting.length > 0) {
setCleanupEndDate(endSetting[0].value);
}
console.log(`[BOOT] Cleanup End Date: ${getCleanupEndDate()}`);
} catch (e) { } catch (e) {
console.warn(`[BOOT] IP logging settings fetch failed:`, e.message); console.warn(`[BOOT] Cleanup settings fetch failed:`, e.message);
setLogUserIps(!!cfg.websrv.log_user_ips); setEnableCleanup(!!cfg.websrv.enable_cleanup);
setHashUserIps(!!cfg.websrv.hash_user_ips);
} }
// Load bypass_duplicate_check from config.json (static — not a DB setting) // Load bypass_duplicate_check from config.json (static — not a DB setting)
@@ -790,6 +796,9 @@ process.on('uncaughtException', err => {
enable_profile_description: !!cfg.websrv.enable_profile_description, enable_profile_description: !!cfg.websrv.enable_profile_description,
get private_messages() { return getPrivateMessages(); }, get private_messages() { return getPrivateMessages(); },
get enable_pdf() { return getEnablePdf(); }, get enable_pdf() { return getEnablePdf(); },
get enable_cleanup() { return getEnableCleanup(); },
get cleanup_start_date() { return getCleanupStartDate(); },
get cleanup_end_date() { return getCleanupEndDate(); },
matrix_enabled: cfg.clients.find(c => c.type === 'matrix')?.enabled || false, matrix_enabled: cfg.clients.find(c => c.type === 'matrix')?.enabled || false,
ts: Date.now(), ts: Date.now(),
get default_layout() { return getDefaultLayout(); }, get default_layout() { return getDefaultLayout(); },

View File

@@ -20,6 +20,9 @@
<li><a href="/admin/memes">Meme Manager</a></li> <li><a href="/admin/memes">Meme Manager</a></li>
<li><a href="/admin/halls">Hall Manager</a></li> <li><a href="/admin/halls">Hall Manager</a></li>
<li><a href="/admin/motd">MOTD Manager</a></li> <li><a href="/admin/motd">MOTD Manager</a></li>
@if(enable_cleanup)
<li><a href="/admin/cleanup">Cleanup Manager</a></li>
@endif
<li><a href="/admin/about">About Page</a></li> <li><a href="/admin/about">About Page</a></li>
<li><a href="/admin/rules">Rules Page</a></li> <li><a href="/admin/rules">Rules Page</a></li>
<li><a href="/admin/terms">ToS Page</a></li> <li><a href="/admin/terms">ToS Page</a></li>
@@ -40,26 +43,19 @@
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
</div> </div>
@if(typeof enable_cleanup_config === "undefined" || enable_cleanup_config !== false)
<div class="settings-toggle" style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 4px; display: flex; align-items: center; justify-content: space-between; margin-top: 10px;"> <div class="settings-toggle" style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 4px; display: flex; align-items: center; justify-content: space-between; margin-top: 10px;">
<div> <div>
<label style="display: block; font-weight: bold; color: var(--accent);">Log User IPs</label> <label style="display: block; font-weight: bold; color: var(--accent);">Enable Cleanup Action</label>
<p style="margin: 2px 0 0 0; font-size: 0.8em; color: #aaa;">Log all historical IPs for user accounts.</p> <p style="margin: 2px 0 0 0; font-size: 0.8em; color: #aaa;">Enable the automated cleanup system and manager.</p>
</div> </div>
<label class="switch"> <label class="switch">
<input type="checkbox" id="log_user_ips_toggle" {{ log_user_ips ? 'checked' : '' }} onchange="saveAdminSettings()"> <input type="checkbox" id="enable_cleanup_toggle" {{ enable_cleanup ? 'checked' : '' }} onchange="saveAdminSettings()">
<span class="slider round"></span>
</label>
</div>
<div class="settings-toggle" style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 4px; display: flex; align-items: center; justify-content: space-between; margin-top: 10px;">
<div>
<label style="display: block; font-weight: bold; color: var(--accent);">Hash User IPs</label>
<p style="margin: 2px 0 0 0; font-size: 0.8em; color: #aaa;">Anonymize IPs by hashing them (same as failed logins).</p>
</div>
<label class="switch">
<input type="checkbox" id="hash_user_ips_toggle" {{ hash_user_ips ? 'checked' : '' }} onchange="saveAdminSettings()">
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
</div> </div>
@endif
@if(registration_web_toggle_enabled) @if(registration_web_toggle_enabled)
<div class="settings-toggle" style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 4px; display: flex; align-items: center; justify-content: space-between; margin-top: 10px;"> <div class="settings-toggle" style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 4px; display: flex; align-items: center; justify-content: space-between; margin-top: 10px;">
@@ -119,8 +115,7 @@
const status = document.getElementById('settings-status'); const status = document.getElementById('settings-status');
const approvalToggle = document.getElementById('manual_approval_toggle'); const approvalToggle = document.getElementById('manual_approval_toggle');
const registrationToggle = document.getElementById('registration_open_toggle'); const registrationToggle = document.getElementById('registration_open_toggle');
const logIpsToggle = document.getElementById('log_user_ips_toggle'); const cleanupToggle = document.getElementById('enable_cleanup_toggle');
const hashIpsToggle = document.getElementById('hash_user_ips_toggle');
const minTagsInput = document.getElementById('min_tags_input'); const minTagsInput = document.getElementById('min_tags_input');
const trustedUploadsInput = document.getElementById('trusted_uploads_input'); const trustedUploadsInput = document.getElementById('trusted_uploads_input');
@@ -136,8 +131,7 @@
}, },
body: new URLSearchParams({ body: new URLSearchParams({
manual_approval: approvalToggle.checked ? 'on' : 'off', manual_approval: approvalToggle.checked ? 'on' : 'off',
log_user_ips: logIpsToggle.checked ? 'on' : 'off', enable_cleanup: cleanupToggle.checked ? 'on' : 'off',
hash_user_ips: hashIpsToggle.checked ? 'on' : 'off',
...(registrationToggle ? { registration_open: registrationToggle.checked ? 'on' : 'off' } : {}), ...(registrationToggle ? { registration_open: registrationToggle.checked ? 'on' : 'off' } : {}),
min_tags: minTagsInput.value, min_tags: minTagsInput.value,
trusted_uploads: trustedUploadsInput.value, trusted_uploads: trustedUploadsInput.value,

155
views/admin/cleanup.html Normal file
View File

@@ -0,0 +1,155 @@
@include(snippets/header)
<div class="pagewrapper">
<div id="main" class="admin-container">
<div class="container">
<h2>Cleanup Manager</h2>
<p style="color: #ccc; margin-bottom: 20px;">Delete old posts that have no engagement (no comments, no favorites, and no subscriptions) to regain disk space.</p>
<div class="admin-cleanup-form" style="background: rgba(0,0,0,0.2); padding: 25px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.05);">
<form id="cleanup-form" action="/admin/cleanup" method="POST" onsubmit="event.preventDefault(); saveCleanupSettings(this);">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="settings-toggle" style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 25px; padding-bottom: 20px; border-bottom: 1px solid rgba(255,255,255,0.05);">
<div>
<label style="display: block; font-weight: bold; color: var(--accent); font-size: 1.1em;">Enable Cleanup Action</label>
<p style="margin: 5px 0 0 0; font-size: 0.9em; color: #aaa;">This must be enabled to allow running the manual cleanup process.</p>
</div>
<label class="switch">
<input type="checkbox" name="enable_cleanup" id="enable_cleanup_toggle" {{ enable_cleanup ? 'checked' : '' }}>
<span class="slider round"></span>
</label>
</div>
<div class="settings-item" style="margin-bottom: 30px;">
<label style="display: block; font-weight: bold; color: var(--accent); margin-bottom: 10px;">Cleanup Date Range</label>
<div style="display: flex; align-items: center; gap: 15px; flex-wrap: wrap;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="color: #888;">From:</span>
<input type="date" id="cleanup_start_date" name="cleanup_start_date" value="{{ cleanup_start_date }}" style="background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); color: #fff; padding: 10px; border-radius: 4px;">
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="color: #888;">To:</span>
<input type="date" id="cleanup_end_date" name="cleanup_end_date" value="{{ cleanup_end_date }}" style="background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); color: #fff; padding: 10px; border-radius: 4px;">
</div>
<span style="color: #888; flex-basis: 100%; margin-top: 5px;">Posts uploaded within this period with no engagement will be targeted.</span>
</div>
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<button type="submit" class="btn-primary" style="width: auto; padding: 12px 40px; font-weight: bold;">Save Configuration</button>
<span id="cleanup-status" style="margin-left: 15px; font-weight: bold; display: none;"></span>
</div>
</form>
</div>
<div class="admin-cleanup-run" style="margin-top: 30px; background: rgba(255,255,255,0.02); padding: 25px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.05);">
<h3 style="margin-top: 0; color: var(--accent);">Execute Cleanup</h3>
<p>Click the button below to start the cleanup process based on the timeframe saved above.</p>
<div style="display: flex; gap: 20px; align-items: center; margin-top: 20px;">
<button onclick="runCleanup()" id="run-cleanup-btn" class="btn-remove" style="width: auto; padding: 15px 40px; font-weight: bold; font-size: 1.1em; background: #c0392b;">RUN CLEANUP NOW</button>
<div id="run-status" style="font-weight: bold; font-size: 1.1em;"></div>
</div>
</div>
<div style="margin-top: 40px; padding: 20px; background: rgba(217, 83, 79, 0.1); border-left: 4px solid #d9534f; border-radius: 4px;">
<h4 style="color: #d9534f; margin-top: 0;">Danger Zone</h4>
<p style="margin-bottom: 0; color: #eee;">Deletions performed by the cleanup manager are <strong>permanent</strong> and cannot be undone. Files will be removed from disk to regain space. Only posts with 0 comments, 0 favorites, and 0 subscriptions are targeted.</p>
</div>
</div>
<style>
.switch { position: relative; display: inline-block; width: 60px; height: 34px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #333; transition: .4s; border-radius: 34px; }
.slider:before { position: absolute; content: ""; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background-color: var(--accent); }
input:checked + .slider:before { transform: translateX(26px); background-color: #000; }
.btn-remove:disabled { opacity: 0.5; cursor: not-allowed; }
</style>
<script>
async function saveCleanupSettings(form) {
const status = document.getElementById('cleanup-status');
status.textContent = 'Saving...';
status.style.color = 'var(--accent)';
status.style.display = 'inline';
try {
const formData = new FormData(form);
if (!document.getElementById('enable_cleanup_toggle').checked) {
formData.delete('enable_cleanup');
formData.append('enable_cleanup', 'off');
} else {
formData.set('enable_cleanup', 'on');
}
const res = await fetch(form.action, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': window.f0ckSession?.csrf_token || '{{ csrf_token }}'
},
body: new URLSearchParams(formData)
});
const cleanup_response = await res.json();
if (cleanup_response.success) {
status.textContent = 'Settings saved!';
status.style.color = '#28a745';
setTimeout(() => {
status.style.display = 'none';
}, 3000);
} else {
throw new Error(cleanup_response.msg || 'Unknown error');
}
} catch (err) {
console.error('Cleanup Save Error:', err);
status.textContent = 'Error: ' + err.message;
status.style.color = '#d9534f';
}
}
async function runCleanup() {
const btn = document.getElementById('run-cleanup-btn');
const status = document.getElementById('run-status');
if (!confirm('Are you absolutely sure? This will PERMANENTLY delete files from disk. This action cannot be undone.')) return;
btn.disabled = true;
btn.textContent = 'CLEANING UP...';
status.textContent = 'Processing files, please wait...';
status.style.color = 'var(--accent)';
try {
const res = await fetch('/admin/cleanup/run', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-Token': window.f0ckSession?.csrf_token || '{{ csrf_token }}'
}
});
const cleanup_response = await res.json();
if (cleanup_response.success) {
status.textContent = 'Success! ' + cleanup_response.msg;
status.style.color = '#28a745';
} else {
throw new Error(cleanup_response.msg || 'Unknown error during cleanup');
}
} catch (err) {
console.error('Cleanup Run Error:', err);
status.textContent = 'Error: ' + err.message;
status.style.color = '#d9534f';
} finally {
btn.disabled = false;
btn.textContent = 'RUN CLEANUP NOW';
}
}
</script>
</div>
</div>
@include(snippets/footer)