WhatsApp-Logger-Self-Hosted-

A privacy-focused, self-hosted...
Log | Files | Refs | Activity | README | LICENSE

root / index.html

index.html (139695B)


      1 <!DOCTYPE html>
      2 <html lang="en">
      3 <head>
      4     <meta charset="UTF-8">
      5     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
      6     <title>WhatsApp Logger</title>
      7     
      8     <!-- PWA Manifest -->
      9     <link rel="manifest" href="/assets/manifest-wpChat.json">
     10     <meta name="theme-color" content="#006C4C">
     11     
     12     <!-- SVG Favicon -->
     13     <link rel="icon" href="/assets/icon.svg">
     14 
     15     <!-- Fonts & Icons -->
     16     <link rel="preconnect" href="https://fonts.googleapis.com">
     17     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
     18     <link href="https://fonts.googleapis.com/css2?family=Lexend:wght@300;400;500;600;700&display=swap" rel="stylesheet">
     19     <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0" />
     20 
     21     <!-- Trusted Types Bypass for CSP -->
     22     <script>
     23         if (window.trustedTypes && trustedTypes.createPolicy && !trustedTypes.defaultPolicy) {
     24             trustedTypes.createPolicy('default', {
     25                 createHTML: string => string,
     26                 createScriptURL: string => string,
     27                 createScript: string => string,
     28             });
     29         }
     30     </script>
     31 
     32     <style>
     33         /* Base / Default Theme (Catppuccin Latte - Light) */
     34         :root {
     35             --md-sys-color-primary: #40a02b; /* Green */
     36             --md-sys-color-on-primary: #eff1f5; /* Base */
     37             --md-sys-color-primary-container: #a6d189; /* Lighter Green approximation for container */
     38             --md-sys-color-on-primary-container: #1e1e2e; 
     39             
     40             --md-sys-color-secondary: #179299; /* Teal */
     41             --md-sys-color-on-secondary: #eff1f5;
     42             --md-sys-color-secondary-container: #99d1db; /* Sky used as container */
     43             --md-sys-color-on-secondary-container: #4c4f69;
     44             
     45             --md-sys-color-surface: #eff1f5; /* Base */
     46             --md-sys-color-surface-dim: #e6e9ef; /* Mantle */
     47             --md-sys-color-surface-container: #dce0e8; /* Crust */
     48             --md-sys-color-surface-container-high: #ccd0da; /* Surface0 */
     49             --md-sys-color-surface-container-highest: #bcc0cc; /* Surface1 */
     50             
     51             --md-sys-color-on-surface: #4c4f69; /* Text */
     52             --md-sys-color-on-surface-variant: #5c5f77; /* Subtext1 */
     53             --md-sys-color-outline: #8c8fa1; /* Overlay1 */
     54             --md-sys-color-outline-variant: #acb0be; /* Surface2 */
     55 
     56             --md-sys-color-error: #d20f39; /* Red */
     57             --md-sys-color-error-container: #ea76cb; /* Pink approx */
     58             --md-sys-color-on-error-container: #eff1f5;
     59             --md-sys-color-on-error: #eff1f5; /* Fixed text for errors */
     60             
     61             --chat-bubble-me: #a6d189; /* Green/Primary approx for light */
     62             --chat-on-bubble-me: #4c4f69;
     63             --chat-bubble-them: #ffffff;
     64             --chat-on-bubble-them: #4c4f69;
     65             --chat-bg-color: #e6e9ef; /* Mantle */
     66             --chat-bg-image-opacity: 0.05;
     67             --search-highlight: #df8e1d; /* Yellow */
     68             
     69             /* Elevations & Shapes */
     70             --elevation-1: 0px 1px 3px 1px rgba(0,0,0,0.15), 0px 1px 2px 0px rgba(0,0,0,0.3);
     71             --elevation-2: 0px 2px 6px 2px rgba(0,0,0,0.15), 0px 1px 2px 0px rgba(0,0,0,0.3);
     72             
     73             /* Settings */
     74             --base-font-size: 15px;
     75         }
     76 
     77         /* Dark Theme Tokens (Catppuccin Mocha) */
     78         [data-theme="mocha"] {
     79             --md-sys-color-primary: #a6e3a1; /* Green */
     80             --md-sys-color-on-primary: #1e1e2e; /* Base */
     81             --md-sys-color-primary-container: #40a02b; /* Darker green */
     82             --md-sys-color-on-primary-container: #a6e3a1;
     83             
     84             --md-sys-color-secondary: #94e2d5; /* Teal */
     85             --md-sys-color-on-secondary: #1e1e2e;
     86             --md-sys-color-secondary-container: #89dceb; /* Sky */
     87             --md-sys-color-on-secondary-container: #1e1e2e;
     88             
     89             --md-sys-color-surface: #1e1e2e; /* Base */
     90             --md-sys-color-surface-dim: #181825; /* Mantle */
     91             --md-sys-color-surface-container: #11111b; /* Crust */
     92             --md-sys-color-surface-container-high: #313244; /* Surface0 */
     93             --md-sys-color-surface-container-highest: #45475a; /* Surface1 */
     94             
     95             --md-sys-color-on-surface: #cdd6f4; /* Text */
     96             --md-sys-color-on-surface-variant: #bac2de; /* Subtext1 */
     97             --md-sys-color-outline: #7f849c; /* Overlay1 */
     98             --md-sys-color-outline-variant: #585b70; /* Surface2 */
     99 
    100             --md-sys-color-error: #f38ba8; /* Red */
    101             --md-sys-color-error-container: #d20f39; 
    102             --md-sys-color-on-error-container: #f38ba8;
    103             --md-sys-color-on-error: #1e1e2e; /* Fixed text for errors */
    104             
    105             --chat-bubble-me: #a6e3a1;
    106             --chat-on-bubble-me: #1e1e2e;
    107             --chat-bubble-them: #313244; /* Surface0 */
    108             --chat-on-bubble-them: #cdd6f4;
    109             --chat-bg-color: #181825; /* Mantle */
    110             --chat-bg-image-opacity: 0.03;
    111             --search-highlight: #f9e2af; /* Yellow */
    112         }
    113         
    114         /* True Black / AMOLED Theme */
    115         [data-theme="black"] {
    116             --md-sys-color-primary: #a6e3a1; 
    117             --md-sys-color-on-primary: #000000; 
    118             --md-sys-color-primary-container: #40a02b; 
    119             --md-sys-color-on-primary-container: #a6e3a1;
    120             
    121             --md-sys-color-secondary: #94e2d5; 
    122             --md-sys-color-on-secondary: #000000;
    123             --md-sys-color-secondary-container: #89dceb; 
    124             --md-sys-color-on-secondary-container: #000000;
    125             
    126             /* The real differences for AMOLED */
    127             --md-sys-color-surface: #000000;
    128             --md-sys-color-surface-dim: #000000; 
    129             --md-sys-color-surface-container: #000000; 
    130             --md-sys-color-surface-container-high: #1e1e2e; /* Still use Mocha base for slight elevation */
    131             --md-sys-color-surface-container-highest: #313244; 
    132             
    133             --md-sys-color-on-surface: #cdd6f4; 
    134             --md-sys-color-on-surface-variant: #bac2de; 
    135             --md-sys-color-outline: #7f849c; 
    136             --md-sys-color-outline-variant: #585b70; 
    137 
    138             --md-sys-color-error: #f38ba8; 
    139             --md-sys-color-error-container: #d20f39; 
    140             --md-sys-color-on-error-container: #f38ba8;
    141             --md-sys-color-on-error: #000000; /* Fixed text for errors */
    142             
    143             --chat-bubble-me: #a6e3a1;
    144             --chat-on-bubble-me: #000000;
    145             --chat-bubble-them: #1e1e2e;
    146             --chat-on-bubble-them: #cdd6f4;
    147             --chat-bg-color: #000000;
    148             --chat-bg-image-opacity: 0.05;
    149             --search-highlight: #f9e2af;
    150         }
    151 
    152         * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
    153 
    154         body, *:not(.material-symbols-rounded) {
    155             font-family: 'Lexend', sans-serif !important;
    156         }
    157         body {
    158             background-color: var(--md-sys-color-surface);
    159             color: var(--md-sys-color-on-surface);
    160             height: 100vh;
    161             overflow: hidden;
    162             display: flex;
    163             justify-content: center;
    164             transition: background-color 0.3s, color 0.3s;
    165         }
    166 
    167         /* --- ANIMATIONS --- */
    168         .hidden { display: none !important; }
    169         .fade-in { animation: fadeIn 0.4s cubic-bezier(0.2, 0.0, 0.0, 1.0); }
    170         @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
    171         .ripple-surface { position: relative; overflow: hidden; transform: translate3d(0, 0, 0); transition: transform 0.1s; }
    172         .ripple-surface:active { transform: scale(0.98); }
    173         
    174         @keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
    175         @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
    176         .animate-enter { animation: scaleIn 0.3s cubic-bezier(0.2, 0.0, 0, 1.0); }
    177         .shake { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; }
    178         @keyframes shake { 10%, 90% { transform: translate3d(-1px, 0, 0); } 20%, 80% { transform: translate3d(2px, 0, 0); } 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } 40%, 60% { transform: translate3d(4px, 0, 0); } }
    179 
    180         /* --- M3 STAGGERED ENTRANCE --- */
    181         @keyframes fadeInUp {
    182             from { opacity: 0; transform: translateY(30px); }
    183             to { opacity: 1; transform: translateY(0); }
    184         }
    185         .anim-up { opacity: 0; animation: fadeInUp 0.6s cubic-bezier(0.2, 0, 0, 1) forwards; }
    186         .delay-1 { animation-delay: 0.1s; }
    187         .delay-2 { animation-delay: 0.2s; }
    188         .delay-3 { animation-delay: 0.3s; }
    189         .delay-4 { animation-delay: 0.4s; }
    190         .delay-5 { animation-delay: 0.5s; }
    191 
    192         /* --- AUTH SCREEN --- */
    193         #auth-screen {
    194             position: fixed; inset: 0; z-index: 999;
    195             background-color: var(--md-sys-color-surface-dim);
    196             display: flex; flex-direction: column; align-items: center; justify-content: center;
    197             padding: 24px; height: 100dvh;
    198             transition: opacity 0.4s cubic-bezier(0.2, 0, 0, 1);
    199         }
    200         
    201         .auth-card { 
    202             background: var(--md-sys-color-surface);
    203             border-radius: 28px;
    204             padding: 32px 24px; 
    205             width: 90%; max-width: 380px;
    206             box-shadow: var(--elevation-2);
    207             border: 1px solid var(--md-sys-color-outline-variant);
    208             display: flex; flex-direction: column; gap: 20px;
    209             position: relative;
    210         }
    211 
    212         .brand-icon { 
    213             width: 64px; height: 64px; 
    214             display: flex; align-items: center; justify-content: center; 
    215             margin: 0 auto 8px;
    216         }
    217         .brand-icon img { width: 100%; height: 100%; object-fit: contain; }
    218 
    219         /* Custom Spinner for the Boot Loader */
    220         #authLoader .spinner { 
    221              border-color: rgba(128,128,128,0.2) !important; 
    222              border-top-color: var(--md-sys-color-primary) !important; 
    223         }
    224 
    225         /* --- LOCK SCREEN (PIN/BIO) --- */
    226         #lock-screen {
    227             position: fixed; inset: 0; z-index: 2000;
    228             background: var(--md-sys-color-surface);
    229             display: flex; flex-direction: column; align-items: center; justify-content: space-between;
    230             padding: 48px 24px; opacity: 0; visibility: hidden; transition: opacity 0.3s;
    231         }
    232         #lock-screen.active { opacity: 1; visibility: visible; }
    233         
    234         .lock-header { text-align: center; margin-top: 4vh; }
    235         .lock-icon { width: 64px; height: 64px; margin-bottom: 16px; color: var(--md-sys-color-primary); }
    236         .lock-title { font-family: 'Lexend', sans-serif; font-size: 24px; margin-bottom: 8px; }
    237         .lock-sub { color: var(--md-sys-color-on-surface-variant); font-size: 14px; }
    238 
    239         /* PIN Dots */
    240         .pin-dots { display: flex; gap: 16px; margin: 32px 0; justify-content: center; }
    241         .pin-dot {
    242             width: 14px; height: 14px; border-radius: 50%;
    243             border: 1px solid var(--md-sys-color-outline);
    244             transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
    245         }
    246         .pin-dot.filled { background: var(--md-sys-color-on-surface); border-color: var(--md-sys-color-on-surface); transform: scale(1.1); }
    247         .pin-dot.error { border-color: var(--md-sys-color-error); background: var(--md-sys-color-error); }
    248 
    249         /* Native Keypad */
    250         .keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px 32px; margin-bottom: 2vh; }
    251         .key-btn {
    252             width: 72px; height: 72px; border-radius: 50%; border: none; background: transparent;
    253             font-size: 28px; font-weight: 400; color: var(--md-sys-color-on-surface);
    254             display: flex; align-items: center; justify-content: center; cursor: pointer;
    255             transition: background 0.2s; font-family: 'Lexend', sans-serif;
    256         }
    257         .key-btn:active { background: var(--md-sys-color-surface-container-high); }
    258         .key-backspace { font-size: 24px; color: var(--md-sys-color-on-surface-variant); }
    259 
    260         .forgot-pin-btn { margin-top: 16px; color: var(--md-sys-color-primary); background: none; border: none; font-weight: 500; font-size: 14px; padding: 8px 16px; border-radius: 20px; cursor: pointer; }
    261 
    262         /* --- LOCKOUT UI --- */
    263         .lockout-container { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; width: 100%; animation: fadeIn 0.3s; flex: 1; }
    264         .lockout-icon-bg { width: 80px; height: 80px; border-radius: 50%; background: var(--md-sys-color-error-container); color: var(--md-sys-color-on-error-container); display: flex; align-items: center; justify-content: center; margin-bottom: 24px; }
    265         .lockout-icon { font-size: 40px; }
    266         .lockout-title { font-family: 'Lexend', sans-serif; font-size: 28px; margin-bottom: 8px; color: var(--md-sys-color-on-surface); }
    267         .lockout-msg { color: var(--md-sys-color-on-surface-variant); margin-bottom: 32px; font-size: 16px; }
    268         .lockout-timer { font-family: 'Lexend', sans-serif; font-size: 56px; font-weight: 400; color: var(--md-sys-color-error); font-feature-settings: "tnum"; margin-bottom: 8px; }
    269         .lockout-sub { margin-top: 16px; color: var(--md-sys-color-on-surface-variant); font-size: 14px; }
    270         .lockout-actions { margin-top: 24px; display: flex; gap: 12px; }
    271         .locked-mode #pinUI, .locked-mode #bioUI, .locked-mode .forgot-pin-btn { display: none !important; }
    272 
    273         /* --- UI ELEMENTS --- */
    274         .m3-field {
    275             position: relative; margin-bottom: 0;
    276             background: var(--md-sys-color-surface-container-highest);
    277             border-radius: 4px 4px 0 0; border-bottom: 1px solid var(--md-sys-color-outline);
    278             transition: background 0.3s; height: 56px;
    279         }
    280         .m3-field:hover { background: rgba(128, 128, 128, 0.08); }
    281         .m3-field:focus-within { border-bottom: 2px solid var(--md-sys-color-primary); }
    282         .m3-input { width: 100%; height: 100%; padding: 24px 16px 8px 16px; border: none; background: transparent; outline: none; font-size: 16px; color: var(--md-sys-color-on-surface); font-family: inherit; }
    283         .m3-label { position: absolute; left: 16px; top: 18px; color: var(--md-sys-color-on-surface-variant); font-size: 16px; pointer-events: none; transition: 0.2s cubic-bezier(0.2, 0, 0, 1); }
    284         .m3-input:focus ~ .m3-label, .m3-input:not(:placeholder-shown) ~ .m3-label { top: 8px; font-size: 12px; color: var(--md-sys-color-primary); }
    285         .eye-toggle { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); border: none; background: none; cursor: pointer; color: var(--md-sys-color-on-surface-variant); padding: 8px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background 0.2s; }
    286         .eye-toggle:hover { background: rgba(128,128,128,0.1); color: var(--md-sys-color-on-surface); }
    287 
    288         .m3-btn {
    289             width: 100%; height: 48px; border-radius: 24px; border: none; outline: none; cursor: pointer;
    290             font-family: 'Lexend', sans-serif; font-weight: 500; font-size: 14px;
    291             display: flex; align-items: center; justify-content: center; gap: 8px;
    292             transition: 0.2s; position: relative; overflow: hidden;
    293             background: transparent; color: var(--md-sys-color-primary);
    294         }
    295         .m3-btn.filled { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); box-shadow: none; }
    296         .m3-btn.filled:hover { box-shadow: var(--elevation-1); opacity: 0.9;}
    297         .m3-btn:disabled { opacity: 0.38; cursor: not-allowed; background: var(--md-sys-color-on-surface); color: var(--md-sys-color-surface); box-shadow: none; }
    298 
    299         .spinner { width: 20px; height: 20px; border: 2px solid rgba(128,128,128,0.3); border-radius: 50%; border-top-color: inherit; animation: spin 0.8s linear infinite; display: none; }
    300         .m3-btn.loading .btn-text { visibility: hidden; }
    301         .m3-btn.loading .btn-icon { display: none; }
    302         .m3-btn.loading .spinner { display: block; position: absolute; }
    303         @keyframes spin { to { transform: rotate(360deg); } }
    304         
    305         .m3-select {
    306             appearance: none;
    307             width: 100%; height: 56px;
    308             background: var(--md-sys-color-surface-container-highest);
    309             border: none; border-bottom: 1px solid var(--md-sys-color-outline);
    310             border-radius: 4px 4px 0 0;
    311             padding: 8px 16px;
    312             color: var(--md-sys-color-on-surface);
    313             font-family: inherit; font-size: 16px;
    314             outline: none; cursor: pointer;
    315         }
    316         .m3-select:focus { border-bottom: 2px solid var(--md-sys-color-primary); }
    317         .select-wrapper { position: relative; }
    318         .select-wrapper::after {
    319             content: 'expand_more';
    320             font-family: 'Material Symbols Rounded';
    321             position: absolute; right: 16px; top: 50%; transform: translateY(-50%);
    322             color: var(--md-sys-color-on-surface-variant); pointer-events: none;
    323         }
    324 
    325         /* --- APP LAYOUT --- */
    326         #app-layout { display: flex; width: 100%; height: 100%; max-width: 1600px; background: var(--md-sys-color-surface); opacity: 0; transition: opacity 0.6s ease-in-out; visibility: hidden; }
    327         #app-layout.visible { opacity: 1; visibility: visible; }
    328 
    329         /* SIDEBAR */
    330         .sidebar { width: 400px; display: flex; flex-direction: column; border-right: 1px solid var(--md-sys-color-outline-variant); background: var(--md-sys-color-surface); z-index: 10; }
    331         .sidebar-header { padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; }
    332         .icon-btn { width: 40px; height: 40px; border-radius: 50%; border: none; background: transparent; cursor: pointer; color: var(--md-sys-color-on-surface-variant); display: flex; align-items: center; justify-content: center; transition: 0.2s; }
    333         .icon-btn:hover { background: rgba(128,128,128,0.1); color: var(--md-sys-color-on-surface); }
    334         .search-container { padding: 0 16px 12px; }
    335         .search-bar { background: var(--md-sys-color-surface-container-high); border-radius: 28px; height: 48px; display: flex; align-items: center; padding: 0 16px; }
    336         .search-bar input { border: none; background: transparent; outline: none; flex: 1; margin-left: 12px; font-size: 16px; color: var(--md-sys-color-on-surface); }
    337         .contact-list { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 8px; }
    338         .contact-item { display: flex; align-items: center; padding: 12px 16px; border-radius: 16px; cursor: pointer; margin-bottom: 2px; animation: slideUp 0.3s cubic-bezier(0.2, 0.0, 0, 1.0) backwards; }
    339         .contact-item:hover { background: rgba(128,128,128,0.05); }
    340         .contact-item.active { background: var(--md-sys-color-secondary-container); }
    341         .contact-item.active .contact-name { color: var(--md-sys-color-on-secondary-container); font-weight: 700; }
    342         .avatar { width: 48px; height: 48px; border-radius: 50%; background: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container); display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 500; flex-shrink: 0; margin-right: 16px; }
    343         .contact-info { flex: 1; min-width: 0; }
    344         .contact-name { font-size: 16px; font-weight: 500; color: var(--md-sys-color-on-surface); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    345         .contact-sub { font-size: 14px; color: var(--md-sys-color-on-surface-variant); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    346 
    347         /* CHAT AREA */
    348         .chat-area { flex: 1; display: flex; flex-direction: column; position: relative; background-color: var(--chat-bg-color); transition: background-color 0.3s; overflow: hidden; }
    349         .chat-bg-pattern { position: absolute; inset: 0; opacity: var(--chat-bg-image-opacity); z-index: 0; pointer-events: none; background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%236e7874' fill-opacity='1' fill-rule='evenodd'/%3E%3C/svg%3E"); }
    350         .chat-header { height: 64px; background: var(--md-sys-color-surface); display: flex; align-items: center; padding: 0 16px; border-bottom: 1px solid var(--md-sys-color-outline-variant); z-index: 5; }
    351         
    352         .chat-content {
    353             flex: 1; overflow-y: auto; padding: 16px; z-index: 1;
    354             display: flex; flex-direction: column; gap: 4px;
    355             scroll-behavior: auto !important; 
    356             transition: padding-bottom 0.4s cubic-bezier(0.2, 0, 0, 1);
    357         }
    358         
    359         /* Forces bottom-up alignment when content is sparse */
    360         .chat-content::before {
    361             content: '';
    362             flex: 1 1 auto;
    363         }
    364 
    365         .chat-content.syncing {
    366             padding-bottom: 72px; /* Cinematic push up */
    367         }
    368 
    369         .msg-bubble { max-width: 80%; padding: 8px 12px; position: relative; font-size: var(--base-font-size); line-height: 1.4; box-shadow: 0 1px 2px rgba(0,0,0,0.1); display: flex; flex-direction: column; animation: scaleIn 0.2s cubic-bezier(0.2, 0.0, 0, 1.0) backwards; }
    370         .msg-in { align-self: flex-start; background: var(--chat-bubble-them); border-radius: 0 16px 16px 16px; color: var(--chat-on-bubble-them, var(--md-sys-color-on-surface)); }
    371         .msg-out { align-self: flex-end; background: var(--chat-bubble-me); border-radius: 16px 0 16px 16px; color: var(--chat-on-bubble-me, var(--md-sys-color-on-surface)); }
    372         
    373         .msg-out.group-top { border-bottom-right-radius: 4px; margin-bottom: 2px; }
    374         .msg-out.group-mid { border-top-right-radius: 4px; border-bottom-right-radius: 4px; margin-bottom: 2px; }
    375         .msg-out.group-bot { border-top-right-radius: 4px; margin-top: 0; }
    376         .msg-in.group-top { border-bottom-left-radius: 4px; margin-bottom: 2px; }
    377         .msg-in.group-mid { border-top-left-radius: 4px; border-bottom-left-radius: 4px; margin-bottom: 2px; }
    378         .msg-in.group-bot { border-top-left-radius: 4px; margin-top: 0; }
    379 
    380         .msg-sender { font-size: 12px; font-weight: 700; color: var(--md-sys-color-primary); margin-bottom: 2px; }
    381         .msg-text { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; }
    382         .msg-meta { font-size: 10px; opacity: 0.7; align-self: flex-end; margin-top: 2px; }
    383         
    384         /* DATE DIVIDERS */
    385         .chat-date-group { display: flex; flex-direction: column; gap: 4px; position: relative; }
    386         .date-divider { align-self: center; margin: 16px 0; background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface-variant); font-size: 12px; font-weight: 500; padding: 6px 12px; border-radius: 12px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); position: sticky; top: 8px; z-index: 2; }
    387 
    388         .highlight { background-color: var(--search-highlight); color: black; border-radius: 2px; padding: 0 2px; }
    389 
    390         /* Sync Loading Spinner (Cinematic UI) */
    391         .sync-spinner-wrapper {
    392             position: absolute; bottom: 16px; left: 0; right: 0;
    393             display: flex; justify-content: center;
    394             pointer-events: none; z-index: 10;
    395             transform: translateY(150%); opacity: 0;
    396             transition: all 0.4s cubic-bezier(0.2, 0, 0, 1);
    397         }
    398         .sync-spinner-wrapper.active {
    399             transform: translateY(0); opacity: 1;
    400         }
    401         .sync-chip {
    402             background: var(--md-sys-color-surface-container-highest);
    403             color: var(--md-sys-color-on-surface);
    404             padding: 8px 16px; border-radius: 24px;
    405             display: flex; align-items: center; gap: 10px;
    406             font-size: 13px; font-weight: 500;
    407             box-shadow: var(--elevation-2);
    408             border: 1px solid var(--md-sys-color-outline-variant);
    409         }
    410         .sync-chip .spinner {
    411             border-color: rgba(128,128,128,0.2);
    412             border-top-color: var(--md-sys-color-primary);
    413             width: 16px; height: 16px; 
    414             display: block; position: static;
    415         }
    416 
    417         /* Search In Chat */
    418         .in-chat-search { background: var(--md-sys-color-surface-container); padding: 8px 16px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid var(--md-sys-color-outline-variant); animation: slideUp 0.3s cubic-bezier(0.2, 0.0, 0, 1.0); }
    419         .in-chat-search input { border: none; background: transparent; outline: none; flex: 1; color: var(--md-sys-color-on-surface); }
    420 
    421         /* Menu */
    422         .menu-container { position: relative; }
    423         .dropdown-menu { position: absolute; top: 100%; right: 0; background: var(--md-sys-color-surface-container-high); border-radius: 12px; padding: 8px 0; min-width: 200px; display: none; flex-direction: column; z-index: 100; box-shadow: 0 4px 8px rgba(0,0,0,0.2); animation: scaleIn 0.2s cubic-bezier(0.2, 0.0, 0, 1.0); transform-origin: top right; }
    424         .dropdown-menu.show { display: flex; }
    425         .menu-item { padding: 12px 16px; display: flex; align-items: center; gap: 12px; color: var(--md-sys-color-on-surface); background: none; border: none; font-family: inherit; font-size: 14px; cursor: pointer; text-align: left; }
    426         .menu-item:hover { background: rgba(128,128,128,0.1); }
    427         
    428         /* Scroll To Bottom FAB */
    429         .scroll-fab { position: fixed; bottom: 24px; right: 24px; width: 40px; height: 40px; border-radius: 50%; background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface-variant); border: 1px solid var(--md-sys-color-outline-variant); box-shadow: 0 2px 6px rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 20; opacity: 0; transform: translateY(20px); pointer-events: none; transition: 0.3s; }
    430         .scroll-fab.visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
    431         .new-msg-badge { position: absolute; top: -2px; right: -2px; width: 12px; height: 12px; background: var(--md-sys-color-error); border-radius: 50%; border: 2px solid var(--md-sys-color-surface-container-high); }
    432 
    433         /* Offline Indicator */
    434         .offline-banner { position: fixed; top: 0; left: 50%; transform: translateX(-50%); background: var(--md-sys-color-error); color: var(--md-sys-color-on-error); padding: 4px 16px; border-radius: 0 0 8px 8px; font-size: 12px; z-index: 9999; display: flex; align-items: center; gap: 6px; box-shadow: var(--elevation-1); }
    435 
    436         /* Calendar Modal */
    437         .calendar-card { background: var(--md-sys-color-surface); padding: 0; border-radius: 28px; width: 320px; overflow: hidden; display: flex; flex-direction: column; }
    438         .cal-header { background: var(--md-sys-color-surface-container-high); padding: 16px 24px; }
    439         .cal-header h3 { font-size: 14px; color: var(--md-sys-color-on-surface-variant); font-weight: 500; }
    440         .cal-header h2 { font-size: 32px; color: var(--md-sys-color-on-surface); font-weight: 400; margin-top: 4px; }
    441         .cal-grid { padding: 16px 12px 0; }
    442         .cal-nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding: 0 12px; }
    443         .cal-days { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; margin-bottom: 8px; }
    444         .cal-day-label { font-size: 12px; color: var(--md-sys-color-on-surface-variant); width: 36px; }
    445         .cal-dates { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-bottom: 16px; }
    446         .cal-date { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: 50%; font-size: 14px; cursor: pointer; color: var(--md-sys-color-on-surface); }
    447         .cal-date:hover { background: var(--md-sys-color-surface-container-high); }
    448         .cal-date.active { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }
    449         .cal-date.empty { pointer-events: none; }
    450 
    451         /* Settings Modal */
    452         .modal-overlay { position: fixed; inset: 0; z-index: 2000; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; opacity: 0; visibility: hidden; transition: 0.3s; }
    453         .modal-overlay.active { opacity: 1; visibility: visible; }
    454         .settings-card { background: var(--md-sys-color-surface); width: 90%; max-width: 600px; height: 80vh; border-radius: 28px; display: flex; flex-direction: column; overflow: hidden; animation: scaleIn 0.3s var(--anim-enter); }
    455         .settings-header { padding: 20px 24px; border-bottom: 1px solid var(--md-sys-color-outline-variant); display: flex; justify-content: space-between; align-items: center; }
    456         .settings-content { flex: 1; overflow-y: auto; padding: 24px; }
    457         .setting-section { margin-bottom: 32px; }
    458         .setting-title { font-size: 14px; font-weight: 500; color: var(--md-sys-color-primary); margin-bottom: 16px; text-transform: uppercase; letter-spacing: 1px; }
    459         .setting-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
    460         .setting-label h4 { font-size: 16px; font-weight: 400; margin-bottom: 4px; }
    461         .setting-label p { font-size: 14px; color: var(--md-sys-color-on-surface-variant); }
    462         
    463         .switch { position: relative; display: inline-block; width: 50px; height: 28px; }
    464         .switch input { opacity: 0; width: 0; height: 0; }
    465         .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--md-sys-color-surface-container-high); border-radius: 34px; transition: .4s; border: 1px solid var(--md-sys-color-outline); }
    466         .slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 4px; bottom: 3px; background-color: var(--md-sys-color-outline); border-radius: 50%; transition: .4s; }
    467         input:checked + .slider { background-color: var(--md-sys-color-primary); border-color: var(--md-sys-color-primary); }
    468         input:checked + .slider:before { transform: translateX(20px); background-color: var(--md-sys-color-on-primary); }
    469 
    470         .faq-item { border-bottom: 1px solid var(--md-sys-color-outline-variant); }
    471         .faq-question { width: 100%; padding: 16px 0; text-align: left; background: none; border: none; font-size: 16px; color: var(--md-sys-color-on-surface); cursor: pointer; display: flex; justify-content: space-between; }
    472         .faq-answer { display: none; padding-bottom: 16px; color: var(--md-sys-color-on-surface-variant); line-height: 1.5; }
    473         .faq-item.open .faq-answer { display: block; }
    474 
    475         .install-sheet { display: none; }
    476         .install-card { background: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container); padding: 16px; border-radius: 12px; margin-bottom: 32px; display: flex; align-items: center; justify-content: space-between; }
    477         .install-card h4 { margin-bottom: 4px; }
    478         .install-card p { font-size: 12px; }
    479         .install-card .m3-btn { width: auto; height: 36px; padding: 0 16px; font-size: 14px; white-space: nowrap; flex-shrink: 0; box-shadow: none; }
    480 
    481         @media (max-width: 768px) {
    482             .sidebar { width: 100%; flex: 1; border-right: none; }
    483             .chat-area { position: fixed; inset: 0; z-index: 20; transform: translateX(100%); transition: transform 0.3s; background-color: var(--chat-bg-color); }
    484             .chat-area.active { transform: translateX(0); }
    485             .back-btn { display: flex !important; margin-right: 8px; }
    486             .install-sheet { display: block; position: fixed; bottom: 0; left: 0; right: 0; z-index: 3000; background: var(--md-sys-color-surface); border-top-left-radius: 28px; border-top-right-radius: 28px; padding: 24px; box-shadow: 0 -4px 20px rgba(0,0,0,0.15); transform: translateY(100%); transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1); }
    487             .install-sheet.visible { transform: translateY(0); }
    488             .sheet-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
    489             .sheet-content { text-align: center; }
    490             .sheet-actions { display: flex; flex-direction: column; gap: 12px; margin-top: 24px; }
    491         }
    492         .back-btn { display: none; margin-right: 8px; }
    493 
    494         .modal-card { background-color: var(--md-sys-color-surface); padding: 24px; border-radius: 28px; width: 90%; max-width: 320px; transform: scale(0.9); transition: 0.3s cubic-bezier(0.2, 0, 0, 1); animation: scaleIn 0.3s var(--anim-enter); }
    495         .modal-overlay.active .modal-card { transform: scale(1); }
    496         .modal-card.utility { width: 320px; padding: 24px; border-radius: 28px; }
    497         .modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 24px; }
    498         .modal-actions .m3-btn { width: auto; min-width: 80px; height: 40px; padding: 0 16px; }
    499         .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; width: 100%; color: var(--md-sys-color-outline); text-align: center; animation: fadeIn 0.5s; }
    500         .skeleton { background: linear-gradient(90deg, var(--md-sys-color-surface-container-high) 25%, var(--md-sys-color-surface-container) 50%, var(--md-sys-color-surface-container-high) 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite linear; border-radius: 4px; }
    501         @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
    502         .skel-contact { display: flex; align-items: center; padding: 12px 16px; gap: 16px; width: 100%; }
    503         .skel-avatar { width: 48px; height: 48px; border-radius: 50%; }
    504         .skel-text-col { flex: 1; display: flex; flex-direction: column; gap: 8px; }
    505         .skel-msg-group { display: flex; flex-direction: column; gap: 12px; padding: 20px; width: 100%; }
    506         .skel-bubble { height: 40px; border-radius: 12px; width: 40%; margin-bottom: 8px;}
    507         .skel-left { align-self: flex-start; border-bottom-left-radius: 0; }
    508         .skel-right { align-self: flex-end; border-bottom-right-radius: 0; width: 50%; }
    509 
    510         /* --- SETTINGS SIDEBAR --- */
    511         .settings-sidebar {
    512             position: fixed;
    513             top: 0; left: 0; bottom: 0;
    514             width: 400px;
    515             background: var(--md-sys-color-surface);
    516             z-index: 2000;
    517             display: flex;
    518             flex-direction: column;
    519             border-right: 1px solid var(--md-sys-color-outline-variant);
    520             box-shadow: var(--elevation-2);
    521             transform: translateX(-100%);
    522             transition: transform 0.4s cubic-bezier(0.2, 0, 0, 1);
    523         }
    524 
    525         .settings-sidebar.active {
    526             transform: translateX(0);
    527         }
    528 
    529         /* Mobile full-width override */
    530         @media (max-width: 768px) {
    531             .settings-sidebar {
    532                 width: 100%;
    533                 border-right: none;
    534             }
    535         }
    536 
    537         @media (max-width: 768px) {
    538         /* Pin all utility modals to the bottom */
    539         .modal-card.utility {
    540             position: absolute;
    541             bottom: 0;
    542             width: 100% !important;
    543             max-width: 100%;
    544             border-radius: 28px 28px 0 0;
    545             padding: 32px 24px 24px;
    546             transform: translateY(100%) scale(1);
    547             animation: none; /* Override the desktop scaleIn */
    548             transition: transform 0.4s cubic-bezier(0.2, 0, 0, 1);
    549         }
    550         
    551         /* Add the M3 drag handle indicator */
    552         .modal-card.utility::before {
    553             content: '';
    554             position: absolute;
    555             top: 12px;
    556             left: 50%;
    557             transform: translateX(-50%);
    558             width: 32px;
    559             height: 4px;
    560             border-radius: 2px;
    561             background: var(--md-sys-color-outline-variant);
    562         }
    563 
    564         .modal-overlay.active .modal-card.utility {
    565             transform: translateY(0) scale(1);
    566         }
    567     }
    568 
    569     /* Logs view CSS */
    570     .logs-container { max-height: 150px; overflow-y: auto; background: var(--md-sys-color-surface-container); color: var(--md-sys-color-primary); font-family: monospace; font-size: 12px; padding: 12px; border-radius: 8px; margin-top: 8px; border: 1px solid var(--md-sys-color-outline-variant); }
    571     .logs-container .error { color: var(--md-sys-color-error); }
    572     .logs-container div { margin-bottom: 4px; }
    573     
    574     </style>
    575 </head>
    576 <body>
    577 
    578     <div id="offlineIndicator" class="offline-banner hidden">
    579         <span class="material-symbols-rounded" style="font-size: 16px;">cloud_off</span> You are offline
    580     </div>
    581 
    582     <!-- AUTH SCREEN -->
    583     <div id="auth-screen">
    584         <div class="auth-card">
    585             
    586             <!-- Dynamic Initial Loader -->
    587             <div id="authLoader" style="text-align: center; padding: 24px; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; min-height: 200px;">
    588                 <div class="spinner" style="display:block; position:static; width:36px; height:36px;"></div>
    589                 <p style="margin-top:20px; color:var(--md-sys-color-on-surface-variant); font-size: 15px; font-family:'Lexend', sans-serif;">Authenticating session...</p>
    590             </div>
    591 
    592             <!-- Login Fields (Hidden initially) -->
    593             <div id="authForm" class="hidden">
    594                 <div style="text-align: center;" class="anim-up delay-1">
    595                     <div class="brand-icon">
    596                         <img src="/assets/icon.svg" alt="App Logo">
    597                     </div>
    598                     <h1 style="font-family:'Lexend', sans-serif; font-size:28px; margin:8px 0; font-weight:400;">Welcome Back</h1>
    599                     <p style="color:var(--md-sys-color-on-surface-variant); font-size:14px;">Sign in to your chat log</p>
    600                 </div>
    601 
    602                 <div class="m3-field anim-up delay-2" style="margin-top: 12px;">
    603                     <input type="text" id="username" class="m3-input" placeholder=" ">
    604                     <label class="m3-label">Username</label>
    605                 </div>
    606 
    607                 <div class="m3-field anim-up delay-3" style="margin-top: 12px;">
    608                     <input type="password" id="password" class="m3-input" placeholder=" ">
    609                     <label class="m3-label">Password</label>
    610                     <button class="eye-toggle" onclick="togglePasswordVisibility('password', 'passIcon')">
    611                         <span class="material-symbols-rounded" id="passIcon">visibility</span>
    612                     </button>
    613                 </div>
    614 
    615                 <div class="anim-up delay-4" style="display:flex; align-items:center; padding-left:4px; margin-top: 12px;">
    616                     <div style="display:flex; align-items:center; gap:8px;">
    617                         <input type="checkbox" id="rememberMe" style="width:18px; height:18px; accent-color: var(--md-sys-color-primary);">
    618                         <label for="rememberMe" style="font-size: 14px; color: var(--md-sys-color-on-surface);">Remember me</label>
    619                     </div>
    620                 </div>
    621 
    622                 <div class="anim-up delay-5" style="display: flex; flex-direction: column; gap: 12px; margin-top: 16px;">
    623                     <button class="m3-btn filled" id="loginBtn" onclick="attemptLogin()">
    624                         <span class="btn-text">Sign In</span>
    625                         <span class="material-symbols-rounded btn-icon" style="font-size:20px;">arrow_forward</span>
    626                         <div class="spinner"></div>
    627                     </button>
    628                     <p id="authError" style="color: var(--md-sys-color-error); font-size: 14px; opacity: 0; text-align: center;">Invalid Credentials</p>
    629                 </div>
    630             </div>
    631 
    632         </div>
    633     </div>
    634 
    635     <!-- LOCK SCREEN -->
    636     <div id="lock-screen">
    637         <div id="lockoutUI" class="hidden lockout-container">
    638             <div class="lockout-icon-bg"><span class="material-symbols-rounded lockout-icon" id="lockoutIcon">lock_clock</span></div>
    639             <h2 class="lockout-title" id="lockoutTitle">Locked</h2>
    640             <p class="lockout-msg" id="lockoutMsg">Too many attempts.</p>
    641             <div class="lockout-timer" id="lockoutTimer">00:30</div>
    642             <p class="lockout-sub" id="lockoutSub">Try again later</p>
    643             <div id="permanentLockActions" class="hidden lockout-actions">
    644                  <button class="m3-btn filled" onclick="showForgotPin()" style="background:var(--md-sys-color-error); width:auto; padding:0 32px;">Forgot PIN</button>
    645             </div>
    646         </div>
    647 
    648         <div class="lock-header" id="lockHeader">
    649             <span class="material-symbols-rounded lock-icon" id="lockIcon">lock</span>
    650             <div class="lock-title" id="lockTitle">Enter PIN</div>
    651             <div class="lock-sub" id="lockSub">Protected by App Lock</div>
    652         </div>
    653         
    654         <div id="pinUI">
    655             <div class="pin-dots" id="pinDots">
    656                 <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div>
    657                 <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div>
    658             </div>
    659             <div class="keypad">
    660                 <button class="key-btn ripple-surface" onclick="pressKey(1)">1</button>
    661                 <button class="key-btn ripple-surface" onclick="pressKey(2)">2</button>
    662                 <button class="key-btn ripple-surface" onclick="pressKey(3)">3</button>
    663                 <button class="key-btn ripple-surface" onclick="pressKey(4)">4</button>
    664                 <button class="key-btn ripple-surface" onclick="pressKey(5)">5</button>
    665                 <button class="key-btn ripple-surface" onclick="pressKey(6)">6</button>
    666                 <button class="key-btn ripple-surface" onclick="pressKey(7)">7</button>
    667                 <button class="key-btn ripple-surface" onclick="pressKey(8)">8</button>
    668                 <button class="key-btn ripple-surface" onclick="pressKey(9)">9</button>
    669                 <button class="key-btn ripple-surface" style="visibility:hidden"> </button>
    670                 <button class="key-btn ripple-surface" onclick="pressKey(0)">0</button>
    671                 <button class="key-btn ripple-surface" onclick="pressKey(-1)"><span class="material-symbols-rounded key-backspace">backspace</span></button>
    672             </div>
    673         </div>
    674 
    675         <div id="bioUI" class="hidden" style="flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center;">
    676              <div class="ripple-surface" onclick="triggerBiometricAuth()" style="width:96px; height:96px; border-radius:50%; background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container); display:flex; align-items:center; justify-content:center; cursor:pointer;">
    677                 <span class="material-symbols-rounded" style="font-size:48px;">fingerprint</span>
    678             </div>
    679             <p style="margin-top:16px; color:var(--md-sys-color-on-surface-variant);">Tap to Unlock</p>
    680         </div>
    681 
    682         <div style="text-align: center;"><button class="forgot-pin-btn" onclick="showForgotPin()">Forgot PIN / Use Password</button></div>
    683     </div>
    684 
    685     <!-- MAIN APP -->
    686     <div id="app-layout">
    687         <aside class="sidebar" id="sidebar">
    688             <div class="sidebar-header">
    689                 <div style="display: flex; align-items: center; gap: 12px; padding: 12px 0;">
    690                     <h2 style="font-family: 'Lexend', sans-serif; font-size: 20px; font-weight: 500; color: var(--md-sys-color-on-surface); margin: 0;">Chat Log</h2>
    691                     <span style="font-family: 'Lexend', sans-serif; font-size: 12px; font-weight: 500; color: var(--md-sys-color-on-surface-variant); background-color: var(--md-sys-color-surface-container-high); padding: 2px 8px; border-radius: 100px; display: inline-flex; align-items: center;">v4.2.3</span>
    692                 </div>
    693                 <div style="display:flex; gap: 8px;">
    694                     <button class="icon-btn ripple-surface" onclick="refreshApp()"><span class="material-symbols-rounded">refresh</span></button>
    695                     <button class="icon-btn ripple-surface" onclick="openSettings()" title="Settings"><span class="material-symbols-rounded">settings</span></button>
    696                 </div>
    697             </div>
    698             <div class="search-container">
    699                 <div class="search-bar">
    700                     <span class="material-symbols-rounded" style="color: var(--md-sys-color-on-surface-variant);">search</span>
    701                     <input type="text" id="searchInput" placeholder="Search...">
    702                 </div>
    703             </div>
    704             <div class="contact-list" id="contactList"></div>
    705         </aside>
    706 
    707         <main class="chat-area" id="chatArea">
    708             <header class="chat-header">
    709                 <button class="icon-btn back-btn ripple-surface" onclick="closeChatMobile()"><span class="material-symbols-rounded">arrow_back</span></button>
    710                 <div id="chatHeaderContent" style="display:flex; align-items:center; gap:12px; flex:1; opacity:0;">
    711                     <div class="avatar" id="headerAvatar">A</div>
    712                     <div style="flex:1;">
    713                         <div id="headerName" style="font-weight: 500; font-size: 16px;">Alice</div>
    714                         <div id="headerId" style="font-size: 12px; opacity: 0.7;">Online</div>
    715                     </div>
    716                     <div class="menu-container">
    717                         <button class="icon-btn ripple-surface" onclick="toggleChatMenu(event)"><span class="material-symbols-rounded">more_vert</span></button>
    718                         <div class="dropdown-menu" id="chatMenu">
    719                             <button class="menu-item" onclick="toggleChatSearch()"><span class="material-symbols-rounded">search</span>Search in Chat</button>
    720                             <button class="menu-item" onclick="openDateSearch()"><span class="material-symbols-rounded">calendar_month</span>Search by Date</button>
    721                             <button class="menu-item" onclick="showRenameModal()"><span class="material-symbols-rounded">edit</span>Rename</button>
    722                             <button class="menu-item" onclick="downloadChat()"><span class="material-symbols-rounded">download</span>Download</button>
    723                         </div>
    724                     </div>
    725                 </div>
    726             </header>
    727             
    728             <div id="inChatSearch" class="in-chat-search hidden">
    729                 <input type="text" id="msgSearchInput" placeholder="Find message...">
    730                 <div id="searchCount" style="font-size:12px; margin-right:8px;"></div>
    731                 <button class="icon-btn" style="width:30px; height:30px;" onclick="toggleChatSearch()"><span class="material-symbols-rounded">close</span></button>
    732             </div>
    733 
    734             <div class="chat-bg-pattern"></div>
    735             
    736             <div class="chat-content" id="messagesContainer">
    737                 <div class="empty-state">
    738                     <span class="material-symbols-rounded" style="font-size: 64px; opacity: 0.2;">chat</span>
    739                     <p>Select a conversation</p>
    740                 </div>
    741             </div>
    742             
    743             <button class="scroll-fab" id="scrollFab" onclick="scrollToBottom('smooth')">
    744                 <span class="material-symbols-rounded">arrow_downward</span>
    745                 <span id="newMsgBadge" class="new-msg-badge hidden"></span>
    746             </button>
    747         </main>
    748     </div>
    749     
    750     <!-- Global Sync Indicator -->
    751     <div class="sync-spinner-wrapper" id="globalSyncSpinner">
    752         <div class="sync-chip">
    753             <div class="spinner"></div>
    754             <span>Syncing fresh messages...</span>
    755         </div>
    756     </div>
    757 
    758     <!-- MODALS SECTION -->
    759     <!-- SETTINGS SIDEBAR -->
    760     <div id="settingsSidebar" class="settings-sidebar">
    761         <div class="settings-header" style="height: 64px; padding: 0 16px; justify-content: flex-start; gap: 24px; background: var(--md-sys-color-surface-container-high);">
    762             <button class="icon-btn ripple-surface" onclick="closeSettingsSidebar()">
    763                 <span class="material-symbols-rounded">arrow_back</span>
    764             </button>
    765             <h2 style="font-family:'Lexend'; font-size: 20px; font-weight: 500; margin: 0;">Settings</h2>
    766         </div>
    767         
    768         <div class="settings-content">
    769             <div id="installAppSection" class="install-card hidden">
    770                 <div><h4>Install App</h4><p>Get the app for better experience</p></div>
    771                 <button class="m3-btn filled" onclick="installPWA()">Install</button>
    772             </div>
    773             
    774             <div class="setting-section hidden" id="mobileSecuritySection">
    775                 <div class="setting-title">App Security</div>
    776                 <div class="setting-row"><div class="setting-label"><h4>PIN Lock</h4><p>Require PIN on startup</p></div><label class="switch"><input type="checkbox" id="pinLockToggle" onchange="togglePinSetting()"><span class="slider"></span></label></div>
    777                 <div class="setting-row"><div class="setting-label"><h4>Fingerprint Lock</h4><p>Use biometrics</p></div><label class="switch"><input type="checkbox" id="bioLockToggle" onchange="toggleBioSetting()"><span class="slider"></span></label></div>
    778             </div>
    779             
    780             <div class="setting-section">
    781                 <div class="setting-title">Profile</div>
    782                 <div class="m3-field"><input type="text" id="profileName" class="m3-input" placeholder=" " onchange="saveProfile()"><label class="m3-label">Your Name</label></div>
    783                 <div class="m3-field"><input type="text" id="profilePhone" class="m3-input" placeholder=" " onchange="saveProfile()"><label class="m3-label">Phone Number</label></div>
    784             </div>
    785             
    786             <div class="setting-section">
    787                 <div class="setting-title">Appearance</div>
    788                 <div class="setting-row" style="display: block;">
    789                     <div class="setting-label" style="margin-bottom: 8px;">
    790                         <h4>Theme</h4>
    791                         <p>Select your preferred color palette</p>
    792                     </div>
    793                     <div class="select-wrapper">
    794                         <select id="themeSelector" class="m3-select" onchange="changeTheme(this.value)">
    795                             <option value="latte">Latte (Light)</option>
    796                             <option value="mocha">Mocha (Dark)</option>
    797                             <option value="black">Amoled (True Black)</option>
    798                         </select>
    799                     </div>
    800                 </div>
    801                 <div class="setting-row"><div class="setting-label"><h4>Text Size</h4><p>Adjust message font size</p></div><input type="range" min="12" max="20" value="15" oninput="changeFontSize(this.value)"></div>
    802             </div>
    803             
    804             <div class="setting-section">
    805                 <div class="setting-title">Storage and Data</div>
    806                 <div class="setting-row">
    807                     <div class="setting-label">
    808                         <h4>Local Storage Used</h4>
    809                         <p id="localStorageSizeDisplay">Calculating...</p>
    810                     </div>
    811                 </div>
    812                 <div class="setting-row">
    813                     <div class="setting-label">
    814                         <h4>Clear Cache</h4>
    815                         <p>Delete all messages, cached chats, and media records locally.</p>
    816                     </div>
    817                     <button class="m3-btn filled" style="background: var(--md-sys-color-error); width: auto; padding: 0 16px; height: 36px; font-size: 13px;" onclick="clearAppCacheData()">Clear</button>
    818                 </div>
    819             </div>
    820             
    821             <!-- SYSTEM LOGS SECTION -->
    822             <div class="setting-section" id="logsSection">
    823                 <div class="setting-title">System Diagnostics</div>
    824                 <div class="setting-row">
    825                     <div class="setting-label">
    826                         <h4>Live Server Logs</h4>
    827                         <p>Real-time events from your connected instance.</p>
    828                     </div>
    829                     <label class="switch"><input type="checkbox" id="logsToggle" onchange="toggleLogs()"><span class="slider"></span></label>
    830                 </div>
    831                 <div id="logsViewer" class="logs-container hidden"></div>
    832             </div>
    833 
    834             <div class="setting-section">
    835                 <div class="setting-title">Help & Support</div>
    836                 <div id="faqContainer"></div>
    837                 <div style="margin-top:24px; display:flex; gap:12px;">
    838                     <button class="m3-btn filled" style="background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container); cursor: pointer;" onclick="window.open('https://amit.is-a.dev/logger#faq', '_blank')">View FAQ</button>
    839                 </div>
    840                 <div style="margin-top:24px; display:flex; gap:12px;">
    841                     <button class="m3-btn filled" style="background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container); cursor: pointer;" onclick="window.open('https://amit.is-a.dev/logger', '_blank')">View Docs</button>
    842                 </div>
    843             </div>
    844             
    845             <button class="m3-btn filled" style="background:var(--md-sys-color-error); margin-top: 16px;" onclick="showLogoutModal()">Log Out</button>
    846         </div>
    847     </div>
    848 
    849     <!-- UTILITY MODALS -->
    850     <div id="logoutModal" class="modal-overlay">
    851         <div class="modal-card utility">
    852             <h3 class="modal-title">Log Out?</h3>
    853             <p style="margin:16px 0; color:var(--md-sys-color-on-surface-variant);">Session will end.</p>
    854             <div class="modal-actions">
    855                 <button class="m3-btn text" onclick="closeModal('logoutModal')">Cancel</button>
    856                 <button class="m3-btn filled" onclick="handleLogout()">Logout</button>
    857             </div>
    858         </div>
    859     </div>
    860     
    861     <div id="forgotPinModal" class="modal-overlay">
    862         <div class="modal-card utility">
    863             <h3 class="modal-title">Reset App Lock</h3>
    864             <p style="margin-top:8px; color:var(--md-sys-color-on-surface-variant); font-size:14px;">Enter your server credentials to reset local security.</p>
    865             <div class="m3-field" style="margin-top:16px;"><input type="text" id="resetUsername" class="m3-input" placeholder=" "><label class="m3-label">Username</label></div>
    866             <div class="m3-field"><input type="password" id="resetPassword" class="m3-input" placeholder=" "><label class="m3-label">Password</label><button class="eye-toggle" onclick="togglePasswordVisibility('resetPassword', 'resetPassIcon')"><span class="material-symbols-rounded" id="resetPassIcon">visibility</span></button></div>
    867             <p id="resetError" style="color:var(--md-sys-color-error); font-size:12px; margin-bottom:12px; display:none;">Invalid credentials</p>
    868             <div class="modal-actions"><button class="m3-btn text" onclick="closeModal('forgotPinModal')">Cancel</button><button class="m3-btn filled" id="resetBtn" onclick="attemptPinReset()"><span class="btn-text">Reset</span><div class="spinner"></div></button></div>
    869         </div>
    870     </div>
    871 
    872     <div id="renameModal" class="modal-overlay">
    873         <div class="modal-card utility">
    874             <h3 class="modal-title">Rename Chat</h3>
    875             <div class="m3-field" style="margin-top:16px;"><input type="text" id="renameInput" class="m3-input"><label class="m3-label">New Name</label></div>
    876             <div class="modal-actions"><button class="m3-btn text" onclick="closeModal('renameModal')">Cancel</button><button class="m3-btn filled" onclick="saveRename()">Save</button></div>
    877         </div>
    878     </div>
    879     
    880     <!-- APP OPTIONS MODAL -->
    881     <div id="appOptionsModal" class="modal-overlay">
    882         <div class="modal-card utility">
    883             <h3 class="modal-title">App Options</h3>
    884             <p style="margin:8px 0 16px; color:var(--md-sys-color-on-surface-variant); font-size:14px;">Choose how you want to refresh the app.</p>
    885             <div style="display:flex; flex-direction:column; gap:12px;">
    886                 <button class="m3-btn filled" onclick="executePageRefresh()">
    887                     <span class="material-symbols-rounded" style="font-size:20px;">refresh</span> Reload App
    888                 </button>
    889                 <button class="m3-btn text" style="color:var(--md-sys-color-error);" onclick="openHardReset()">
    890                     <span class="material-symbols-rounded" style="font-size:20px;">delete_forever</span> Hard Reset
    891                 </button>
    892                 <button class="m3-btn text" onclick="closeModal('appOptionsModal')">Cancel</button>
    893             </div>
    894         </div>
    895     </div>
    896 
    897     <!-- HARD REFRESH MODAL -->
    898     <div id="refreshModal" class="modal-overlay">
    899         <div class="modal-card utility">
    900             <h3 class="modal-title">System Sync</h3>
    901             
    902             <div id="refreshAuthStep">
    903                 <p style="margin-top:8px; color:var(--md-sys-color-on-surface-variant); font-size:14px;">Enter credentials to perform a full hard sync. This will clear the cache and pull all data.</p>
    904                 <div class="m3-field" style="margin-top:16px;">
    905                     <input type="text" id="refreshUsername" class="m3-input" placeholder=" "><label class="m3-label">Username</label>
    906                 </div>
    907                 <div class="m3-field">
    908                     <input type="password" id="refreshPassword" class="m3-input" placeholder=" ">
    909                     <label class="m3-label">Password</label>
    910                     <button class="eye-toggle" onclick="togglePasswordVisibility('refreshPassword', 'refreshPassIcon')"><span class="material-symbols-rounded" id="refreshPassIcon">visibility</span></button>
    911                 </div>
    912                 <p id="refreshError" style="color:var(--md-sys-color-error); font-size:12px; margin-bottom:12px; display:none;">Invalid credentials</p>
    913                 <div class="modal-actions">
    914                     <button class="m3-btn text" onclick="closeModal('refreshModal')">Cancel</button>
    915                     <button class="m3-btn filled" id="refreshBtn" onclick="startHardRefresh()">
    916                         <span class="btn-text">Sync</span><div class="spinner"></div>
    917                     </button>
    918                 </div>
    919             </div>
    920 
    921             <div id="refreshProgressStep" class="hidden" style="text-align:center; padding: 16px 0;">
    922                 <p id="refreshStatusText" style="font-size:14px; font-weight:500; margin-bottom:16px; color:var(--md-sys-color-on-surface);">Starting sync...</p>
    923                 <div style="width:100%; height:8px; background:var(--md-sys-color-surface-container-highest); border-radius:4px; overflow:hidden;">
    924                     <div id="refreshProgressBar" style="width:0%; height:100%; background:var(--md-sys-color-primary); transition:width 0.3s ease;"></div>
    925                 </div>
    926                 <p id="refreshPercentText" style="font-size:12px; color:var(--md-sys-color-on-surface-variant); margin-top:8px;">0%</p>
    927             </div>
    928         </div>
    929     </div>
    930 
    931     <div id="notFoundModal" class="modal-overlay">
    932         <div class="modal-card utility">
    933             <h3 class="modal-title">No Messages Found</h3>
    934             <p style="margin:16px 0; color:var(--md-sys-color-on-surface-variant);">There are no messages from this date onwards.</p>
    935             <div class="modal-actions"><button class="m3-btn filled" onclick="closeModal('notFoundModal')">OK</button></div>
    936         </div>
    937     </div>
    938     
    939     <div id="pinSetupModal" class="modal-overlay">
    940         <div class="modal-card utility" style="text-align:center;">
    941              <h3 class="modal-title" id="pinSetupTitle">Set 6-Digit PIN</h3>
    942              <div class="pin-dots" id="setupDots" style="margin: 24px 0;"><div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div></div>
    943             <p style="color:var(--md-sys-color-on-surface-variant); font-size:12px;" id="pinSetupMsg">Use the number pad below</p>
    944             <div class="keypad" style="transform: scale(0.9); margin-bottom:0;">
    945                 <button class="key-btn ripple-surface" onclick="pressSetupKey(1)">1</button><button class="key-btn ripple-surface" onclick="pressSetupKey(2)">2</button><button class="key-btn ripple-surface" onclick="pressSetupKey(3)">3</button>
    946                 <button class="key-btn ripple-surface" onclick="pressSetupKey(4)">4</button><button class="key-btn ripple-surface" onclick="pressSetupKey(5)">5</button><button class="key-btn ripple-surface" onclick="pressSetupKey(6)">6</button>
    947                 <button class="key-btn ripple-surface" onclick="pressSetupKey(7)">7</button><button class="key-btn ripple-surface" onclick="pressSetupKey(8)">8</button><button class="key-btn ripple-surface" onclick="pressSetupKey(9)">9</button>
    948                 <button class="key-btn ripple-surface" style="visibility:hidden"> </button><button class="key-btn ripple-surface" onclick="pressSetupKey(0)">0</button><button class="key-btn ripple-surface" onclick="pressSetupKey(-1)"><span class="material-symbols-rounded key-backspace">backspace</span></button>
    949             </div>
    950              <div class="modal-actions" style="justify-content:center; margin-top:8px;"><button class="m3-btn text" onclick="cancelPinSetup()">Cancel</button></div>
    951         </div>
    952     </div>
    953     
    954     <div id="calendarModal" class="modal-overlay">
    955         <div class="calendar-card animate-enter">
    956             <div class="cal-header"><h3>Select date</h3><h2 id="calSelectedDateStr">Tue, Oct 24</h2></div>
    957             <div class="cal-grid">
    958                 <div class="cal-nav">
    959                     <span id="calMonthYear" style="font-weight:500">October 2023</span>
    960                     <div style="display:flex; gap:12px;"><button class="icon-btn" onclick="changeCalMonth(-1)"><span class="material-symbols-rounded">chevron_left</span></button><button class="icon-btn" onclick="changeCalMonth(1)"><span class="material-symbols-rounded">chevron_right</span></button></div>
    961                 </div>
    962                 <div class="cal-days"><span class="cal-day-label">S</span><span class="cal-day-label">M</span><span class="cal-day-label">T</span><span class="cal-day-label">W</span><span class="cal-day-label">T</span><span class="cal-day-label">F</span><span class="cal-day-label">S</span></div>
    963                 <div class="cal-dates" id="calDatesGrid"></div>
    964             </div>
    965             <div class="modal-actions" style="padding: 16px 24px;"><button class="m3-btn text" onclick="closeModal('calendarModal')">Cancel</button><button class="m3-btn filled" onclick="jumpToDate()" style="width:auto; padding:0 24px; height:40px;">OK</button></div>
    966         </div>
    967     </div>
    968 
    969     <div id="installSheet" class="install-sheet">
    970         <div class="sheet-header"><h3 style="font-family:'Lexend', sans-serif; margin: 0;">Install App</h3><button class="icon-btn" onclick="closeInstallSheet()"><span class="material-symbols-rounded">close</span></button></div>
    971         <div class="sheet-content">
    972             <div style="width:64px; height:64px; margin:0 auto 16px; display:flex; align-items:center; justify-content:center;"><img src="/assets/icon.svg" alt="App Logo" style="width:100%; height:100%; object-fit: contain;"></div>
    973             <p style="color:var(--md-sys-color-on-surface-variant); margin-bottom:24px;">Install WhatsApp Logger App for a better experience, full screen view, and enhanced security.</p>
    974             <div class="sheet-actions"><button class="m3-btn filled" onclick="installPWA()">Install App</button><button class="m3-btn text" onclick="closeInstallSheet()">Continue in Browser</button></div>
    975         </div>
    976     </div>
    977 
    978     <!-- MAIN APP SCRIPT -->
    979     <script>
    980         // Catch global errors to prevent silent forever-spinners
    981         window.onerror = function(msg, url, line) {
    982             const loader = document.getElementById('authLoader');
    983             if(loader && !loader.classList.contains('hidden')) {
    984                 loader.innerHTML = `<div style="color:var(--md-sys-color-error); font-size:13px; text-align:left; padding:16px; border: 1px solid var(--md-sys-color-error); border-radius: 8px;">
    985                     <strong>System Error:</strong><br>${msg}<br>Line: ${line}
    986                 </div>`;
    987             }
    988         };
    989 
    990         const RENDER_BACKEND_URL = ""; 
    991         
    992         let activeChatId = null;
    993         let activeChatIds = [];
    994         let isCurrentChatGroup = false;
    995         let deferredPrompt;
    996         let currentMessages = [];
    997         let rawChatsMap = new Map(); 
    998         
    999         // --- SSE CONNECTION STATE ---
   1000         let chatEventSource = null;
   1001         let chatEventRetryCount = 0;
   1002         
   1003         let syncEventSource = null;
   1004         let syncEventRetryCount = 0;
   1005 
   1006         // --- INDEXED DB CACHING ---
   1007         const DB_NAME = 'WPLoggerDB';
   1008         const DB_VERSION = 2; 
   1009         let dbInstance = null;
   1010         let isIdbSupported = true;
   1011         let maxCachedTimestamp = 0;
   1012 
   1013         function initIndexedDB() {
   1014             return new Promise((resolve) => {
   1015                 let idb;
   1016                 try {
   1017                     idb = window.indexedDB;
   1018                 } catch (e) {
   1019                     console.warn("IDB Access Denied:", e);
   1020                     isIdbSupported = false;
   1021                     resolve();
   1022                     return;
   1023                 }
   1024 
   1025                 if (!idb) {
   1026                     isIdbSupported = false;
   1027                     resolve();
   1028                     return;
   1029                 }
   1030 
   1031                 let isResolved = false;
   1032                 const safeResolve = () => {
   1033                     if(!isResolved) {
   1034                         isResolved = true;
   1035                         resolve();
   1036                     }
   1037                 };
   1038 
   1039                 const fallbackTimeout = setTimeout(() => {
   1040                     console.warn('IDB init timed out');
   1041                     isIdbSupported = false;
   1042                     safeResolve();
   1043                 }, 3000);
   1044 
   1045                 try {
   1046                     const req = indexedDB.open(DB_NAME, DB_VERSION);
   1047                     req.onupgradeneeded = (e) => {
   1048                         const database = e.target.result;
   1049                         if (!database.objectStoreNames.contains('messages')) {
   1050                             const store = database.createObjectStore('messages', { keyPath: 'id' });
   1051                             store.createIndex('chatId', 'chatId', { unique: false });
   1052                             store.createIndex('timestamp', 'timestamp', { unique: false });
   1053                         }
   1054                         if (!database.objectStoreNames.contains('chats')) {
   1055                             database.createObjectStore('chats', { keyPath: 'id' });
   1056                         }
   1057                     };
   1058                     req.onsuccess = (e) => { 
   1059                         clearTimeout(fallbackTimeout);
   1060                         dbInstance = e.target.result; 
   1061                         
   1062                         dbInstance.onversionchange = () => {
   1063                             dbInstance.close();
   1064                             dbInstance = null;
   1065                         };
   1066                         
   1067                         safeResolve(); 
   1068                     };
   1069                     req.onerror = (e) => { 
   1070                         clearTimeout(fallbackTimeout);
   1071                         console.warn('IDB init error', e); 
   1072                         isIdbSupported = false; 
   1073                         safeResolve(); 
   1074                     };
   1075                     req.onblocked = (e) => {
   1076                         console.warn('IDB init blocked. Waiting for other connections to close...');
   1077                     };
   1078                 } catch (e) {
   1079                     clearTimeout(fallbackTimeout);
   1080                     console.warn('IDB open exception', e);
   1081                     isIdbSupported = false;
   1082                     safeResolve();
   1083                 }
   1084             });
   1085         }
   1086 
   1087         async function getCachedMessages(chatId) {
   1088             if(!isIdbSupported || !dbInstance) return [];
   1089             return new Promise((resolve) => {
   1090                 try {
   1091                     const tx = dbInstance.transaction('messages', 'readonly');
   1092                     const store = tx.objectStore('messages');
   1093                     const index = store.index('chatId');
   1094                     const req = index.getAll(chatId);
   1095                     req.onsuccess = () => resolve(req.result || []);
   1096                     req.onerror = () => resolve([]);
   1097                 } catch(e) { resolve([]); }
   1098             });
   1099         }
   1100         
   1101         async function getMaxCachedTimestamp() {
   1102             if(!isIdbSupported || !dbInstance) return 0;
   1103             return new Promise((resolve) => {
   1104                 try {
   1105                     const tx = dbInstance.transaction('messages', 'readonly');
   1106                     const store = tx.objectStore('messages');
   1107                     const index = store.index('timestamp');
   1108                     // Open a cursor going backwards to get the absolute highest timestamp stored
   1109                     const req = index.openCursor(null, 'prev');
   1110                     req.onsuccess = (e) => {
   1111                         const cursor = e.target.result;
   1112                         if (cursor) resolve(cursor.key);
   1113                         else resolve(0);
   1114                     };
   1115                     req.onerror = () => resolve(0);
   1116                 } catch(e) { resolve(0); }
   1117             });
   1118         }
   1119 
   1120         async function saveCachedMessages(messages) {
   1121             if(!isIdbSupported || !dbInstance) return;
   1122             return new Promise((resolve) => {
   1123                 try {
   1124                     const tx = dbInstance.transaction('messages', 'readwrite');
   1125                     const store = tx.objectStore('messages');
   1126                     messages.forEach(m => store.put(m));
   1127                     tx.oncomplete = () => resolve();
   1128                     tx.onerror = () => resolve();
   1129                 } catch(e) { resolve(); }
   1130             });
   1131         }
   1132 
   1133         async function getCachedChats() {
   1134             if(!isIdbSupported || !dbInstance) return [];
   1135             return new Promise((resolve) => {
   1136                 try {
   1137                     const tx = dbInstance.transaction('chats', 'readonly');
   1138                     const store = tx.objectStore('chats');
   1139                     const req = store.getAll();
   1140                     req.onsuccess = () => resolve(req.result || []);
   1141                     req.onerror = () => resolve([]);
   1142                 } catch(e) { resolve([]); }
   1143             });
   1144         }
   1145 
   1146         async function saveCachedChats(chats) {
   1147             if(!isIdbSupported || !dbInstance) return;
   1148             return new Promise((resolve) => {
   1149                 try {
   1150                     const tx = dbInstance.transaction('chats', 'readwrite');
   1151                     const store = tx.objectStore('chats');
   1152                     chats.forEach(c => store.put(c));
   1153                     tx.oncomplete = () => resolve();
   1154                     tx.onerror = () => resolve();
   1155                 } catch(e) { resolve(); }
   1156             });
   1157         }
   1158         
   1159         // --- SECURITY VARS ---
   1160         let enteredPin = ""; let setupPin = ""; let setupStep = 0; let isLocked = false;
   1161         let failAttempts = parseInt(localStorage.getItem('wp_fail_attempts') || '0');
   1162         let lockUntil = parseInt(localStorage.getItem('wp_lock_until') || '0');
   1163         let lockoutTimerInterval;
   1164         let calCurrentDate = new Date(); let calSelectedDate = new Date();
   1165 
   1166         const arrayBufferToBase64 = (buffer) => {
   1167             let binary = ''; const bytes = new Uint8Array(buffer); const len = bytes.byteLength;
   1168             for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
   1169             return window.btoa(binary);
   1170         };
   1171         const base64ToArrayBuffer = (base64) => {
   1172             const binary_string = window.atob(base64); const len = binary_string.length; const bytes = new Uint8Array(len);
   1173             for (let i = 0; i < len; i++) bytes[i] = binary_string.charCodeAt(i);
   1174             return bytes.buffer;
   1175         };
   1176         
   1177         function formatDividerDate(ts) {
   1178             const date = new Date(ts * 1000); const today = new Date(); const yesterday = new Date();
   1179             yesterday.setDate(yesterday.getDate() - 1);
   1180             if (date.toDateString() === today.toDateString()) return "Today";
   1181             if (date.toDateString() === yesterday.toDateString()) return "Yesterday";
   1182             return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
   1183         }
   1184 
   1185         function formatLastActive(ts) {
   1186             if (!ts) return 'Unknown';
   1187             const date = new Date(ts * 1000);
   1188             const today = new Date();
   1189             const yesterday = new Date();
   1190             yesterday.setDate(yesterday.getDate() - 1);
   1191             
   1192             const timeOpts = { hour: '2-digit', minute: '2-digit' };
   1193             if (date.toDateString() === today.toDateString()) {
   1194                 return `Today at ${date.toLocaleTimeString([], timeOpts)}`;
   1195             } else if (date.toDateString() === yesterday.toDateString()) {
   1196                 return `Yesterday at ${date.toLocaleTimeString([], timeOpts)}`;
   1197             }
   1198             return `${date.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' })} at ${date.toLocaleTimeString([], timeOpts)}`;
   1199         }
   1200 
   1201         // HTML Sanitizer to prevent Code Injection via chat messages
   1202         function escapeHTML(str) {
   1203             if (!str) return '';
   1204             return str.replace(/[&<>'"]/g, 
   1205                 tag => ({
   1206                     '&': '&amp;',
   1207                     '<': '&lt;',
   1208                     '>': '&gt;',
   1209                     "'": '&#39;',
   1210                     '"': '&quot;'
   1211                 }[tag] || tag)
   1212             );
   1213         }
   1214 
   1215         // --- PWA INSTALLATION ---
   1216         if ('serviceWorker' in navigator && (window.location.protocol === 'https:' || window.location.protocol === 'http:')) {
   1217             navigator.serviceWorker.register('/assets/sw-wpChat.js').catch(err => {
   1218                 console.warn('SW Register Fail:', err);
   1219             });
   1220             window.addEventListener('online', () => {
   1221                 if (navigator.serviceWorker.controller) navigator.serviceWorker.controller.postMessage('sync_on_reconnect');
   1222             });
   1223         }
   1224         
   1225         window.addEventListener('beforeinstallprompt', (e) => {
   1226             e.preventDefault(); deferredPrompt = e;
   1227             document.getElementById('installAppSection').classList.remove('hidden');
   1228             if (window.innerWidth <= 768) {
   1229                 setTimeout(() => document.getElementById('installSheet').classList.add('visible'), 2000);
   1230             }
   1231         });
   1232         
   1233         async function installPWA() {
   1234             if (!deferredPrompt) return;
   1235             deferredPrompt.prompt();
   1236             const { outcome } = await deferredPrompt.userChoice;
   1237             if (outcome === 'accepted') {
   1238                 closeInstallSheet();
   1239                 document.getElementById('installAppSection').classList.add('hidden');
   1240             }
   1241             deferredPrompt = null;
   1242         }
   1243 
   1244         function closeInstallSheet() { document.getElementById('installSheet').classList.remove('visible'); }
   1245 
   1246         // --- BOOT SEQUENCE ---
   1247         function isMobileApp() {
   1248             const isStandalone = (window.matchMedia('(display-mode: standalone)').matches) || (window.navigator.standalone) || document.referrer.includes('android-app://');
   1249             return isStandalone && window.innerWidth <= 768;
   1250         }
   1251 
   1252         window.onload = async () => {
   1253             try {
   1254                 await initIndexedDB();
   1255 
   1256                 if (!navigator.onLine) {
   1257                     document.getElementById('offlineIndicator').classList.remove('hidden');
   1258                 }
   1259                 window.addEventListener('online', () => {
   1260                     document.getElementById('offlineIndicator').classList.add('hidden');
   1261                 });
   1262                 window.addEventListener('offline', () => {
   1263                     document.getElementById('offlineIndicator').classList.remove('hidden');
   1264                 });
   1265 
   1266                 // Apply saved theme
   1267                 const savedTheme = localStorage.getItem('theme') || 'latte';
   1268                 document.body.setAttribute('data-theme', savedTheme);
   1269                 const themeSelector = document.getElementById('themeSelector');
   1270                 if(themeSelector) themeSelector.value = savedTheme;
   1271                 
   1272                 document.getElementById('profileName').value = localStorage.getItem('profileName') || '';
   1273                 document.getElementById('profilePhone').value = localStorage.getItem('profilePhone') || '';
   1274                 
   1275                 populateFAQ();
   1276 
   1277                 const authValid = localStorage.getItem('wp_api_token') && localStorage.getItem('wp_auth_expiry') > Date.now();
   1278                 
   1279                 if (authValid) {
   1280                     document.getElementById('auth-screen').style.opacity = '0';
   1281                     
   1282                     setTimeout(() => {
   1283                         document.getElementById('auth-screen').style.display = 'none';
   1284                         if (isMobileApp()) {
   1285                             const hasPin = localStorage.getItem('wp_app_pin');
   1286                             const useBio = localStorage.getItem('wp_app_bio') === 'true';
   1287                             document.getElementById('mobileSecuritySection').classList.remove('hidden');
   1288                             document.getElementById('pinLockToggle').checked = !!hasPin;
   1289                             document.getElementById('bioLockToggle').checked = useBio;
   1290                             
   1291                             if (hasPin || useBio) {
   1292                                 initLockScreen(hasPin, useBio);
   1293                             } else {
   1294                                 document.getElementById('app-layout').classList.add('visible');
   1295                                 startAppSync();
   1296                             }
   1297                         } else {
   1298                             document.getElementById('app-layout').classList.add('visible');
   1299                             startAppSync();
   1300                         }
   1301                     }, 400); 
   1302                 } else {
   1303                     document.getElementById('authLoader').classList.add('hidden');
   1304                     document.getElementById('authForm').classList.remove('hidden');
   1305                     document.getElementById('authForm').classList.add('fade-in');
   1306                     
   1307                     if (isMobileApp()) {
   1308                         document.getElementById('mobileSecuritySection').classList.remove('hidden');
   1309                     }
   1310                 }
   1311                 
   1312                 document.getElementById('messagesContainer').addEventListener('scroll', function() {
   1313                     const fab = document.getElementById('scrollFab');
   1314                     if (this.scrollHeight - this.scrollTop - this.clientHeight > 500) {
   1315                         fab.classList.add('visible');
   1316                     } else {
   1317                         fab.classList.remove('visible');
   1318                         document.getElementById('newMsgBadge').classList.add('hidden');
   1319                     }
   1320                 });
   1321 
   1322                 document.querySelectorAll('input[type="text"], input[type="password"]').forEach(input => {
   1323                     input.addEventListener('focus', () => {
   1324                         if (window.innerWidth <= 768) {
   1325                             setTimeout(() => input.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300);
   1326                         }
   1327                     });
   1328                 });
   1329             } catch (err) {
   1330                 console.error("Critical Boot Error:", err);
   1331                 const loader = document.getElementById('authLoader');
   1332                 if (loader) {
   1333                     loader.innerHTML = `<div style="color:var(--md-sys-color-error); padding: 16px; text-align: center;">
   1334                         <span class="material-symbols-rounded" style="font-size: 32px; margin-bottom: 8px;">error</span>
   1335                         <p style="font-size: 14px; font-weight: 500;">App failed to load.</p>
   1336                         <p style="font-size: 12px; opacity: 0.8; margin-top: 4px;">${err.message}</p>
   1337                     </div>`;
   1338                 }
   1339             }
   1340         };
   1341         
   1342         async function startAppSync() {
   1343             // Initiate the two main SSE streams
   1344             loadChats();
   1345             connectGlobalSyncStream();
   1346         }
   1347 
   1348         // --- LOCK SCREEN LOGIC ---
   1349         function initLockScreen(hasPin, useBio) {
   1350             const lockScreen = document.getElementById('lock-screen');
   1351             lockScreen.classList.add('active'); isLocked = true; enteredPin = "";
   1352             updatePinDots(0);
   1353             if (checkLockoutState()) return;
   1354 
   1355             if(hasPin) {
   1356                 document.getElementById('pinUI').classList.remove('hidden'); document.getElementById('bioUI').classList.add('hidden');
   1357                 document.getElementById('lockTitle').innerText = "Enter PIN"; document.getElementById('lockIcon').innerText = "lock";
   1358             } else if (useBio && !hasPin) {
   1359                 document.getElementById('pinUI').classList.add('hidden'); document.getElementById('bioUI').classList.remove('hidden');
   1360                 document.getElementById('lockTitle').innerText = "Scan Fingerprint"; document.getElementById('lockIcon').innerText = "fingerprint";
   1361                 triggerBiometricAuth();
   1362             }
   1363         }
   1364 
   1365         function checkLockoutState() {
   1366             const now = Date.now();
   1367             if (failAttempts >= 5) { renderPermanentLock(); return true; }
   1368             if (lockUntil > now) { startLockoutTimer(); return true; }
   1369             if (lockUntil > 0 && lockUntil <= now) {
   1370                 lockUntil = 0; localStorage.setItem('wp_lock_until', '0');
   1371                 document.getElementById('lockoutUI').classList.add('hidden');
   1372                 document.getElementById('lockHeader').classList.remove('hidden');
   1373                 document.getElementById('lock-screen').classList.remove('locked-mode');
   1374             }
   1375             return false;
   1376         }
   1377 
   1378         function handleFailedAttempt() {
   1379             failAttempts++; localStorage.setItem('wp_fail_attempts', failAttempts);
   1380             if (failAttempts === 3 || failAttempts === 4) {
   1381                 lockUntil = Date.now() + (30 * 1000); localStorage.setItem('wp_lock_until', lockUntil);
   1382                 startLockoutTimer();
   1383             } else if (failAttempts >= 5) {
   1384                 renderPermanentLock();
   1385             } else {
   1386                 document.getElementById('pinDots').classList.add('shake'); updatePinDots(6, true);
   1387                 setTimeout(() => { document.getElementById('pinDots').classList.remove('shake'); enteredPin = ""; updatePinDots(0); }, 400);
   1388             }
   1389         }
   1390 
   1391         function startLockoutTimer() {
   1392             const ui = document.getElementById('lockoutUI'); const screen = document.getElementById('lock-screen'); const timerDisplay = document.getElementById('lockoutTimer');
   1393             ui.classList.remove('hidden'); document.getElementById('lockHeader').classList.add('hidden'); screen.classList.add('locked-mode'); 
   1394             document.getElementById('lockoutTitle').innerText = "Locked"; document.getElementById('lockoutMsg').innerText = `Too many attempts (${failAttempts})`;
   1395             document.getElementById('lockoutSub').innerText = "Try again later"; document.getElementById('permanentLockActions').classList.add('hidden');
   1396             document.getElementById('lockoutTimer').classList.remove('hidden');
   1397             if(lockoutTimerInterval) clearInterval(lockoutTimerInterval);
   1398             
   1399             lockoutTimerInterval = setInterval(() => {
   1400                 const left = Math.ceil((lockUntil - Date.now()) / 1000);
   1401                 if (left <= 0) {
   1402                     clearInterval(lockoutTimerInterval); timerDisplay.innerText = "00:00";
   1403                     initLockScreen(localStorage.getItem('wp_app_pin'), localStorage.getItem('wp_app_bio') === 'true');
   1404                     document.getElementById('lockoutUI').classList.add('hidden'); document.getElementById('lockHeader').classList.remove('hidden');
   1405                     screen.classList.remove('locked-mode');
   1406                 } else {
   1407                     timerDisplay.innerText = `00:${left < 10 ? '0'+left : left}`;
   1408                 }
   1409             }, 1000);
   1410         }
   1411 
   1412         function renderPermanentLock() {
   1413             document.getElementById('lockoutUI').classList.remove('hidden');
   1414             document.getElementById('lockHeader').classList.add('hidden');
   1415             document.getElementById('lock-screen').classList.add('locked-mode');
   1416             document.getElementById('lockoutIcon').innerText = "gpp_bad";
   1417             document.getElementById('lockoutTitle').innerText = "Security Lockout";
   1418             document.getElementById('lockoutMsg').innerText = "Maximum attempts exceeded. App is locked.";
   1419             document.getElementById('lockoutSub').innerText = "Use Forgot Password to reset.";
   1420             document.getElementById('lockoutTimer').classList.add('hidden');
   1421             document.getElementById('permanentLockActions').classList.remove('hidden');
   1422         }
   1423 
   1424         function pressKey(num) {
   1425             if(num === -1) enteredPin = enteredPin.slice(0, -1);
   1426             else if (enteredPin.length < 6) enteredPin += num;
   1427             updatePinDots(enteredPin.length);
   1428             if(enteredPin.length === 6) verifyPin();
   1429         }
   1430 
   1431         function updatePinDots(count, isError=false) {
   1432             document.querySelectorAll('#pinDots .pin-dot').forEach((d, i) => {
   1433                 if(isError) { d.classList.add('error'); setTimeout(() => d.classList.remove('error'), 400); } 
   1434                 else { d.classList.remove('error'); if(i < count) d.classList.add('filled'); else d.classList.remove('filled'); }
   1435             });
   1436         }
   1437 
   1438         function verifyPin() {
   1439             if(checkLockoutState()) return;
   1440             const storedPin = localStorage.getItem('wp_app_pin');
   1441             if(storedPin && enteredPin === storedPin) {
   1442                 failAttempts = 0; localStorage.setItem('wp_fail_attempts', '0'); localStorage.setItem('wp_lock_until', '0');
   1443                 if (localStorage.getItem('wp_app_bio') === 'true') {
   1444                     document.getElementById('pinUI').classList.add('hidden'); document.getElementById('bioUI').classList.remove('hidden');
   1445                     document.getElementById('lockTitle').innerText = "Verify Identity"; document.getElementById('lockIcon').innerText = "fingerprint";
   1446                     triggerBiometricAuth();
   1447                 } else unlockApp();
   1448             } else handleFailedAttempt();
   1449         }
   1450 
   1451         async function triggerBiometricAuth() {
   1452             if(checkLockoutState()) return;
   1453             if(!document.getElementById('bioUI').classList.contains('hidden')) document.getElementById('lockTitle').innerText = "Verify Identity";
   1454             try {
   1455                 const storedId = localStorage.getItem('wp_bio_id');
   1456                 const options = { publicKey: { challenge: new Uint8Array([1, 2, 3, 4]), timeout: 60000, userVerification: "required" } };
   1457                 if (storedId) options.publicKey.allowCredentials = [{ type: 'public-key', id: base64ToArrayBuffer(storedId), transports: ['internal'] }];
   1458                 const credential = await navigator.credentials.get(options);
   1459                 if(credential) { failAttempts = 0; localStorage.setItem('wp_fail_attempts', '0'); localStorage.setItem('wp_lock_until', '0'); unlockApp(); }
   1460             } catch (e) { console.warn("Bio failed", e); }
   1461         }
   1462 
   1463         function unlockApp() {
   1464             isLocked = false;
   1465             document.getElementById('lock-screen').classList.remove('active');
   1466             document.getElementById('app-layout').classList.add('visible');
   1467             startAppSync();
   1468         }
   1469 
   1470         // --- SECURITY SETTINGS ---
   1471         function togglePinSetting() {
   1472             if(document.getElementById('pinLockToggle').checked) openPinSetup(); else localStorage.removeItem('wp_app_pin');
   1473         }
   1474 
   1475         function toggleBioSetting() {
   1476             if(document.getElementById('bioLockToggle').checked) registerBiometric();
   1477             else { localStorage.removeItem('wp_app_bio'); localStorage.removeItem('wp_bio_id'); }
   1478         }
   1479 
   1480         function openPinSetup() {
   1481             document.getElementById('pinSetupModal').classList.add('active');
   1482             setupStep = 1; setupPin = ""; enteredPin = ""; updateSetupDots(0);
   1483             document.getElementById('pinSetupTitle').innerText = "Set 6-Digit PIN";
   1484             document.getElementById('pinSetupMsg').innerText = "Enter a new PIN";
   1485         }
   1486         
   1487         function updateSetupDots(count) {
   1488             document.querySelectorAll('#setupDots .pin-dot').forEach((d, i) => i < count ? d.classList.add('filled') : d.classList.remove('filled'));
   1489         }
   1490 
   1491         function pressSetupKey(num) {
   1492             if(num === -1) enteredPin = enteredPin.slice(0, -1);
   1493             else if (enteredPin.length < 6) enteredPin += num;
   1494             updateSetupDots(enteredPin.length);
   1495 
   1496             if(enteredPin.length === 6) {
   1497                 if(setupStep === 1) {
   1498                     setupPin = enteredPin; enteredPin = ""; setupStep = 2;
   1499                     setTimeout(() => { updateSetupDots(0); document.getElementById('pinSetupTitle').innerText = "Confirm PIN"; document.getElementById('pinSetupMsg').innerText = "Re-enter to confirm"; }, 200);
   1500                 } else if (setupStep === 2) {
   1501                     if(enteredPin === setupPin) {
   1502                         localStorage.setItem('wp_app_pin', setupPin);
   1503                         closeModal('pinSetupModal'); enteredPin = "";
   1504                     } else {
   1505                         const dots = document.getElementById('setupDots'); dots.classList.add('shake');
   1506                         setTimeout(() => { dots.classList.remove('shake'); enteredPin = ""; setupPin = ""; setupStep = 1; updateSetupDots(0); document.getElementById('pinSetupTitle').innerText = "Mismatch. Try Again"; document.getElementById('pinSetupMsg').innerText = "Enter a new PIN"; }, 500);
   1507                     }
   1508                 }
   1509             }
   1510         }
   1511 
   1512         function cancelPinSetup() { document.getElementById('pinLockToggle').checked = false; closeModal('pinSetupModal'); }
   1513 
   1514         async function registerBiometric() {
   1515             try {
   1516                 const iconPath = new URL('/assets/icon.svg', window.location.href).toString();
   1517                 const credential = await navigator.credentials.create({
   1518                     publicKey: { challenge: new Uint8Array([1, 2, 3, 4]), rp: { name: "WhatsApp Logger", icon: iconPath }, user: { id: new Uint8Array([1]), name: "User", displayName: "App User" }, pubKeyCredParams: [{type: "public-key", alg: -7}], authenticatorSelection: { authenticatorAttachment: "platform", userVerification: "required" }, timeout: 60000 }
   1519                 });
   1520                 
   1521                 if(credential) {
   1522                     localStorage.setItem('wp_bio_id', arrayBufferToBase64(credential.rawId));
   1523                     localStorage.setItem('wp_app_bio', 'true');
   1524                 } else throw new Error();
   1525             } catch (e) { document.getElementById('bioLockToggle').checked = false; }
   1526         }
   1527 
   1528         function showForgotPin() { document.getElementById('forgotPinModal').classList.add('active'); }
   1529         
   1530         async function attemptPinReset() {
   1531              const btn = document.getElementById('resetBtn'); const err = document.getElementById('resetError');
   1532              btn.classList.add('loading'); btn.disabled = true; err.style.display = 'none';
   1533              
   1534              try {
   1535                 const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, {
   1536                     method: 'POST', headers: {'Content-Type': 'application/json'},
   1537                     body: JSON.stringify({ username: document.getElementById('resetUsername').value, password: document.getElementById('resetPassword').value })
   1538                 });
   1539                 const data = await response.json();
   1540                 
   1541                 if(data.success) {
   1542                     ['wp_app_pin','wp_app_bio','wp_bio_id','wp_fail_attempts','wp_lock_until'].forEach(k => localStorage.removeItem(k));
   1543                     failAttempts = 0; lockUntil = 0;
   1544                     document.getElementById('pinLockToggle').checked = false; document.getElementById('bioLockToggle').checked = false;
   1545                     document.getElementById('lockoutUI').classList.add('hidden'); document.getElementById('lockHeader').classList.remove('hidden');
   1546                     document.getElementById('lock-screen').classList.remove('locked-mode');
   1547                     closeModal('forgotPinModal'); unlockApp();
   1548                 } else throw new Error();
   1549              } catch (e) { err.style.display = 'block'; } finally { btn.classList.remove('loading'); btn.disabled = false; }
   1550         }
   1551 
   1552         function togglePasswordVisibility(id, iconId) {
   1553             const input = document.getElementById(id); const icon = document.getElementById(iconId);
   1554             if (input.type === 'password') { input.type = 'text'; icon.innerText = 'visibility_off'; } 
   1555             else { input.type = 'password'; icon.innerText = 'visibility'; }
   1556         }
   1557 
   1558         async function attemptLogin() {
   1559             const btn = document.getElementById('loginBtn'); const err = document.getElementById('authError');
   1560             btn.classList.add('loading'); btn.disabled = true; err.style.opacity = '0';
   1561             try {
   1562                 const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, {
   1563                     method: 'POST', headers: {'Content-Type': 'application/json'},
   1564                     body: JSON.stringify({ username: document.getElementById('username').value, password: document.getElementById('password').value })
   1565                 });
   1566                 const data = await response.json();
   1567                 if(data.success && data.token) {
   1568                     localStorage.setItem('wp_api_token', data.token);
   1569                     
   1570                     if(document.getElementById('rememberMe').checked) localStorage.setItem('wp_auth_expiry', Date.now() + (3 * 24 * 60 * 60 * 1000));
   1571                     else localStorage.setItem('wp_auth_expiry', Date.now() + (1 * 60 * 60 * 1000));
   1572                     
   1573                     revealApp();
   1574                 } else throw new Error();
   1575             } catch (e) { err.style.opacity = '1'; btn.classList.remove('loading'); btn.disabled = false; }
   1576         }
   1577 
   1578         function revealApp() {
   1579              document.getElementById('auth-screen').style.opacity = '0';
   1580              setTimeout(() => {
   1581                  document.getElementById('auth-screen').style.display = 'none';
   1582                  document.getElementById('app-layout').classList.add('visible'); 
   1583                  startAppSync();
   1584              }, 400);
   1585         }
   1586         
   1587         function openSettings() { 
   1588             document.getElementById('settingsSidebar').classList.add('active'); 
   1589             calculateLocalStorageUsage();
   1590         }
   1591 
   1592         function closeSettingsSidebar() {
   1593             document.getElementById('settingsSidebar').classList.remove('active');
   1594         }
   1595 
   1596         async function clearAppCacheData() {
   1597             if (confirm("Are you absolutely sure you want to clear all cached data? This will clear your offline cache databases and instantly reload the workspace application.")) {
   1598                 closeSettingsSidebar();
   1599                 document.getElementById('refreshModal').classList.add('active');
   1600                 document.getElementById('refreshAuthStep').classList.add('hidden');
   1601                 document.getElementById('refreshProgressStep').classList.remove('hidden');
   1602                 
   1603                 await executeHardRefresh();
   1604             }
   1605         }
   1606         function closeModal(id) { document.getElementById(id).classList.remove('active'); }
   1607         
   1608         function changeTheme(themeName) {
   1609             document.body.setAttribute('data-theme', themeName);
   1610             localStorage.setItem('theme', themeName);
   1611         }
   1612 
   1613         function changeFontSize(val) { document.documentElement.style.setProperty('--base-font-size', `${val}px`); }
   1614         
   1615         function saveProfile() { localStorage.setItem('profileName', document.getElementById('profileName').value); localStorage.setItem('profilePhone', document.getElementById('profilePhone').value); }
   1616         function populateFAQ() {
   1617             const faqs = [
   1618                 {q: "Is this secure?", a: "Yes, data is secured on your own custom backend server and access is guarded by your API Token."},
   1619                 {q: "Does it track deleted messages?", a: "Yes, once a message is received by the server, it is logged forever, even if the sender deletes it."},
   1620                 {q: "Does my phone need to be online?", a: "No. Once linked, your Render server handles logging even if your phone is powered off."},
   1621                 {q: "Is this legal?", a: "The tool uses standard WhatsApp Web protocols to function as a linked device. It is intended for personal archiving of your own conversations."},
   1622                 {q: "Does it save photos/videos?", a: "No. To keep the project free, the logger only saves text content. Media files would fill up your free tier storage very quickly."},
   1623                 {q: "Does it save View Once messages?", a: "No. WhatsApp does not send the content of `View Once` media to linked devices/web clients, so the logger cannot see them."}
   1624             ];
   1625             const container = document.getElementById('faqContainer');
   1626             faqs.forEach(item => {
   1627                 const div = document.createElement('div'); div.className = 'faq-item';
   1628                 div.innerHTML = `<button class="faq-question" onclick="this.parentElement.classList.toggle('open')">${item.q} <span class="material-symbols-rounded">expand_more</span></button><div class="faq-answer">${item.a}</div>`;
   1629                 container.appendChild(div);
   1630             });
   1631         }
   1632 
   1633         function toggleChatMenu(event) { if(event) event.stopPropagation(); document.getElementById('chatMenu').classList.toggle('show'); }
   1634         document.addEventListener('click', function(e) { const menu = document.getElementById('chatMenu'); if(menu.classList.contains('show') && !e.target.closest('.menu-container')) menu.classList.remove('show'); });
   1635 
   1636         function toggleChatSearch() { 
   1637             const searchContainer = document.getElementById('inChatSearch');
   1638             searchContainer.classList.toggle('hidden'); 
   1639             document.getElementById('chatMenu').classList.remove('show'); 
   1640             
   1641             const input = document.getElementById('msgSearchInput');
   1642             if (searchContainer.classList.contains('hidden')) {
   1643                 input.value = '';
   1644                 input.dispatchEvent(new Event('input'));
   1645             } else {
   1646                 input.focus(); 
   1647             }
   1648         }
   1649         
   1650         document.getElementById('msgSearchInput').addEventListener('input', (e) => {
   1651             const term = e.target.value.toLowerCase();
   1652             let count = 0;
   1653 
   1654             document.querySelectorAll('.msg-bubble').forEach(b => {
   1655                 const textDiv = b.querySelector('.msg-text'); 
   1656                 if(textDiv) {
   1657                     if (!textDiv.hasAttribute('data-orig')) {
   1658                         textDiv.setAttribute('data-orig', textDiv.innerHTML);
   1659                     }
   1660                     
   1661                     const originalHTML = textDiv.getAttribute('data-orig');
   1662                     const rawText = textDiv.textContent || "";
   1663                     
   1664                     if(term && rawText.toLowerCase().includes(term)) {
   1665                         b.style.display = 'flex';
   1666                         const safeTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
   1667                         const regex = new RegExp(`(?![^<]*>)(${safeTerm})`, 'gi');
   1668                         
   1669                         textDiv.innerHTML = originalHTML.replace(regex, '<span class="highlight">$1</span>');
   1670                         count++;
   1671                     } else {
   1672                         b.style.display = term ? 'none' : 'flex';
   1673                         textDiv.innerHTML = originalHTML; 
   1674                     }
   1675                 }
   1676             });
   1677             document.getElementById('searchCount').innerText = term ? `${count} found` : '';
   1678         });
   1679 
   1680         // --- SSE: CHATS LIST ---
   1681         async function loadChats() {
   1682             const list = document.getElementById('contactList'); 
   1683             const token = localStorage.getItem('wp_api_token');
   1684             if(!token) return handleLogout();
   1685             
   1686             const cachedChats = await getCachedChats();
   1687             if (cachedChats && cachedChats.length > 0) {
   1688                 cachedChats.sort((a,b) => (b.lastActive || 0) - (a.lastActive || 0) || a.id.localeCompare(b.id));
   1689                 renderChatList(cachedChats);
   1690             } else {
   1691                 list.innerHTML = '';
   1692                 for(let i=0; i<5; i++) list.innerHTML += `<div class="skel-contact fade-in"><div class="skeleton skel-avatar"></div><div class="skel-text-col"><div class="skeleton" style="width:60%; height:16px;"></div><div class="skeleton" style="width:40%; height:12px;"></div></div></div>`;
   1693             }
   1694 
   1695             if (!navigator.onLine) return; 
   1696 
   1697             if (chatEventSource) {
   1698                 chatEventSource.close();
   1699                 chatEventSource = null;
   1700             }
   1701 
   1702             function connectChatsStream() {
   1703                 chatEventSource = new EventSource(`${RENDER_BACKEND_URL}/api/chats/stream?token=${token}`);
   1704 
   1705                 chatEventSource.addEventListener('initial', async (e) => {
   1706                     chatEventRetryCount = 0;
   1707                     try {
   1708                         const rawChats = JSON.parse(e.data);
   1709                         // Normalize subIds to ids to prevent silent crashes
   1710                         const chats = rawChats.map(g => ({ ...g, ids: g.subIds || g.ids }));
   1711                         
   1712                         rawChatsMap.clear();
   1713                         chats.forEach(g => {
   1714                             if (g.ids) {
   1715                                 g.ids.forEach(id => {
   1716                                     rawChatsMap.set(id, { id: id, lastActive: g.lastActive, phoneNumber: g.phoneNumber, customName: g.customName, displayName: g.displayName, preview: g.preview });
   1717                                 });
   1718                             }
   1719                         });
   1720                         
   1721                         chats.sort((a,b) => (b.lastActive || 0) - (a.lastActive || 0) || a.id.localeCompare(b.id));
   1722                         await saveCachedChats(chats);
   1723                         renderChatList(chats);
   1724                     } catch (err) {
   1725                         console.error("Chat list initial parse error:", err);
   1726                     }
   1727                 });
   1728 
   1729                 chatEventSource.addEventListener('update', async (e) => {
   1730                     try {
   1731                         const changes = JSON.parse(e.data);
   1732                         let needsRebuild = false;
   1733                         
   1734                         changes.forEach(change => {
   1735                             if (change.type === 'added' || change.type === 'modified') {
   1736                                 rawChatsMap.set(change.doc.id, change.doc);
   1737                                 needsRebuild = true;
   1738                             } else if (change.type === 'removed') {
   1739                                 rawChatsMap.delete(change.doc.id);
   1740                                 needsRebuild = true;
   1741                             }
   1742                         });
   1743                         
   1744                         if (needsRebuild) {
   1745                             const groups = {};
   1746                             rawChatsMap.forEach((d, docId) => {
   1747                                 const phone = d.phoneNumber || docId.split('@')[0];
   1748                                 if (!groups[phone]) {
   1749                                     groups[phone] = { main: { id: docId, ...d }, ids: [docId] };
   1750                                 } else {
   1751                                     groups[phone].ids.push(docId);
   1752                                     if ((d.lastActive || 0) > (groups[phone].main.lastActive || 0)) {
   1753                                         groups[phone].main = { id: docId, ...d };
   1754                                     }
   1755                                 }
   1756                             });
   1757                             
   1758                             const chats = Object.values(groups).map(g => ({...g.main, ids: g.ids})).sort((a,b) => (b.lastActive || 0) - (a.lastActive || 0) || a.id.localeCompare(b.id));
   1759                             await saveCachedChats(chats);
   1760                             renderChatList(chats);
   1761                         }
   1762                     } catch (err) {
   1763                         console.error("Chat list update parse error:", err);
   1764                     }
   1765                 });
   1766 
   1767                 chatEventSource.onerror = (err) => {
   1768                     chatEventSource.close();
   1769                     chatEventRetryCount++;
   1770                     const backoff = Math.min(1000 * Math.pow(2, chatEventRetryCount), 30000);
   1771                     setTimeout(connectChatsStream, backoff);
   1772                 };
   1773             }
   1774             
   1775             connectChatsStream();
   1776         }
   1777 
   1778         function renderChatList(chats) {
   1779             const list = document.getElementById('contactList');
   1780             
   1781             if (chats.length === 0) {
   1782                 list.innerHTML = `<div style="text-align:center; padding:32px; color:var(--md-sys-color-outline);">No chats found</div>`;
   1783                 return;
   1784             }
   1785 
   1786             if (list.querySelector('.skel-contact') || list.innerHTML.includes('No chats found')) {
   1787                 list.innerHTML = '';
   1788             }
   1789 
   1790             const currentIds = new Set(chats.map(c => 'chat-' + c.id.replace(/[^a-zA-Z0-9]/g, '-')));
   1791 
   1792             chats.forEach((c) => {
   1793                 const safeDomId = 'chat-' + c.id.replace(/[^a-zA-Z0-9]/g, '-');
   1794                 let el = document.getElementById(safeDomId);
   1795                 
   1796                 const primaryName = c.customName || c.displayName || c.phoneNumber || c.id;
   1797                 const isGroup = c.id.includes('@g.us');
   1798 
   1799                 if (!el) {
   1800                     el = document.createElement('div');
   1801                     el.id = safeDomId;
   1802                     el.className = 'contact-item ripple-surface';
   1803                     el.innerHTML = `
   1804                         <div class="avatar">${isGroup ? '<span class="material-symbols-rounded">group</span>' : escapeHTML(primaryName[0]).toUpperCase()}</div>
   1805                         <div class="contact-info" style="min-width: 0;">
   1806                             <div class="contact-name" style="display:flex; justify-content:space-between; align-items:baseline; width:100%;">
   1807                                 <span class="name-text" style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis; flex:1;">${escapeHTML(primaryName)}</span>
   1808                                 <span class="chat-time" style="font-size: 12px; color: var(--md-sys-color-on-surface-variant); font-weight: 400; margin-left: 8px; flex-shrink: 0;"></span>
   1809                             </div>
   1810                             <div class="contact-sub preview-text" style="display:flex; align-items:center; margin-top:2px; color:var(--md-sys-color-on-surface-variant);">
   1811                                 <span class="preview-content" style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis; width:100%;"></span>
   1812                             </div>
   1813                         </div>
   1814                     `;
   1815                     el.dataset.lastActive = c.lastActive;
   1816                 } else {
   1817                     const nameSpan = el.querySelector('.name-text');
   1818                     if (nameSpan.innerText !== primaryName) {
   1819                         nameSpan.innerText = primaryName;
   1820                         el.querySelector('.avatar').innerHTML = isGroup ? '<span class="material-symbols-rounded">group</span>' : escapeHTML(primaryName[0]).toUpperCase();
   1821                     }
   1822                     el.dataset.lastActive = c.lastActive;
   1823                 }
   1824 
   1825                 list.appendChild(el);
   1826                 
   1827                 el.onclick = () => openChat(c.ids, primaryName, el, isGroup, c.phoneNumber || c.id, c.lastActive);
   1828 
   1829                 // Update server-side preview directly without extra network reads
   1830                 const contentSpan = el.querySelector('.preview-content');
   1831                 const timeSpan = el.querySelector('.chat-time');
   1832 
   1833                 if (c.lastActive) {
   1834                     const msgDate = new Date(c.lastActive * 1000);
   1835                     const today = new Date();
   1836                     const isToday = msgDate.toDateString() === today.toDateString();
   1837                     
   1838                     timeSpan.innerText = isToday 
   1839                         ? msgDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) 
   1840                         : msgDate.toLocaleDateString([], { month: 'short', day: 'numeric' });
   1841                         
   1842                     contentSpan.innerHTML = escapeHTML(c.preview || 'Media / Call');
   1843                 } else {
   1844                     contentSpan.innerHTML = `<span style="font-style: italic; opacity: 0.7;">No messages</span>`;
   1845                 }
   1846 
   1847                 if (activeChatId && c.ids.includes(activeChatId)) {
   1848                     const baseId = c.phoneNumber || c.id;
   1849                     document.getElementById('headerId').innerText = `${baseId} • ${formatLastActive(c.lastActive)}`;
   1850                 }
   1851             });
   1852 
   1853             Array.from(list.children).forEach(child => {
   1854                 if (child.id && child.id.startsWith('chat-') && !currentIds.has(child.id)) {
   1855                     list.removeChild(child);
   1856                 }
   1857             });
   1858         }
   1859         
   1860         // --- SSE: GLOBAL MESSAGES SYNC ---
   1861         // Instead of a connection per chat, we open ONE connection globally to pull missing messages
   1862         async function connectGlobalSyncStream() {
   1863             const token = localStorage.getItem('wp_api_token');
   1864             if(!token || !navigator.onLine) return;
   1865             
   1866             if(syncEventSource) {
   1867                 syncEventSource.close();
   1868                 syncEventSource = null;
   1869             }
   1870             
   1871             maxCachedTimestamp = await getMaxCachedTimestamp();
   1872             
   1873             // Wait slightly for IDB to settle, show small UI if caching up
   1874             const spinner = document.getElementById('globalSyncSpinner');
   1875             if(maxCachedTimestamp > 0) spinner.classList.add('active');
   1876             
   1877             syncEventSource = new EventSource(`${RENDER_BACKEND_URL}/api/sync/stream?since=${maxCachedTimestamp}&token=${token}`);
   1878             
   1879             const handleSyncData = async (dataArray, eventType) => {
   1880                 const newToCache = [];
   1881                 let activeChatUpdated = false;
   1882                 
   1883                 dataArray.forEach(item => {
   1884                     const data = eventType === 'initial' ? item : item.doc;
   1885                     if (eventType === 'update' && item.type === 'removed') return;
   1886                     
   1887                     // The server must include chatId for this wildcard payload to work
   1888                     if(!data.chatId) return;
   1889                     
   1890                     newToCache.push(data);
   1891                     
   1892                     if (activeChatIds && activeChatIds.includes(data.chatId)) {
   1893                         activeChatUpdated = true;
   1894                     }
   1895                 });
   1896                 
   1897                 if (newToCache.length > 0) {
   1898                     await saveCachedMessages(newToCache);
   1899                     // Update our tracker
   1900                     const maxTs = Math.max(...newToCache.map(m => m.timestamp || 0));
   1901                     if (maxTs > maxCachedTimestamp) maxCachedTimestamp = maxTs;
   1902                 }
   1903                 
   1904                 // If the currently open chat received messages, re-render from cache
   1905                 if (activeChatUpdated && activeChatId) {
   1906                     refreshActiveChatUI();
   1907                 }
   1908                 
   1909                 if (eventType === 'initial') {
   1910                     spinner.classList.remove('active');
   1911                 }
   1912             };
   1913             
   1914             syncEventSource.addEventListener('initial', (e) => {
   1915                 syncEventRetryCount = 0;
   1916                 try { handleSyncData(JSON.parse(e.data), 'initial'); } catch(err) { console.error("Sync initial err", err); }
   1917             });
   1918             
   1919             syncEventSource.addEventListener('update', (e) => {
   1920                 try { handleSyncData(JSON.parse(e.data), 'update'); } catch(err) { console.error("Sync update err", err); }
   1921             });
   1922             
   1923             syncEventSource.onerror = (err) => {
   1924                 syncEventSource.close();
   1925                 syncEventRetryCount++;
   1926                 spinner.classList.remove('active');
   1927                 const backoff = Math.min(1000 * Math.pow(2, syncEventRetryCount), 60000);
   1928                 setTimeout(connectGlobalSyncStream, backoff);
   1929             };
   1930         }
   1931 
   1932         // --- LOCAL CHAT UI ---
   1933         // opening a chat no longer reaches out to the server via SSE directly
   1934         async function openChat(ids, name, el, isGroup, displayId, lastActive) {
   1935             activeChatId = ids[0];
   1936             activeChatIds = ids;
   1937             isCurrentChatGroup = isGroup;
   1938             
   1939             document.getElementById('headerName').innerText = name;
   1940             document.getElementById('chatHeaderContent').style.opacity = '1';
   1941             
   1942             const timeStr = lastActive ? formatLastActive(lastActive) : "Online";
   1943             document.getElementById('headerId').innerText = `${displayId} • ${timeStr}`;
   1944             document.getElementById('headerAvatar').innerHTML = isGroup ? `<span class="material-symbols-rounded">group</span>` : escapeHTML(name[0]).toUpperCase();
   1945             
   1946             if (window.matchMedia('(max-width: 768px)').matches) {
   1947                 window.history.pushState({chatOpen: true}, "");
   1948                 document.getElementById('sidebar').classList.add('inactive');
   1949                 document.getElementById('chatArea').classList.add('active');
   1950             }
   1951             
   1952             const container = document.getElementById('messagesContainer');
   1953             // Show skeleton loader initially before DB read completes
   1954             container.innerHTML = `<div class="skel-msg-group"><div class="skeleton skel-bubble skel-left"></div><div class="skeleton skel-bubble skel-right" style="width:60%"></div><div class="skeleton skel-bubble skel-left" style="width:30%"></div></div>`;
   1955             
   1956             await refreshActiveChatUI(true);
   1957         }
   1958         
   1959         async function refreshActiveChatUI(isInitialOpen = false) {
   1960             if (!activeChatIds || activeChatIds.length === 0) return;
   1961             
   1962             let cachedMsgs = [];
   1963             for (const id of activeChatIds) {
   1964                 const msgs = await getCachedMessages(id);
   1965                 cachedMsgs = cachedMsgs.concat(msgs);
   1966             }
   1967             
   1968             const msgsMap = new Map();
   1969             cachedMsgs.forEach(m => msgsMap.set(m.id, m));
   1970             
   1971             const c = document.getElementById('messagesContainer');
   1972             const wasAtBottom = (c.scrollHeight - c.scrollTop - c.clientHeight) <= 100;
   1973             
   1974             if (cachedMsgs.length > 0) {
   1975                 renderMsgs(msgsMap);
   1976                 
   1977                 if (isInitialOpen || wasAtBottom) {
   1978                     if (isInitialOpen) {
   1979                         scrollToBottom('auto'); // Sync scroll prevents flashing top messages
   1980                     } else {
   1981                         setTimeout(() => scrollToBottom('smooth'), 50); // Smooth scroll for new live messages
   1982                     }
   1983                 } else if (!isInitialOpen && !wasAtBottom) {
   1984                     document.getElementById('newMsgBadge').classList.remove('hidden');
   1985                     document.getElementById('scrollFab').classList.add('visible');
   1986                 }
   1987             } else {
   1988                 c.innerHTML = '<div class="empty-state"><p>No messages recorded</p></div>';
   1989             }
   1990         }
   1991 
   1992         function renderMsgs(map) {
   1993             const container = document.getElementById('messagesContainer');
   1994             container.innerHTML = '';
   1995             currentMessages = Array.from(map.values()).sort((a,b) => a.timestamp - b.timestamp);
   1996             
   1997             if(currentMessages.length === 0) {
   1998                 container.innerHTML = '<div class="empty-state"><p>No messages recorded</p></div>';
   1999                 return;
   2000             }
   2001 
   2002             let lastDateStr = ""; let currentGroup = null;
   2003 
   2004             currentMessages.forEach((m, index) => {
   2005                 const date = new Date(m.timestamp * 1000);
   2006                 const dateStr = date.toDateString();
   2007 
   2008                 if(dateStr !== lastDateStr) {
   2009                     if(currentGroup) container.appendChild(currentGroup);
   2010                     currentGroup = document.createElement('div');
   2011                     currentGroup.className = 'chat-date-group';
   2012                     const div = document.createElement('div'); div.className = 'date-divider'; div.innerText = formatDividerDate(m.timestamp);
   2013                     currentGroup.appendChild(div);
   2014                     lastDateStr = dateStr;
   2015                 }
   2016 
   2017                 const div = document.createElement('div');
   2018                 div.className = `msg-bubble ${m.fromMe ? 'msg-out' : 'msg-in'}`;
   2019                 
   2020                 const nextMsg = currentMessages[index + 1]; const prevMsg = currentMessages[index - 1];
   2021                 const isSamePrev = (prevMsg && prevMsg.fromMe === m.fromMe && (!isCurrentChatGroup || prevMsg.senderName === m.senderName) && new Date(prevMsg.timestamp*1000).toDateString() === dateStr);
   2022                 const isSameNext = (nextMsg && nextMsg.fromMe === m.fromMe && (!isCurrentChatGroup || nextMsg.senderName === m.senderName) && new Date(nextMsg.timestamp*1000).toDateString() === dateStr);
   2023 
   2024                 if (isSamePrev && isSameNext) div.classList.add('group-mid'); else if (isSamePrev) div.classList.add('group-bot'); else if (isSameNext) div.classList.add('group-top');
   2025                 
   2026                 const showSenderName = !m.fromMe && isCurrentChatGroup && m.senderName && m.senderName !== "Unknown" && !isSamePrev; 
   2027                 div.id = `msg-${m.timestamp}`; div.dataset.date = dateStr;
   2028 
   2029                 const safeText = escapeHTML(m.text || '');
   2030                 const urlRegex = /(https?:\/\/[^\s]+)/g;
   2031                 const formattedText = safeText ? safeText.replace(urlRegex, url => `<a href="${url}" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: underline; font-weight: 600;">${url}</a>`) : '';
   2032 
   2033                 div.innerHTML = `${showSenderName ? `<div class="msg-sender">${escapeHTML(m.senderName)}</div>` : ''}<div class="msg-text">${formattedText || 'Media'}</div><div class="msg-meta">${date.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}</div>`;
   2034                 if (currentGroup) currentGroup.appendChild(div);
   2035             });
   2036 
   2037             if(currentGroup) container.appendChild(currentGroup);
   2038         }
   2039 
   2040         function scrollToBottom(behavior = 'auto') {
   2041             const c = document.getElementById('messagesContainer');
   2042             c.scrollTo({ top: c.scrollHeight, behavior: behavior });
   2043             document.getElementById('newMsgBadge').classList.add('hidden');
   2044         }
   2045 
   2046         // --- CALENDAR LOGIC ---
   2047         function openDateSearch() { document.getElementById('chatMenu').classList.remove('show'); document.getElementById('calendarModal').classList.add('active'); renderCalendar(calCurrentDate); }
   2048 
   2049         function renderCalendar(date) {
   2050             const year = date.getFullYear(); const month = date.getMonth();
   2051             document.getElementById('calMonthYear').innerText = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric' }).format(date);
   2052             const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate();
   2053             const grid = document.getElementById('calDatesGrid'); grid.innerHTML = '';
   2054             for(let i=0; i<firstDay; i++) grid.innerHTML += `<div class="cal-date empty"></div>`;
   2055             for(let d=1; d<=daysInMonth; d++) {
   2056                 const dayEl = document.createElement('div'); dayEl.className = 'cal-date ripple-surface'; dayEl.innerText = d;
   2057                 if(calSelectedDate && calSelectedDate.getDate() === d && calSelectedDate.getMonth() === month && calSelectedDate.getFullYear() === year) dayEl.classList.add('active');
   2058                 dayEl.onclick = () => selectDate(new Date(year, month, d)); grid.appendChild(dayEl);
   2059             }
   2060         }
   2061 
   2062         function changeCalMonth(delta) { calCurrentDate.setMonth(calCurrentDate.getMonth() + delta); renderCalendar(calCurrentDate); }
   2063         function selectDate(date) { calSelectedDate = date; document.getElementById('calSelectedDateStr').innerText = date.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }); renderCalendar(calCurrentDate); }
   2064 
   2065         function jumpToDate() {
   2066             if(!currentMessages.length) { closeModal('calendarModal'); return; }
   2067             const targetTs = calSelectedDate.setHours(0,0,0,0) / 1000;
   2068             const msg = currentMessages.find(m => m.timestamp >= targetTs);
   2069             closeModal('calendarModal');
   2070             if(msg) {
   2071                 const el = document.getElementById(`msg-${msg.timestamp}`);
   2072                 if(el) { el.scrollIntoView({behavior: 'smooth', block: 'center'}); el.style.backgroundColor = 'var(--search-highlight)'; setTimeout(() => el.style.backgroundColor = '', 2000); }
   2073             } else document.getElementById('notFoundModal').classList.add('active');
   2074         }
   2075 
   2076         function handleLogout() { localStorage.removeItem('wp_api_token'); localStorage.removeItem('wp_auth_expiry'); localStorage.removeItem('wp_app_pin'); localStorage.removeItem('wp_app_bio'); location.reload(); }
   2077         
   2078         function closeChatMobile() { 
   2079             window.history.back(); 
   2080             activeChatId = null;
   2081             activeChatIds = [];
   2082         }
   2083         
   2084         window.addEventListener('popstate', () => { document.getElementById('chatArea').classList.remove('remove'); document.getElementById('chatArea').classList.remove('active'); });
   2085         
   2086         async function downloadChat() {
   2087             if(!activeChatId || !activeChatIds || activeChatIds.length === 0) return;
   2088             const token = localStorage.getItem('wp_api_token');
   2089             try {
   2090                 const res = await fetch(`${RENDER_BACKEND_URL}/api/export?token=${token}`, { headers: { 'Authorization': `Bearer ${token}` } });
   2091                 
   2092                 if (res.ok) {
   2093                     const exportData = await res.json();
   2094                     
   2095                     let allMessages = [];
   2096                     activeChatIds.forEach(id => {
   2097                         if (exportData.messages[id]) {
   2098                             allMessages = allMessages.concat(exportData.messages[id]);
   2099                         }
   2100                     });
   2101                     
   2102                     if (allMessages.length === 0) return;
   2103                     
   2104                     // Sort by time
   2105                     allMessages.sort((a,b) => a.timestamp - b.timestamp);
   2106                     const text = allMessages.map(m => `[${new Date(m.timestamp*1000).toLocaleString()}] ${m.senderName}: ${m.text}`).join('\n');
   2107                     
   2108                     const blob = new Blob([text], {type:'text/plain'});
   2109                     const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'chat.txt'; a.click();
   2110                 }
   2111             } catch (err) {
   2112                 console.error("Export download error:", err);
   2113             }
   2114         }
   2115         
   2116         function showRenameModal() { if(!activeChatId) return; document.getElementById('renameModal').classList.add('active'); document.getElementById('renameInput').value = document.getElementById('headerName').innerText; document.getElementById('chatMenu').classList.remove('show'); }
   2117         
   2118         async function saveRename() { 
   2119             const name = document.getElementById('renameInput').value; 
   2120             const token = localStorage.getItem('wp_api_token');
   2121             if(name && token) { 
   2122                 await fetch(`${RENDER_BACKEND_URL}/api/rename`, {
   2123                     method: 'POST',
   2124                     headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
   2125                     body: JSON.stringify({ id: activeChatId, customName: name })
   2126                 });
   2127                 
   2128                 document.getElementById('headerName').innerText = name; 
   2129                 closeModal('renameModal'); 
   2130                 // Will automatically refresh in sidebar via SSE update hook
   2131             } 
   2132         }
   2133         
   2134         function showLogoutModal() { document.getElementById('logoutModal').classList.add('active'); }
   2135 
   2136         document.getElementById('searchInput').addEventListener('input', (e) => {
   2137             const val = e.target.value.toLowerCase();
   2138             document.querySelectorAll('.contact-item').forEach(el => { el.style.display = el.innerText.toLowerCase().includes(val) ? 'flex' : 'none'; });
   2139         });
   2140 
   2141         function refreshApp() {
   2142             document.getElementById('appOptionsModal').classList.add('active');
   2143         }
   2144 
   2145         function executePageRefresh() {
   2146             window.location.reload(true);
   2147         }
   2148 
   2149         function openHardReset() {
   2150             closeModal('appOptionsModal');
   2151             document.getElementById('refreshAuthStep').classList.remove('hidden');
   2152             document.getElementById('refreshProgressStep').classList.add('hidden');
   2153             document.getElementById('refreshUsername').value = '';
   2154             document.getElementById('refreshPassword').value = '';
   2155             document.getElementById('refreshError').style.display = 'none';
   2156             document.getElementById('refreshModal').classList.add('active');
   2157         }
   2158 
   2159         async function startHardRefresh() {
   2160             const btn = document.getElementById('refreshBtn'); const err = document.getElementById('refreshError');
   2161             const u = document.getElementById('refreshUsername').value; const p = document.getElementById('refreshPassword').value;
   2162 
   2163             btn.classList.add('loading'); btn.disabled = true; err.style.display = 'none';
   2164 
   2165             try {
   2166                 const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, {
   2167                     method: 'POST', headers: {'Content-Type': 'application/json'},
   2168                     body: JSON.stringify({ username: u, password: p })
   2169                 });
   2170                 const data = await response.json();
   2171 
   2172                 if (!data.success) throw new Error();
   2173 
   2174                 // Re-sync Token to be safe
   2175                 if (data.token) localStorage.setItem('wp_api_token', data.token);
   2176 
   2177                 document.getElementById('refreshAuthStep').classList.add('hidden');
   2178                 document.getElementById('refreshProgressStep').classList.remove('hidden');
   2179                 
   2180                 await executeHardRefresh();
   2181 
   2182             } catch (e) {
   2183                 err.style.display = 'block';
   2184                 btn.classList.remove('loading'); btn.disabled = false;
   2185             }
   2186         }
   2187 
   2188         function updateRefreshProgress(percent, text) {
   2189             document.getElementById('refreshProgressBar').style.width = `${percent}%`;
   2190             document.getElementById('refreshPercentText').innerText = `${Math.floor(percent)}%`;
   2191             document.getElementById('refreshStatusText').innerText = text;
   2192         }
   2193 
   2194         async function executeHardRefresh() {
   2195             try {
   2196                 updateRefreshProgress(5, "Clearing caches...");
   2197                 if ('caches' in window) { try { const keys = await caches.keys(); await Promise.all(keys.map(key => caches.delete(key))); } catch(e) {} }
   2198                 if ('serviceWorker' in navigator) { const regs = await navigator.serviceWorker.getRegistrations(); for (let r of regs) await r.unregister(); }
   2199                 
   2200                 updateRefreshProgress(10, "Clearing local storage...");
   2201                 const theme = localStorage.getItem('theme');
   2202                 const tkn = localStorage.getItem('wp_api_token');
   2203                 localStorage.clear();
   2204                 if(theme) localStorage.setItem('theme', theme);
   2205                 if(tkn) localStorage.setItem('wp_api_token', tkn);
   2206 
   2207                 updateRefreshProgress(15, "Wiping database...");
   2208                 if(dbInstance) { dbInstance.close(); dbInstance = null; }
   2209                 
   2210                 await new Promise(r => setTimeout(r, 100));
   2211 
   2212                 await new Promise(res => { 
   2213                     try {
   2214                         const req = indexedDB.deleteDatabase(DB_NAME); 
   2215                         req.onsuccess = res; 
   2216                         req.onerror = res; 
   2217                         req.onblocked = () => {
   2218                             res();
   2219                         };
   2220                     } catch(e) { res(); }
   2221                 });
   2222                 
   2223                 await new Promise(r => setTimeout(r, 200));
   2224                 
   2225                 updateRefreshProgress(25, "Rebuilding database...");
   2226                 isIdbSupported = true;
   2227                 await initIndexedDB();
   2228 
   2229                 updateRefreshProgress(30, "Fetching chat list...");
   2230                 
   2231                 const token = localStorage.getItem('wp_api_token');
   2232                 const res = await fetch(`${RENDER_BACKEND_URL}/api/export/chats`, { headers: { 'Authorization': `Bearer ${token}` } });
   2233                 const exportData = await res.json();
   2234                 
   2235                 const chatsData = exportData.chats || [];
   2236                 const totalMessages = Math.max(1, exportData.totalMessages || 1); // Prevent division by zero
   2237                 const groups = {};
   2238                 const excluded = ['917278779512@s.whatsapp.net', '201554426618024@lid'];
   2239 
   2240                 chatsData.forEach(d => {
   2241                     if (!d.lastActive || excluded.includes(d.id)) return;
   2242                     const phone = d.phoneNumber || d.id.split('@')[0];
   2243                     if (!groups[phone]) groups[phone] = { main: d, ids: [d.id] };
   2244                     else {
   2245                         groups[phone].ids.push(d.id);
   2246                         if (d.lastActive > groups[phone].main.lastActive) groups[phone].main = d;
   2247                     }
   2248                 });
   2249 
   2250                 const chats = Object.values(groups).map(g => ({...g.main, ids: g.ids})).sort((a,b) => (b.lastActive || 0) - (a.lastActive || 0) || a.id.localeCompare(b.id));
   2251                 await saveCachedChats(chats);
   2252 
   2253                 let messagesFetchedSoFar = 0;
   2254 
   2255                 for (const chat of chats) {
   2256                     const name = chat.customName || chat.displayName || chat.phoneNumber || chat.id;
   2257                     
   2258                     let currentPercent = Math.min(99, 30 + ((messagesFetchedSoFar / totalMessages) * 70));
   2259                     updateRefreshProgress(currentPercent, `Fetching: ${name}...`);
   2260                     
   2261                     for (const id of chat.ids) {
   2262                         try {
   2263                             const msgRes = await fetch(`${RENDER_BACKEND_URL}/api/export/messages?chatId=${encodeURIComponent(id)}&token=${token}`, { headers: { 'Authorization': `Bearer ${token}` } });
   2264                             if (msgRes.ok) {
   2265                                 const msgData = await msgRes.json();
   2266                                 const msgs = msgData.messages || [];
   2267                                 if (msgs.length > 0) {
   2268                                     msgs.forEach(m => m.chatId = id);
   2269                                     await saveCachedMessages(msgs);
   2270                                     messagesFetchedSoFar += msgs.length;
   2271                                     
   2272                                     currentPercent = Math.min(99, 30 + ((messagesFetchedSoFar / totalMessages) * 70));
   2273                                     updateRefreshProgress(currentPercent, `Fetching: ${name}...`);
   2274                                 }
   2275                             }
   2276                         } catch (err) {
   2277                             console.warn(`Failed to fetch messages for ${id}`, err);
   2278                         }
   2279                     }
   2280                 }
   2281 
   2282                 updateRefreshProgress(100, "Sync complete! Restarting...");
   2283                 setTimeout(() => window.location.reload(), 1000);
   2284 
   2285             } catch (e) {
   2286                 console.error("Hard refresh error", e);
   2287                 updateRefreshProgress(100, "Error during sync. Reloading...");
   2288                 setTimeout(() => window.location.reload(), 2000);
   2289             }
   2290         }
   2291         
   2292         function calculateLocalStorageUsage() {
   2293             let totalBytes = 0;
   2294             for (let key in localStorage) {
   2295                 if (!localStorage.hasOwnProperty(key)) continue;
   2296                 totalBytes += ((key.length + localStorage[key].length) * 2);
   2297             }
   2298             
   2299             const sizeInKB = (totalBytes / 1024).toFixed(2);
   2300             const displayElement = document.getElementById('localStorageSizeDisplay');
   2301             
   2302             if (displayElement) {
   2303                 if (sizeInKB >= 1024) {
   2304                     displayElement.innerText = `${(sizeInKB / 1024).toFixed(2)} MB occupied`;
   2305                 } else {
   2306                     displayElement.innerText = `${sizeInKB} KB occupied`;
   2307                 }
   2308             }
   2309         }
   2310 
   2311         // --- OPTIONAL: DIAGNOSTICS LOGS ---
   2312         let logsInterval = null;
   2313 
   2314         function toggleLogs() {
   2315             const isChecked = document.getElementById('logsToggle').checked;
   2316             const viewer = document.getElementById('logsViewer');
   2317             const token = localStorage.getItem('wp_api_token');
   2318             
   2319             if (isChecked && token) {
   2320                 viewer.classList.remove('hidden');
   2321                 fetchAndRenderLogs(token);
   2322                 logsInterval = setInterval(() => fetchAndRenderLogs(token), 10000);
   2323             } else {
   2324                 viewer.classList.add('hidden');
   2325                 if (logsInterval) clearInterval(logsInterval);
   2326             }
   2327         }
   2328 
   2329         async function fetchAndRenderLogs(token) {
   2330             try {
   2331                 const res = await fetch(`${RENDER_BACKEND_URL}/logs?format=json&token=${token}`);
   2332                 if (res.ok) {
   2333                     const logs = await res.json();
   2334                     const viewer = document.getElementById('logsViewer');
   2335                     viewer.innerHTML = '';
   2336                     
   2337                     logs.forEach(l => {
   2338                         const time = new Date(l.timestamp).toLocaleTimeString();
   2339                         const div = document.createElement('div');
   2340                         if (l.level === 'error') div.className = 'error';
   2341                         div.innerText = `[${time}] [${l.level.toUpperCase()}] ${l.message}`;
   2342                         viewer.appendChild(div);
   2343                     });
   2344                     
   2345                     viewer.scrollTop = viewer.scrollHeight;
   2346                 }
   2347             } catch (e) {}
   2348         }
   2349         
   2350     </script>
   2351 </body>
   2352 </html>
© notamitgamer • Site Built: 2026-09-05 01:53:16 UTC • git-mirror commit: c170d72 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror