mirror of
https://github.com/mivodev/plugin-mivo-theme.git
synced 2026-01-26 05:15:27 +07:00
322 lines
12 KiB
JavaScript
322 lines
12 KiB
JavaScript
/**
|
|
* MIVO Theme Configuration & Main Utilities
|
|
*/
|
|
|
|
// 1. Configuration
|
|
window.MivoConfig = {
|
|
// API Configuration
|
|
// Example: "http://192.168.1.1/mivo/public"
|
|
apiBaseUrl: "https://mivo.der.my.id",
|
|
|
|
// Your Mivo Session Name
|
|
apiSession: "my-router",
|
|
|
|
// Set to true to force Check Voucher tab even if apiBaseUrl is empty (for dev/test)
|
|
debugMode: true
|
|
};
|
|
|
|
// 2. Global Utilities
|
|
window.formatTime = function(str) {
|
|
if (!str || str === '-') return '-';
|
|
|
|
// Normalize string: specific fix for "7 h 12 m 48 s" (remove spaces between value and unit)
|
|
// Converts "7 h 12 m" -> "7h12m" for easier parsing, while keeping standard "1w2d" intact.
|
|
const normalized = str.toLowerCase().replace(/\s+/g, '');
|
|
|
|
// Regex to parse MikroTik time format (e.g. 1w6d20h56m25s)
|
|
const regex = /(\d+)([wdhms])/g;
|
|
let match;
|
|
const parts = [];
|
|
|
|
while ((match = regex.exec(normalized)) !== null) {
|
|
const val = match[1];
|
|
const unit = match[2];
|
|
// Use i18n to get localized unit name. Fallback to code if not found.
|
|
const unitName = (window.i18n && window.i18n.translations?.time?.[unit]) || unit;
|
|
parts.push(`${val} ${unitName}`);
|
|
}
|
|
|
|
return parts.length > 0 ? parts.join(' ') : str;
|
|
};
|
|
|
|
// Helper to parse time string into total seconds for Live Timer
|
|
window.parseTimeSeconds = function(str) {
|
|
if (!str || str === '-') return 0;
|
|
const normalized = str.toLowerCase().replace(/\s+/g, '');
|
|
const regex = /(\d+)([wdhms])/g;
|
|
let match;
|
|
let totalSeconds = 0;
|
|
|
|
while ((match = regex.exec(normalized)) !== null) {
|
|
const val = parseInt(match[1]);
|
|
const unit = match[2];
|
|
|
|
switch(unit) {
|
|
case 'w': totalSeconds += val * 604800; break;
|
|
case 'd': totalSeconds += val * 86400; break;
|
|
case 'h': totalSeconds += val * 3600; break;
|
|
case 'm': totalSeconds += val * 60; break;
|
|
case 's': totalSeconds += val; break;
|
|
}
|
|
}
|
|
return totalSeconds;
|
|
};
|
|
|
|
// Helper to format seconds back to string (e.g. 70s -> 1m 10s)
|
|
window.formatSeconds = function(seconds) {
|
|
if (seconds <= 0) return '0s';
|
|
|
|
const w = Math.floor(seconds / 604800);
|
|
seconds %= 604800;
|
|
const d = Math.floor(seconds / 86400);
|
|
seconds %= 86400;
|
|
const h = Math.floor(seconds / 3600);
|
|
seconds %= 3600;
|
|
const m = Math.floor(seconds / 60);
|
|
const s = seconds % 60;
|
|
|
|
const parts = [];
|
|
const t = window.i18n?.translations?.time || {};
|
|
|
|
|
|
if (w > 0) parts.push(`${w} ${t.w || 'w'}`);
|
|
if (d > 0) parts.push(`${d} ${t.d || 'd'}`);
|
|
if (h > 0) parts.push(`${h} ${t.h || 'h'}`);
|
|
if (m > 0) parts.push(`${m} ${t.m || 'm'}`);
|
|
if (s > 0) parts.push(`${s} ${t.s || 's'}`);
|
|
|
|
return parts.join(' ');
|
|
};
|
|
|
|
// Helper to format bytes
|
|
window.bytesToSize = function(bytes) {
|
|
if (!bytes || bytes === 0) return '0 B';
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = parseInt(Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)));
|
|
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
|
|
};
|
|
|
|
// Status Page Logic
|
|
window.initStatusPage = function(props) {
|
|
return {
|
|
// Props from HTML
|
|
uptimeStr: props.uptime || '-',
|
|
username: props.username || '',
|
|
limitTimeStr: props.limitTime || '',
|
|
limitBytesStr: props.limitBytes || '',
|
|
remainBytesStr: props.remainBytes || '',
|
|
remainTimeStr: props.remainTime || '',
|
|
|
|
// State
|
|
uptimeSecs: 0,
|
|
formattedUptime: '-',
|
|
apiData: null,
|
|
fetchError: null, // New debug state
|
|
hasApi: window.MivoConfig?.apiBaseUrl !== '',
|
|
|
|
// Derived Limits & Usage
|
|
limitTimeSecs: 0,
|
|
limitBytes: 0,
|
|
remainBytes: 0,
|
|
limitTimeStrDisplay: '',
|
|
|
|
init() {
|
|
// Initial Parse (Native)
|
|
this.uptimeSecs = window.parseTimeSeconds(this._clean(this.uptimeStr));
|
|
this.updateUptime();
|
|
|
|
// Start Timer
|
|
setInterval(() => {
|
|
this.uptimeSecs++;
|
|
this.updateUptime();
|
|
}, 1000);
|
|
|
|
// Initial Calculation
|
|
this.calculateLimits();
|
|
|
|
// Smart Fetch: If limits are missing/invalid AND we have API, fetch it.
|
|
if (this.shouldFetchApi() && this.hasApi) {
|
|
console.log("MivoStatus: Local/Dev mode detected. Fetching API...");
|
|
this.fetchStatus();
|
|
}
|
|
},
|
|
|
|
updateUptime() {
|
|
this.formattedUptime = window.formatSeconds(this.uptimeSecs);
|
|
},
|
|
|
|
_clean(val) {
|
|
return (!val || val.startsWith('$')) ? '' : val;
|
|
},
|
|
|
|
shouldFetchApi() {
|
|
// Fetch if username is valid BUT limits are empty (dev mode or no limit set)
|
|
// 1. Unreplaced Template Check ('$')
|
|
if (this.limitTimeStr.startsWith('$') || this.limitBytesStr.startsWith('$') || this.username.startsWith('$')) return true;
|
|
|
|
// 2. Empty/Zero Check (Visual Fallback)
|
|
// If native variables are present but "0" or empty, it usually means "Unlimited" in RouterOS.
|
|
// HOWEVER, in some custom setups or dev environments, they might just be missing.
|
|
// We'll trust the API check here: If we HAVE an API config, let's double check it
|
|
// if we see "0" limits, just in case the API has better info (like your case).
|
|
const isTimeEmpty = !this.limitTimeStr || this.limitTimeStr === '0' || this.limitTimeStr === '0s';
|
|
const isDataEmpty = !this.limitBytesStr || this.limitBytesStr === '0';
|
|
|
|
if (isTimeEmpty && isDataEmpty) return true;
|
|
|
|
return false;
|
|
},
|
|
|
|
calculateLimits() {
|
|
// 1. Clean Inputs
|
|
let cTime = this._clean(this.limitTimeStr);
|
|
let cBytes = this._clean(this.limitBytesStr);
|
|
let cRemainBytes = this._clean(this.remainBytesStr);
|
|
let cRemainTime = this._clean(this.remainTimeStr);
|
|
|
|
// 2. Parse Native
|
|
this.limitTimeSecs = window.parseTimeSeconds(cTime);
|
|
this.limitBytes = parseInt(cBytes) || 0;
|
|
this.remainBytes = parseInt(cRemainBytes) || 0;
|
|
this.limitTimeStrDisplay = cTime;
|
|
|
|
// 3. API Overrides (Hybrid Fallback)
|
|
if (this.apiData) {
|
|
// If native failed (0), try API
|
|
if (this.limitBytes === 0 && this.apiData.limit_quota) {
|
|
this.limitBytes = parseInt(this.apiData.limit_quota) || 0;
|
|
}
|
|
if (this.limitTimeSecs === 0 && this.apiData.limit_uptime) {
|
|
this.limitTimeStrDisplay = this.apiData.limit_uptime;
|
|
this.limitTimeSecs = window.parseTimeSeconds(this.limitTimeStrDisplay);
|
|
}
|
|
|
|
// For Data Remaining, API usually gives "data_left".
|
|
// If we don't have native remainBytes, try parsing API
|
|
if (this.remainBytes === 0 && this.apiData.data_left) {
|
|
// Try parsing "4.4 MiB" or raw number
|
|
// Simple regex for raw number check
|
|
if (!isNaN(this.apiData.data_left)) {
|
|
// It's a number (bytes)
|
|
this.remainBytes = parseInt(this.apiData.data_left);
|
|
} else {
|
|
// It's formatted. We can't easily get exact bytes without a reverse parser.
|
|
// But for the Progress Bar, we need bytes.
|
|
// If we can't parse, we might skip the bar update or implementation a parseBytes helper later.
|
|
// For now, let's assume API sends raw bytes OR we skip.
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Fallback: Infer Limit from Uptime + Remaining
|
|
// If Limit is 0, but we have Uptime and Remaining, we can calculate the Limit.
|
|
if (this.limitTimeSecs === 0 && this.uptimeSecs > 0) {
|
|
let remainingSecs = 0;
|
|
|
|
// Try from Native String
|
|
if (this.remainTimeStr && !this.remainTimeStr.startsWith('$')) {
|
|
remainingSecs = window.parseTimeSeconds(this.remainTimeStr);
|
|
}
|
|
|
|
// Try from API if native failed
|
|
if (remainingSecs === 0 && this.apiData && this.apiData.time_left) {
|
|
remainingSecs = window.parseTimeSeconds(this.apiData.time_left);
|
|
}
|
|
|
|
// Apply Inference
|
|
if (remainingSecs > 0) {
|
|
this.limitTimeSecs = this.uptimeSecs + remainingSecs;
|
|
this.limitTimeStrDisplay = window.formatSeconds(this.limitTimeSecs) + ' (Est)';
|
|
}
|
|
}
|
|
},
|
|
|
|
getTimePercent() {
|
|
if (this.limitTimeSecs <= 0) return 0;
|
|
// Native Uptime is counting UP.
|
|
// Bar: Width = Remaining %.
|
|
// Remaining = Limit - Uptime.
|
|
let remaining = this.limitTimeSecs - this.uptimeSecs;
|
|
let p = (remaining / this.limitTimeSecs) * 100;
|
|
return p < 0 ? 0 : (p > 100 ? 100 : p);
|
|
},
|
|
|
|
getDataPercent() {
|
|
if (this.limitBytes <= 0) return 0;
|
|
|
|
// 1. Use Remain Bytes (Native or Parsed API)
|
|
if (this.remainBytes > 0) {
|
|
let p = (this.remainBytes / this.limitBytes) * 100;
|
|
return p > 100 ? 100 : p;
|
|
}
|
|
|
|
// 2. Fallback: Estimate from API "data_left" string if needed?
|
|
// (Skipped for safety to avoid NaN)
|
|
|
|
return 0;
|
|
},
|
|
|
|
getBarColor(percent) {
|
|
if (percent > 50) return 'bg-emerald-500';
|
|
if (percent > 20) return 'bg-yellow-500';
|
|
return 'bg-red-500';
|
|
},
|
|
|
|
getRemainTimeDisplay() {
|
|
const native = this._clean(this.remainTimeStr);
|
|
if (native) return native;
|
|
|
|
if (this.apiData && this.apiData.time_left) {
|
|
return window.formatTime(this.apiData.time_left);
|
|
}
|
|
return '';
|
|
},
|
|
|
|
hasRemainTime() {
|
|
// Check native
|
|
if (this._clean(this.remainTimeStr)) return true;
|
|
// Check API
|
|
if (this.apiData && this.apiData.time_left) return true;
|
|
return false;
|
|
},
|
|
|
|
refresh() {
|
|
window.location.reload();
|
|
},
|
|
|
|
async fetchStatus() {
|
|
try {
|
|
// Determine Username: Prop or Fallback
|
|
let user = this._clean(this.username);
|
|
// Fallback to 'customer' for debugging if username is unreplaced
|
|
if (!user && (this.username.startsWith('$') || window.MivoConfig?.debugMode)) {
|
|
user = "customer";
|
|
}
|
|
|
|
if (!user) return; // Can't fetch without user
|
|
|
|
const url = `${window.MivoConfig.apiBaseUrl}/api/voucher/check/${user}`;
|
|
const res = await fetch(url, {
|
|
headers: { 'X-Mivo-Session': window.MivoConfig.apiSession }
|
|
});
|
|
|
|
if (res.ok) {
|
|
const json = await res.json();
|
|
if (json && json.data) {
|
|
this.apiData = json.data;
|
|
this.calculateLimits();
|
|
this.fetchError = "Success";
|
|
} else {
|
|
this.fetchError = "No Data in JSON";
|
|
}
|
|
} else {
|
|
this.fetchError = "HTTP " + res.status;
|
|
}
|
|
} catch (e) {
|
|
console.error('MivoStatus: API Error', e);
|
|
this.fetchError = e.message;
|
|
}
|
|
}
|
|
};
|
|
};
|