Voucher Sync — Function Backup
Read-only backup of both sync functions. Copy and save to your computer in case you ever need to restore them.
🔒 These windows are read-only — you cannot accidentally change the code here. Use the Copy button to save.
🟢 Regular Sync (runs every 30 min automatically)
functions/syncVoucherUsageFromCHR
// v7 - sessions-only, time-filtered (only sessions since last run)
// At 40,000 vouchers this only touches the ~30-50 that actually got activated recently
import { createClientFromRequest } from 'npm:@base44/sdk@0.8.23';
async function chrGet(chrIp, user, pass, path) {
const auth = btoa(`${user}:${pass}`);
const res = await fetch(`http://${chrIp}/rest${path}`, {
headers: { 'Authorization': `Basic ${auth}` },
signal: AbortSignal.timeout(15000),
});
const text = await res.text();
if (!res.ok) throw new Error(`HTTP ${res.status}: ${text}`);
return text ? JSON.parse(text) : [];
}
function parseUptime(str) {
if (!str || str === '0') return 0;
let total = 0;
const d = str.match(/(\d+)d/); if (d) total += parseInt(d[1]) * 86400;
const h = str.match(/(\d+)h/); if (h) total += parseInt(h[1]) * 3600;
const m = str.match(/(\d+)m(?!s)/); if (m) total += parseInt(m[1]) * 60;
const s = str.match(/(\d+)s/); if (s) total += parseInt(s[1]);
if (total > 0) return total;
const parts = str.split(':').map(Number);
if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
return 0;
}
function safeArr(raw) {
if (Array.isArray(raw)) return raw;
if (typeof raw === 'string') { try { const p = JSON.parse(raw); return Array.isArray(p) ? p : []; } catch { return []; } }
return [];
}
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
Deno.serve(async (req) => {
try {
const base44 = createClientFromRequest(req);
const user = await base44.auth.me();
if (!user || user.role !== 'admin') {
return Response.json({ error: 'Unauthorized' }, { status: 403 });
}
const CHR_IP = '34.125.7.222';
const CHR_USER = Deno.env.get('MIKROTIK_USERNAME') || 'admin';
const CHR_PASS = Deno.env.get('MIKROTIK_PASSWORD') || '';
// Cutoff = 35 minutes ago (a bit more than the 30-min schedule to avoid gaps)
const cutoffMs = Date.now() - (35 * 60 * 1000);
const cutoffDate = new Date(cutoffMs);
console.log(`Sync cutoff: ${cutoffDate.toISOString()} (sessions since then)`);
let sessions = [];
try {
sessions = await chrGet(CHR_IP, CHR_USER, CHR_PASS, '/user-manager/session');
} catch(e) {
console.warn('Sessions endpoint failed:', e.message);
return Response.json({ error: 'Could not reach CHR sessions: ' + e.message, success: false }, { status: 500 });
}
console.log(`Total CHR sessions: ${sessions.length}`);
const recentSessions = sessions.filter(s => {
if (!s.ended && !s['end-time']) return true;
const started = s.started || s['start-time'];
if (started) {
const t = new Date(started.replace(' ', 'T'));
if (!isNaN(t) && t > cutoffDate) return true;
}
const ended = s.ended || s['end-time'];
if (ended) {
const t = new Date(ended.replace(' ', 'T'));
if (!isNaN(t) && t > cutoffDate) return true;
}
return false;
});
console.log(`Recent sessions (since cutoff): ${recentSessions.length}`);
const recentCodes = new Set();
for (const s of recentSessions) {
const code = s.user || s.username || s.name;
if (code) recentCodes.add(code);
}
console.log(`Unique codes with recent activity: ${recentCodes.size}`);
if (recentCodes.size === 0) {
return Response.json({
success: true,
message: 'No new session activity since last sync — nothing to update',
totalSessions: sessions.length,
recentSessions: 0,
activeCodes: 0,
vouchersUpdated: 0,
});
}
const usageMap = {};
for (const s of sessions) {
const code = s.user || s.username || s.name;
if (!code || !recentCodes.has(code)) continue;
if (!usageMap[code]) usageMap[code] = { uptime: 0, sessions: 0, last_seen: null };
usageMap[code].sessions++;
usageMap[code].uptime += parseUptime(s.uptime || s['session-time'] || '0');
const t = s.ended || s.started;
if (t && (!usageMap[code].last_seen || t > usageMap[code].last_seen)) usageMap[code].last_seen = t;
}
const codes = Array.from(recentCodes);
const allVouchers = [];
const BATCH = 20;
for (let i = 0; i < codes.length; i += BATCH) {
const batch = codes.slice(i, i + BATCH);
const raw = await base44.asServiceRole.entities.Voucher.filter(
{ code: { '$in': batch } }, '-created_date', BATCH
);
allVouchers.push(...safeArr(raw));
await sleep(100);
}
console.log(`Vouchers found in DB: ${allVouchers.length}`);
const now = new Date().toISOString();
const updated = [];
for (let i = 0; i < allVouchers.length; i += 10) {
const batch = allVouchers.slice(i, i + 10);
const results = await Promise.all(batch.map(async (voucher) => {
const usage = usageMap[voucher.code];
if (!usage) return null;
const hoursUsed = Math.round((usage.uptime / 3600) * 100) / 100;
const hoursChanged = Math.abs(hoursUsed - (voucher.hours_used || 0)) > 0.01;
const needsActivation = voucher.status === 'unused' && usage.sessions > 0;
if (!hoursChanged && !needsActivation) return null;
const data = { hours_used: hoursUsed, mikrotik_synced: true, last_sync_date: now };
if (needsActivation) {
data.status = 'active';
data.activation_date = usage.last_seen ? new Date(usage.last_seen).toISOString() : now;
}
await base44.entities.Voucher.update(voucher.id, data);
return { code: voucher.code, hoursUsed, sessions: usage.sessions, activated: needsActivation };
}));
updated.push(...results.filter(Boolean));
}
return Response.json({
success: true,
message: `Synced ${updated.length} vouchers (from ${recentCodes.size} active session codes)`,
totalCHRSessions: sessions.length,
recentSessions: recentSessions.length,
activeCodes: recentCodes.size,
vouchersFoundInDB: allVouchers.length,
vouchersUpdated: updated.length,
updated,
});
} catch(error) {
console.error('[SYNC] Fatal:', error.message);
return Response.json({ error: error.message, success: false }, { status: 500 });
}
});🟠 Emergency Force-Repair (orange button — always fixes it)
functions/repairVoucherSync
// Emergency Repair - forces correct data on ALL vouchers that appear in CHR sessions
// No delta check — overwrites everything. Safe to run anytime.
import { createClientFromRequest } from 'npm:@base44/sdk@0.8.23';
async function chrGet(chrIp, user, pass, path) {
const auth = btoa(`${user}:${pass}`);
const res = await fetch(`http://${chrIp}/rest${path}`, {
headers: { 'Authorization': `Basic ${auth}` },
signal: AbortSignal.timeout(20000),
});
const text = await res.text();
if (!res.ok) throw new Error(`HTTP ${res.status}: ${text}`);
return text ? JSON.parse(text) : [];
}
function parseUptime(str) {
if (!str || str === '0') return 0;
let total = 0;
const d = str.match(/(\d+)d/); if (d) total += parseInt(d[1]) * 86400;
const h = str.match(/(\d+)h/); if (h) total += parseInt(h[1]) * 3600;
const m = str.match(/(\d+)m(?!s)/); if (m) total += parseInt(m[1]) * 60;
const s = str.match(/(\d+)s/); if (s) total += parseInt(s[1]);
if (total > 0) return total;
const parts = str.split(':').map(Number);
if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
return 0;
}
function safeArr(raw) {
if (Array.isArray(raw)) return raw;
if (typeof raw === 'string') { try { const p = JSON.parse(raw); return Array.isArray(p) ? p : []; } catch { return []; } }
return [];
}
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
Deno.serve(async (req) => {
try {
const base44 = createClientFromRequest(req);
const user = await base44.auth.me();
if (!user || user.role !== 'admin') {
return Response.json({ error: 'Unauthorized' }, { status: 403 });
}
const CHR_IP = '34.125.7.222';
const CHR_USER = Deno.env.get('MIKROTIK_USERNAME') || 'admin';
const CHR_PASS = Deno.env.get('MIKROTIK_PASSWORD') || '';
let sessions = [];
try { sessions = await chrGet(CHR_IP, CHR_USER, CHR_PASS, '/user-manager/session'); }
catch(e) { console.warn('Sessions endpoint failed:', e.message); }
const usageMap = {};
for (const s of sessions) {
const code = s.user || s.username || s.name;
if (!code) continue;
if (!usageMap[code]) usageMap[code] = { uptime: 0, sessions: 0, last_seen: null };
usageMap[code].sessions++;
usageMap[code].uptime += parseUptime(s.uptime || s['session-time'] || '0');
const t = s.ended || s.started;
if (t && (!usageMap[code].last_seen || t > usageMap[code].last_seen)) usageMap[code].last_seen = t;
}
const codes = Object.keys(usageMap);
console.log(`Session codes to repair: ${codes.length}`);
if (codes.length === 0) {
return Response.json({ success: true, message: 'No session data found on CHR — nothing to repair', repaired: 0 });
}
const allVouchers = [];
const BATCH = 20;
for (let i = 0; i < codes.length; i += BATCH) {
const batch = codes.slice(i, i + BATCH);
const raw = await base44.asServiceRole.entities.Voucher.filter(
{ code: { '$in': batch } }, '-created_date', BATCH
);
allVouchers.push(...safeArr(raw));
await sleep(100);
}
console.log(`Vouchers found in DB: ${allVouchers.length}`);
const now = new Date().toISOString();
const repaired = [];
const errors = [];
for (let i = 0; i < allVouchers.length; i += 10) {
const batch = allVouchers.slice(i, i + 10);
await Promise.all(batch.map(async (voucher) => {
const usage = usageMap[voucher.code];
if (!usage) return;
const hoursUsed = Math.round((usage.uptime / 3600) * 100) / 100;
const data = {
hours_used: hoursUsed,
mikrotik_synced: true,
last_sync_date: now,
};
if (usage.sessions > 0 && voucher.status === 'unused') {
data.status = 'active';
data.activation_date = usage.last_seen ? new Date(usage.last_seen).toISOString() : now;
}
try {
await base44.entities.Voucher.update(voucher.id, data);
repaired.push({ code: voucher.code, hoursUsed, sessions: usage.sessions, wasStatus: voucher.status, newStatus: data.status || voucher.status });
} catch(e) {
errors.push({ code: voucher.code, error: e.message });
}
}));
await sleep(100);
}
return Response.json({
success: true,
message: `Emergency repair complete: ${repaired.length} vouchers force-corrected`,
sessionCodes: codes.length,
vouchersFoundInDB: allVouchers.length,
repaired: repaired.length,
errors: errors.length,
details: repaired,
errorDetails: errors,
});
} catch(error) {
console.error('[REPAIR] Fatal:', error.message);
return Response.json({ error: error.message, success: false }, { status: 500 });
}
});📋 How to restore if something goes wrong:
- Click Copy Code on the function you need to restore
- Go to Base44 Dashboard → Code → Functions
- Click the function name (syncVoucherUsageFromCHR or repairVoucherSync)
- Select all the existing code and paste the copied code
- Save — it will deploy automatically in seconds
