diff --git a/frontend/index.html b/frontend/index.html
index 65dd8e3..529d6dd 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -191,6 +191,9 @@
min-width: 80px;
}
+ .browser-safari .tab .tab-icon {
+ display: none;
+ }
.browser-safari .tab:hover {
background: var(--tab-hover);
@@ -205,6 +208,9 @@
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
+ .browser-safari .tab-active .tab-icon {
+ display: inline;
+ }
.tab-about .tab-title {
font-weight: 600;
@@ -297,11 +303,17 @@
border-radius: 8px;
}
+ .browser-safari .iframe-urlbar .url-title {
+ display: none;
+ }
.iframe-urlbar .tab-icon {
flex-shrink: 0;
}
+ .url-title-mobile {
+ display: none;
+ }
.iframe-urlbar .url-text {
font-size: 14px;
@@ -330,36 +342,26 @@
min-width: 0;
}
+ /* Mobile: title above address bar */
@media (max-width: 640px) {
+ .iframe-toolbar {
+ flex-wrap: wrap;
+ }
+ .iframe-toolbar .url-title-mobile {
+ width: 100%;
+ font-size: 13px;
+ color: var(--text);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding: 0 4px 4px;
+ order: -1;
+ }
.iframe-urlbar .url-title {
display: none;
}
}
- /* Title bar above address bar — hidden on desktop (title goes in urlbar) */
- .iframe-titlebar {
- display: none;
- padding: 6px 12px 2px;
- background: var(--tab-bg);
- font-size: 13px;
- color: var(--text);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-
- @media (max-width: 640px) {
- .iframe-titlebar {
- display: block;
- text-align: center;
- }
- }
-
- /* Paused marquee row */
- .tab-row.paused {
- animation-play-state: paused !important;
- }
-
.iframe-close {
background: none;
border: none;
@@ -377,7 +379,7 @@
.iframe-viewer iframe {
width: 100%;
- height: calc(75vh - 78px);
+ height: calc(75vh - 56px);
border: none;
background: white;
}
diff --git a/frontend/site.js b/frontend/site.js
index 9aa1c38..8a8ef42 100644
--- a/frontend/site.js
+++ b/frontend/site.js
@@ -17,8 +17,6 @@ const loadingEl = document.getElementById("loading");
// Detect browser and OS, set classes on body for tab styling
function detectBrowser() {
const ua = navigator.userAgent;
- // Mobile defaults to chrome styling
- if (/iPhone|iPad|Android/.test(ua)) return "chrome";
if (ua.includes("Firefox")) return "firefox";
if (ua.includes("Edg/")) return "edge";
if (ua.includes("Safari") && !ua.includes("Chrome") && !ua.includes("CriOS")) return "safari";
@@ -157,9 +155,7 @@ function startRowAnimation(row) {
let pos = stagger * halfWidth;
let lastTime = null;
- row._rafId = true;
function tick(now) {
- if (row._paused) { lastTime = null; requestAnimationFrame(tick); return; }
if (lastTime === null) { lastTime = now; requestAnimationFrame(tick); return; }
pos += ((now - lastTime) / 1000) * pxPerSec;
lastTime = now;
@@ -264,7 +260,6 @@ window.addEventListener("scroll", async () => {
// Inline iframe viewer
let activeViewer = null;
let activeTab = null;
-let activeRow = null;
function openInlineViewer(tabEl, entry, url) {
// Close existing viewer
@@ -277,25 +272,10 @@ function openInlineViewer(tabEl, entry, url) {
// Find the row this tab belongs to
const row = tabEl.closest(".tab-row");
- // Pause the marquee on the selected tab's row
- row.classList.add("paused");
- if (row._rafId) {
- // Firefox rAF: store paused state
- row._paused = true;
- } else {
- // CSS animation: getAnimations() to pause
- row.getAnimations().forEach(a => a.pause());
- }
-
// Build the viewer
const viewer = document.createElement("div");
viewer.className = "iframe-viewer";
- // Title bar
- const titlebar = document.createElement("div");
- titlebar.className = "iframe-titlebar";
- titlebar.textContent = entry.title || entry.url;
-
// Toolbar (address bar area)
const toolbar = document.createElement("div");
toolbar.className = "iframe-toolbar";
@@ -326,6 +306,12 @@ function openInlineViewer(tabEl, entry, url) {
toolbar.appendChild(urlbar);
+ // Mobile: title shown above the URL bar (hidden on desktop via CSS)
+ const mobileTitle = document.createElement("span");
+ mobileTitle.className = "url-title-mobile";
+ mobileTitle.textContent = entry.title || "";
+ toolbar.appendChild(mobileTitle);
+
const close = document.createElement("button");
close.className = "iframe-close";
close.textContent = "✕";
@@ -336,14 +322,12 @@ function openInlineViewer(tabEl, entry, url) {
iframe.sandbox = "allow-scripts allow-same-origin allow-forms";
iframe.src = url;
- viewer.appendChild(titlebar);
viewer.appendChild(toolbar);
viewer.appendChild(iframe);
// Insert after the row
row.after(viewer);
activeViewer = viewer;
- activeRow = row;
// Scroll so the viewer is visible
viewer.scrollIntoView({ behavior: "smooth", block: "start" });
@@ -354,15 +338,6 @@ function closeInlineViewer() {
activeViewer.remove();
activeViewer = null;
}
- if (activeRow) {
- activeRow.classList.remove("paused");
- if (activeRow._paused) {
- activeRow._paused = false;
- } else {
- activeRow.getAnimations().forEach(a => a.play());
- }
- activeRow = null;
- }
if (activeTab) {
activeTab.classList.remove("tab-active");
activeTab = null;
diff --git a/infra/ec2-userdata.sh b/infra/ec2-userdata.sh
index 380a3cf..594ccfa 100755
--- a/infra/ec2-userdata.sh
+++ b/infra/ec2-userdata.sh
@@ -9,14 +9,6 @@ export HOME=/root
echo "=== EveryTab EC2 Bootstrap ==="
-# --- EBS readahead ---
-# Large readahead improves bundle gen throughput by prefetching icon files into page cache.
-# 16MB readahead (32768 sectors × 512 bytes). Safe for all pipeline stages.
-echo "--- Setting EBS readahead ---"
-ROOT_DEV=$(findmnt -no SOURCE / | sed 's/p[0-9]*$//')
-sudo blockdev --setra 32768 "$ROOT_DEV"
-echo "Readahead: $(blockdev --getra "$ROOT_DEV") sectors on $ROOT_DEV"
-
# --- File descriptor limits ---
echo "--- Raising file descriptor limits ---"
echo '* soft nofile 65536' | sudo tee -a /etc/security/limits.conf
diff --git a/pipeline/03_icon_download/main.go b/pipeline/03_icon_download/main.go
index 9df2b99..063418a 100644
--- a/pipeline/03_icon_download/main.go
+++ b/pipeline/03_icon_download/main.go
@@ -5,6 +5,7 @@ import (
"flag"
"fmt"
"log"
+ "math/rand"
"os"
"sync"
"sync/atomic"
@@ -137,6 +138,9 @@ func main() {
break
}
+ rand.Shuffle(len(icons), func(i, j int) {
+ icons[i], icons[j] = icons[j], icons[i]
+ })
for _, icon := range icons {
iconCh <- icon
}
diff --git a/pipeline/run.sh b/pipeline/run.sh
deleted file mode 100755
index ca91570..0000000
--- a/pipeline/run.sh
+++ /dev/null
@@ -1,104 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-# Full pipeline run — chain all stages sequentially.
-# Run in tmux. Monitor from another pane with psql or htop.
-# Stops on any failure. Resume by commenting out completed stages.
-
-usage() {
- cat <<'EOF'
-Usage: ./pipeline/run.sh --db-url DATABASE_URL [OPTIONS]
-
-Required:
- --db-url URL Postgres connection string
-
-Optional:
- --limit N CC-Index host limit (default: 0 = all)
- --icons-dir DIR Icon storage directory (default: ~/icons)
- --site-bucket NAME S3 bucket for bundles (default: everytab-site)
- --help Show this help message
-
-Example:
- ./pipeline/run.sh --db-url "$DATABASE_URL"
- ./pipeline/run.sh --db-url "$DATABASE_URL" --limit 3000000
-EOF
- exit 0
-}
-
-# Defaults
-DB_URL=""
-LIMIT=0
-ICONS_DIR="$HOME/icons"
-SITE_BUCKET="everytab-site"
-
-if [ $# -eq 0 ]; then usage; fi
-
-while [ $# -gt 0 ]; do
- case "$1" in
- --help) usage ;;
- --db-url) DB_URL="$2"; shift 2 ;;
- --limit) LIMIT="$2"; shift 2 ;;
- --icons-dir) ICONS_DIR="$2"; shift 2 ;;
- --site-bucket) SITE_BUCKET="$2"; shift 2 ;;
- *) echo "Unknown option: $1"; usage ;;
- esac
-done
-
-if [ -z "$DB_URL" ]; then
- echo "ERROR: --db-url is required"
- exit 1
-fi
-
-SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
-REPO_DIR="$(dirname "$SCRIPT_DIR")"
-START_TIME=$(date +%s)
-
-echo "=========================================="
-echo " EveryTab Pipeline"
-echo " $(date)"
-echo " Limit: $LIMIT (0 = full run)"
-echo "=========================================="
-echo ""
-
-# --- Stage 1: CC-Index ---
-echo ">>> Stage 1: CC-Index Query"
-"$SCRIPT_DIR/01_cc_index/query.sh" --db-url "$DB_URL" --limit "$LIMIT"
-echo ""
-
-# --- Stage 2: WARC Parsing ---
-echo ">>> Stage 2: WARC Parsing"
-~/warc_parse --db "$DB_URL" --log-file "$HOME/warc_parse.log" --log-errors-only
-echo ""
-
-# --- Stage 3: Icon Download ---
-echo ">>> Stage 3: Icon Download"
-GOMEMLIMIT=12GiB ~/icon_download --db "$DB_URL" --icons-dir "$ICONS_DIR" --log-file "$HOME/icon_download.log" --log-errors-only
-echo ""
-
-# --- Stage 4: Best Icon Selection ---
-echo ">>> Stage 4: Best Icon Selection"
-psql "$DB_URL" -f "$SCRIPT_DIR/04_best_icon/select.sql"
-echo ""
-
-# --- Stage 5: Bundle Generation ---
-echo ">>> Stage 5: Bundle Generation"
-~/bundle_gen --db "$DB_URL" --icons-dir "$ICONS_DIR" --site-bucket "$SITE_BUCKET" --log-file "$HOME/bundle_gen.log" --log-errors-only
-echo ""
-
-# --- Stage 6: Frontend Deploy ---
-echo ">>> Stage 6: Frontend Deploy"
-TOTAL_BUNDLES=$(jq -r '.bundles_created' stats/05_bundle_gen.json)
-"$SCRIPT_DIR/06_frontend/deploy.sh" --total-bundles "$TOTAL_BUNDLES"
-echo ""
-
-# --- Done ---
-END_TIME=$(date +%s)
-DURATION=$(( END_TIME - START_TIME ))
-HOURS=$(( DURATION / 3600 ))
-MINS=$(( (DURATION % 3600) / 60 ))
-
-echo "=========================================="
-echo " Pipeline Complete"
-echo " $(date)"
-echo " Total duration: ${HOURS}h ${MINS}m"
-echo "=========================================="