Blinkie
Social Media Control Panel

Welcome back

Sign in to your Blinkie dashboard

Email
Password

Two-factor auth

Enter the 6-digit code from Google Authenticator

Blinkie · Google Authenticator
Get Involved
Overview
Overview
Here's what's happening today
Loading...
Modules
Access all Blinkie features
📤

Publisher

Manage clients and schedule social posts

📊

Meta Ads

Monitor Facebook & Instagram campaigns

Coming soon
🔍

Google Ads

Track search & display campaigns

Coming soon
📈

Analytics

Cross-platform reporting

Coming soon
🔔

Alerts

Budget & deadline notifications

Coming soon
👥

Team

Manage admin accounts and access

Daily Briefing
Loading...
Blinkie
Your social media manager
Hi! I'm Blinkie. Ask me anything about your calendar, clients, or request a report for a specific client.
Blinkie Report
The Blink Agency - theblink.gr
"";} function buildAnalyticsTab(c, key, i) { setTimeout(function() { loadGA4Analytics(key, i); }, 300); return "
" + "
Google Analytics 4 data for the last 30 days.
" + "
" + "
" + "
Loading analytics data...
" + "
" + "" + "" + "
";} function loadGA4Analytics(key, i) { if (!key) { document.getElementById("ga4-loading-" + i).style.display = "none"; document.getElementById("ga4-error-" + i).style.display = "block"; document.getElementById("ga4-error-title-" + i).textContent = "No GA4 Property Configured"; document.getElementById("ga4-error-msg-" + i).textContent = "Add your GA4 Property ID in the Settings tab to view analytics."; return; } api("GET", "/api/analytics/" + key).then(function(data) { document.getElementById("ga4-loading-" + i).style.display = "none"; document.getElementById("ga4-content-" + i).style.display = "block"; var metrics = [ { label: "Users", value: data.totals.activeUsers, color: "#007aff", icon: "[USERS]" }, { label: "Sessions", value: data.totals.sessions, color: "#34c759", icon: "[CHART]" }, { label: "Page Views", value: data.totals.pageviews, color: "#ff9500", icon: "[VIEWS]" }, { label: "Engagement", value: data.totals.engagementRate + "%", color: "#af52de", icon: "[!]" }, { label: "Bounce Rate", value: data.totals.bounceRate + "%", color: "#ff3b30", icon: "[BOUNCE]" } ]; var metricsHtml = ""; metrics.forEach(function(m) { metricsHtml += "
" + "
" + "" + m.icon + "" + "
" + m.label.toUpperCase() + "
" + "
" + "
" + (typeof m.value === 'number' ? m.value.toLocaleString() : m.value) + "
" + "
Last 30 days
" + "
"; }); document.getElementById("ga4-metrics-" + i).innerHTML = metricsHtml; var ctx = document.getElementById("ga4-chart-" + i); if (!ctx) return; if (ctx._chart) ctx._chart.destroy(); var labels = data.timeSeries.map(function(d) { var date = d.date; return date.substr(4,2) + "/" + date.substr(6,2); }); var users = data.timeSeries.map(function(d) { return d.activeUsers; }); var sessions = data.timeSeries.map(function(d) { return d.sessions; }); ctx._chart = new Chart(ctx, { type: "line", data: { labels: labels, datasets: [{ label: "Active Users", data: users, borderColor: "#007aff", backgroundColor: "rgba(0,122,255,0.08)", borderWidth: 2.5, tension: 0.4, fill: true, pointRadius: 0, pointHoverRadius: 6, pointHoverBackgroundColor: "#007aff", pointHoverBorderColor: "#fff", pointHoverBorderWidth: 2 }, { label: "Sessions", data: sessions, borderColor: "#34c759", backgroundColor: "rgba(52,199,89,0.08)", borderWidth: 2.5, tension: 0.4, fill: true, pointRadius: 0, pointHoverRadius: 6, pointHoverBackgroundColor: "#34c759", pointHoverBorderColor: "#fff", pointHoverBorderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { display: true, position: 'top', align: 'end', labels: { usePointStyle: true, pointStyle: 'circle', padding: 15, font: { size: 12, weight: '600', family: 'Inter' }, color: '#6e6e73' } }, tooltip: { backgroundColor: 'rgba(28,28,30,0.95)', titleColor: '#fff', bodyColor: '#fff', padding: 12, borderColor: 'rgba(255,255,255,0.1)', borderWidth: 1, cornerRadius: 8, displayColors: true, titleFont: { size: 12, weight: '600' }, bodyFont: { size: 13, weight: '500' }, callbacks: { label: function(context) { return context.dataset.label + ': ' + context.parsed.y.toLocaleString(); } } } }, scales: { x: { grid: { display: false, drawBorder: false }, ticks: { color: '#6e6e73', font: { size: 11, family: 'Inter' }, maxRotation: 0, autoSkip: true, maxTicksLimit: 10 } }, y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.04)', drawBorder: false }, ticks: { color: '#6e6e73', font: { size: 11, family: 'Inter' }, padding: 8, callback: function(value) { if (value >= 1000) return (value/1000).toFixed(1) + 'k'; return value; } } } } } }); }).catch(function(err) { document.getElementById("ga4-loading-" + i).style.display = "none"; document.getElementById("ga4-error-" + i).style.display = "block"; document.getElementById("ga4-error-title-" + i).textContent = "Unable to Load Analytics"; document.getElementById("ga4-error-msg-" + i).textContent = err.message || "Check your GA4 Property ID and service account permissions."; }); } function renderAnalyticsCharts(i) { var c = clientsCache[i]; if (!c) return; api("GET", "/api/client-stats/" + encodeURIComponent(c.name)).then(function(s) { // Stats bar chart var ctx1 = document.getElementById("chart-status-" + i); if (!ctx1) return; if (ctx1._chart) ctx1._chart.destroy(); ctx1._chart = new Chart(ctx1, { type: "bar", data: { labels: ["Published", "Approved", "Scheduled", "Pending", "Failed"], datasets: [{ label: "Posts", data: [s.published, s.approved, s.scheduled, s.pending, s.failed], backgroundColor: ["rgba(52,199,89,.8)","rgba(0,122,255,.8)","rgba(175,82,222,.8)","rgba(255,149,0,.8)","rgba(255,59,48,.8)"], borderRadius: 8, borderSkipped: false }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } }, x: { grid: { display: false } } } } }); // Update mini stats var ms = document.getElementById("analytics-stats-" + i); if (ms) ms.innerHTML = "
" + s.published + "
Published
" + "
" + s.scheduled + "
Scheduled
" + "
" + s.failed + "
Failed
"; }).catch(function() {}); } function toggleClientCard(i) { var body = document.getElementById("cc-body-" + i); var chev = document.getElementById("cc-chev-" + i); var isOpen = body.style.display !== "none"; body.style.display = isOpen ? "none" : "block"; chev.style.transform = isOpen ? "" : "rotate(90deg)"; if (!isOpen) loadClientStats(i);} function loadClientStats(i) { var c = clientsCache[i]; if (!c) return; api("GET", "/api/client-stats/" + encodeURIComponent(c.name)).then(function(s) { var updateMini = function(id) { var el = document.getElementById(id); if (el) el.innerHTML = "
" + s.published + "
Published
" + "
" + s.approved + "
Approved
" + "
" + s.scheduled + "
Scheduled
"; }; updateMini("mini-stats-" + i); var aEl = document.getElementById("analytics-stats-" + i); if (aEl) aEl.innerHTML = "
" + s.published + "
Published
" + "
" + s.scheduled + "
Scheduled
" + "
" + s.failed + "
Failed
"; }).catch(function() {});} function switchClientTab(i, tab, btn) { ["brand","suggestions","analytics","settings"].forEach(function(t) { var el = document.getElementById("cctab-" + t + "-" + i); if (el) el.classList.toggle("on", t === tab); }); btn.closest(".client-tabs").querySelectorAll(".ctab").forEach(function(b) { b.classList.remove("on"); }); btn.classList.add("on");} function generateSuggestions(i, btn) { var c = clientsCache[i]; if (!c) return; var el = document.getElementById("sug-content-" + i); el.innerHTML = "
Blinkie AI is analyzing " + c.name + "...
"; spinBtn(btn, true); api("POST", "/api/ai/suggestions", { clientName: c.name, brandTone: c.brandTone, goals: c.goals, industry: c.industry, competitors: c.competitors }) .then(function(d) { var html = ""; if (d.audienceInsights) html += "
Audience Insights
" + d.audienceInsights + "
"; if (d.contentPillars && d.contentPillars.length) { html += "
Content Pillars
"; d.contentPillars.forEach(function(p) { html += "
" + p.title + "
" + p.description + "
"; if (p.examples && p.examples.length) html += "
" + p.examples.map(function(e) { return "" + e + ""; }).join("") + "
"; html += "
"; }); html += "
"; } if (d.postIdeas && d.postIdeas.length) { html += "
Post Ideas
"; d.postIdeas.slice(0, 4).forEach(function(p) { var platCls = p.platform === "Instagram" ? "b-ig" : p.platform === "Facebook" ? "b-fb" : "b-grey"; html += "
" + p.title + "
" + p.platform + "" + p.type + "
"; if (p.hook) html += "
" + p.hook + "
"; if (p.hashtags && p.hashtags.length) html += "
" + p.hashtags.slice(0,6).map(function(h) { return "" + h + ""; }).join("") + "
"; html += "
"; }); html += "
"; } if (d.quickWins && d.quickWins.length) { html += "
Quick Wins
"; d.quickWins.forEach(function(w) { html += "
" + w + "
"; }); html += "
"; } el.innerHTML = html || "
No suggestions generated. Try again.
"; }) .catch(function(e) { el.innerHTML = "
Could not generate: " + e.message + "
"; }) .then(function() { spinBtn(btn, false); });} function toggleClient(key, cb) { api("POST", "/api/clients/" + key + "/toggle").then(function(d) { toast(d.active ? "Auto-posting resumed" : "Auto-posting paused", "ok"); }).catch(function(e) { toast(e.message, "err"); cb.checked = !cb.checked; });} function saveAdsConfig(key, i) { var ga4Id = (document.getElementById("ads-ga4-id-" + i) || {}).value || ""; var metaId = (document.getElementById("ads-meta-id-" + i) || {}).value || ""; var metaTok = (document.getElementById("ads-meta-tok-" + i) || {}).value || ""; var gId = (document.getElementById("ads-google-id-" + i) || {}).value || ""; var gTok = (document.getElementById("ads-google-tok-" + i) || {}).value || ""; api("POST", "/api/clients/" + key + "/ads-config", { ga4PropertyId: ga4Id, metaAdAccountId: metaId, googleAdsCustomerId: gId, metaAdsToken: metaTok !== undefined ? metaTok : undefined, googleAdsToken: gTok !== undefined ? gTok : undefined }) .then(function() { toast("Credentials saved and encrypted", "ok"); loadClients(); }).catch(function(e) { toast(e.message, "err"); });} function loadAtClientsForFilter() { api("GET", "/api/airtable-clients").then(function(cl) { var fSel = document.getElementById("f-client"); var ncAt = document.getElementById("nc-at"); if (fSel) fSel.innerHTML = "" + cl.map(function(c) { return ""; }).join(""); if (ncAt) ncAt.innerHTML = "" + cl.map(function(c) { return ""; }).join(""); }).catch(function() {});} function loadAutoposterConfig(key,callback){api("GET","/api/clients/"+key+"/autoposter").then(function(data){callback(data);}).catch(function(e){console.error("Failed to load autoposter config:",e);callback({enabled:true,facebook:{},instagram:{},tiktok:{}});});}function saveAutoposterConfig(key,i){var enabled=(document.getElementById("autoposter-enabled-"+i)||{}).checked||false;var fbToken=(document.getElementById("autoposter-fb-token-"+i)||{}).value||"";var ttToken=(document.getElementById("autoposter-tiktok-token-"+i)||{}).value||"";var payload={enabled:enabled};if(fbToken&&fbToken!=="********"){payload.facebookToken=fbToken;}if(ttToken&&ttToken!=="********"){payload.tiktokToken=ttToken;}api("POST","/api/clients/"+key+"/autoposter",payload).then(function(result){toast("Autoposter settings saved and encrypted","ok");if(result.igUserId){var igField=document.getElementById("autoposter-ig-userid-"+i);if(igField)igField.value=result.igUserId;}loadClients();}).catch(function(e){toast(e.message,"err");});}function renderAutoposterSection(i,key,config){var container=document.getElementById("autoposter-section-"+i);if(!container)return;var fbConnected=config.facebook.connected;var igConnected=config.instagram.connected;var ttConnected=config.tiktok.connected;var enabled=config.enabled;var html="";html+="
Enable or disable autoposter for this client (credentials are preserved when OFF)
";html+="
";html+="
Facebook Page Access Token iHow to get:
1. Go to Facebook Business Manager
2. System Users -> Generate Token
3. Select your Page
4. Permissions: pages_manage_posts, pages_read_engagement, instagram_content_publish
5. Token expiry: Never
6. Copy token and paste below
";html+="";html+="
"+(fbConnected?"[OK] Connected - Page ID: "+config.facebook.pageId:"o Not connected")+"
";html+="
TikTok Access Token iHow to get:
1. Go to TikTok for Developers (developers.tiktok.com)
2. Create an App
3. Add Video Upload & Publish permissions
4. Generate Access Token
5. Note: Requires app review/approval
";html+="";html+="
"+( ttConnected?"[OK] TikTok Connected":"o TikTok not connected")+"
";html+="
";html+="
Instagram Business Account ID iAuto-filled when you add Facebook token. This is the Instagram Business Account linked to your Facebook Page. Cannot be edited manually.
";html+="";html+="
"+(igConnected?"[OK] Instagram Connected":"o No Instagram linked to Page")+"
";html+="
[GUIDE] Quick Setup Guide:* Facebook & Instagram share the same token
* Page ID and IG User ID auto-fill from token
* TikTok requires separate developer credentials
* All tokens encrypted with AES-256-GCM
* Turning autoposter OFF preserves credentials
* Posts will fail if autoposter is disabled
";html+="
";html+="";container.innerHTML=html;} function submitClient() { var name = document.getElementById("nc-name").value.trim(); var tok = document.getElementById("nc-tok").value.trim(); var rec = document.getElementById("nc-at").value; var err = document.getElementById("nc-err"); var btn = document.getElementById("btn-submit-client"); if (!name || !tok) { err.textContent = "Name and token are required"; return; } err.textContent = ""; spinBtn(btn, true); api("POST", "/api/clients", { name: name, pageAccessToken: tok, recordId: rec }) .then(function() { closeModal("m-addclient"); toast("Client connected and token encrypted", "ok"); document.getElementById("nc-name").value = ""; document.getElementById("nc-tok").value = ""; loadClients(); loadStats(); }) .catch(function(e) { err.textContent = e.message; }) .then(function() { spinBtn(btn, false); });} function loadPosts() { var wrap = document.getElementById("posts-body"); wrap.innerHTML = "
Loading...
"; var st = document.getElementById("f-status").value; var cl = document.getElementById("f-client").value; var qs = new URLSearchParams(); if (st) qs.set("status", st); if (cl) qs.set("client", cl); api("GET", "/api/posts?" + qs).then(function(posts) { if (!posts.length) { wrap.innerHTML = "
No posts found

Try adjusting the filters

"; return; } var sbadge = function(s) { var m = { "Approved":"b-green","Published":"b-blue","Failed":"b-red","Scheduled":"b-purple","In progress":"b-orange","Send for Approval":"b-orange","Revisions Requested":"b-red" }; return "" + s + ""; }; var rows = posts.map(function(p) { var d = p.date ? new Date(p.date).toLocaleString("el-GR", { day:"2-digit", month:"2-digit", year:"2-digit", hour:"2-digit", minute:"2-digit" }) : "-"; var pfB = p.platform === "Facebook" ? "FB" : p.platform === "Instagram" ? "IG" : "" + p.platform + ""; var flags = (!p.hasCaption ? "No caption " : "") + (!p.hasVisual ? "No visual" : ""); var acts = ""; if (p.status !== "Approved" && p.status !== "Published") acts += " "; if (p.status === "Approved") acts += " "; if (p.status === "Failed") acts += ""; return "
" + p.title + "
" + p.client + "" + pfB + "" + p.kind + "" + d + "" + sbadge(p.status) + "" + (flags.trim() || "OK") + "" + acts + ""; }).join(""); wrap.innerHTML = "" + rows + "
PostClientPlatformTypeDateStatusFlagsActions
"; wrap.querySelectorAll(".post-action").forEach(function(btn) { btn.addEventListener("click", function() { setStatus(btn.dataset.id, btn.dataset.status, btn); }); }); }).catch(function(e) { wrap.innerHTML = "

Failed: " + e.message + "

"; });} function setStatus(id, status, btn) { spinBtn(btn, true); api("POST", "/api/posts/" + id + "/status", { status: status }).then(function() { toast("Status: " + status, "ok"); loadPosts(); }).catch(function(e) { toast(e.message, "err"); spinBtn(btn, false); });} function loadUsers() { var el = document.getElementById("users-body"); el.innerHTML = "
Loading...
"; api("GET", "/api/users").then(function(users) { el.innerHTML = users.map(function(u) { return "
" + "
"+ "
" + u.name.charAt(0) + "
" + "
" + u.name + "
" + u.email + "
" + "" + (u.twoFAEnabled ? "2FA ON" : "No 2FA") + "" + "" + "
" + "
" + "
Set new password
" + "" + "
" + "" + "" + "
" + "
" + "
"; }).join(""); el.querySelectorAll(".user-eh").forEach(function(h) { h.addEventListener("click", function() { toggleEc("ec-u-" + h.dataset.id); }); }); el.querySelectorAll(".user-reset-pw").forEach(function(btn) { btn.addEventListener("click", function() { resetUserPw(btn.dataset.id); }); }); el.querySelectorAll(".user-del").forEach(function(btn) { btn.addEventListener("click", function() { confirmDel("user", btn.dataset.id, "Remove " + btn.dataset.name + "?", "They will lose access immediately."); }); }); setTimeout(function() { el.querySelectorAll(".fu").forEach(function(e) { e.classList.add("in"); }); }, 50); }).catch(function(e) { el.innerHTML = "

Failed

"; });} function submitUser() { var name = document.getElementById("nu-name").value.trim(); var email = document.getElementById("nu-email").value.trim(); var pw = document.getElementById("nu-pw").value; var err = document.getElementById("nu-err"); var btn = document.getElementById("btn-submit-user"); if (!name || !email || !pw) { err.textContent = "All fields required"; return; } if (pw.length < 8) { err.textContent = "Min 8 characters"; return; } err.textContent = ""; spinBtn(btn, true); api("POST", "/api/users", { name: name, email: email, password: pw }) .then(function() { closeModal("m-adduser"); toast("Member added", "ok"); document.getElementById("nu-name").value = ""; document.getElementById("nu-email").value = ""; document.getElementById("nu-pw").value = ""; loadUsers(); }) .catch(function(e) { err.textContent = e.message; }) .then(function() { spinBtn(btn, false); });} function resetUserPw(id) { var pw = (document.getElementById("upw-" + id) || {}).value || ""; if (pw.length < 8) { toast("Min 8 characters", "err"); return; } api("POST", "/api/users/" + id + "/password", { newPassword: pw }).then(function() { toast("Password updated", "ok"); }).catch(function(e) { toast(e.message, "err"); });} function saveProfile() { var name = document.getElementById("profile-name").value.trim(); var email = document.getElementById("profile-email").value.trim(); var msg = document.getElementById("profile-msg"); if (!name || !email) { msg.textContent = "Name and email required"; msg.style.color = "var(--red)"; return; } api("GET", "/api/users").then(function(users) { var self = users.find(function(u) { return u.email === ME.email; }); if (!self) throw new Error("Cannot find your account"); return api("POST", "/api/users/" + self.id + "/profile", { name: name, email: email }); }).then(function() { ME.email = email; ME.name = name; document.getElementById("sb-uname").textContent = name; document.getElementById("sb-av").textContent = name.charAt(0).toUpperCase(); msg.textContent = "Profile updated!"; msg.style.color = "var(--green)"; setTimeout(function() { msg.textContent = ""; }, 3000); }).catch(function(e) { msg.textContent = e.message; msg.style.color = "var(--red)"; }); } function changePw() { var cur = document.getElementById("pw-cur").value; var nw = document.getElementById("pw-new").value; var con = document.getElementById("pw-con").value; if (nw !== con) { toast("Passwords do not match", "err"); return; } if (nw.length < 8) { toast("Min 8 characters", "err"); return; } var btn = document.getElementById("btn-change-pw"); spinBtn(btn, true); api("GET", "/api/users").then(function(users) { var self = users.find(function(u) { return u.email === ME.email; }); if (!self) throw new Error("Cannot find your account"); return api("POST", "/api/users/" + self.id + "/password", { newPassword: nw }); }).then(function() { toast("Password updated", "ok"); ["pw-cur","pw-new","pw-con"].forEach(function(id) { document.getElementById(id).value = ""; }); }) .catch(function(e) { toast(e.message, "err"); }).then(function() { spinBtn(btn, false); });} function loadProfileFields() { api("GET", "/api/users").then(function(users) { var self = users.find(function(u) { return u.email === ME.email; }); if (!self) return; var n = document.getElementById("profile-name"); var e = document.getElementById("profile-email"); if (n) n.value = self.name || ""; if (e) e.value = self.email || ""; }).catch(function() {}); } function loadTFASettings() { api("GET", "/api/users").then(function(users) { var self = users.find(function(u) { return u.email === ME.email; }); if (!self) return; var body = document.getElementById("tfa-settings-body"); var lbl = document.getElementById("tfa-status-lbl"); if (self.twoFAEnabled) { lbl.textContent = "2FA is active on your account"; body.innerHTML = "
Two-factor authentication is enabled
" + "
Enter 2FA code to disable
" + "" + ""; document.getElementById("btn-disable-2fa").addEventListener("click", disable2FA); } else { lbl.textContent = "Protect with Google Authenticator"; body.innerHTML = "
After setup, you will enter a 6-digit code from Google Authenticator on every login.
" + ""; document.getElementById("btn-setup-2fa-inner").addEventListener("click", setup2FA); } }).catch(function() {});} function setup2FA() { api("POST", "/api/2fa/setup").then(function(d) { document.getElementById("qr-img").src = d.qrUrl; document.getElementById("qr-secret").textContent = d.secret; document.getElementById("setup-code").value = ""; document.getElementById("setup-err").textContent = ""; openModal("m-2fa-setup"); setTimeout(function() { document.getElementById("setup-code").focus(); }, 300); }).catch(function(e) { toast(e.message, "err"); });} function confirm2FA() { var code = document.getElementById("setup-code").value.trim(); var err = document.getElementById("setup-err"); var btn = document.getElementById("btn-confirm-2fa"); err.textContent = ""; spinBtn(btn, true); api("POST", "/api/2fa/confirm", { code: code }) .then(function() { closeModal("m-2fa-setup"); toast("2FA enabled! Account is now protected.", "ok"); loadTFASettings(); }) .catch(function(e) { err.textContent = e.message; }) .then(function() { spinBtn(btn, false); });} function disable2FA() { var code = (document.getElementById("disable-code") || {}).value || ""; if (!code) { toast("Enter your current 2FA code", "err"); return; } api("POST", "/api/2fa/disable", { code: code }).then(function() { toast("2FA disabled", "ok"); loadTFASettings(); }).catch(function(e) { toast(e.message, "err"); });} function toggleEc(id) { var c = document.getElementById(id); if (c) c.classList.toggle("open"); } function confirmDel(type, id, title, sub) { document.getElementById("del-title").textContent = title; document.getElementById("del-sub").textContent = sub; openModal("m-del"); delCb = function() { var endpoint = type === "client" ? "/api/clients/" + id : "/api/users/" + id; api("DELETE", endpoint).then(function() { closeModal("m-del"); toast("Removed", "ok"); if (type === "client") { loadClients(); loadStats(); } if (type === "user") loadUsers(); }).catch(function(e) { toast(e.message, "err"); }); };} function sendChat() { var input = document.getElementById("cp-input"); var msg = input.value.trim(); if (!msg) return; input.value = ""; input.style.height = "42px"; appendMsg(msg, "user"); chatHistory.push({ role: "user", content: msg }); var tid = "t" + Date.now(); var msgs = document.getElementById("cp-messages"); msgs.innerHTML += "
"; msgs.scrollTop = msgs.scrollHeight; api("POST", "/api/ai/chat", { message: msg, history: chatHistory.slice(-8) }).then(function(d) { var te = document.getElementById(tid); if (te) te.remove(); var reply = d.reply.replace("[REPORT]", "").trim(); chatHistory.push({ role: "assistant", content: reply }); var el = appendMsg(reply, "ai"); if (d.isReport) { var rb = document.createElement("button"); rb.className = "report-btn"; rb.textContent = "View & Print Report"; rb.addEventListener("click", function() { showReport(reply); }); el.appendChild(rb); } }).catch(function(e) { var te = document.getElementById(tid); if (te) te.remove(); appendMsg("Something went wrong. Please try again.", "ai"); });} function appendMsg(text, type) { var msgs = document.getElementById("cp-messages"); var el = document.createElement("div"); el.className = "msg msg-" + type; el.textContent = text; msgs.appendChild(el); msgs.scrollTop = msgs.scrollHeight; return el;} function showReport(text) { document.getElementById("report-content").textContent = text.replace("[REPORT]", "").trim(); document.getElementById("report-frame").style.display = "block";} function initScrollFade() { var obs = new IntersectionObserver(function(entries) { entries.forEach(function(e) { if (e.isIntersecting) e.target.classList.add("in"); }); }, { threshold: 0.06 }); document.querySelectorAll(".fu:not(.in)").forEach(function(el) { obs.observe(el); });} (function() { document.getElementById("login-btn").addEventListener("click", doLogin); document.getElementById("l-email").addEventListener("keydown", function(e) { if (e.key === "Enter") doLogin(); }); document.getElementById("l-pw").addEventListener("keydown", function(e) { if (e.key === "Enter") doLogin(); }); document.getElementById("tfa-btn").addEventListener("click", verify2FA); document.getElementById("tfa-code").addEventListener("keydown", function(e) { if (e.key === "Enter") verify2FA(); }); document.getElementById("tfa-back").addEventListener("click", function() { document.getElementById("step-2fa").style.display = "none"; document.getElementById("step-pw").style.display = ""; }); document.querySelectorAll(".nb[data-page]").forEach(function(btn) { btn.addEventListener("click", function() { goPage(btn.dataset.page); }); }); document.querySelectorAll(".tb[data-page]").forEach(function(btn) { btn.addEventListener("click", function() { goPage(btn.dataset.page); }); }); document.getElementById("btn-back").addEventListener("click", function() { if (pageHistory.length > 1) { pageHistory.pop(); history.back(); } }); document.getElementById("btn-logout").addEventListener("click", doLogout); document.getElementById("mc-publisher").addEventListener("click", function() { goPage("clients"); }); document.getElementById("mc-team").addEventListener("click", function() { goPage("users"); }); document.getElementById("btn-refresh-stats").addEventListener("click", loadStats); document.getElementById("btn-refresh-posts").addEventListener("click", loadPosts); document.getElementById("f-status").addEventListener("change", loadPosts); document.getElementById("f-client").addEventListener("change", loadPosts); document.getElementById("btn-add-client").addEventListener("click", function() { openModal("m-addclient"); }); document.getElementById("btn-cancel-addclient").addEventListener("click", function() { closeModal("m-addclient"); }); document.getElementById("btn-submit-client").addEventListener("click", submitClient); document.getElementById("btn-add-user").addEventListener("click", function() { openModal("m-adduser"); }); document.getElementById("btn-cancel-adduser").addEventListener("click", function() { closeModal("m-adduser"); }); document.getElementById("btn-submit-user").addEventListener("click", submitUser); document.getElementById("btn-cancel-del").addEventListener("click", function() { closeModal("m-del"); }); document.getElementById("btn-confirm-del").addEventListener("click", function() { if (delCb) delCb(); }); document.getElementById("ec-profile-h").addEventListener("click", function() { toggleEc("ec-profile"); }); document.getElementById("btn-save-profile").addEventListener("click", saveProfile); document.getElementById("ec-pw-h").addEventListener("click", function() { toggleEc("ec-pw"); }); document.getElementById("btn-change-pw").addEventListener("click", changePw); document.getElementById("ec-2fa-h").addEventListener("click", function() { toggleEc("ec-2fa"); }); document.getElementById("btn-setup-2fa").addEventListener("click", setup2FA); document.getElementById("btn-cancel-2fa").addEventListener("click", function() { closeModal("m-2fa-setup"); }); document.getElementById("btn-confirm-2fa").addEventListener("click", confirm2FA); document.getElementById("setup-code").addEventListener("keydown", function(e) { if (e.key === "Enter") confirm2FA(); }); document.getElementById("sb-uchip").addEventListener("click", function() { goPage("settings"); }); document.querySelectorAll(".overlay").forEach(function(o) { o.addEventListener("click", function(e) { if (e.target === o) o.classList.remove("open"); }); }); document.getElementById("briefing-btn").addEventListener("click", function() { document.getElementById("briefing-sidebar").classList.toggle("open"); }); document.getElementById("briefing-close").addEventListener("click", function() { document.getElementById("briefing-sidebar").classList.remove("open"); }); document.getElementById("chat-bubble").addEventListener("click", function() { document.getElementById("chat-panel").classList.toggle("open"); }); document.getElementById("cp-close").addEventListener("click", function() { document.getElementById("chat-panel").classList.remove("open"); }); document.getElementById("cp-send").addEventListener("click", sendChat); document.getElementById("cp-input").addEventListener("keydown", function(e) { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendChat(); } }); document.getElementById("cp-input").addEventListener("input", function() { this.style.height = "42px"; this.style.height = Math.min(this.scrollHeight, 120) + "px"; }); document.getElementById("btn-print-report").addEventListener("click", function() { window.print(); }); document.getElementById("btn-close-report").addEventListener("click", function() { document.getElementById("report-frame").style.display = "none"; }); window.addEventListener("popstate", function(e) { var page = (e.state && e.state.page) || "overview"; if (pageHistory.length > 1) pageHistory.pop(); renderPage(page); }); history.replaceState({ page: "overview" }, "", "/overview"); checkAuth();})(); })();