mirror of
https://git.coastit.co.za/caitlin/CRM_ENVR.git
synced 2026-07-10 03:05:43 +02:00
Update on auto setup
This commit is contained in:
+139
-139
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Loading CoastIT CRM</title>
|
||||
<style>
|
||||
/* ── Global Reset ──────────────────────────────────────────────── */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: #0a0a1a;
|
||||
@@ -18,7 +19,8 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Robot */
|
||||
/* ── Robot Animation ────────────────────────────────────────────── */
|
||||
/* Animated robot mascot that bobs, swings arms, and blinks while loading */
|
||||
.robot {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
@@ -128,7 +130,9 @@
|
||||
100% { transform: translateY(-4px) rotate(8deg); }
|
||||
}
|
||||
|
||||
/* Status indicators */
|
||||
/* ── Status Indicators ────────────────────────────────────────── */
|
||||
/* Shows AI Server, Scraper, and Frontend status dots side by side.
|
||||
Each transitions from dim → cyan (ready) or red (failed). */
|
||||
.services {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -207,7 +211,7 @@
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Loading text */
|
||||
/* ── Loading Text ──────────────────────────────────────────────── */
|
||||
.loading-text {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
@@ -222,7 +226,9 @@
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Launch gear (inline, below everything) */
|
||||
/* ── Launch Gear ────────────────────────────────────────────────── */
|
||||
/* Appears below the loading text after all services are ready.
|
||||
Shows a spinning gear + "Launching gear!" text, then redirects to /login. */
|
||||
.launch-overlay {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
@@ -255,7 +261,9 @@
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Error state */
|
||||
/* ── Error State ────────────────────────────────────────────────── */
|
||||
/* When a service fails to start within the timeout (60s), show
|
||||
red text, an error message, and a retry button. */
|
||||
.loading-text.error {
|
||||
background: linear-gradient(135deg, #ef4444, #f97316);
|
||||
-webkit-background-clip: text;
|
||||
@@ -294,7 +302,12 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* ── Setup Wizard ──────────────────────────────────────────── */
|
||||
/* ── Setup Wizard ──────────────────────────────────────────────── */
|
||||
/* Full-screen 4-step wizard for first-time setup:
|
||||
1. Environment check (Ollama, model, browser, Facebook login)
|
||||
2. Browser auto-detection & selection
|
||||
3. Ollama model pull with progress bar
|
||||
4. Done — close wizard, start normal loading flow */
|
||||
.setup-wizard {
|
||||
display: none;
|
||||
position: fixed;
|
||||
@@ -416,7 +429,7 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Robot -->
|
||||
<!-- ── Robot Mascot ────────────────────────────────────────────── -->
|
||||
<div class="robot">
|
||||
<div class="robot-antenna"></div>
|
||||
<div class="robot-head">
|
||||
@@ -431,7 +444,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Services -->
|
||||
<!-- ── Service Status Indicators ───────────────────────────────── -->
|
||||
<div class="services">
|
||||
<div class="service" id="svc-ai">
|
||||
<span class="service-name">AI Server</span>
|
||||
@@ -464,26 +477,27 @@
|
||||
|
||||
<div class="loading-text" id="loading-text">Loading...</div>
|
||||
|
||||
<!-- Launch gear (inline, below everything) -->
|
||||
<!-- ── Launch Gear ──────────────────────────────────────────── -->
|
||||
<div class="launch-overlay" id="launch-overlay">
|
||||
<div class="launch-gear"></div>
|
||||
<div class="launch-text">🚀 Launching gear!</div>
|
||||
<div class="launch-text">Launching gear!</div>
|
||||
</div>
|
||||
|
||||
<div class="error-msg" id="error-msg"></div>
|
||||
<button class="retry-btn" id="retry-btn" onclick="location.reload()">Try Again</button>
|
||||
|
||||
<!-- ── Setup Wizard ── -->
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
Setup Wizard (4 steps, shown only on first run)
|
||||
══════════════════════════════════════════════════════════════ -->
|
||||
<div class="setup-wizard" id="setup-wizard">
|
||||
<div class="setup-steps">
|
||||
<div class="setup-dot current" id="sdot-1"></div>
|
||||
<div class="setup-dot" id="sdot-2"></div>
|
||||
<div class="setup-dot" id="sdot-3"></div>
|
||||
<div class="setup-dot" id="sdot-4"></div>
|
||||
<div class="setup-dot" id="sdot-5"></div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Welcome -->
|
||||
<!-- Step 1: Environment check -->
|
||||
<div class="setup-step active" id="sstep-1">
|
||||
<div class="robot" style="margin-bottom:20px">
|
||||
<div class="robot-antenna"></div>
|
||||
@@ -504,43 +518,22 @@
|
||||
<button class="setup-btn" id="welcome-next" onclick="goStep(2)">Next →</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Browser Profile -->
|
||||
<!-- Step 2: Browser auto-detection & selection -->
|
||||
<div class="setup-step" id="sstep-2">
|
||||
<div class="setup-title">Browser Profile</div>
|
||||
<div class="setup-subtitle">We need a Firefox or Chrome profile with Facebook logged in</div>
|
||||
<div class="setup-card">
|
||||
<div class="check-row">
|
||||
<span class="check-icon" id="prof-auto-icon">⏳</span>
|
||||
<span class="check-label">Auto-detected</span>
|
||||
<span class="check-value" id="prof-auto-val">scanning...</span>
|
||||
</div>
|
||||
<div style="margin-top:14px;font-size:13px;opacity:0.5;margin-bottom:8px">Or enter the path manually:</div>
|
||||
<input class="setup-input" id="prof-input" placeholder="C:\Users\You\AppData\Roaming\Mozilla\Firefox\Profiles\..." oninput="onProfileInput()">
|
||||
<div style="margin-top:6px;font-size:12px;color:#22d3ee" id="prof-feedback"></div>
|
||||
<div class="setup-title">Detect your browser</div>
|
||||
<div class="setup-subtitle">We found these browsers with Facebook login. Click one to select it.</div>
|
||||
<div class="setup-card" id="browser-list">
|
||||
<div id="browser-scanning" style="text-align:center;padding:16px;opacity:0.6">🔍 Scanning for browsers...</div>
|
||||
<div id="browser-results" style="display:none"></div>
|
||||
</div>
|
||||
<div class="setup-btns">
|
||||
<button class="setup-btn outline" onclick="goStep(1)">← Back</button>
|
||||
<button class="setup-btn" id="prof-next" onclick="saveProfile()" disabled>Next →</button>
|
||||
<button class="setup-btn" id="browser-confirm" onclick="confirmBrowser()" disabled>Confirm →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Facebook Login -->
|
||||
<!-- Step 3: Ollama Model Pull -->
|
||||
<div class="setup-step" id="sstep-3">
|
||||
<div class="setup-title">Facebook Login</div>
|
||||
<div class="setup-subtitle">Make sure you're logged into Facebook in your browser</div>
|
||||
<div class="setup-card" style="text-align:center">
|
||||
<div style="font-size:48px;margin-bottom:12px" id="fb-icon">🔍</div>
|
||||
<div style="font-size:14px;opacity:0.7" id="fb-status">Click "Check Login" to verify</div>
|
||||
<div class="progress-bar" id="fb-progress" style="display:none;margin-top:16px"><div class="progress-fill" id="fb-progress-fill"></div></div>
|
||||
</div>
|
||||
<div class="setup-btns">
|
||||
<button class="setup-btn outline" onclick="goStep(2)">← Back</button>
|
||||
<button class="setup-btn" id="fb-check-btn" onclick="checkFacebookLogin()">Check Login</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Ollama Model -->
|
||||
<div class="setup-step" id="sstep-4">
|
||||
<div class="setup-title">AI Model</div>
|
||||
<div class="setup-subtitle">We need to pull the AI model for the scraper to work</div>
|
||||
<div class="setup-card" style="text-align:center">
|
||||
@@ -554,8 +547,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 5: Done -->
|
||||
<div class="setup-step" id="sstep-5">
|
||||
<!-- Step 4: Done → Launch -->
|
||||
<div class="setup-step" id="sstep-4">
|
||||
<div style="font-size:64px;margin-bottom:16px">🎉</div>
|
||||
<div class="setup-title">All set!</div>
|
||||
<div class="setup-subtitle">Your environment is configured. We'll now start all services.</div>
|
||||
@@ -567,13 +560,18 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// Client-side JavaScript
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
// ── State ──
|
||||
let setupData = null;
|
||||
let currentStep = 1;
|
||||
const TOTAL_STEPS = 5;
|
||||
const TOTAL_STEPS = 4;
|
||||
let selectedBrowser = "";
|
||||
|
||||
// ── Normal loading state ──
|
||||
const MAX_ATTEMPTS = 30;
|
||||
const MAX_ATTEMPTS = 30; // 30 attempts × 2s = 60s timeout
|
||||
let attempts = 0;
|
||||
const CHECKS = [
|
||||
{ id: 'ai', key: 'ai', ready: false, failed: false },
|
||||
@@ -582,6 +580,7 @@ const CHECKS = [
|
||||
];
|
||||
const MSGS = { ai: 'AI Ready', scraper: 'Scraper Ready', frontend: 'Frontend Ready' };
|
||||
const NAMES = { ai: 'AI Server', scraper: 'Scraper', frontend: 'Frontend' };
|
||||
const BROWSE_ICONS = { firefox: '🦊', opera: '🔵', chrome: '🌐', edge: '🔷' };
|
||||
|
||||
// ── Step navigation ──
|
||||
function goStep(n) {
|
||||
@@ -593,21 +592,32 @@ function goStep(n) {
|
||||
el.classList.toggle('current', i + 1 === n);
|
||||
el.classList.toggle('done', i + 1 < n);
|
||||
});
|
||||
if (n === 2) renderBrowserCards();
|
||||
}
|
||||
|
||||
// ── Step 1: Welcome / Env check ──
|
||||
// ── Step 1: Welcome + Environment check ──
|
||||
// Fetches /setup/status from the AI server, displays checkmarks/crosses
|
||||
// for each environment requirement. If first_run is false, skips the
|
||||
// wizard entirely and goes straight to the normal loading flow.
|
||||
async function checkEnv() {
|
||||
try {
|
||||
const res = await fetch('/setup/status');
|
||||
setupData = await res.json();
|
||||
} catch {
|
||||
setupData = { first_run: true, ollama_running: false, model_available: false, profile_detected: false, facebook_logged_in: false };
|
||||
setupData = { first_run: true, ollama_running: false, model_available: false, facebook_logged_in: false, browsers: {} };
|
||||
}
|
||||
|
||||
setEnvStatus('ollama', setupData.ollama_running, setupData.ollama_running ? 'Running' : 'Not running');
|
||||
setEnvStatus('model', setupData.model_available, setupData.model_available ? `${setupData.model_name} ✓` : 'Not pulled');
|
||||
setEnvStatus('profile', setupData.profile_detected, setupData.profile_path || 'Not found');
|
||||
setEnvStatus('fb', setupData.facebook_logged_in, setupData.facebook_logged_in ? 'Logged in' : 'Not checked');
|
||||
|
||||
// Browser summary
|
||||
const detected = Object.entries(setupData.browsers || {}).filter(([, v]) => v.path)
|
||||
const loggedIn = detected.filter(([, v]) => v.logged_in)
|
||||
const browserSummary = loggedIn.length
|
||||
? loggedIn.map(([b]) => `${b.charAt(0).toUpperCase()+b.slice(1)} ✓`).join(', ')
|
||||
: detected.length ? 'Found but not logged into Facebook' : 'None detected'
|
||||
setEnvStatus('profile', detected.length > 0, browserSummary)
|
||||
setEnvStatus('fb', setupData.facebook_logged_in, setupData.facebook_logged_in ? 'Logged in' : loggedIn.length ? '' : 'Not checked')
|
||||
|
||||
if (!setupData.first_run) {
|
||||
document.getElementById('setup-wizard').classList.remove('active');
|
||||
@@ -615,8 +625,6 @@ async function checkEnv() {
|
||||
return;
|
||||
}
|
||||
document.getElementById('setup-wizard').classList.add('active');
|
||||
// Pre-fill step 2 fields
|
||||
updateProfileUI();
|
||||
}
|
||||
|
||||
function setEnvStatus(id, ok, label) {
|
||||
@@ -624,94 +632,85 @@ function setEnvStatus(id, ok, label) {
|
||||
document.getElementById('env-' + id + '-val').textContent = label;
|
||||
}
|
||||
|
||||
// ── Step 2: Browser Profile ──
|
||||
function updateProfileUI() {
|
||||
const icon = document.getElementById('prof-auto-icon');
|
||||
const val = document.getElementById('prof-auto-val');
|
||||
const input = document.getElementById('prof-input');
|
||||
if (setupData.profile_path) {
|
||||
icon.textContent = '✅';
|
||||
val.textContent = setupData.profile_path;
|
||||
input.value = setupData.profile_path;
|
||||
document.getElementById('prof-next').disabled = false;
|
||||
} else {
|
||||
icon.textContent = '❌';
|
||||
val.textContent = 'None detected';
|
||||
// ── Step 2: Browser selection ──
|
||||
// Renders clickable cards for each detected browser.
|
||||
// Auto-selects the first one that has Facebook login.
|
||||
// No manual path input needed — all detection is automatic.
|
||||
function renderBrowserCards() {
|
||||
const scanning = document.getElementById('browser-scanning');
|
||||
const results = document.getElementById('browser-results');
|
||||
const browsers = setupData.browsers || {};
|
||||
const detected = Object.entries(browsers).filter(([, v]) => v.path)
|
||||
|
||||
if (detected.length === 0) {
|
||||
scanning.textContent = '❌ No browsers with Facebook detected. Log in on your browser and click Retry.';
|
||||
const retryBtn = document.createElement('button');
|
||||
retryBtn.className = 'setup-btn';
|
||||
retryBtn.textContent = 'Retry Detection';
|
||||
retryBtn.onclick = () => location.reload();
|
||||
scanning.after(retryBtn);
|
||||
return;
|
||||
}
|
||||
|
||||
scanning.style.display = 'none';
|
||||
results.style.display = 'block';
|
||||
|
||||
// Auto-select first logged-in browser, or first detected
|
||||
const firstLoggedIn = detected.find(([, v]) => v.logged_in)
|
||||
selectedBrowser = firstLoggedIn ? firstLoggedIn[0] : detected[0][0]
|
||||
|
||||
results.innerHTML = detected.map(([name, info]) => {
|
||||
const isSel = name === selectedBrowser
|
||||
const icon = BROWSE_ICONS[name] || '🌐'
|
||||
const status = info.logged_in ? '✅ Logged in' : info.path ? '⚠️ Not logged in' : '❌ Not found'
|
||||
const pathShort = info.path ? info.path.length > 45 ? '...' + info.path.slice(-42) : info.path : ''
|
||||
return `<div class="browser-card ${isSel ? 'selected' : ''}" data-browser="${name}" onclick="pickBrowser('${name}')" style="display:flex;align-items:center;gap:12px;padding:12px;border:2px solid ${isSel ? '#6366f1' : 'rgba(255,255,255,0.08)'};border-radius:10px;margin-bottom:8px;cursor:pointer;transition:all 0.2s">
|
||||
<span style="font-size:24px">${icon}</span>
|
||||
<div style="flex:1"><strong style="text-transform:capitalize">${name}</strong><div style="font-size:12px;opacity:0.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${pathShort}</div></div>
|
||||
<span style="font-size:13px">${status}</span>
|
||||
</div>`
|
||||
}).join('')
|
||||
|
||||
document.getElementById('browser-confirm').disabled = false
|
||||
}
|
||||
|
||||
function onProfileInput() {
|
||||
const val = document.getElementById('prof-input').value.trim();
|
||||
const fb = document.getElementById('prof-feedback');
|
||||
const btn = document.getElementById('prof-next');
|
||||
if (!val) { fb.textContent = ''; btn.disabled = true; return }
|
||||
fb.textContent = 'Entered: ' + val;
|
||||
btn.disabled = false;
|
||||
function pickBrowser(name) {
|
||||
selectedBrowser = name
|
||||
document.querySelectorAll('.browser-card').forEach(el => {
|
||||
el.style.borderColor = el.dataset.browser === name ? '#6366f1' : 'rgba(255,255,255,0.08)'
|
||||
})
|
||||
}
|
||||
|
||||
async function saveProfile() {
|
||||
const path = document.getElementById('prof-input').value.trim();
|
||||
if (!path) return;
|
||||
const btn = document.getElementById('prof-next');
|
||||
btn.textContent = 'Saving...';
|
||||
btn.disabled = true;
|
||||
// Saves the selected browser to .env.local via the AI server's /setup/profile endpoint
|
||||
async function confirmBrowser() {
|
||||
if (!selectedBrowser) return
|
||||
const btn = document.getElementById('browser-confirm')
|
||||
btn.textContent = 'Saving...'
|
||||
btn.disabled = true
|
||||
|
||||
const path = setupData.browsers[selectedBrowser].path
|
||||
try {
|
||||
const res = await fetch('/setup/profile', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path }) });
|
||||
const data = await res.json();
|
||||
const res = await fetch('/setup/profile', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ browser: selectedBrowser, path })
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
setupData.profile_detected = true;
|
||||
setupData.profile_path = path;
|
||||
goStep(3);
|
||||
setupData.selected_browser = selectedBrowser
|
||||
goStep(3)
|
||||
} else {
|
||||
document.getElementById('prof-feedback').textContent = 'Error: ' + (data.error || 'failed');
|
||||
}
|
||||
} catch { document.getElementById('prof-feedback').textContent = 'Error: could not save'; }
|
||||
btn.textContent = 'Next →';
|
||||
btn.disabled = false;
|
||||
}
|
||||
|
||||
// ── Step 3: Facebook Login ──
|
||||
async function checkFacebookLogin() {
|
||||
const btn = document.getElementById('fb-check-btn');
|
||||
const status = document.getElementById('fb-status');
|
||||
const icon = document.getElementById('fb-icon');
|
||||
const prog = document.getElementById('fb-progress');
|
||||
const fill = document.getElementById('fb-progress-fill');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Checking...';
|
||||
prog.style.display = 'block';
|
||||
fill.style.width = '50%';
|
||||
status.textContent = 'Opening Facebook...';
|
||||
|
||||
try {
|
||||
const path = setupData.profile_path || '';
|
||||
const res = await fetch('/setup/check-login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profile_path: path }) });
|
||||
const data = await res.json();
|
||||
if (data.logged_in) {
|
||||
fill.style.width = '100%';
|
||||
icon.textContent = '✅';
|
||||
status.textContent = 'You are logged into Facebook!';
|
||||
status.className = 'status-ok';
|
||||
setupData.facebook_logged_in = true;
|
||||
setTimeout(() => goStep(4), 1000);
|
||||
} else {
|
||||
fill.style.width = '100%';
|
||||
icon.textContent = '❌';
|
||||
status.textContent = 'Not logged in. Log into Facebook in your browser and try again.';
|
||||
status.className = 'status-err';
|
||||
btn.textContent = 'Retry';
|
||||
btn.disabled = false;
|
||||
alert('Failed to save: ' + (data.error || 'unknown'))
|
||||
btn.disabled = false
|
||||
}
|
||||
} catch {
|
||||
icon.textContent = '❌';
|
||||
status.textContent = 'Could not reach the scraper service';
|
||||
status.className = 'status-err';
|
||||
btn.textContent = 'Retry';
|
||||
btn.disabled = false;
|
||||
alert('Could not save profile')
|
||||
btn.disabled = false
|
||||
}
|
||||
btn.textContent = 'Confirm →'
|
||||
}
|
||||
|
||||
// ── Step 4: Ollama Model ──
|
||||
// ── Step 3: Ollama Model Pull ──
|
||||
// Kicks off an "ollama pull" via the AI server and polls for progress.
|
||||
let pullPolling = false;
|
||||
async function pullModel() {
|
||||
if (pullPolling) return;
|
||||
@@ -720,7 +719,6 @@ async function pullModel() {
|
||||
const status = document.getElementById('model-status');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Starting...';
|
||||
|
||||
try {
|
||||
const res = await fetch('/setup/ollama/pull', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
@@ -745,12 +743,12 @@ async function pollPullProgress() {
|
||||
const data = await res.json();
|
||||
fill.style.width = data.progress + '%';
|
||||
if (data.status === 'done') {
|
||||
status.textContent = '✅ Model downloaded successfully!';
|
||||
status.textContent = '✅ Model downloaded!';
|
||||
status.className = 'status-ok';
|
||||
btn.textContent = 'Done';
|
||||
setupData.model_available = true;
|
||||
pullPolling = false;
|
||||
setTimeout(() => goStep(5), 1500);
|
||||
setTimeout(() => goStep(4), 1500);
|
||||
return;
|
||||
}
|
||||
if (data.status === 'failed') {
|
||||
@@ -769,25 +767,26 @@ async function pollPullProgress() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 5: Finish ──
|
||||
// ── Step 4: Finish Setup ──
|
||||
// Closes the wizard and starts the normal loading flow.
|
||||
function finishSetup() {
|
||||
document.getElementById('setup-wizard').classList.remove('active');
|
||||
startNormalFlow();
|
||||
}
|
||||
|
||||
// ── Normal loading flow ──
|
||||
function startNormalFlow() {
|
||||
poll();
|
||||
}
|
||||
// ── Normal Loading Flow ──
|
||||
// Polls /status every 2 seconds for up to 60 seconds.
|
||||
// Tracks each service's state (ready/waiting/failed).
|
||||
// All ready → show launch gear, redirect to /login after 5s.
|
||||
// Any failed → show error message + retry button.
|
||||
function startNormalFlow() { poll() }
|
||||
|
||||
async function checkAllServices() {
|
||||
try {
|
||||
const res = await fetch('/status', { cache: 'no-store' });
|
||||
const data = await res.json();
|
||||
for (const svc of CHECKS) svc.ready = data[svc.key] === true;
|
||||
} catch {
|
||||
for (const svc of CHECKS) svc.ready = false;
|
||||
}
|
||||
} catch { for (const svc of CHECKS) svc.ready = false }
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
@@ -829,7 +828,8 @@ async function poll() {
|
||||
}
|
||||
|
||||
// ── Boot ──
|
||||
// Entry point: check environment → shows wizard or starts normal flow
|
||||
checkEnv();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user