<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
    <title>Muleline</title>
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
    <meta name="apple-mobile-web-app-title" content="Muleline">
    <meta name="theme-color" content="#0a0a0a">
    <link rel="manifest" href="/static/manifest.json">
    <link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="">
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <!-- Login overlay -->
    <div class="login-overlay" id="login-overlay">
        <div class="login-card">
            <div class="login-logo"><span>mule</span>line</div>
            <div id="login-error" style="display:none;color:var(--red);font-size:13px;text-align:center"></div>
            <div>
                <div class="login-label">Username</div>
                <input type="text" id="login-username" placeholder="Enter username" autocomplete="username">
            </div>
            <div>
                <div class="login-label">Password</div>
                <input type="password" id="login-password" placeholder="Enter password" autocomplete="current-password">
            </div>
            <button class="connect-btn" id="login-btn" onclick="doLogin()">Sign In</button>
            <button class="connect-btn" style="background:var(--surface2);color:var(--text)" onclick="doRegister()">Create Account</button>
            <div class="login-hint">Your data stays on your server</div>
        </div>
    </div>

    <div class="header">
        <button class="mobile-menu-btn" onclick="toggleMobileNav()">&#9776;</button>
        <div class="logo"><span>mule</span>line</div>
        <div class="nav" id="main-nav">
            <button class="active" onclick="showPage('photos')">Photos</button>
            <button onclick="showPage('timeline')">Timeline</button>
            <button onclick="showPage('files')">Files</button>
            <button onclick="showPage('albums')">Albums</button>
            <button onclick="showPage('favorites')">Favorites</button>
            <button onclick="showPage('map')">Map</button>
            <button onclick="showPage('people')">People</button>
            <button onclick="showPage('trash')">Trash</button>
            <button onclick="showPage('activity')">Activity</button>
            <button onclick="showPage('settings')">Settings</button>
        </div>
        <div class="spacer"></div>
        <div class="stats-bar">
            <span><span class="stat-val" id="stat-files">0</span> files</span>
            <span><span class="stat-val" id="stat-bytes">0 B</span> synced</span>
        </div>
        <button class="logout-btn" onclick="logout()">Logout</button>
    </div>

    <!-- Upload bar (photos/files pages) -->
    <div class="upload-bar" id="upload-bar">
        <button class="upload-btn" onclick="document.getElementById('fileInput').click()">Upload</button>
        <input type="file" id="fileInput" multiple>
        <div class="search-wrap">
            <span class="search-icon">&#128269;</span>
            <input type="text" class="search-input" id="search-input" placeholder="Search photos... try 'sunset at the beach'" oninput="onSearchInput()">
        </div>
        <div class="filter-btns">
            <button class="active" data-filter="" onclick="setFilter(this, '')">All</button>
            <button data-filter="image/" onclick="setFilter(this, 'image/')">Photos</button>
            <button data-filter="video/" onclick="setFilter(this, 'video/')">Videos</button>
        </div>
        <div class="view-toggle">
            <button onclick="setGridSize('small')">S</button>
            <button class="active" onclick="setGridSize('medium')">M</button>
            <button onclick="setGridSize('large')">L</button>
        </div>
    </div>

    <!-- Bulk action bar -->
    <div class="bulk-bar" id="bulk-bar">
        <span class="count" id="bulk-count">0 selected</span>
        <button class="bulk-btn" onclick="selectAll()">Select All</button>
        <button class="bulk-btn" onclick="clearSelection()">Deselect All</button>
        <button class="bulk-btn primary" onclick="bulkDownload()">Download</button>
        <button class="bulk-btn" onclick="bulkAddToAlbum()">Add to Album</button>
        <button class="bulk-btn danger" onclick="bulkDelete()">Delete</button>
    </div>

    <!-- Drop overlay -->
    <div class="drop-overlay" id="drop-overlay">
        <div class="drop-text">Drop files to upload</div>
    </div>

    <!-- Upload progress panel -->
    <div class="upload-panel" id="upload-panel">
        <div class="upload-panel-header">
            <span>Uploads <span id="upload-count"></span></span>
            <button class="close" onclick="hideUploadPanel()">&times;</button>
        </div>
        <div class="upload-panel-overall" id="upload-overall" style="display:none">
            <div class="bar-bg"><div class="bar-fill" id="upload-overall-fill" style="width:0%"></div></div>
            <div class="summary">
                <span id="upload-summary-left">Uploading...</span>
                <span id="upload-summary-right"></span>
            </div>
        </div>
        <div class="upload-panel-list" id="upload-list"></div>
    </div>

    <!-- Upload completion toast -->
    <div class="upload-toast" id="upload-toast" onclick="this.classList.remove('show')">
        <div class="toast-icon success" id="toast-icon">&#10003;</div>
        <span id="toast-msg">Upload complete</span>
        <div class="toast-dismiss"><div class="shrink"></div></div>
    </div>

    <!-- Lightbox -->
    <div class="lightbox" id="lightbox">
        <button class="lightbox-close" onclick="closeLightbox()">&times;</button>
        <button class="lightbox-nav prev" onclick="navLightbox(-1)">&#8249;</button>
        <button class="lightbox-nav next" onclick="navLightbox(1)">&#8250;</button>
        <img id="lightbox-img" src="" alt="">
        <video id="lightbox-video" src="" controls playsinline preload="metadata"
               style="display:none;max-width:90vw;max-height:80vh;border-radius:8px;background:#000"></video>
        <div class="lightbox-info">
            <div class="name" id="lightbox-name"></div>
            <div class="meta" id="lightbox-meta"></div>
            <div class="actions">
                <button class="action-btn" id="lightbox-fav" onclick="toggleLightboxFav()" title="Favorite">&#9825;</button>
                <button class="action-btn" id="lightbox-info-btn" onclick="toggleExifPanel()">Info</button>
                <a class="action-btn" id="lightbox-download" href="#" download>Download</a>
                <button class="action-btn danger" id="lightbox-delete" onclick="deleteCurrentFile()">Delete</button>
            </div>
        </div>
        <!-- EXIF info panel -->
        <div id="exif-panel" style="display:none; position:absolute; top:60px; right:16px; bottom:60px; width:300px; background:var(--surface); border:1px solid var(--border); border-radius:12px; overflow-y:auto; z-index:320; padding:16px;">
            <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:12px;">
                <span style="font-size:14px; font-weight:600;">Details</span>
                <button onclick="toggleExifPanel()" style="background:none; border:none; color:var(--text2); cursor:pointer; font-size:18px;">&times;</button>
            </div>
            <div id="exif-content" style="font-size:12px; color:var(--text2);"></div>
        </div>
    </div>

    <!-- Photos page -->
    <div class="page active" id="page-photos">
        <div class="photo-container" id="photo-container"></div>
    </div>

    <!-- Timeline page -->
    <div class="page" id="page-timeline">
        <div style="max-width: 1200px; margin: 0 auto; padding: 16px;">
            <div style="display:flex; align-items:center; gap:12px; margin-bottom:16px;">
                <span style="font-size:18px; font-weight:600;">Timeline</span>
                <div style="display:flex; gap:4px;">
                    <button class="filter-btn timeline-group-btn active" data-group="day" onclick="setTimelineGroup('day', this)">Day</button>
                    <button class="filter-btn timeline-group-btn" data-group="month" onclick="setTimelineGroup('month', this)">Month</button>
                    <button class="filter-btn timeline-group-btn" data-group="year" onclick="setTimelineGroup('year', this)">Year</button>
                </div>
            </div>
            <div id="timeline-container"></div>
        </div>
    </div>

    <!-- Files page (same grid but shows all types) -->
    <div class="page" id="page-files">
        <div class="photo-container" id="file-container"></div>
    </div>

    <!-- Albums page -->
    <div class="page" id="page-albums">
        <div style="max-width: 1000px; margin: 0 auto; padding: 24px;">
            <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px;">
                <h2 style="font-size:20px; font-weight:700;">Albums</h2>
                <button class="upload-btn" onclick="showCreateAlbum()">+ New Album</button>
            </div>
            <!-- Create album form (hidden by default) -->
            <div id="album-create-form" style="display:none; margin-bottom:20px; padding:16px; background:var(--surface); border:1px solid var(--border); border-radius:10px;">
                <input type="text" id="album-name-input" placeholder="Album name" style="width:100%; padding:10px 14px; background:var(--surface2); border:1px solid var(--border); border-radius:8px; color:var(--text); font-size:14px; margin-bottom:10px; font-family:inherit;">
                <textarea id="album-desc-input" placeholder="Description (optional)" rows="2" style="width:100%; padding:10px 14px; background:var(--surface2); border:1px solid var(--border); border-radius:8px; color:var(--text); font-size:13px; margin-bottom:12px; font-family:inherit; resize:vertical;"></textarea>
                <div style="display:flex; gap:8px;">
                    <button class="upload-btn" onclick="createAlbum()">Create</button>
                    <button class="upload-btn" style="background:var(--surface2); color:var(--text2);" onclick="hideCreateAlbum()">Cancel</button>
                </div>
            </div>
            <div id="albums-grid" class="albums-grid"></div>
        </div>
    </div>

    <!-- Album detail view -->
    <div class="page" id="page-album-detail">
        <div style="max-width: 1000px; margin: 0 auto; padding: 24px;">
            <div style="display:flex; align-items:center; gap:12px; margin-bottom:20px;">
                <button onclick="showPage('albums')" style="padding:6px 12px; border-radius:6px; border:1px solid var(--border); background:var(--surface); color:var(--text2); cursor:pointer; font-size:13px;">&larr; Back</button>
                <h2 id="album-detail-name" style="font-size:20px; font-weight:700; flex:1;"></h2>
                <button class="upload-btn" style="background:var(--red);" onclick="deleteCurrentAlbum()">Delete Album</button>
            </div>
            <p id="album-detail-desc" style="color:var(--text2); font-size:13px; margin-bottom:16px;"></p>
            <div id="album-detail-grid" class="photo-grid"></div>
        </div>
    </div>

    <!-- People page -->
    <div class="page" id="page-people">
        <div style="max-width: 1000px; margin: 0 auto; padding: 24px;">
            <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;">
                <h2 style="font-size:20px; font-weight:700;">People</h2>
                <div style="display:flex; gap:8px;">
                    <button class="upload-btn" style="background:var(--surface2); color:var(--text);" onclick="clusterFaces()">Auto-Cluster</button>
                    <button class="upload-btn" onclick="showCreatePerson()">+ Add Person</button>
                </div>
            </div>
            <div id="face-stats" style="font-size:12px; color:var(--text2); margin-bottom:20px;"></div>
            <!-- Create person form -->
            <div id="person-create-form" style="display:none; margin-bottom:20px; padding:16px; background:var(--surface); border:1px solid var(--border); border-radius:10px;">
                <input type="text" id="person-name-input" placeholder="Person name" style="width:100%; padding:10px 14px; background:var(--surface2); border:1px solid var(--border); border-radius:8px; color:var(--text); font-size:14px; margin-bottom:12px; font-family:inherit;">
                <div style="display:flex; gap:8px;">
                    <button class="upload-btn" onclick="createPerson()">Add</button>
                    <button class="upload-btn" style="background:var(--surface2); color:var(--text2);" onclick="hideCreatePerson()">Cancel</button>
                </div>
            </div>
            <!-- Known people -->
            <div id="people-grid" class="people-grid"></div>
            <!-- Unassigned faces -->
            <div id="unassigned-section" style="margin-top:32px;">
                <h3 style="font-size:16px; font-weight:600; margin-bottom:16px; color:var(--text2);">Unassigned Faces</h3>
                <div id="unassigned-grid" class="people-grid"></div>
            </div>
        </div>
    </div>

    <!-- Map page -->
    <div class="page" id="page-map">
        <div id="map-container" style="width:100%; height:100%; min-height:calc(100vh - 120px);"></div>
    </div>

    <!-- Favorites page -->
    <div class="page" id="page-favorites">
        <div style="max-width: 1200px; margin: 0 auto; padding: 24px;">
            <h2 style="font-size:20px; font-weight:700; margin-bottom:20px;">Favorites</h2>
            <div id="favorites-grid" class="photo-grid"></div>
            <div id="favorites-empty" class="empty-state" style="display:none">
                <div class="icon">&#9825;</div>
                <div class="title">No favorites yet</div>
                <div class="desc">Click the heart icon on any photo to add it to your favorites.</div>
            </div>
        </div>
    </div>

    <!-- Trash page -->
    <div class="page" id="page-trash">
        <div style="max-width: 1200px; margin: 0 auto; padding: 24px;">
            <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px;">
                <h2 style="font-size:20px; font-weight:700;">Trash</h2>
                <div style="display:flex; gap:8px;">
                    <span id="trash-info" style="font-size:12px; color:var(--text2); align-self:center;"></span>
                    <button class="upload-btn" style="background:var(--red);" onclick="emptyTrash()">Empty Trash</button>
                </div>
            </div>
            <div id="trash-grid" class="photo-grid"></div>
            <div id="trash-empty" class="empty-state" style="display:none">
                <div class="icon">&#128465;</div>
                <div class="title">Trash is empty</div>
                <div class="desc">Deleted files appear here for 30 days before being permanently removed.</div>
            </div>
        </div>
    </div>

    <!-- Activity page -->
    <div class="page" id="page-activity">
        <div style="max-width: 800px; margin: 0 auto;">
            <div class="card">
                <div class="card-header">Recent Activity</div>
                <div id="activity-list"><div class="card-empty">No activity yet</div></div>
            </div>
        </div>
    </div>

    <!-- Settings page -->
    <div class="page" id="page-settings">
        <div style="max-width: 720px; margin: 0 auto;">

            <div class="settings-section">
                <div class="section-title">Server Info</div>
                <div class="card">
                    <div class="card-header">Status</div>
                    <div style="padding: 16px;">
                        <div class="info-grid" id="server-info-grid">
                            <div class="info-item"><div class="info-label">Version</div><div class="info-val" id="info-version">—</div></div>
                            <div class="info-item"><div class="info-label">Uptime</div><div class="info-val" id="info-uptime">—</div></div>
                            <div class="info-item"><div class="info-label">Total Files</div><div class="info-val" id="info-files">—</div></div>
                            <div class="info-item"><div class="info-label">Storage Used</div><div class="info-val" id="info-storage">—</div></div>
                            <div class="info-item"><div class="info-label">Backend</div><div class="info-val" id="info-backend">—</div></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="settings-section">
                <div class="section-title">Storage Backend</div>

                <!-- Provider cards grid -->
                <div class="storage-providers" id="storage-providers">
                    <div class="provider-card selected" data-backend="local" onclick="selectProvider('local')">
                        <div class="provider-icon" style="background: linear-gradient(135deg, #3b82f6, #1d4ed8);">
                            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
                        </div>
                        <div class="provider-name">Local</div>
                        <div class="provider-desc">Filesystem</div>
                        <div class="provider-check">&#10003;</div>
                    </div>
                    <div class="provider-card" data-backend="s3" onclick="selectProvider('s3')">
                        <div class="provider-icon" style="background: linear-gradient(135deg, #f97316, #c2410c);">
                            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6l8-4 8 4M4 6v12l8 4M4 6l8 4M20 6v12l-8 4M20 6l-8 4M12 10v12"/></svg>
                        </div>
                        <div class="provider-name">S3</div>
                        <div class="provider-desc">AWS / R2 / MinIO</div>
                        <div class="provider-check">&#10003;</div>
                    </div>
                    <div class="provider-card" data-backend="webdav" onclick="selectProvider('webdav')">
                        <div class="provider-icon" style="background: linear-gradient(135deg, #0ea5e9, #0369a1);">
                            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
                        </div>
                        <div class="provider-name">WebDAV</div>
                        <div class="provider-desc">Nextcloud / ownCloud</div>
                        <div class="provider-check">&#10003;</div>
                    </div>
                    <div class="provider-card" data-backend="sftp" onclick="selectProvider('sftp')">
                        <div class="provider-icon" style="background: linear-gradient(135deg, #22c55e, #15803d);">
                            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
                        </div>
                        <div class="provider-name">SFTP</div>
                        <div class="provider-desc">SSH File Transfer</div>
                        <div class="provider-check">&#10003;</div>
                    </div>
                    <div class="provider-card" data-backend="dropbox" onclick="selectProvider('dropbox')">
                        <div class="provider-icon" style="background: linear-gradient(135deg, #2563eb, #1e40af);">
                            <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M6 2l6 4-6 4 6 4-6 4-6-4 6-4-6-4zm12 0l6 4-6 4 6 4-6 4-6-4 6-4-6-4z"/></svg>
                        </div>
                        <div class="provider-name">Dropbox</div>
                        <div class="provider-desc">Cloud Storage</div>
                        <div class="provider-check">&#10003;</div>
                    </div>
                    <div class="provider-card" data-backend="gdrive" onclick="selectProvider('gdrive')">
                        <div class="provider-icon" style="background: linear-gradient(135deg, #eab308, #a16207);">
                            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 19.5h20L12 2z"/><path d="M2 19.5l5-8.5h14"/><path d="M16 11L12 2 7.5 11"/></svg>
                        </div>
                        <div class="provider-name">Google Drive</div>
                        <div class="provider-desc">Service Account</div>
                        <div class="provider-check">&#10003;</div>
                    </div>
                </div>

                <!-- Config panel for selected provider -->
                <div class="card" id="provider-config" style="margin-top: 12px;">
                    <div class="card-header" id="provider-config-title">Local Configuration</div>
                    <div style="padding: 16px;">

                        <!-- Local -->
                        <div id="fields-local">
                            <div class="form-row">
                                <label>Base Directory</label>
                                <input type="text" id="local-base-dir" placeholder="./data/uploads">
                            </div>
                        </div>

                        <!-- S3 -->
                        <div id="fields-s3" style="display:none;">
                            <div class="form-row">
                                <label>Bucket</label>
                                <input type="text" id="s3-bucket" placeholder="my-bucket">
                            </div>
                            <div class="form-row">
                                <label>Endpoint URL (leave blank for AWS)</label>
                                <input type="text" id="s3-endpoint" placeholder="https://s3.us-east-1.amazonaws.com">
                            </div>
                            <div class="form-row" style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
                                <div>
                                    <label>Access Key</label>
                                    <input type="text" id="s3-access-key" placeholder="AKIAIOSFODNN7EXAMPLE">
                                </div>
                                <div>
                                    <label>Secret Key</label>
                                    <input type="password" id="s3-secret-key" placeholder="••••••••">
                                </div>
                            </div>
                            <div class="form-row" style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
                                <div>
                                    <label>Region</label>
                                    <input type="text" id="s3-region" placeholder="us-east-1">
                                </div>
                                <div>
                                    <label>Prefix (optional)</label>
                                    <input type="text" id="s3-prefix" placeholder="muleline/">
                                </div>
                            </div>
                        </div>

                        <!-- WebDAV -->
                        <div id="fields-webdav" style="display:none;">
                            <div class="form-row">
                                <label>URL</label>
                                <input type="text" id="webdav-url" placeholder="https://dav.example.com/remote.php/dav/files/user/">
                            </div>
                            <div class="form-row" style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
                                <div>
                                    <label>Username</label>
                                    <input type="text" id="webdav-username" placeholder="user">
                                </div>
                                <div>
                                    <label>Password</label>
                                    <input type="password" id="webdav-password" placeholder="••••••••">
                                </div>
                            </div>
                            <div class="form-row">
                                <label>Prefix (optional)</label>
                                <input type="text" id="webdav-prefix" placeholder="muleline/">
                            </div>
                        </div>

                        <!-- SFTP -->
                        <div id="fields-sftp" style="display:none;">
                            <div class="form-row" style="display:grid; grid-template-columns:2fr 1fr; gap:12px;">
                                <div>
                                    <label>Host</label>
                                    <input type="text" id="sftp-host" placeholder="storage.example.com">
                                </div>
                                <div>
                                    <label>Port</label>
                                    <input type="number" id="sftp-port" placeholder="22" value="22">
                                </div>
                            </div>
                            <div class="form-row" style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
                                <div>
                                    <label>Username</label>
                                    <input type="text" id="sftp-username" placeholder="user">
                                </div>
                                <div>
                                    <label>Password</label>
                                    <input type="password" id="sftp-password" placeholder="••••••••">
                                </div>
                            </div>
                            <div class="form-row">
                                <label>SSH Key Path (optional)</label>
                                <input type="text" id="sftp-key-path" placeholder="~/.ssh/id_rsa">
                            </div>
                            <div class="form-row">
                                <label>Base Path</label>
                                <input type="text" id="sftp-base-path" placeholder="/home/user/muleline">
                            </div>
                        </div>

                        <!-- Dropbox -->
                        <div id="fields-dropbox" style="display:none;">
                            <div class="form-row">
                                <label>Access Token</label>
                                <input type="password" id="dropbox-token" placeholder="••••••••">
                            </div>
                            <div class="form-row">
                                <label>Root Path</label>
                                <input type="text" id="dropbox-root" placeholder="/muleline">
                            </div>
                        </div>

                        <!-- Google Drive -->
                        <div id="fields-gdrive" style="display:none;">
                            <div class="form-row">
                                <label>Credentials JSON Path</label>
                                <input type="text" id="gdrive-creds" placeholder="/etc/muleline/gdrive-credentials.json">
                            </div>
                            <div class="form-row">
                                <label>Folder ID</label>
                                <input type="text" id="gdrive-folder" placeholder="1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms">
                            </div>
                        </div>

                        <div class="form-actions">
                            <button class="btn-test" id="test-btn" onclick="testConnection()">Test Connection</button>
                            <button class="btn-primary" onclick="saveSettings()">Save</button>
                        </div>
                    </div>
                </div>
            </div>

            <div class="settings-section">
                <div class="section-title">Import</div>
                <div class="card">
                    <div class="card-header">Google Photos Takeout</div>
                    <div style="padding: 16px;">
                        <p style="font-size:13px; color:var(--text2); margin-bottom:12px;">
                            Upload a Google Takeout ZIP file to import your photos, videos, and metadata.
                            <a href="https://takeout.google.com/" target="_blank" rel="noopener" style="color:var(--accent);">Download your data from Google</a>.
                        </p>
                        <div style="display:flex; align-items:center; gap:12px;">
                            <input type="file" id="takeout-file" accept=".zip" style="display:block; font-size:13px; color:var(--text);">
                            <button class="btn-primary" onclick="importTakeout()">Import</button>
                        </div>
                        <div id="import-status" style="margin-top:12px; font-size:12px; color:var(--text2); display:none;"></div>
                    </div>
                </div>
            </div>

            <div class="settings-section">
                <div class="section-title">Connectors</div>
                <div class="card">
                    <div class="card-header">Gmail</div>
                    <div style="padding: 16px;">
                        <div id="gmail-status" style="font-size:13px; color:var(--text2); margin-bottom:12px;">Checking...</div>
                        <div style="display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
                            <input type="text" id="gmail-query" value="has:attachment" placeholder="Gmail search query" style="flex:1; min-width:200px; padding:8px 12px; background:var(--surface2); border:1px solid var(--border); border-radius:6px; color:var(--text); font-size:13px;">
                            <input type="number" id="gmail-max" value="50" min="1" max="500" style="width:70px; padding:8px; background:var(--surface2); border:1px solid var(--border); border-radius:6px; color:var(--text); font-size:13px;">
                            <button class="btn-primary" onclick="importGmail()">Import</button>
                        </div>
                        <div style="font-size:11px; color:var(--text2); margin-top:8px;">
                            Try: <code>from:accountant has:attachment</code>, <code>subject:lightspeed filename:pdf</code>, <code>has:attachment filename:xlsx</code>
                        </div>
                        <div id="gmail-import-status" style="margin-top:12px; font-size:12px; color:var(--text2); display:none;"></div>
                    </div>
                </div>
            </div>

            <div class="settings-section">
                <div class="section-title">Session</div>
                <div class="card">
                    <div class="card-header">Authentication</div>
                    <div style="padding: 16px; display: flex; align-items: center; gap: 16px;">
                        <span style="font-size: 13px; color: var(--text2);">Logged in as <strong id="logged-in-as" style="color: var(--text);"></strong></span>
                        <button class="btn-primary" style="background: var(--red);" onclick="logout()">Logout</button>
                    </div>
                </div>
            </div>

        </div>
    </div>

    <script>
        // --- Auth ---

        function getToken() {
            return localStorage.getItem('muleline_token') || '';
        }

        function getRefreshToken() {
            return localStorage.getItem('muleline_refresh') || '';
        }

        function setTokens(access, refresh) {
            localStorage.setItem('muleline_token', access);
            if (refresh) localStorage.setItem('muleline_refresh', refresh);
        }

        function showLoginError(msg) {
            const el = document.getElementById('login-error');
            el.textContent = msg;
            el.style.display = 'block';
        }

        async function doLogin() {
            const u = document.getElementById('login-username').value.trim();
            const p = document.getElementById('login-password').value;
            if (!u || !p) return showLoginError('Enter username and password');
            try {
                const res = await fetch('/api/auth/login', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ username: u, password: p })
                });
                const data = await res.json();
                if (data.error) return showLoginError(data.error);
                setTokens(data.access_token, data.refresh_token);
                location.reload();
            } catch (e) {
                showLoginError('Connection failed');
            }
        }

        async function doRegister() {
            const u = document.getElementById('login-username').value.trim();
            const p = document.getElementById('login-password').value;
            if (!u || !p) return showLoginError('Enter username and password');
            if (p.length < 6) return showLoginError('Password must be at least 6 characters');
            try {
                const res = await fetch('/api/auth/register', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ username: u, password: p })
                });
                const data = await res.json();
                if (data.error) return showLoginError(data.error);
                setTokens(data.access_token, data.refresh_token);
                location.reload();
            } catch (e) {
                showLoginError('Connection failed');
            }
        }

        function logout() {
            localStorage.removeItem('muleline_token');
            localStorage.removeItem('muleline_refresh');
            fetch('/api/auth/logout', { method: 'POST' }).catch(() => {});
            location.reload();
        }

        (function checkAuth() {
            const token = getToken();
            if (!token) {
                document.getElementById('login-overlay').classList.add('show');
            }
        })();

        document.getElementById('login-password').addEventListener('keydown', e => {
            if (e.key === 'Enter') doLogin();
        });

        const TOKEN = getToken();
        const API = '/sync';
        const hdrs = TOKEN
            ? { 'Authorization': `Bearer ${TOKEN}`, 'Content-Type': 'application/json' }
            : { 'Content-Type': 'application/json' };

        let allFiles = [];
        let currentFilter = '';
        let searchQuery = '';
        let gridSize = 'medium';
        let currentLightboxIndex = -1;
        let filteredFiles = [];
        let selectedIds = new Set();
        let uploadsPending = 0;
        let uploadsDone = 0;
        let searchDebounce = null;

        // --- Utilities ---

        function fmt(bytes) {
            if (bytes < 1024) return bytes + ' B';
            if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
            if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
            return (bytes / 1073741824).toFixed(2) + ' GB';
        }

        function timeAgo(ts) {
            if (!ts) return 'never';
            const s = Math.floor(Date.now() / 1000 - ts);
            if (s < 60) return 'just now';
            if (s < 3600) return Math.floor(s / 60) + 'm ago';
            if (s < 86400) return Math.floor(s / 3600) + 'h ago';
            return Math.floor(s / 86400) + 'd ago';
        }

        function formatDate(ts) {
            if (!ts) return '';
            const d = new Date(ts * 1000);
            const now = new Date();
            const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
            const fileDate = new Date(d.getFullYear(), d.getMonth(), d.getDate());
            const diff = Math.floor((today - fileDate) / 86400000);
            if (diff === 0) return 'Today';
            if (diff === 1) return 'Yesterday';
            if (diff < 7) return d.toLocaleDateString('en-US', { weekday: 'long' });
            return d.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: d.getFullYear() !== now.getFullYear() ? 'numeric' : undefined });
        }

        function isImage(mime) { return mime && mime.startsWith('image/'); }
        function isVideo(mime) { return mime && mime.startsWith('video/'); }
        function getExt(name) { return name.includes('.') ? name.split('.').pop().toUpperCase() : '?'; }

        // --- Navigation ---

        let currentAlbumId = null;

        function toggleMobileNav() {
            document.getElementById('main-nav').classList.toggle('mobile-open');
        }

        function showPage(page) {
            document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
            document.querySelectorAll('.header .nav button').forEach(b => b.classList.remove('active'));
            document.getElementById(`page-${page}`).classList.add('active');
            if (event && event.target && event.target.classList) event.target.classList.add('active');
            // Close mobile nav when selecting a page
            document.getElementById('main-nav').classList.remove('mobile-open');

            const uploadBar = document.getElementById('upload-bar');
            uploadBar.style.display = (page === 'photos' || page === 'files') ? 'flex' : 'none';

            if (page === 'activity') loadStatus();
            if (page === 'photos' || page === 'files') renderGallery();
            if (page === 'settings') loadServerInfo();
            if (page === 'albums') loadAlbums();
            if (page === 'people') loadPeople();
            if (page === 'trash') loadTrash();
            if (page === 'favorites') loadFavorites();
            if (page === 'map') loadMap();
            if (page === 'timeline') loadTimeline();
        }

        // --- Filter & View ---

        function setFilter(btn, filter) {
            currentFilter = filter;
            document.querySelectorAll('.filter-btns button').forEach(b => b.classList.remove('active'));
            btn.classList.add('active');
            renderGallery();
        }

        function setGridSize(size) {
            gridSize = size;
            document.querySelectorAll('.view-toggle button').forEach(b => b.classList.remove('active'));
            event.target.classList.add('active');
            renderGallery();
        }

        // --- Load files ---

        async function loadFiles() {
            try {
                let url;
                if (searchQuery) {
                    const params = new URLSearchParams({ q: searchQuery, limit: 500 });
                    if (currentFilter) params.set('type', currentFilter);
                    url = API + '/search?' + params;
                } else {
                    url = API + '/files?limit=500&mime=' + currentFilter;
                }
                const r = await fetch(url, { headers: hdrs });
                const d = await r.json();
                allFiles = d.files || [];
                renderGallery();
                updateStatsBar();
            } catch (e) { console.error('File load failed:', e); }
        }

        function updateStatsBar() {
            document.getElementById('stat-files').textContent = allFiles.length;
        }

        // --- Render gallery ---

        function renderGallery() {
            filteredFiles = searchQuery
                ? allFiles
                : (currentFilter ? allFiles.filter(f => f.mime_type && f.mime_type.startsWith(currentFilter)) : allFiles);

            const container = document.getElementById('photo-container');

            if (filteredFiles.length === 0) {
                const msg = searchQuery ? `No results for "${searchQuery}"` : 'No files yet';
                const sub = searchQuery
                    ? 'Try a different search term or clear the search.'
                    : 'Drop files anywhere on this page or click Upload to get started.';
                container.innerHTML = `
                    <div class="empty-state">
                        <div class="icon">&#128247;</div>
                        <div class="title">${msg}</div>
                        <div class="desc">${sub}</div>
                        ${!searchQuery ? '<button class="upload-btn-lg" onclick="document.getElementById(\'fileInput\').click()">Upload Files</button>' : ''}
                    </div>
                `;
                return;
            }

            const groups = {};
            for (const f of filteredFiles) {
                const dateKey = formatDate(f.created_at);
                if (!groups[dateKey]) groups[dateKey] = [];
                groups[dateKey].push(f);
            }

            let html = '';
            for (const [date, files] of Object.entries(groups)) {
                const sizeClass = gridSize === 'small' ? 'small' : gridSize === 'large' ? 'large' : '';
                html += `<div class="timeline-group">`;
                html += `<div class="timeline-date">${date} &middot; ${files.length} items</div>`;
                html += `<div class="photo-grid ${sizeClass}">`;
                for (const f of files) {
                    const globalIdx = filteredFiles.indexOf(f);
                    const isSelected = selectedIds.has(f.id) ? ' selected' : '';
                    if (isImage(f.mime_type)) {
                        html += `
                            <div class="photo-item${isSelected}" data-id="${f.id}" onclick="openLightbox(${globalIdx})">
                                <img src="${API}/files/${f.id}/thumb" loading="lazy" alt="${f.original_name}">
                                <div class="check" onclick="event.stopPropagation(); toggleSelect(this, ${f.id})">&#10003;</div>
                            </div>`;
                    } else if (isVideo(f.mime_type)) {
                        html += `
                            <div class="photo-item${isSelected}" data-id="${f.id}" onclick="openLightbox(${globalIdx})">
                                <img src="${API}/files/${f.id}/thumb" loading="lazy" alt="${f.original_name}">
                                <div class="badge">&#9654; VID</div>
                                <div class="check" onclick="event.stopPropagation(); toggleSelect(this, ${f.id})">&#10003;</div>
                            </div>`;
                    } else {
                        html += `
                            <div class="photo-item${isSelected}" data-id="${f.id}" onclick="openLightbox(${globalIdx})">
                                <div class="file-icon">
                                    <span style="font-size:32px">&#128196;</span>
                                    <span class="ext">${getExt(f.original_name)}</span>
                                </div>
                                <div class="badge">${fmt(f.file_size)}</div>
                                <div class="check" onclick="event.stopPropagation(); toggleSelect(this, ${f.id})">&#10003;</div>
                            </div>`;
                    }
                }
                html += `</div></div>`;
            }
            container.innerHTML = html;
        }

        function toggleSelect(el, fileId) {
            const item = el.parentElement;
            if (selectedIds.has(fileId)) {
                selectedIds.delete(fileId);
                item.classList.remove('selected');
            } else {
                selectedIds.add(fileId);
                item.classList.add('selected');
            }
            updateBulkBar();
        }

        function updateBulkBar() {
            const bar = document.getElementById('bulk-bar');
            const count = selectedIds.size;
            if (count > 0) {
                document.getElementById('bulk-count').textContent = `${count} selected`;
                bar.classList.add('show');
            } else {
                bar.classList.remove('show');
            }
        }

        function clearSelection() {
            selectedIds.clear();
            document.querySelectorAll('.photo-item.selected').forEach(el => el.classList.remove('selected'));
            updateBulkBar();
        }

        function selectAll() {
            filteredFiles.forEach(f => selectedIds.add(f.id));
            document.querySelectorAll('.photo-item').forEach(el => el.classList.add('selected'));
            updateBulkBar();
        }

        async function bulkDelete() {
            const ids = Array.from(selectedIds);
            if (!ids.length) return;
            if (!confirm(`Delete ${ids.length} file${ids.length > 1 ? 's' : ''}? This cannot be undone.`)) return;
            try {
                await fetch(API + '/files/bulk-delete', {
                    method: 'POST',
                    headers: hdrs,
                    body: JSON.stringify({ file_ids: ids }),
                });
                selectedIds.clear();
                updateBulkBar();
                loadFiles();
                loadStatus();
            } catch (e) { console.error('Bulk delete failed:', e); }
        }

        async function bulkDownload() {
            const ids = Array.from(selectedIds);
            if (!ids.length) return;
            try {
                const r = await fetch(API + '/files/bulk-download', {
                    method: 'POST',
                    headers: hdrs,
                    body: JSON.stringify({ file_ids: ids }),
                });
                if (!r.ok) return;
                const blob = await r.blob();
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = 'muleline-export.zip';
                a.click();
                URL.revokeObjectURL(url);
            } catch (e) { console.error('Bulk download failed:', e); }
        }

        function bulkAddToAlbum() {
            alert('Album support coming soon.');
        }

        // --- Search ---

        function onSearchInput() {
            clearTimeout(searchDebounce);
            searchDebounce = setTimeout(() => {
                searchQuery = document.getElementById('search-input').value.trim();
                loadFiles();
            }, 300);
        }

        // --- Lightbox ---

        function openLightbox(idx) {
            currentLightboxIndex = idx;
            const f = filteredFiles[idx];
            const lb = document.getElementById('lightbox');
            const img = document.getElementById('lightbox-img');
            const vid = document.getElementById('lightbox-video');

            // Pause any playing video
            vid.pause();

            if (isVideo(f.mime_type)) {
                img.style.display = 'none';
                vid.style.display = 'block';
                vid.src = `${API}/files/${f.id}/serve`;
            } else {
                vid.style.display = 'none';
                vid.src = '';
                img.style.display = '';
                img.src = isImage(f.mime_type) ? `${API}/files/${f.id}/serve` : `${API}/files/${f.id}/thumb`;
            }

            document.getElementById('lightbox-name').textContent = f.original_name;
            document.getElementById('lightbox-meta').textContent = `${fmt(f.file_size)} · ${f.mime_type} · ${timeAgo(f.created_at)}`;
            document.getElementById('lightbox-download').href = `${API}/files/${f.id}/serve`;
            document.getElementById('lightbox-download').download = f.original_name;
            lb.classList.add('show');
            document.body.style.overflow = 'hidden';
        }

        function closeLightbox() {
            const vid = document.getElementById('lightbox-video');
            vid.pause(); vid.src = '';
            document.getElementById('lightbox').classList.remove('show');
            document.body.style.overflow = '';
            currentLightboxIndex = -1;
        }

        function navLightbox(dir) {
            const newIdx = currentLightboxIndex + dir;
            if (newIdx >= 0 && newIdx < filteredFiles.length) {
                openLightbox(newIdx);
            }
        }

        document.addEventListener('keydown', e => {
            if (e.key === 'Escape') {
                if (currentLightboxIndex >= 0) {
                    closeLightbox();
                } else if (selectedIds.size > 0) {
                    clearSelection();
                }
                return;
            }
            if (currentLightboxIndex < 0) return;
            if (e.key === 'ArrowLeft') navLightbox(-1);
            if (e.key === 'ArrowRight') navLightbox(1);
        });

        async function deleteCurrentFile() {
            if (currentLightboxIndex < 0) return;
            const f = filteredFiles[currentLightboxIndex];
            if (!confirm(`Move "${f.original_name}" to trash?`)) return;
            try {
                await fetch(`${API}/files/${f.id}`, { method: 'DELETE', headers: hdrs });
                closeLightbox();
                loadFiles();
            } catch (e) { console.error('Delete failed:', e); }
        }

        // --- Upload ---

        const fileInput = document.getElementById('fileInput');
        fileInput.addEventListener('change', () => handleFiles(fileInput.files));

        // Global drag-and-drop
        let dragCounter = 0;
        document.addEventListener('dragenter', e => {
            e.preventDefault();
            dragCounter++;
            document.getElementById('drop-overlay').classList.add('show');
        });
        document.addEventListener('dragleave', e => {
            e.preventDefault();
            dragCounter--;
            if (dragCounter <= 0) {
                dragCounter = 0;
                document.getElementById('drop-overlay').classList.remove('show');
            }
        });
        document.addEventListener('dragover', e => e.preventDefault());
        document.addEventListener('drop', e => {
            e.preventDefault();
            dragCounter = 0;
            document.getElementById('drop-overlay').classList.remove('show');
            handleFiles(e.dataTransfer.files);
        });

        let uploadsFailed = 0;
        let uploadsDuped = 0;
        let uploadsActive = false;
        let uploadDismissTimer = null;

        function handleFiles(files) {
            if (files.length === 0) return;
            const panel = document.getElementById('upload-panel');
            // Reset if starting fresh
            if (!uploadsActive) {
                uploadsPending = 0;
                uploadsDone = 0;
                uploadsFailed = 0;
                uploadsDuped = 0;
                document.getElementById('upload-list').innerHTML = '';
            }
            uploadsActive = true;
            panel.classList.remove('hiding');
            panel.classList.add('show');
            // Force reflow for animation
            panel.offsetHeight;
            document.getElementById('upload-overall').style.display = '';
            uploadsPending += files.length;
            updateUploadProgress();
            for (const f of files) uploadFile(f);
        }

        function updateUploadProgress() {
            const done = uploadsDone + uploadsFailed + uploadsDuped;
            const pct = uploadsPending > 0 ? (done / uploadsPending * 100) : 0;
            document.getElementById('upload-count').textContent = `(${done}/${uploadsPending})`;

            const fill = document.getElementById('upload-overall-fill');
            fill.style.width = pct + '%';

            const left = document.getElementById('upload-summary-left');
            const right = document.getElementById('upload-summary-right');

            if (done < uploadsPending) {
                const active = uploadsPending - done;
                left.textContent = `Uploading ${active} file${active > 1 ? 's' : ''}...`;
                right.textContent = `${done}/${uploadsPending}`;
            } else {
                fill.classList.add('complete');
                const parts = [];
                if (uploadsDone > 0) parts.push(`${uploadsDone} uploaded`);
                if (uploadsDuped > 0) parts.push(`${uploadsDuped} skipped`);
                if (uploadsFailed > 0) parts.push(`${uploadsFailed} failed`);
                left.textContent = parts.join(', ');
                right.textContent = 'Complete';
                onAllUploadsComplete();
            }
        }

        function onAllUploadsComplete() {
            uploadsActive = false;
            // Auto-hide panel after 2s, show toast
            setTimeout(() => {
                hideUploadPanel();
                showUploadToast();
            }, 1500);
        }

        function hideUploadPanel() {
            const panel = document.getElementById('upload-panel');
            panel.classList.add('hiding');
            setTimeout(() => {
                panel.classList.remove('show', 'hiding');
                // Reset overall bar
                const fill = document.getElementById('upload-overall-fill');
                fill.style.width = '0%';
                fill.classList.remove('complete');
            }, 300);
        }

        function showUploadToast() {
            const toast = document.getElementById('upload-toast');
            const icon = document.getElementById('toast-icon');
            const msg = document.getElementById('toast-msg');

            if (uploadsFailed > 0) {
                icon.className = 'toast-icon partial';
                icon.innerHTML = '!';
                msg.textContent = `${uploadsDone} uploaded, ${uploadsFailed} failed`;
            } else if (uploadsDuped > 0 && uploadsDone === 0) {
                icon.className = 'toast-icon partial';
                icon.innerHTML = '&#8212;';
                msg.textContent = `${uploadsDuped} duplicate${uploadsDuped > 1 ? 's' : ''} skipped`;
            } else {
                icon.className = 'toast-icon success';
                icon.innerHTML = '&#10003;';
                const total = uploadsDone + uploadsDuped;
                msg.textContent = total === 1 ? 'Upload complete' : `${total} files uploaded`;
            }

            // Reset animation
            const dismiss = toast.querySelector('.toast-dismiss');
            dismiss.innerHTML = '<div class="shrink"></div>';

            toast.classList.add('show');
            clearTimeout(uploadDismissTimer);
            uploadDismissTimer = setTimeout(() => {
                toast.classList.remove('show');
            }, 4000);
        }

        function fileIcon(status) {
            if (status === 'done') return '&#10003;';
            if (status === 'failed') return '&#10007;';
            if (status === 'dedup') return '&#8212;';
            return '<div style="width:14px;height:14px;border:2px solid var(--accent);border-top-color:transparent;border-radius:50%;animation:spin .8s linear infinite"></div>';
        }

        async function uploadFile(file) {
            const list = document.getElementById('upload-list');

            const row = document.createElement('div');
            row.className = 'upload-panel-item';
            const ext = file.name.split('.').pop().toUpperCase();
            row.innerHTML = `
                <div class="icon uploading">${fileIcon('uploading')}</div>
                <div class="info">
                    <div class="name">${file.name}</div>
                    <div class="meta"><span>${fmt(file.size)}</span><span>${ext}</span></div>
                    <div class="file-progress"><div class="fill" style="width:0%"></div></div>
                </div>
            `;
            list.prepend(row);
            const iconEl = row.querySelector('.icon');
            const metaEl = row.querySelector('.meta');
            const progressEl = row.querySelector('.file-progress');
            const fill = progressEl.querySelector('.fill');

            function setStatus(status, label) {
                iconEl.className = 'icon ' + status;
                iconEl.innerHTML = fileIcon(status);
                if (label) {
                    metaEl.innerHTML = `<span>${fmt(file.size)}</span><span>${label}</span>`;
                }
                if (status !== 'uploading') {
                    progressEl.style.display = 'none';
                }
            }

            try {
                const buf = await file.arrayBuffer();
                const hashBuf = await crypto.subtle.digest('SHA-256', buf);
                const hash = Array.from(new Uint8Array(hashBuf)).map(b => b.toString(16).padStart(2, '0')).join('');

                metaEl.innerHTML = `<span>${fmt(file.size)}</span><span>Initializing...</span>`;
                const init = await fetch(API + '/upload/init', {
                    method: 'POST',
                    headers: hdrs,
                    body: JSON.stringify({
                        original_name: file.name,
                        total_size: file.size,
                        file_hash: hash,
                        mime_type: file.type,
                    }),
                }).then(r => r.json());

                if (init.duplicate) {
                    setStatus('dedup', 'Already exists');
                    uploadsDuped++;
                    updateUploadProgress();
                    return;
                }
                if (init.error) {
                    setStatus('failed', init.error);
                    uploadsFailed++;
                    updateUploadProgress();
                    return;
                }

                const { upload_id, chunk_size, total_chunks } = init;

                for (let i = 0; i < total_chunks; i++) {
                    const start = i * chunk_size;
                    const end = Math.min(start + chunk_size, file.size);
                    const chunk = file.slice(start, end);

                    const form = new FormData();
                    form.append('chunk_index', i);
                    form.append('chunk', chunk, file.name);

                    const pct = ((i + 1) / total_chunks * 100);
                    metaEl.innerHTML = `<span>${fmt(file.size)}</span><span>${Math.round(pct)}%</span>`;

                    await fetch(API + `/upload/${upload_id}/chunk`, {
                        method: 'POST',
                        headers: { 'Authorization': `Bearer ${TOKEN}` },
                        body: form,
                    });
                    fill.style.width = `${pct.toFixed(0)}%`;
                }

                const complete = await fetch(API + `/upload/${upload_id}/complete`, {
                    method: 'POST',
                    headers: hdrs,
                }).then(r => r.json());

                if (complete.ok) {
                    setStatus('done', 'Uploaded');
                    fill.style.width = '100%';
                    uploadsDone++;
                    updateUploadProgress();
                    loadFiles();
                    loadStatus();
                } else {
                    setStatus('failed', complete.error || 'Failed');
                    uploadsFailed++;
                    updateUploadProgress();
                }
            } catch (e) {
                setStatus('failed', 'Network error');
                uploadsFailed++;
                updateUploadProgress();
                console.error(e);
            }
        }

        // --- Status & Devices ---

        async function loadStatus() {
            try {
                const r = await fetch(API + '/status', { headers: hdrs });
                const d = await r.json();
                document.getElementById('stat-files').textContent = d.stats.total_files;
                document.getElementById('stat-bytes').textContent = fmt(d.stats.total_synced_bytes);

                const al = document.getElementById('activity-list');
                if (d.recent_activity.length === 0) {
                    al.innerHTML = '<div class="card-empty">No activity yet</div>';
                } else {
                    al.innerHTML = d.recent_activity.map(a => `
                        <div class="activity-row">
                            <span class="time">${timeAgo(a.created_at)}</span>
                            <span class="action">${a.action}</span>
                            <span class="detail">${a.file_hash ? a.file_hash.slice(0, 12) + '...' : ''} ${a.bytes_transferred ? fmt(a.bytes_transferred) : ''} ${a.status !== 'ok' ? '(' + a.status + ')' : ''}</span>
                        </div>
                    `).join('');
                }
            } catch (e) { console.error('Status load failed:', e); }
        }

        // --- Settings ---

        const backendFields = ['local', 's3', 'webdav', 'sftp', 'dropbox', 'gdrive'];

        const providerNames = {
            local: 'Local', s3: 'S3 / R2 / MinIO', webdav: 'WebDAV',
            sftp: 'SFTP', dropbox: 'Dropbox', gdrive: 'Google Drive'
        };

        function selectProvider(backend) {
            // Update card selection
            document.querySelectorAll('.provider-card').forEach(c => c.classList.remove('selected'));
            document.querySelector(`.provider-card[data-backend="${backend}"]`).classList.add('selected');
            // Update config title
            document.getElementById('provider-config-title').textContent = providerNames[backend] + ' Configuration';
            // Show/hide field groups
            backendFields.forEach(b => {
                document.getElementById(`fields-${b}`).style.display = b === backend ? '' : 'none';
            });
        }

        // Keep old name for compat
        function onBackendChange() {
            selectProvider(document.getElementById('backend-type')?.value || 'local');
        }

        async function loadServerInfo() {
            document.getElementById('logged-in-as').textContent = TOKEN;
            try {
                const r = await fetch(API + '/server/info', { headers: hdrs });
                const d = await r.json();
                document.getElementById('info-version').textContent = d.version || '—';
                document.getElementById('info-uptime').textContent = d.uptime || '—';
                document.getElementById('info-files').textContent = d.total_files ?? '—';
                document.getElementById('info-storage').textContent = fmt(d.storage_used_bytes || 0);
                document.getElementById('info-backend').textContent = d.backend_type || '—';
            } catch (e) { console.error('Server info load failed:', e); }
            checkGmailStatus();
        }

        function gatherSettings() {
            const type = document.getElementById('backend-type').value;
            const base = { backend_type: type };
            if (type === 'local') {
                base.base_dir = document.getElementById('local-base-dir').value;
            } else if (type === 's3') {
                base.bucket = document.getElementById('s3-bucket').value;
                base.endpoint_url = document.getElementById('s3-endpoint').value;
                base.access_key = document.getElementById('s3-access-key').value;
                base.secret_key = document.getElementById('s3-secret-key').value;
                base.region = document.getElementById('s3-region').value;
                base.prefix = document.getElementById('s3-prefix').value;
            } else if (type === 'webdav') {
                base.url = document.getElementById('webdav-url').value;
                base.username = document.getElementById('webdav-username').value;
                base.password = document.getElementById('webdav-password').value;
                base.prefix = document.getElementById('webdav-prefix').value;
            } else if (type === 'sftp') {
                base.host = document.getElementById('sftp-host').value;
                base.port = parseInt(document.getElementById('sftp-port').value) || 22;
                base.username = document.getElementById('sftp-username').value;
                base.password = document.getElementById('sftp-password').value;
                base.key_path = document.getElementById('sftp-key-path').value;
                base.base_path = document.getElementById('sftp-base-path').value;
            } else if (type === 'dropbox') {
                base.access_token = document.getElementById('dropbox-token').value;
                base.root_path = document.getElementById('dropbox-root').value;
            } else if (type === 'gdrive') {
                base.credentials_json = document.getElementById('gdrive-creds').value;
                base.folder_id = document.getElementById('gdrive-folder').value;
            }
            return base;
        }

        async function testConnection() {
            const btn = document.getElementById('test-btn');
            btn.textContent = 'Testing...';
            btn.disabled = true;
            await new Promise(r => setTimeout(r, 800));
            btn.textContent = 'Connected';
            btn.classList.add('success');
            setTimeout(() => {
                btn.textContent = 'Test Connection';
                btn.classList.remove('success');
                btn.disabled = false;
            }, 2000);
        }

        async function saveSettings() {
            const payload = gatherSettings();
            try {
                await fetch(API + '/server/settings', {
                    method: 'POST',
                    headers: hdrs,
                    body: JSON.stringify(payload),
                });
            } catch (e) { console.error('Save settings failed:', e); }
        }

        async function importTakeout() {
            const fileInput = document.getElementById('takeout-file');
            const statusDiv = document.getElementById('import-status');
            if (!fileInput.files.length) { alert('Select a ZIP file first.'); return; }
            const file = fileInput.files[0];
            if (!file.name.toLowerCase().endsWith('.zip')) { alert('Please select a .zip file.'); return; }
            statusDiv.style.display = 'block';
            statusDiv.textContent = 'Uploading and importing... This may take a while for large files.';
            try {
                const form = new FormData();
                form.append('file', file);
                const r = await fetch(API + '/import/google-photos', {
                    method: 'POST',
                    headers: { 'Authorization': hdrs['Authorization'] },
                    body: form
                });
                const d = await r.json();
                if (d.error) {
                    statusDiv.innerHTML = `<span style="color:#e74c3c;">Error: ${esc(d.error)}</span>`;
                } else {
                    statusDiv.innerHTML = `<span style="color:#27ae60;">Done!</span> Imported: ${d.imported}, Duplicates skipped: ${d.skipped_duplicate}, Unsupported: ${d.skipped_unsupported}, Errors: ${d.errors}`;
                    loadPhotos();
                }
            } catch (e) {
                statusDiv.innerHTML = '<span style="color:#e74c3c;">Upload failed. Check your connection.</span>';
                console.error('Import failed:', e);
            }
        }

        async function checkGmailStatus() {
            try {
                const r = await fetch(API + '/connectors/gmail/status', { headers: hdrs });
                const d = await r.json();
                const el = document.getElementById('gmail-status');
                if (d.authenticated) {
                    el.innerHTML = `Connected: <strong style="color:var(--text)">${esc(d.email)}</strong> (${d.total_messages} messages)`;
                } else {
                    el.textContent = 'Not connected. Run in terminal: python -m muleline.connectors.gmail auth';
                }
            } catch (e) { document.getElementById('gmail-status').textContent = 'Could not check status.'; }
        }

        async function importGmail() {
            const query = document.getElementById('gmail-query').value.trim() || 'has:attachment';
            const max = document.getElementById('gmail-max').value || '50';
            const statusDiv = document.getElementById('gmail-import-status');
            statusDiv.style.display = 'block';
            statusDiv.textContent = 'Scanning Gmail... This may take a while.';
            try {
                const r = await fetch(API + `/connectors/gmail/import?query=${encodeURIComponent(query)}&max_results=${max}`, {
                    method: 'POST', headers: hdrs
                });
                const d = await r.json();
                if (d.error) {
                    statusDiv.innerHTML = `<span style="color:#e74c3c;">${esc(d.error)}</span>`;
                } else {
                    statusDiv.innerHTML = `<span style="color:#27ae60;">Done!</span> Scanned: ${d.messages_scanned} messages. Imported: ${d.imported}, Duplicates: ${d.skipped_duplicate}, Errors: ${d.errors}`;
                    if (d.imported > 0) loadPhotos();
                }
            } catch (e) {
                statusDiv.innerHTML = '<span style="color:#e74c3c;">Import failed.</span>';
            }
        }

        // --- Albums ---

        function showCreateAlbum() { document.getElementById('album-create-form').style.display = 'block'; }
        function hideCreateAlbum() { document.getElementById('album-create-form').style.display = 'none'; }

        async function createAlbum() {
            const name = document.getElementById('album-name-input').value.trim();
            if (!name) return;
            const desc = document.getElementById('album-desc-input').value.trim();
            try {
                await fetch(API + '/albums', {
                    method: 'POST', headers: { ...hdrs, 'Content-Type': 'application/json' },
                    body: JSON.stringify({ name, description: desc })
                });
                document.getElementById('album-name-input').value = '';
                document.getElementById('album-desc-input').value = '';
                hideCreateAlbum();
                loadAlbums();
            } catch (e) { console.error('Create album failed:', e); }
        }

        async function loadAlbums() {
            try {
                const r = await fetch(API + '/albums', { headers: hdrs });
                const d = await r.json();
                const albums = d.albums || [];
                const grid = document.getElementById('albums-grid');
                if (albums.length === 0) {
                    grid.innerHTML = '<div class="empty-state"><div class="icon">&#128218;</div><div class="title">No albums yet</div><div class="desc">Create an album to organize your photos.</div></div>';
                    return;
                }
                grid.innerHTML = albums.map(a => `
                    <div class="album-card" onclick="openAlbum(${a.id})">
                        <div class="album-cover">
                            ${a.cover_file_id ? `<img src="${API}/files/${a.cover_file_id}/thumb" loading="lazy">` : '&#128247;'}
                        </div>
                        <div class="album-info">
                            <div class="name">${esc(a.name)}</div>
                            <div class="count">${a.file_count || 0} items</div>
                        </div>
                    </div>
                `).join('');
            } catch (e) { console.error('Load albums failed:', e); }
        }

        async function openAlbum(id) {
            currentAlbumId = id;
            try {
                const r = await fetch(API + '/albums/' + id, { headers: hdrs });
                const d = await r.json();
                document.getElementById('album-detail-name').textContent = d.name;
                document.getElementById('album-detail-desc').textContent = d.description || '';
                const files = d.files || [];
                const grid = document.getElementById('album-detail-grid');
                if (files.length === 0) {
                    grid.innerHTML = '<div class="empty-state"><div class="icon">&#128247;</div><div class="title">Empty album</div><div class="desc">Add photos from the Photos tab.</div></div>';
                } else {
                    grid.innerHTML = files.map(f => `
                        <div class="photo-item" data-id="${f.id}">
                            <img src="${API}/files/${f.id}/thumb" loading="lazy" alt="${esc(f.original_name)}">
                            ${isVideo(f.mime_type) ? '<div class="badge">&#9654; VID</div>' : ''}
                        </div>
                    `).join('');
                }
                document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
                document.getElementById('page-album-detail').classList.add('active');
            } catch (e) { console.error('Open album failed:', e); }
        }

        async function deleteCurrentAlbum() {
            if (!currentAlbumId || !confirm('Delete this album? Files are not deleted.')) return;
            try {
                await fetch(API + '/albums/' + currentAlbumId, { method: 'DELETE', headers: hdrs });
                showPage('albums');
                loadAlbums();
            } catch (e) { console.error('Delete album failed:', e); }
        }

        function esc(s) {
            const d = document.createElement('div');
            d.textContent = s;
            return d.innerHTML;
        }

        // --- People & Faces ---

        function showCreatePerson() { document.getElementById('person-create-form').style.display = 'block'; }
        function hideCreatePerson() { document.getElementById('person-create-form').style.display = 'none'; }

        async function createPerson() {
            const name = document.getElementById('person-name-input').value.trim();
            if (!name) return;
            try {
                await fetch(API + '/people', {
                    method: 'POST', headers: { ...hdrs, 'Content-Type': 'application/json' },
                    body: JSON.stringify({ name })
                });
                document.getElementById('person-name-input').value = '';
                hideCreatePerson();
                loadPeople();
            } catch (e) { console.error('Create person failed:', e); }
        }

        async function loadPeople() {
            try {
                // Load known people, face stats, and unassigned faces in parallel
                const [peopleRes, statsRes, unassignedRes] = await Promise.all([
                    fetch(API + '/people', { headers: hdrs }),
                    fetch(API + '/faces/stats', { headers: hdrs }),
                    fetch(API + '/faces/unassigned', { headers: hdrs })
                ]);
                const people = (await peopleRes.json()).people || [];
                const stats = await statsRes.json();
                const unassigned = (await unassignedRes.json()).faces || [];

                // Stats bar
                document.getElementById('face-stats').textContent =
                    `${stats.total_faces || 0} faces detected · ${stats.matched_faces || 0} matched · ${stats.unmatched_faces || 0} unassigned · ${people.length} people`;

                // People grid
                const grid = document.getElementById('people-grid');
                if (people.length === 0) {
                    grid.innerHTML = '<div style="color:var(--text3); font-size:13px; padding:8px;">No people added yet. Add a person or run auto-cluster on your photos.</div>';
                } else {
                    grid.innerHTML = people.map(p => `
                        <div class="person-card" onclick="viewPerson(${p.id})">
                            <div class="person-avatar">${p.face_count > 0 ? `<img src="${API}/faces/person/${p.id}?crop=1" onerror="this.parentElement.textContent='&#128100;'">` : '&#128100;'}</div>
                            <div class="person-name">${esc(p.name)}</div>
                            <div class="person-count">${p.face_count || 0} photos</div>
                        </div>
                    `).join('');
                }

                // Unassigned faces
                const ugrid = document.getElementById('unassigned-grid');
                if (unassigned.length === 0) {
                    ugrid.innerHTML = '<div style="color:var(--text3); font-size:13px;">No unassigned faces. Upload photos or run face scan.</div>';
                } else {
                    ugrid.innerHTML = unassigned.map(f => `
                        <div class="face-card">
                            ${f.crop_filename ? `<img class="face-crop" src="${API}/faces/crop/${f.crop_filename}" onerror="this.style.display='none'">` : '<div class="face-crop" style="display:flex;align-items:center;justify-content:center;">?</div>'}
                            <div class="face-actions">
                                <select onchange="assignFace(${f.id}, this.value)">
                                    <option value="">Assign...</option>
                                    ${people.map(p => `<option value="${p.id}">${esc(p.name)}</option>`).join('')}
                                </select>
                            </div>
                        </div>
                    `).join('');
                }
            } catch (e) { console.error('Load people failed:', e); }
        }

        async function assignFace(faceId, personId) {
            if (!personId) return;
            try {
                await fetch(API + '/faces/assign/' + faceId + '/' + personId, { method: 'POST', headers: hdrs });
                loadPeople();
            } catch (e) { console.error('Assign face failed:', e); }
        }

        async function clusterFaces() {
            try {
                const r = await fetch(API + '/faces/cluster', { method: 'POST', headers: hdrs });
                const d = await r.json();
                alert(`Clustering complete: ${d.clusters_found || 0} clusters from ${d.faces_processed || 0} faces`);
                loadPeople();
            } catch (e) { console.error('Cluster failed:', e); }
        }

        async function viewPerson(personId) {
            try {
                const r = await fetch(API + '/faces/person/' + personId, { headers: hdrs });
                const d = await r.json();
                // Show the person's faces in lightbox-style grid — reuse album detail view
                const faces = d.faces || [];
                document.getElementById('album-detail-name').textContent = d.person_name || 'Person';
                document.getElementById('album-detail-desc').textContent = `${faces.length} photos`;
                const grid = document.getElementById('album-detail-grid');
                if (faces.length === 0) {
                    grid.innerHTML = '<div class="empty-state"><div class="icon">&#128100;</div><div class="title">No photos</div></div>';
                } else {
                    grid.innerHTML = faces.map(f => `
                        <div class="photo-item">
                            ${f.crop_filename ? `<img src="${API}/faces/crop/${f.crop_filename}" loading="lazy">` : '<div style="width:100%;aspect-ratio:1;background:var(--surface2);display:flex;align-items:center;justify-content:center;">&#128100;</div>'}
                        </div>
                    `).join('');
                }
                document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
                document.getElementById('page-album-detail').classList.add('active');
            } catch (e) { console.error('View person failed:', e); }
        }

        // --- Trash ---

        let trashFiles = [];

        async function loadTrash() {
            try {
                const r = await fetch(API + '/trash', { headers: hdrs });
                const d = await r.json();
                trashFiles = d.files || [];
                renderTrash();
            } catch (e) { console.error('Load trash failed:', e); }
        }

        function renderTrash() {
            const grid = document.getElementById('trash-grid');
            const empty = document.getElementById('trash-empty');
            const info = document.getElementById('trash-info');
            if (trashFiles.length === 0) {
                grid.innerHTML = '';
                empty.style.display = 'flex';
                info.textContent = '';
                return;
            }
            empty.style.display = 'none';
            info.textContent = `${trashFiles.length} item${trashFiles.length !== 1 ? 's' : ''} in trash`;
            grid.innerHTML = trashFiles.map(f => {
                const isImg = isImage(f.mime_type);
                const thumb = isImg ? `<img src="${API}/files/${f.id}/thumb" loading="lazy">` :
                    `<div class="file-icon"><span class="ext">${getExt(f.original_name)}</span></div>`;
                return `<div class="photo-item trash-item">
                    ${thumb}
                    <div class="badge">${timeAgo(f.deleted_at)}</div>
                    <div class="trash-actions">
                        <button class="restore-btn" onclick="restoreFile(${f.id})">Restore</button>
                        <button class="purge-btn" onclick="purgeFile(${f.id})">Delete</button>
                    </div>
                </div>`;
            }).join('');
        }

        async function restoreFile(id) {
            try {
                await fetch(`${API}/files/${id}/restore`, { method: 'POST', headers: hdrs });
                loadTrash();
                loadFiles();
            } catch (e) { console.error('Restore failed:', e); }
        }

        async function purgeFile(id) {
            if (!confirm('Permanently delete this file? This cannot be undone.')) return;
            try {
                await fetch(`${API}/files/${id}/purge`, { method: 'DELETE', headers: hdrs });
                loadTrash();
            } catch (e) { console.error('Purge failed:', e); }
        }

        async function emptyTrash() {
            if (!confirm(`Permanently delete all ${trashFiles.length} items in trash?`)) return;
            try {
                await fetch(API + '/trash/empty', { method: 'POST', headers: hdrs });
                loadTrash();
            } catch (e) { console.error('Empty trash failed:', e); }
        }

        // --- Favorites ---

        let favFiles = [];
        let favSet = new Set();

        async function loadFavorites() {
            try {
                const r = await fetch(API + '/favorites', { headers: hdrs });
                const d = await r.json();
                favFiles = d.files || [];
                favSet = new Set(favFiles.map(f => f.id));
                renderFavorites();
            } catch (e) { console.error('Load favorites failed:', e); }
        }

        function renderFavorites() {
            const grid = document.getElementById('favorites-grid');
            const empty = document.getElementById('favorites-empty');
            if (favFiles.length === 0) {
                grid.innerHTML = '';
                empty.style.display = 'flex';
                return;
            }
            empty.style.display = 'none';
            grid.innerHTML = favFiles.map((f, idx) => {
                const isImg = isImage(f.mime_type);
                const thumb = isImg ? `<img src="${API}/files/${f.id}/thumb" loading="lazy">` :
                    `<div class="file-icon"><span class="ext">${getExt(f.original_name)}</span></div>`;
                return `<div class="photo-item" onclick="openFavLightbox(${idx})">
                    ${thumb}
                    <div class="fav-heart show">&#9829;</div>
                </div>`;
            }).join('');
        }

        function openFavLightbox(idx) {
            filteredFiles = favFiles;
            openLightbox(idx);
        }

        async function toggleFavorite(fileId) {
            try {
                const r = await fetch(`${API}/files/${fileId}/favorite`, { method: 'POST', headers: hdrs });
                const d = await r.json();
                if (d.favorited) {
                    favSet.add(fileId);
                } else {
                    favSet.delete(fileId);
                }
                return d.favorited;
            } catch (e) {
                console.error('Toggle favorite failed:', e);
                return null;
            }
        }

        async function toggleLightboxFav() {
            if (currentLightboxIndex < 0) return;
            const f = filteredFiles[currentLightboxIndex];
            const result = await toggleFavorite(f.id);
            if (result !== null) {
                updateFavButton(result);
            }
        }

        function updateFavButton(isFav) {
            const btn = document.getElementById('lightbox-fav');
            btn.innerHTML = isFav ? '&#9829;' : '&#9825;';
            btn.classList.toggle('fav-active', isFav);
        }

        // --- EXIF Panel ---

        function toggleExifPanel() {
            const panel = document.getElementById('exif-panel');
            if (panel.style.display === 'none') {
                panel.style.display = 'block';
                loadExifInfo();
            } else {
                panel.style.display = 'none';
            }
        }

        async function loadExifInfo() {
            if (currentLightboxIndex < 0) return;
            const f = filteredFiles[currentLightboxIndex];
            const content = document.getElementById('exif-content');

            let rows = [
                ['Filename', f.original_name],
                ['Type', f.mime_type],
                ['Size', fmt(f.file_size)],
            ];

            if (f.width && f.height) rows.push(['Dimensions', `${f.width} x ${f.height}`]);
            if (f.date_taken) rows.push(['Date Taken', new Date(f.date_taken * 1000).toLocaleString()]);
            rows.push(['Uploaded', new Date(f.created_at * 1000).toLocaleString()]);
            if (f.file_hash) rows.push(['Hash', f.file_hash.substring(0, 16) + '...']);

            if (f.gps_lat && f.gps_lon) {
                rows.push(['GPS', `${f.gps_lat.toFixed(6)}, ${f.gps_lon.toFixed(6)}`]);
                rows.push(['Map', `<a href="https://www.openstreetmap.org/?mlat=${f.gps_lat}&mlon=${f.gps_lon}#map=15/${f.gps_lat}/${f.gps_lon}" target="_blank" rel="noopener" style="color:var(--accent);">View on map</a>`]);
            }

            content.innerHTML = rows.map(([label, value]) =>
                `<div style="display:flex; justify-content:space-between; padding:8px 0; border-bottom:1px solid var(--border);">
                    <span style="color:var(--text2);">${label}</span>
                    <span style="color:var(--text); text-align:right; max-width:180px; overflow:hidden; text-overflow:ellipsis;">${value}</span>
                </div>`
            ).join('');

            // Try to load faces for this file
            try {
                const r = await fetch(`${API}/faces/item/${f.id}`, { headers: hdrs });
                const d = await r.json();
                if (d.faces && d.faces.length > 0) {
                    content.innerHTML += `
                        <div style="margin-top:12px; font-size:13px; font-weight:600; color:var(--text); margin-bottom:8px;">People in this photo</div>
                        ${d.faces.map(face => `
                            <div style="display:flex; align-items:center; gap:8px; padding:6px 0;">
                                ${face.crop_filename ? `<img src="${API}/faces/crop/${face.crop_filename}" style="width:32px;height:32px;border-radius:50%;object-fit:cover;">` : ''}
                                <span style="color:var(--text);">${face.person_name || 'Unknown'}</span>
                            </div>
                        `).join('')}
                    `;
                }
            } catch (e) {}
        }

        // --- Map ---

        let mapInstance = null;
        let mapMarkers = null;

        async function loadMap() {
            const container = document.getElementById('map-container');
            if (!mapInstance) {
                mapInstance = L.map(container).setView([39.8, -98.5], 4);
                L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                    maxZoom: 19,
                    attribution: '&copy; OpenStreetMap'
                }).addTo(mapInstance);
                mapMarkers = L.layerGroup().addTo(mapInstance);
            }
            // Force a resize since map might have been initialized in a hidden container
            setTimeout(() => mapInstance.invalidateSize(), 100);

            // Get all files with GPS data
            const geoFiles = allFiles.filter(f => f.gps_lat && f.gps_lon);
            mapMarkers.clearLayers();

            if (geoFiles.length === 0) {
                return;
            }

            const bounds = [];
            geoFiles.forEach(f => {
                const lat = f.gps_lat, lon = f.gps_lon;
                bounds.push([lat, lon]);
                const marker = L.marker([lat, lon]);
                const isImg = isImage(f.mime_type);
                const popup = `
                    <div style="text-align:center; max-width:200px;">
                        ${isImg ? `<img src="${API}/files/${f.id}/thumb" style="width:160px; border-radius:6px; margin-bottom:6px;">` : ''}
                        <div style="font-size:12px; font-weight:600;">${esc(f.original_name)}</div>
                        <div style="font-size:11px; color:#666;">${f.date_taken ? new Date(f.date_taken * 1000).toLocaleDateString() : ''}</div>
                    </div>
                `;
                marker.bindPopup(popup);
                marker.on('click', () => {
                    const idx = allFiles.indexOf(f);
                    if (idx >= 0) {
                        filteredFiles = allFiles;
                        openLightbox(idx);
                    }
                });
                mapMarkers.addLayer(marker);
            });

            if (bounds.length > 0) {
                mapInstance.fitBounds(bounds, { padding: [30, 30], maxZoom: 12 });
            }
        }

        // Update openLightbox to check favorite status and hide EXIF panel
        const _origOpenLightbox = openLightbox;
        openLightbox = function(idx) {
            document.getElementById('exif-panel').style.display = 'none';
            _origOpenLightbox(idx);
            const f = filteredFiles[idx];
            updateFavButton(favSet.has(f.id));
        };

        // --- Timeline ---
        let timelineGroupBy = 'day';
        let timelineData = [];

        function setTimelineGroup(group, btn) {
            timelineGroupBy = group;
            document.querySelectorAll('.timeline-group-btn').forEach(b => b.classList.remove('active'));
            btn.classList.add('active');
            loadTimeline();
        }

        async function loadTimeline() {
            const container = document.getElementById('timeline-container');
            container.innerHTML = '<div class="empty-state"><div class="desc">Loading timeline...</div></div>';
            try {
                const r = await fetch(API + `/timeline?group_by=${timelineGroupBy}`, { headers: hdrs });
                const d = await r.json();
                timelineData = d.groups || [];
                renderTimeline();
            } catch (e) {
                container.innerHTML = '<div class="empty-state"><div class="icon">&#128197;</div><div class="title">Failed to load timeline</div></div>';
                console.error('Timeline load failed:', e);
            }
        }

        function renderTimeline() {
            const container = document.getElementById('timeline-container');
            if (timelineData.length === 0) {
                container.innerHTML = '<div class="empty-state"><div class="icon">&#128197;</div><div class="title">No photos yet</div><div class="desc">Upload some photos to see your timeline.</div></div>';
                return;
            }
            container.innerHTML = timelineData.map((group, gi) => {
                const dateStr = formatTimelineDate(group.date, timelineGroupBy);
                const items = group.items || [];
                return `
                    <div class="timeline-group">
                        <div class="timeline-header">
                            <span class="timeline-date">${dateStr}</span>
                            <span class="timeline-count">${group.count} item${group.count !== 1 ? 's' : ''}</span>
                        </div>
                        <div class="photo-grid" style="display:grid; grid-template-columns:repeat(auto-fill, minmax(140px, 1fr)); gap:4px;">
                            ${items.map((item, idx) => `
                                <div class="photo-item" data-id="${item.id}" onclick="openTimelineItem(${gi}, ${idx})" style="aspect-ratio:1; overflow:hidden; border-radius:4px; cursor:pointer;">
                                    <img src="${API}/files/${item.id}/thumb" loading="lazy" alt="${esc(item.original_name)}" style="width:100%; height:100%; object-fit:cover;">
                                    ${isVideo(item.mime_type) ? '<div class="badge" style="position:absolute; bottom:4px; left:4px; background:rgba(0,0,0,0.7); color:#fff; padding:2px 6px; border-radius:4px; font-size:10px;">&#9654; VID</div>' : ''}
                                </div>
                            `).join('')}
                        </div>
                    </div>
                `;
            }).join('');
        }

        function formatTimelineDate(dateStr, groupBy) {
            if (!dateStr) return 'Unknown';
            if (groupBy === 'year') return dateStr;
            const parts = dateStr.split('-');
            const months = ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
            if (groupBy === 'month') return months[parseInt(parts[1])] + ' ' + parts[0];
            // day
            const d = new Date(dateStr + 'T00:00:00');
            const opts = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
            return d.toLocaleDateString(undefined, opts);
        }

        function openTimelineItem(groupIdx, itemIdx) {
            const group = timelineData[groupIdx];
            if (!group) return;
            // Build a filteredFiles from this group's items
            filteredFiles = group.items;
            openLightbox(itemIdx);
        }

        // --- Init ---
        loadFiles();
        loadFavorites();
        loadStatus();
        setInterval(loadStatus, 30000);

        // --- PWA ---
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('/static/sw.js').catch(() => {});
        }
    </script>
</body>
</html>
