xseaqbka/Nawaab_RoBotPublic · Bot Template

AIThis Telegram bot acts as a TOTP authenticator: users can add accounts via Base32 secret, QR code, or otpauth:// URI, list saved keys, and generate current OTP codes on demand with refresh and delete options. It includes an admin panel with user ban/unban, paginated user management, required-channel management, and a broadcast system that sends text, image, or copied replied messages to all private chats. A web dashboard endpoint provides OTP generation and account management over HTTP. The bot also supports an on/off maintenance switch and per-user access gating.

Utilitytotpauthenticatorotp2fabroadcastadmin
ProfileTelegram
59 commands1 envUpdated 10d agoCreated Aug 28, 2026
Back to folder

commands/web_dashboard.js

javascript · 3234 lines

Raw
1/**#command2name: web_dashboard3answer: 4keyboard: 5parse_mode: markdown6aliases: 7allow_only_group: false8need_reply: false9is_web: 010#command**/11 12/* Command: web_dashboard */13 14const method = String((request && request.method) || "GET").toUpperCase();15 16/* =========================17   API / POST REQUESTS18========================= */19 20if (method === "POST") {21  let body = request ? request.body : {};22 23  if (typeof body === "string") {24    try {25      body = JSON.parse(body);26    } catch (e) {27      body = {};28    }29  }30 31  if (!body || typeof body !== "object") {32    body = {};33  }34 35  const action = String(body.action || "");36 37  const keysRaw = await db.user.get("keys", []);38  const keys = Array.isArray(keysRaw) ? keysRaw : [];39 40  /* Generate OTP */41 42  if (action === "otp") {43    const i = parseInt(body.index, 10);44 45    if (!Number.isFinite(i) || i < 0 || !keys[i]) {46      res.status(404);47      res.json({48        ok: false,49        error: "Account not found"50      });51      return;52    }53 54    try {55      const a = keys[i];56 57      const period =58        Number(a.period || 30) > 059          ? Number(a.period || 30)60          : 30;61 62      res.json({63        ok: true,64        otp: String(authUtils.generateTotp(a)),65        period: period,66        remaining: authUtils.secondsRemaining(period)67      });68    } catch (e) {69      res.status(400);70      res.json({71        ok: false,72        error: "Unable to generate this OTP"73      });74    }75 76    return;77  }78 79  /* Add account */80 81  if (action === "add") {82    const raw = String(83      body.secret || body.uri || ""84    ).trim();85 86    const name = String(87      body.name || ""88    ).trim();89 90    const parsed = authUtils.parseOtpAuth(raw);91 92    if (!parsed) {93      res.status(400);94      res.json({95        ok: false,96        error: "Invalid TOTP secret or QR payload"97      });98      return;99    }100 101    if (!name) {102      res.status(400);103      res.json({104        ok: false,105        error: "Please enter an account name"106      });107      return;108    }109 110    const limit = Number(111      await db.bot.get("max_keys_per_user", 0) || 0112    );113 114    if (limit > 0 && keys.length >= limit) {115      res.status(400);116      res.json({117        ok: false,118        error: "Maximum key limit reached"119      });120      return;121    }122 123    parsed.name = name;124    parsed.id = modules.UUID.uuidv4();125    parsed.created_at = Date.now();126 127    keys.push(parsed);128 129    await db.user.set("keys", keys);130 131    res.json({132      ok: true,133      index: keys.length - 1,134      account: {135        index: keys.length - 1,136        name: parsed.name,137        issuer: String(parsed.issuer || ""),138        period: Number(parsed.period || 30),139        digits: Number(parsed.digits || 6)140      }141    });142 143    return;144  }145 146  /* Add multiple migrated accounts */147 148  if (action === "add_batch") {149    const incoming = Array.isArray(body.accounts)150      ? body.accounts151      : [];152 153    if (!incoming.length) {154      res.status(400);155      res.json({156        ok: false,157        error: "No migration accounts found"158      });159      return;160    }161 162    const limit = Number(163      await db.bot.get("max_keys_per_user", 0) || 0164    );165 166    if (167      limit > 0 &&168      keys.length + incoming.length > limit169    ) {170      res.status(400);171      res.json({172        ok: false,173        error: "Maximum key limit reached"174      });175      return;176    }177 178    const added = [];179 180    for (let n = 0; n < incoming.length; n++) {181      const item = incoming[n] || {};182 183      const parsed = authUtils.parseOtpAuth(184        String(item.uri || "")185      );186 187      const name = String(188        item.name ||189        (parsed && parsed.name) ||190        ""191      ).trim();192 193      if (!parsed || !name) {194        res.status(400);195        res.json({196          ok: false,197          error: "Invalid account in migration QR"198        });199        return;200      }201 202      parsed.name = name;203      parsed.id = modules.UUID.uuidv4();204      parsed.created_at = Date.now();205 206      keys.push(parsed);207 208      added.push({209        index: keys.length - 1,210        name: parsed.name,211        issuer: String(parsed.issuer || ""),212        period: Number(parsed.period || 30),213        digits: Number(parsed.digits || 6)214      });215    }216 217    await db.user.set("keys", keys);218 219    res.json({220      ok: true,221      accounts: added222    });223 224    return;225  }226 227  /* Delete account */228 229  if (action === "delete") {230    const i = parseInt(body.index, 10);231 232    if (!Number.isFinite(i) || i < 0 || !keys[i]) {233      res.status(404);234      res.json({235        ok: false,236        error: "Account not found"237      });238      return;239    }240 241    keys.splice(i, 1);242 243    await db.user.set("keys", keys);244 245    res.json({246      ok: true247    });248 249    return;250  }251 252  res.status(400);253 254  res.json({255    ok: false,256    error: "Unknown action"257  });258 259  return;260}261 262 263/* =========================264   GET / WEBAPP265========================= */266 267const rawKeys = await db.user.get("keys", []);268 269const keys = Array.isArray(rawKeys)270  ? rawKeys271  : [];272 273const data = {274  keys: keys.map(function (k, i) {275    return {276      index: i,277      name: String(278        k.name ||279        k.account ||280        ("Account " + (i + 1))281      ),282      issuer: String(k.issuer || ""),283      period: Number(k.period || 30),284      digits: Number(k.digits || 6)285    };286  }),287 288  profile: {289    name: String(290      (user && user.first_name) || "Telegram User"291    ),292    lastName: String(293      (user && user.last_name) || ""294    ),295    username: String(296      (user && user.username) || ""297    ),298    id: String(299      (user && user.id) || ""300    )301  },302 303  support: {304    owner: String(305      await db.bot.get("owner_url", "") || ""306    ),307 308    channel: String(309      await db.bot.get("support_channel_url", "") || ""310    ),311 312    community: String(313      await db.bot.get("support_community_url", "") || ""314    )315  }316};317 318 319/* Safe JSON for inline script */320 321const boot = JSON.stringify(data)322  .replace(/</g, "\\u003c")323  .replace(/>/g, "\\u003e")324  .replace(/&/g, "\\u0026");325 326 327res.html(`<!doctype html>328<html lang="en">329 330<head>331 332<meta charset="utf-8">333 334<meta335  name="viewport"336  content="width=device-width,initial-scale=1,viewport-fit=cover"337>338 339<title>AuthKey</title>340 341<meta342  name="theme-color"343  content="#090b13"344>345 346<script src="https://cdn.tailwindcss.com"></script>347 348<link349  rel="preconnect"350  href="https://fonts.googleapis.com"351>352 353<link354  rel="preconnect"355  href="https://fonts.gstatic.com"356  crossorigin357>358 359<link360  href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap"361  rel="stylesheet"362>363 364<script src="https://unpkg.com/lucide@latest"></script>365 366<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>367 368<script src="https://telegram.org/js/telegram-web-app.js"></script>369 370<style>371 372*{373  box-sizing:border-box374}375 376body{377  margin:0;378  background:#090b13;379  color:#f8fafc;380  font-family:'DM Sans',sans-serif;381  min-height:100vh382}383 384body:before{385  content:"";386  position:fixed;387  inset:0;388  pointer-events:none;389  background:390    radial-gradient(391      circle at 10% 0,392      rgba(139,92,246,.16),393      transparent 32%394    ),395    radial-gradient(396      circle at 95% 18%,397      rgba(34,211,238,.10),398      transparent 28%399    )400}401 402.space{403  font-family:'Space Grotesk',sans-serif404}405 406.app{407  max-width:700px;408  margin:auto;409  padding:410    env(safe-area-inset-top)411    20px412    calc(104px + env(safe-area-inset-bottom));413  min-height:100vh414}415 416.view{417  display:none;418  animation:in .38s cubic-bezier(.2,.8,.2,1)419}420 421.view.active{422  display:block423}424 425@keyframes in{426  from{427    opacity:0;428    transform:translateY(14px)429  }430 431  to{432    opacity:1;433    transform:none434  }435}436 437.card{438  background:439    linear-gradient(440      145deg,441      rgba(28,31,45,.94),442      rgba(16,18,28,.96)443    );444 445  border:1px solid rgba(148,163,184,.13);446 447  box-shadow:448    0 18px 55px rgba(0,0,0,.2);449 450  border-radius:26px451}452 453.btn{454  border:0;455  border-radius:18px;456  padding:15px 17px;457  font-weight:700;458  transition:.2s;459  display:flex;460  align-items:center;461  justify-content:center;462  gap:8px463}464 465.btn:active{466  transform:scale(.98)467}468 469.primary{470  background:471    linear-gradient(472      135deg,473      #7c3aed,474      #a855f7 55%,475      #22c7e5476    );477 478  box-shadow:479    0 12px 30px rgba(124,58,237,.22)480}481 482.soft{483  background:#24283a;484  border:1px solid #30364b485}486 487.input{488  width:100%;489  background:#191d2b;490  border:1px solid #30374c;491  border-radius:18px;492  padding:17px;493  color:#fff;494  outline:none;495  font-size:16px496}497 498.input:focus{499  border-color:#8b5cf6;500 501  box-shadow:502    0 0 0 4px rgba(139,92,246,.12)503}504 505.nav{506  position:fixed;507  bottom:0;508  left:50%;509  transform:translateX(-50%);510  width:min(700px,100%);511  z-index:30;512 513  padding:514    10px515    20px516    calc(12px + env(safe-area-inset-bottom));517 518  background:519    linear-gradient(520      to top,521      #0b0d15 78%,522      transparent523    );524 525  display:flex;526  gap:8px527}528 529.nav button{530  flex:1;531  border:0;532  background:transparent;533  color:#7d8498;534  border-radius:20px;535  padding:12px 5px;536  display:grid;537  place-items:center;538  gap:4px;539  font-size:11px;540  font-weight:700541}542 543.nav button.active{544  color:#fff;545  background:#202536546}547 548.icon{549  width:52px;550  height:52px;551  border-radius:17px;552  background:#202536;553  display:grid;554  place-items:center;555  overflow:hidden;556  flex:none557}558 559.icon img{560  width:100%;561  height:100%;562  object-fit:cover563}564 565.account{566  transition:.22s;567  cursor:pointer568}569 570.account:active{571  transform:scale(.985)572}573 574.toast{575  position:fixed;576  left:50%;577  bottom:105px;578  transform:translate(-50%,18px);579  opacity:0;580  pointer-events:none;581  background:#24283a;582  border:1px solid #3a425a;583  border-radius:15px;584  padding:12px 17px;585  z-index:90;586  transition:.25s587}588 589.toast.show{590  opacity:1;591  transform:translate(-50%,0)592}593 594#splash{595  position:fixed;596  z-index:100;597  inset:0;598  background:#090b13;599  display:grid;600  place-items:center;601  transition:.6s602}603 604#splash.gone{605  opacity:0;606  pointer-events:none607}608 609.modal{610  position:fixed;611  inset:0;612  background:rgba(0,0,0,.62);613  backdrop-filter:blur(10px);614  z-index:70;615  display:none;616  align-items:end;617  padding:18px618}619 620.modal.show{621  display:flex622}623 624.sheet{625  width:min(660px,100%);626  margin:auto;627  background:#171b28;628  border:1px solid #30384e;629  border-radius:28px;630  padding:24px;631  animation:up .3s ease632}633 634@keyframes up{635  from{636    opacity:0;637    transform:translateY(28px)638  }639}640 641.qr-stage{642  min-height:360px;643  display:flex;644  flex-direction:column;645  justify-content:center646}647 648.hidden{649  display:none!important650}651 652</style>653 654</head>655 656<body>657 658 659<div id="splash">660 661  <div class="text-center">662 663    <div class="w-20 h-20 mx-auto rounded-[28px] primary grid place-items-center text-3xl">664      🔐665    </div>666 667    <h1 class="space text-2xl font-bold mt-5">668      AuthKey669    </h1>670 671    <p class="text-slate-500 text-sm mt-2">672      Secure authenticator673    </p>674 675  </div>676 677</div>678 679 680<main class="app">681 682 683<section684  id="home"685  class="view active"686>687 688<div class="pt-6 pb-6 flex items-center justify-between">689 690<div>691 692<p class="text-violet-300 text-xs font-bold tracking-widest">693AUTHKEY694</p>695 696<h1 class="space text-3xl font-bold mt-2">697Your accounts698</h1>699 700</div>701 702<button703  class="icon"704  onclick="showView('profile')"705>706<i data-lucide="user-round"></i>707</button>708 709</div>710 711<div712  id="accountList"713  class="space-y-3"714></div>715 716<button717  onclick="showView('add')"718  class="btn primary w-full mt-5"719>720<i data-lucide="plus"></i>721Add authenticator722</button>723 724</section>725 726 727<section728  id="detail"729  class="view"730>731 732<button733  onclick="showView('home')"734  class="btn soft px-4 mt-5"735>736<i data-lucide="arrow-left"></i>737Back738</button>739 740<div class="text-center pt-8">741 742<div743  id="detailIcon"744  class="icon mx-auto mb-5"745></div>746 747<p748  id="detailIssuer"749  class="text-slate-500 text-sm"750></p>751 752<h1753  id="detailName"754  class="space text-3xl font-bold mt-1"755></h1>756 757</div>758 759<div class="card p-7 mt-8 text-center">760 761<p class="text-slate-500 text-xs tracking-widest">762CURRENT CODE763</p>764 765<div766  id="otp"767  class="space text-5xl tracking-[.14em] font-bold my-7"768>769••••••770</div>771 772<div class="h-2 bg-[#2a3042] rounded-full overflow-hidden">773 774<div775  id="progress"776  class="h-full primary"777></div>778 779</div>780 781<p782  id="timer"783  class="text-slate-400 text-sm mt-4"784></p>785 786<button787  onclick="copyOtp()"788  class="btn soft w-full mt-5"789>790<i data-lucide="copy"></i>791Copy code792</button>793 794</div>795 796<button797  onclick="removeCurrent()"798  class="btn w-full mt-4 border border-red-400/20 text-red-300"799>800Delete account801</button>802 803</section>804 805 806<section807  id="add"808  class="view"809>810 811<div class="pt-6 pb-7">812 813<p class="text-violet-300 text-xs font-bold tracking-widest">814NEW AUTHENTICATOR815</p>816 817<h1 class="space text-4xl font-bold mt-2">818Add account819</h1>820 821<p class="text-slate-400 mt-3">822Add with a secret key or import an authenticator QR.823</p>824 825</div>826 827 828<div class="grid grid-cols-2 gap-3 mb-6">829 830<button831  id="secretTab"832  onclick="selectAdd('secret')"833  class="btn primary"834>835<i data-lucide="key-round"></i>836Secret key837</button>838 839<button840  id="qrTab"841  onclick="selectAdd('qr')"842  class="btn soft"843>844<i data-lucide="scan-line"></i>845QR code846</button>847 848</div>849 850 851<div852  id="secretPane"853  class="space-y-4"854>855 856<input857  id="secretInput"858  class="input"859  placeholder="Secret key or otpauth:// URI"860>861 862<input863  id="nameInput"864  class="input"865  placeholder="Account name"866>867 868<button869  onclick="addSecret()"870  class="btn primary w-full"871>872Add account873</button>874 875</div>876 877 878<div879  id="qrPane"880  class="hidden"881>882 883<div884  id="qrCapture"885  class="card qr-stage p-6"886>887 888<video889  id="camera"890  class="hidden w-full rounded-2xl bg-black"891  playsinline892></video>893 894<div895  id="cameraEmpty"896  class="text-center"897>898 899<div class="icon mx-auto w-16 h-16">900<i data-lucide="scan"></i>901</div>902 903<h2 class="space text-xl font-bold mt-5">904Import QR code905</h2>906 907<p class="text-slate-500 mt-2">908Scan with your camera or choose an image.909</p>910 911</div>912 913 914<div class="grid grid-cols-2 gap-3 mt-7">915 916<button917  onclick="startCamera()"918  class="btn soft" style="font-size: 0.8rem !important"919>920<i data-lucide="camera"></i>921Scan camera922</button>923 924<label class="btn soft cursor-pointer" style="font-size: 0.8rem !important">925 926<i data-lucide="upload"></i>927 928Upload QR929 930<input931  id="qrFile"932  type="file"933  accept="image/*"934  class="hidden"935  onchange="readQrFile(this.files[0])"936>937 938</label>939 940</div>941 942</div>943 944 945<div946  id="qrReview"947  class="hidden"948>949 950<button951  onclick="discardQr()"952  class="btn soft mb-5"953>954<i data-lucide="arrow-left"></i>955Back to scan956</button>957 958<div class="card p-6">959 960<div class="flex items-center gap-3">961 962<div class="icon">963<i data-lucide="badge-check"></i>964</div>965 966<div>967 968<h2 class="space font-bold text-xl">969QR detected970</h2>971 972<p class="text-slate-500 text-sm">973Review the name before saving.974</p>975 976</div>977 978</div>979 980<input981  id="qrName"982  class="input mt-6"983  placeholder="Account name"984>985 986<button987  onclick="addQr()"988  class="btn primary w-full mt-4"989>990Add account991</button>992 993</div>994 995</div>996 997</div>998 999</section>1000 1001 1002<section1003  id="profile"1004  class="view"1005>1006 1007<div class="pt-6 pb-7">1008 1009<p class="text-violet-300 text-xs font-bold tracking-widest">1010YOUR ACCOUNT1011</p>1012 1013<h1 class="space text-4xl font-bold mt-2">1014Profile1015</h1>1016 1017</div>1018 1019 1020<div class="card p-6">1021 1022<div class="flex items-center gap-4">1023 1024<div1025  id="profilePhoto"1026  class="icon !w-20 !h-20"1027></div>1028 1029<div class="min-w-0">1030 1031<h21032  id="profileName"1033  class="space text-xl font-bold truncate"1034></h2>1035 1036<p1037  id="profileHandle"1038  class="text-slate-500 mt-1"1039></p>1040 1041</div>1042 1043</div>1044 1045</div>1046 1047 1048<div class="grid grid-cols-2 gap-3 mt-4">1049 1050<div class="card p-5">1051 1052<p class="text-slate-500 text-xs">1053TOTAL ACCOUNTS1054</p>1055 1056<p1057  id="keyCount"1058  class="space text-4xl font-bold mt-2"1059></p>1060 1061</div>1062 1063 1064<div class="card p-5">1065 1066<p class="text-slate-500 text-xs">1067USER ID1068</p>1069 1070<p1071  id="profileId"1072  class="text-xs font-semibold mt-4 break-all"1073></p>1074 1075</div>1076 1077</div>1078 1079 1080<h2 class="space text-xl font-bold mt-8 mb-4">1081Support1082</h2>1083 1084<div1085  id="supportLinks"1086  class="space-y-3"1087></div>1088 1089</section>1090 1091 1092</main>1093 1094 1095<nav class="nav">1096 1097<button1098  data-nav="home"1099  class="active"1100  onclick="showView('home')"1101>1102<i data-lucide="house"></i>1103Home1104</button>1105 1106<button1107  data-nav="add"1108  onclick="showView('add')"1109>1110<i data-lucide="plus-circle"></i>1111Add1112</button>1113 1114<button1115  data-nav="profile"1116  onclick="showView('profile')"1117>1118<i data-lucide="user-round"></i>1119Profile1120</button>1121 1122</nav>1123 1124 1125<div1126  id="toast"1127  class="toast"1128></div>1129 1130 1131<div1132  id="welcomeModal"1133  class="modal"1134>1135 1136<div class="sheet text-center">1137 1138<div class="text-4xl">1139👋1140</div>1141 1142<h2 class="space text-2xl font-bold mt-4">1143Welcome to AuthKey1144</h2>1145 1146<p class="text-slate-400 mt-2">1147Your private authenticator workspace is ready.1148</p>1149 1150<button1151  onclick="closeWelcome()"1152  class="btn primary w-full mt-6"1153>1154Get started1155</button>1156 1157</div>1158 1159</div>1160 1161 1162<script>1163 1164/* =========================1165   CRITICAL:1166   REMOVE SPLASH IMMEDIATELY1167========================= */1168 1169(function () {1170 1171  function removeSplash() {1172    const splash =1173      document.getElementById("splash");1174 1175    if (!splash) return;1176 1177    splash.classList.add("gone");1178 1179    setTimeout(function () {1180      if (splash && splash.parentNode) {1181        splash.style.display = "none";1182      }1183    }, 800);1184  }1185 1186  /* Backup removal even if app code fails */1187 1188  setTimeout(removeSplash, 500);1189 1190  window.addEventListener(1191    "error",1192    function () {1193      setTimeout(removeSplash, 50);1194    }1195  );1196 1197  window.addEventListener(1198    "unhandledrejection",1199    function () {1200      setTimeout(removeSplash, 50);1201    }1202  );1203 1204})();1205 1206 1207/* =========================1208   BOOT DATA1209========================= */1210 1211const BOOT = ${boot};1212 1213let current = -1;1214let qrPayload = "";1215let timerHandle = null;1216let stream = null;1217let migrationAccounts = [];1218let otpState = null;1219 1220 1221/* =========================1222   TELEGRAM1223========================= */1224 1225const tg =1226  window.Telegram &&1227  window.Telegram.WebApp1228    ? window.Telegram.WebApp1229    : null;1230 1231try {1232  if (tg) {1233    tg.ready();1234    tg.expand();1235  }1236} catch (e) {1237  console.warn("Telegram WebApp init failed", e);1238}1239 1240 1241/* =========================1242   SAFE ICON INITIALIZATION1243========================= */1244 1245function refreshIcons() {1246 1247  try {1248 1249    if (1250      window.lucide &&1251      typeof window.lucide.createIcons === "function"1252    ) {1253      window.lucide.createIcons();1254    }1255 1256  } catch (e) {1257    console.warn("Lucide unavailable", e);1258  }1259 1260}1261 1262 1263/* =========================1264   UTILITIES1265========================= */1266 1267const esc = function (s) {1268 1269  return String(s || "").replace(1270    /[&<>"]/g,1271    function (c) {1272      return {1273        "&": "&amp;",1274        "<": "&lt;",1275        ">": "&gt;",1276        '"': "&quot;"1277      }[c];1278    }1279  );1280 1281};1282 1283 1284function toast(t) {1285 1286  const x =1287    document.getElementById("toast");1288 1289  if (!x) return;1290 1291  x.textContent = t;1292 1293  x.classList.add("show");1294 1295  setTimeout(function () {1296    x.classList.remove("show");1297  }, 2500);1298 1299}1300 1301 1302function clearPendingQr() {1303 1304  qrPayload = "";1305  migrationAccounts = [];1306 1307  const name =1308    document.getElementById("qrName");1309 1310  const file =1311    document.getElementById("qrFile");1312 1313  const review =1314    document.getElementById("qrReview");1315 1316  const capture =1317    document.getElementById("qrCapture");1318 1319  if (name) name.value = "";1320  if (file) file.value = "";1321 1322  if (review) {1323    review.classList.add("hidden");1324  }1325 1326  if (capture) {1327    capture.classList.remove("hidden");1328  }1329 1330  stopCamera();1331 1332}1333 1334 1335/* =========================1336   NAVIGATION1337========================= */1338 1339function showView(id) {1340 1341  try {1342 1343    if (id !== "add") {1344      clearPendingQr();1345    }1346 1347    document1348      .querySelectorAll(".view")1349      .forEach(function (x) {1350        x.classList.remove("active");1351      });1352 1353    const view =1354      document.getElementById(id);1355 1356    if (view) {1357      view.classList.add("active");1358    }1359 1360    document1361      .querySelectorAll("[data-nav]")1362      .forEach(function (x) {1363 1364        x.classList.toggle(1365          "active",1366          x.dataset.nav === id1367        );1368 1369      });1370 1371    if (id !== "detail") {1372      stopOtp();1373    }1374 1375    window.scrollTo({1376      top: 0,1377      behavior: "smooth"1378    });1379 1380    refreshIcons();1381 1382  } catch (e) {1383 1384    console.error(1385      "showView error",1386      e1387    );1388 1389  }1390 1391}1392 1393 1394function selectAdd(type) {1395 1396  try {1397 1398    if (type !== "qr") {1399      clearPendingQr();1400    }1401 1402    const secretPane =1403      document.getElementById("secretPane");1404 1405    const qrPane =1406      document.getElementById("qrPane");1407 1408    const secretTab =1409      document.getElementById("secretTab");1410 1411    const qrTab =1412      document.getElementById("qrTab");1413 1414    if (secretPane) {1415      secretPane.classList.toggle(1416        "hidden",1417        type !== "secret"1418      );1419    }1420 1421    if (qrPane) {1422      qrPane.classList.toggle(1423        "hidden",1424        type !== "qr"1425      );1426    }1427 1428    if (secretTab) {1429      secretTab.className =1430        "btn " +1431        (1432          type === "secret"1433            ? "primary"1434            : "soft"1435        );1436    }1437 1438    if (qrTab) {1439      qrTab.className =1440        "btn " +1441        (1442          type === "qr"1443            ? "primary"1444            : "soft"1445        );1446    }1447 1448    refreshIcons();1449 1450  } catch (e) {1451 1452    console.error(1453      "selectAdd error",1454      e1455    );1456 1457  }1458 1459}1460 1461 1462/* =========================1463   ACCOUNT ICON1464========================= */1465 1466function logo(a) {1467 1468  const name = String(1469    (a && (a.issuer || a.name)) || ""1470  )1471    .toLowerCase()1472    .replace(/[^a-z0-9.]/g, "");1473 1474  return (1475    "https://www.google.com/s2/favicons?domain=" +1476    encodeURIComponent(1477      name + ".com"1478    ) +1479    "&sz=128"1480  );1481 1482}1483 1484 1485function accountIcon(a) {1486 1487  const fallback =1488    "https://api.dicebear.com/9.x/initials/svg?seed=" +1489    encodeURIComponent(1490      (a && a.name) || "Account"1491    );1492 1493  return (1494    '<img src="' +1495    esc(logo(a)) +1496    '" onerror="this.onerror=null;this.src=' +1497    JSON.stringify(fallback) +1498    '">'1499  );1500 1501}1502 1503 1504/* =========================1505   RENDER ACCOUNTS1506========================= */1507 1508function renderAccounts() {1509 1510  try {1511 1512    const e =1513      document.getElementById("accountList");1514 1515    if (!e) return;1516 1517    if (!BOOT.keys || !BOOT.keys.length) {1518 1519      e.innerHTML =1520        '<div class="card p-10 text-center">' +1521        '<div class="icon mx-auto w-16 h-16">' +1522        '<i data-lucide="shield-plus"></i>' +1523        '</div>' +1524        '<h2 class="space text-xl font-bold mt-5">' +1525        'No accounts yet' +1526        '</h2>' +1527        '<p class="text-slate-500 mt-2">' +1528        'Add your first authenticator to get started.' +1529        '</p>' +1530        '</div>';1531 1532    } else {1533 1534      e.innerHTML =1535        BOOT.keys.map(function (a) {1536 1537          return (1538            '<button onclick="openAccount(' +1539            Number(a.index) +1540            ')" class="account card w-full p-4 flex items-center gap-4 text-left">' +1541 1542            '<div class="icon">' +1543            accountIcon(a) +1544            '</div>' +1545 1546            '<div class="min-w-0 flex-1">' +1547 1548            '<b class="block truncate">' +1549            esc(a.name) +1550            '</b>' +1551 1552            '<span class="text-slate-500 text-sm">' +1553            esc(1554              a.issuer ||1555              "TOTP authenticator"1556            ) +1557            '</span>' +1558 1559            '</div>' +1560 1561            '<i data-lucide="chevron-right" class="text-slate-500"></i>' +1562 1563            '</button>'1564          );1565 1566        }).join("");1567 1568    }1569 1570    const count =1571      document.getElementById("keyCount");1572 1573    if (count) {1574      count.textContent =1575        BOOT.keys.length;1576    }1577 1578    refreshIcons();1579 1580  } catch (e) {1581 1582    console.error(1583      "renderAccounts error",1584      e1585    );1586 1587  }1588 1589}1590 1591 1592/* =========================1593   API1594========================= */1595 1596async function api(body) {1597 1598  const r =1599    await fetch(location.href, {1600      method: "POST",1601 1602      headers: {1603        "Content-Type":1604          "application/json"1605      },1606 1607      body:1608        JSON.stringify(body)1609    });1610 1611  let j = {};1612 1613  try {1614    j = await r.json();1615  } catch (e) {}1616 1617  if (!r.ok || !j.ok) {1618    throw new Error(1619      j.error ||1620      "Request failed"1621    );1622  }1623 1624  return j;1625 1626}1627 1628 1629/* =========================1630   OTP1631========================= */1632 1633async function openAccount(i) {1634 1635  current = i;1636 1637  const a =1638    BOOT.keys[i];1639 1640  if (!a) return;1641 1642  const name =1643    document.getElementById("detailName");1644 1645  const issuer =1646    document.getElementById("detailIssuer");1647 1648  const icon =1649    document.getElementById("detailIcon");1650 1651  if (name) {1652    name.textContent = a.name;1653  }1654 1655  if (issuer) {1656    issuer.textContent =1657      a.issuer ||1658      "TOTP authenticator";1659  }1660 1661  if (icon) {1662    icon.innerHTML =1663      accountIcon(a);1664  }1665 1666  showView("detail");1667 1668  await refreshOtp();1669 1670  stopOtp();1671 1672  timerHandle =1673    setInterval(1674      tickOtp,1675      10001676    );1677 1678}1679 1680 1681async function refreshOtp() {1682 1683  try {1684 1685    otpState =1686      await api({1687        action: "otp",1688        index: current1689      });1690 1691    tickOtp();1692 1693  } catch (e) {1694 1695    toast(e.message);1696 1697  }1698 1699}1700 1701 1702function tickOtp() {1703 1704  if (!otpState) return;1705 1706  const p =1707    otpState.period || 30;1708 1709  const left =1710    p -1711    (1712      Math.floor(1713        Date.now() / 10001714      ) % p1715    ) ||1716    p;1717 1718  const otp =1719    document.getElementById("otp");1720 1721  const timer =1722    document.getElementById("timer");1723 1724  const progress =1725    document.getElementById("progress");1726 1727  if (otp) {1728    otp.textContent =1729      otpState.otp;1730  }1731 1732  if (timer) {1733    timer.textContent =1734      left +1735      " seconds remaining";1736  }1737 1738  if (progress) {1739    progress.style.width =1740      (1741        left / p * 1001742      ) +1743      "%";1744  }1745 1746  if (left === p) {1747    refreshOtp();1748  }1749 1750}1751 1752 1753function stopOtp() {1754 1755  if (timerHandle) {1756 1757    clearInterval(1758      timerHandle1759    );1760 1761    timerHandle = null;1762 1763  }1764 1765  otpState = null;1766 1767}1768 1769 1770function copyOtp() {1771 1772  if (!otpState) return;1773 1774  try {1775 1776    if (1777      navigator.clipboard &&1778      navigator.clipboard.writeText1779    ) {1780 1781      navigator.clipboard.writeText(1782        otpState.otp1783      );1784 1785      toast("Code copied");1786 1787    }1788 1789  } catch (e) {1790 1791    toast("Unable to copy code");1792 1793  }1794 1795}1796 1797 1798/* =========================1799   ADD SECRET1800========================= */1801 1802async function addSecret() {1803 1804  const secret =1805    document1806      .getElementById("secretInput")1807      .value1808      .trim();1809 1810  const name =1811    document1812      .getElementById("nameInput")1813      .value1814      .trim();1815 1816  if (!secret || !name) {1817 1818    toast(1819      "Enter both secret key and account name"1820    );1821 1822    return;1823  }1824 1825  try {1826 1827    const r =1828      await api({1829        action: "add",1830        secret: secret,1831        name: name1832      });1833 1834    BOOT.keys.push(1835      r.account1836    );1837 1838    renderAccounts();1839 1840    document1841      .getElementById("secretInput")1842      .value = "";1843 1844    document1845      .getElementById("nameInput")1846      .value = "";1847 1848    showView("home");1849 1850    toast(1851      "Account added"1852    );1853 1854  } catch (e) {1855 1856    toast(e.message);1857 1858  }1859 1860}1861 1862 1863/* =========================1864   QR FUNCTIONS1865========================= */1866 1867function parseQrName(s) {1868 1869  try {1870 1871    const u =1872      new URL(s);1873 1874    const label =1875      decodeURIComponent(1876        (u.pathname || "")1877          .replace(/^\\//, "")1878      );1879 1880    const issuer =1881      u.searchParams.get(1882        "issuer"1883      ) || "";1884 1885    return (1886      label ||1887      issuer ||1888      "Authenticator Account"1889    );1890 1891  } catch (e) {1892 1893    return "Authenticator Account";1894 1895  }1896 1897}1898 1899 1900function b64urlBytes(s) {1901 1902  s = String(s || "")1903    .replace(/-/g, "+")1904    .replace(/_/g, "/");1905 1906  while (1907    s.length % 41908  ) {1909    s += "=";1910  }1911 1912  const b =1913    atob(s);1914 1915  const a =1916    new Uint8Array(1917      b.length1918    );1919 1920  for (1921    let i = 0;1922    i < b.length;1923    i++1924  ) {1925 1926    a[i] =1927      b.charCodeAt(i);1928 1929  }1930 1931  return a;1932 1933}1934 1935 1936function readVarint(a, st) {1937 1938  let v = 0;1939  let sh = 0;1940 1941  while (1942    st.i < a.length1943  ) {1944 1945    const b =1946      a[st.i++];1947 1948    v +=1949      (b & 127) *1950      Math.pow(2, sh);1951 1952    if (!(b & 128)) {1953      return v;1954    }1955 1956    sh += 7;1957 1958    if (sh > 49) {1959      throw Error(1960        "Invalid migration data"1961      );1962    }1963 1964  }1965 1966  throw Error(1967    "Unexpected migration end"1968  );1969 1970}1971 1972 1973function readBytes(a, st) {1974 1975  const n =1976    readVarint(a, st);1977 1978  const e =1979    st.i + n;1980 1981  if (e > a.length) {1982    throw Error(1983      "Invalid migration length"1984    );1985  }1986 1987  const r =1988    a.slice(1989      st.i,1990      e1991    );1992 1993  st.i = e;1994 1995  return r;1996 1997}1998 1999 2000function utf8(a) {2001 2002  return new TextDecoder()2003    .decode(a);2004 2005}2006 2007 2008function skipField(2009  a,2010  st,2011  w2012) {2013 2014  if (w === 0) {2015 2016    readVarint(2017      a,2018      st2019    );2020 2021  } else if (w === 1) {2022 2023    st.i += 8;2024 2025  } else if (w === 2) {2026 2027    st.i +=2028      readVarint(2029        a,2030        st2031      );2032 2033  } else if (w === 5) {2034 2035    st.i += 4;2036 2037  } else {2038 2039    throw Error(2040      "Unsupported migration field"2041    );2042 2043  }2044 2045}2046 2047 2048function parseMigrationParam(a) {2049 2050  const st = {2051    i: 02052  };2053 2054  const o = {2055    secret: "",2056    name: "",2057    issuer: "",2058    algorithm: 1,2059    digits: 1,2060    type: 22061  };2062 2063  while (2064    st.i < a.length2065  ) {2066 2067    const t =2068      readVarint(2069        a,2070        st2071      );2072 2073    const f =2074      Math.floor(t / 8);2075 2076    const w =2077      t % 8;2078 2079    if (2080      f === 1 &&2081      w === 22082    ) {2083 2084      o.secret =2085        readBytes(2086          a,2087          st2088        );2089 2090    } else if (2091      f === 2 &&2092      w === 22093    ) {2094 2095      o.name =2096        utf8(2097          readBytes(2098            a,2099            st2100          )2101        );2102 2103    } else if (2104      f === 3 &&2105      w === 22106    ) {2107 2108      o.issuer =2109        utf8(2110          readBytes(2111            a,2112            st2113          )2114        );2115 2116    } else if (2117      (2118        f === 4 ||2119        f === 5 ||2120        f === 62121      ) &&2122      w === 02123    ) {2124 2125      o[2126        f === 42127          ? "algorithm"2128          : f === 52129            ? "digits"2130            : "type"2131      ] =2132        readVarint(2133          a,2134          st2135        );2136 2137    } else {2138 2139      skipField(2140        a,2141        st,2142        w2143      );2144 2145    }2146 2147  }2148 2149  if (2150    !o.secret.length ||2151    o.type !== 22152  ) {2153    return null;2154  }2155 2156  let sec = "";2157 2158  let bits = 0;2159  let val = 0;2160 2161  for (2162    const x of o.secret2163  ) {2164 2165    val =2166      (val << 8) |2167      x;2168 2169    bits += 8;2170 2171    while (2172      bits >= 52173    ) {2174 2175      sec +=2176        "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[2177          (val >> (bits - 5)) & 312178        ];2179 2180      bits -= 5;2181 2182    }2183 2184  }2185 2186  if (bits) {2187 2188    sec +=2189      "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[2190        (val << (5 - bits)) & 312191      ];2192 2193  }2194 2195  const alg = {2196    1: "SHA1",2197    2: "SHA256",2198    3: "SHA512",2199    4: "MD5"2200  }[2201    o.algorithm2202  ] || "SHA1";2203 2204  const dig = {2205    1: 6,2206    2: 82207  }[2208    o.digits2209  ] || 6;2210 2211  const label =2212    o.name ||2213    o.issuer ||2214    "Authenticator Account";2215 2216  const issuer =2217    o.issuer ||2218    "";2219 2220  return {2221 2222    name: label,2223 2224    uri:2225      "otpauth://totp/" +2226      encodeURIComponent(label) +2227      "?secret=" +2228      encodeURIComponent(sec) +2229      (2230        issuer2231          ? "&issuer=" +2232            encodeURIComponent(2233              issuer2234            )2235          : ""2236      ) +2237      "&algorithm=" +2238      alg +2239      "&digits=" +2240      dig +2241      "&period=30"2242 2243  };2244 2245}2246 2247 2248function parseMigrationQr(p) {2249 2250  const u =2251    new URL(p);2252 2253  const data =2254    u.searchParams.get(2255      "data"2256    );2257 2258  if (!data) {2259    throw Error(2260      "Missing migration payload"2261    );2262  }2263 2264  const a =2265    b64urlBytes(data);2266 2267  const st = {2268    i: 02269  };2270 2271  const out = [];2272 2273  while (2274    st.i < a.length2275  ) {2276 2277    const t =2278      readVarint(2279        a,2280        st2281      );2282 2283    const f =2284      Math.floor(t / 8);2285 2286    const w =2287      t % 8;2288 2289    if (2290      f === 1 &&2291      w === 22292    ) {2293 2294      const x =2295        parseMigrationParam(2296          readBytes(2297            a,2298            st2299          )2300        );2301 2302      if (x) {2303        out.push(x);2304      }2305 2306    } else {2307 2308      skipField(2309        a,2310        st,2311        w2312      );2313 2314    }2315 2316  }2317 2318  return out;2319 2320}2321 2322 2323function useQr(p) {2324 2325  p =2326    String(p || "");2327 2328  migrationAccounts = [];2329 2330  if (2331    /^otpauth-migration:\\/\\//i2332      .test(p)2333  ) {2334 2335    try {2336 2337      migrationAccounts =2338        parseMigrationQr(p);2339 2340      if (2341        !migrationAccounts.length2342      ) {2343 2344        return toast(2345          "No TOTP accounts found in this migration QR"2346        );2347 2348      }2349 2350      qrPayload =2351        migrationAccounts[0].uri;2352 2353      document2354        .getElementById("qrName")2355        .value =2356          migrationAccounts.length === 12357            ? migrationAccounts[0].name2358            : "Google Authenticator migration (" +2359              migrationAccounts.length +2360              " accounts)";2361 2362      document2363        .getElementById("qrCapture")2364        .classList2365        .add("hidden");2366 2367      document2368        .getElementById("qrReview")2369        .classList2370        .remove("hidden");2371 2372      toast(2373        migrationAccounts.length +2374        " accounts detected"2375      );2376 2377      refreshIcons();2378 2379      return;2380 2381    } catch (e) {2382 2383      return toast(2384        "Migration QR could not be decoded"2385      );2386 2387    }2388 2389  }2390 2391  if (2392    !/^otpauth:\\/\\/totp\\//i2393      .test(p)2394  ) {2395 2396    return toast(2397      "This is not a supported TOTP QR code"2398    );2399 2400  }2401 2402  qrPayload = p;2403 2404  document2405    .getElementById("qrName")2406    .value =2407      parseQrName(p);2408 2409  document2410    .getElementById("qrCapture")2411    .classList2412    .add("hidden");2413 2414  document2415    .getElementById("qrReview")2416    .classList2417    .remove("hidden");2418 2419  refreshIcons();2420 2421}2422 2423 2424function discardQr() {2425 2426  migrationAccounts = [];2427 2428  clearPendingQr();2429 2430  refreshIcons();2431 2432}2433 2434 2435function readQrFile(f) {2436 2437  if (!f) return;2438 2439  if (2440    typeof window.jsQR !== "function"2441  ) {2442 2443    toast(2444      "QR scanner is still loading. Please try again."2445    );2446 2447    return;2448 2449  }2450 2451  const img =2452    new Image();2453 2454  img.onload =2455    function () {2456 2457      try {2458 2459        const c =2460          document.createElement(2461            "canvas"2462          );2463 2464        const x =2465          c.getContext("2d");2466 2467        c.width =2468          img.naturalWidth;2469 2470        c.height =2471          img.naturalHeight;2472 2473        x.drawImage(2474          img,2475          0,2476          02477        );2478 2479        const q =2480          window.jsQR(2481            x.getImageData(2482              0,2483              0,2484              c.width,2485              c.height2486            ).data,2487 2488            c.width,2489            c.height,2490 2491            {2492              inversionAttempts:2493                "attemptBoth"2494            }2495          );2496 2497        if (q) {2498          useQr(q.data);2499        } else {2500          toast(2501            "QR code could not be read"2502          );2503        }2504 2505      } catch (e) {2506 2507        toast(2508          "QR code could not be read"2509        );2510 2511      }2512 2513    };2514 2515  img.onerror =2516    function () {2517 2518      toast(2519        "Unable to read this image"2520      );2521 2522    };2523 2524  img.src =2525    URL.createObjectURL(f);2526 2527}2528 2529 2530/* =========================2531   CAMERA2532========================= */2533 2534async function startCamera() {2535 2536  try {2537 2538    if (2539      !navigator.mediaDevices ||2540      !navigator.mediaDevices.getUserMedia2541    ) {2542 2543      toast(2544        "Camera is not available in this WebView"2545      );2546 2547      return;2548 2549    }2550 2551    if (2552      typeof window.jsQR !== "function"2553    ) {2554 2555      toast(2556        "QR scanner is still loading"2557      );2558 2559      return;2560 2561    }2562 2563    stopCamera();2564 2565    stream =2566      await navigator.mediaDevices.getUserMedia({2567        video: {2568          facingMode: {2569            ideal: "environment"2570          }2571        },2572        audio: false2573      });2574 2575    const v =2576      document.getElementById(2577        "camera"2578      );2579 2580    if (!v) return;2581 2582    v.srcObject =2583      stream;2584 2585    v.classList.remove(2586      "hidden"2587    );2588 2589    document2590      .getElementById("cameraEmpty")2591      .classList2592      .add("hidden");2593 2594    await v.play();2595 2596    scanFrame();2597 2598  } catch (e) {2599 2600    toast(2601      "Camera access was denied or unavailable"2602    );2603 2604  }2605 2606}2607 2608 2609function scanFrame() {2610 2611  const v =2612    document.getElementById(2613      "camera"2614    );2615 2616  if (2617    !stream ||2618    !v ||2619    typeof window.jsQR !== "function"2620  ) {2621    return;2622  }2623 2624  const c =2625    document.createElement(2626      "canvas"2627    );2628 2629  const x =2630    c.getContext("2d");2631 2632  c.width =2633    v.videoWidth;2634 2635  c.height =2636    v.videoHeight;2637 2638  if (2639    c.width &&2640    c.height2641  ) {2642 2643    x.drawImage(2644      v,2645      0,2646      02647    );2648 2649    const q =2650      window.jsQR(2651        x.getImageData(2652          0,2653          0,2654          c.width,2655          c.height2656        ).data,2657 2658        c.width,2659        c.height,2660 2661        {2662          inversionAttempts:2663            "attemptBoth"2664        }2665      );2666 2667    if (q) {2668 2669      useQr(q.data);2670 2671      stopCamera();2672 2673      return;2674 2675    }2676 2677  }2678 2679  requestAnimationFrame(2680    scanFrame2681  );2682 2683}2684 2685 2686function stopCamera() {2687 2688  if (stream) {2689 2690    try {2691 2692      stream2693        .getTracks()2694        .forEach(function (t) {2695          t.stop();2696        });2697 2698    } catch (e) {}2699 2700    stream = null;2701 2702  }2703 2704  const v =2705    document.getElementById(2706      "camera"2707    );2708 2709  if (v) {2710 2711    v.srcObject = null;2712 2713    v.classList.add(2714      "hidden"2715    );2716 2717  }2718 2719  const ce =2720    document.getElementById(2721      "cameraEmpty"2722    );2723 2724  if (ce) {2725 2726    ce.classList.remove(2727      "hidden"2728    );2729 2730  }2731 2732}2733 2734 2735/* =========================2736   ADD QR2737========================= */2738 2739async function addQr() {2740 2741  const name =2742    document2743      .getElementById("qrName")2744      .value2745      .trim();2746 2747 2748  if (2749    migrationAccounts.length2750  ) {2751 2752    try {2753 2754      const list =2755        migrationAccounts.map(2756          function (a) {2757 2758            return {2759              uri: a.uri,2760 2761              name:2762                migrationAccounts.length === 12763                  ? (2764                      name ||2765                      a.name2766                    )2767                  : a.name2768            };2769 2770          }2771        );2772 2773      const r =2774        await api({2775          action: "add_batch",2776          accounts: list2777        });2778 2779      Array.prototype.push.apply(2780        BOOT.keys,2781        r.accounts2782      );2783 2784      migrationAccounts = [];2785 2786      clearPendingQr();2787 2788      renderAccounts();2789 2790      showView("home");2791 2792      toast(2793        r.accounts.length +2794        " accounts added"2795      );2796 2797    } catch (e) {2798 2799      toast(e.message);2800 2801    }2802 2803    return;2804 2805  }2806 2807 2808  if (2809    !qrPayload ||2810    !name2811  ) {2812 2813    return toast(2814      "Enter an account name"2815    );2816 2817  }2818 2819 2820  try {2821 2822    const r =2823      await api({2824        action: "add",2825        uri: qrPayload,2826        name: name2827      });2828 2829    BOOT.keys.push(2830      r.account2831    );2832 2833    clearPendingQr();2834 2835    renderAccounts();2836 2837    showView("home");2838 2839    toast(2840      "Account added"2841    );2842 2843  } catch (e) {2844 2845    toast(e.message);2846 2847  }2848 2849}2850 2851 2852/* =========================2853   DELETE2854========================= */2855 2856async function removeCurrent() {2857 2858  if (2859    current < 0 ||2860    !confirm(2861      "Delete this account?"2862    )2863  ) {2864    return;2865  }2866 2867  try {2868 2869    await api({2870      action: "delete",2871      index: current2872    });2873 2874    BOOT.keys.splice(2875      current,2876      12877    );2878 2879    BOOT.keys.forEach(2880      function (a, i) {2881        a.index = i;2882      }2883    );2884 2885    current = -1;2886 2887    renderAccounts();2888 2889    showView("home");2890 2891    toast(2892      "Account deleted"2893    );2894 2895  } catch (e) {2896 2897    toast(e.message);2898 2899  }2900 2901}2902 2903 2904/* =========================2905   PROFILE2906========================= */2907 2908function initProfile() {2909 2910  try {2911 2912    const p =2913      BOOT.profile || {};2914 2915    const name =2916      document.getElementById(2917        "profileName"2918      );2919 2920    const handle =2921      document.getElementById(2922        "profileHandle"2923      );2924 2925    const id =2926      document.getElementById(2927        "profileId"2928      );2929 2930    if (name) {2931 2932      name.textContent =2933        (2934          String(p.name || "") +2935          " " +2936          String(p.lastName || "")2937        ).trim();2938 2939    }2940 2941    if (handle) {2942 2943      handle.textContent =2944        p.username2945          ? "@" + p.username2946          : "Telegram user";2947 2948    }2949 2950    if (id) {2951 2952      id.textContent =2953        p.id || "";2954 2955    }2956 2957 2958    const photo =2959      tg &&2960      tg.initDataUnsafe &&2961      tg.initDataUnsafe.user &&2962      tg.initDataUnsafe.user.photo_url;2963 2964 2965    const profilePhoto =2966      document.getElementById(2967        "profilePhoto"2968      );2969 2970    if (profilePhoto) {2971 2972      if (photo) {2973 2974        profilePhoto.innerHTML =2975          '<img src="' +2976          esc(photo) +2977          '">';2978 2979      } else {2980 2981        profilePhoto.innerHTML =2982          '<img src="https://api.dicebear.com/9.x/initials/svg?seed=' +2983          encodeURIComponent(2984            p.name || "User"2985          ) +2986          '">';2987 2988      }2989 2990    }2991 2992 2993    const links = [2994 2995      [2996        "user-round",2997        "Owner",2998        BOOT.support &&2999        BOOT.support.owner3000      ],3001 3002      [3003        "megaphone",3004        "Channel",3005        BOOT.support &&3006        BOOT.support.channel3007      ],3008 3009      [3010        "users",3011        "Community",3012        BOOT.support &&3013        BOOT.support.community3014      ]3015 3016    ].filter(function (x) {3017      return x[2];3018    });3019 3020 3021    const supportLinks =3022      document.getElementById(3023        "supportLinks"3024      );3025 3026 3027    if (supportLinks) {3028 3029      supportLinks.innerHTML =3030        links.length3031 3032          ? links.map(3033              function (x) {3034 3035                return (3036                  '<a href="' +3037                  esc(x[2]) +3038                  '" target="_blank" class="card p-5 flex items-center justify-between">' +3039 3040                  '<span class="flex gap-3 items-center">' +3041 3042                  '<i data-lucide="' +3043                  x[0] +3044                  '"></i>' +3045 3046                  esc(x[1]) +3047 3048                  '</span>' +3049 3050                  '<i data-lucide="arrow-up-right"></i>' +3051 3052                  '</a>'3053                );3054 3055              }3056            ).join("")3057 3058          : '<div class="card p-5 text-slate-500">' +3059            'Support links are not configured yet.' +3060            '</div>';3061 3062    }3063 3064    refreshIcons();3065 3066  } catch (e) {3067 3068    console.error(3069      "initProfile error",3070      e3071    );3072 3073  }3074 3075}3076 3077 3078/* =========================3079   WELCOME3080========================= */3081 3082function closeWelcome() {3083 3084  const modal =3085    document.getElementById(3086      "welcomeModal"3087    );3088 3089  if (modal) {3090 3091    modal.classList.remove(3092      "show"3093    );3094 3095  }3096 3097  try {3098 3099    localStorage.setItem(3100      "authkeyWelcome",3101      "1"3102    );3103 3104  } catch (e) {}3105 3106}3107 3108 3109/* =========================3110   APP INITIALIZATION3111========================= */3112 3113/*3114  IMPORTANT:3115 3116  Splash removal is deliberately executed3117  before the rest of initialization.3118 3119  Therefore a failed CDN, Lucide error,3120  Telegram error, or rendering error3121  cannot permanently trap the user3122  on the splash screen.3123*/3124 3125(function initApp() {3126 3127  const splash =3128    document.getElementById(3129      "splash"3130    );3131 3132  /* Remove splash immediately */3133 3134  if (splash) {3135 3136    setTimeout(3137      function () {3138 3139        splash.classList.add(3140          "gone"3141        );3142 3143      },3144      1503145    );3146 3147    setTimeout(3148      function () {3149 3150        splash.style.display =3151          "none";3152 3153      },3154      9003155    );3156 3157  }3158 3159 3160  /* Render rest of application safely */3161 3162  try {3163 3164    renderAccounts();3165 3166  } catch (e) {3167 3168    console.error(3169      "Initial account render failed",3170      e3171    );3172 3173  }3174 3175 3176  try {3177 3178    initProfile();3179 3180  } catch (e) {3181 3182    console.error(3183      "Initial profile render failed",3184      e3185    );3186 3187  }3188 3189 3190  try {3191 3192    refreshIcons();3193 3194  } catch (e) {}3195 3196 3197  try {3198 3199    if (3200      !localStorage.getItem(3201        "authkeyWelcome"3202      )3203    ) {3204 3205      setTimeout(3206        function () {3207 3208          const modal =3209            document.getElementById(3210              "welcomeModal"3211            );3212 3213          if (modal) {3214 3215            modal.classList.add(3216              "show"3217            );3218 3219          }3220 3221        },3222        8503223      );3224 3225    }3226 3227  } catch (e) {}3228 3229})();3230 3231</script>3232 3233</body>3234</html>`);