Skip to content

Commit 4afc7d7

Browse files
Claude/fix spin website html a ol iy (#135)
* fix: rewrite GitHub Pages site to display properly with dynamic OSINT detective humor The site at thumpersecure.github.io/Spin was showing the README.md rendered by Jekyll instead of the OSINT toolkit HTML. Root causes: - No index.html at repo root (Pages served README as fallback) - No .nojekyll file to prevent Jekyll processing Changes: - Add .nojekyll at root and docs/ to bypass Jekyll - Add index.html, style.css, app.js at repo root so Pages serves them - Completely rewrite all three files with cleaner, well-structured code - Add animated loading screen with investigator boot sequence - Add rotating OSINT detective jokes system (24 jokes) - Add scrolling intel brief ticker with 20 OSINT tips - Add periodic "Field Note" tip popups - Add rotating header taglines and footer jokes - Add case number generator and session ID - Add classified stamp, scanline overlay, gradient animations - Add investigation status panel with OPSEC level meter - Preserve all existing OSINT tools (phone, email, username, domain, entity extractor, hivemind, bookmarks) - Sync all files to docs/ folder as well https://claude.ai/code/session_01LJNJws3Tyq7jFNGVAYBszq * fix: add GitHub Actions workflow to deploy static site to Pages GitHub Pages was configured to use GitHub Actions as the deployment source, which triggers a default Jekyll build that renders README.md as the site index, completely ignoring our index.html. The .nojekyll file only works in "Deploy from a branch" mode, not Actions mode. This adds a custom pages.yml workflow that uses actions/deploy-pages to explicitly deploy the docs/ folder as a static site, bypassing Jekyll entirely. The workflow triggers on pushes to main and can also be manually dispatched. https://claude.ai/code/session_01LJNJws3Tyq7jFNGVAYBszq * fix: add Jekyll config and front matter to force proper HTML rendering GitHub Pages is running Jekyll regardless of .nojekyll, rendering README.md as the index page with a theme layout. This commit: - Adds `layout: null` front matter to index.html so Jekyll outputs our HTML as-is without wrapping it in a theme layout - Adds _config.yml with `theme: null` to disable any Jekyll theme - Excludes README.md and other non-website files from Jekyll build - Adds pages.yml workflow for GitHub Actions deployment as fallback This covers both deployment modes: - "Deploy from branch": _config.yml + front matter handle Jekyll - "GitHub Actions": pages.yml workflow deploys docs/ as static site https://claude.ai/code/session_01LJNJws3Tyq7jFNGVAYBszq * Remove Jekyll front matter that renders as visible text Since GitHub Pages is now set to "Deploy from branch" with .nojekyll, Jekyll is bypassed and the front matter (layout: null, permalink) was showing as literal text at the top of the page. https://claude.ai/code/session_01LJNJws3Tyq7jFNGVAYBszq * Fix XSS vulnerabilities, bugs, and optimize web toolkit Security fixes: - Add escapeAttr() for safe HTML attribute escaping (quotes) - Fix escapeJs() to handle newlines, tabs, and </script> sequences - Escape entity sources in renderEntityTable() to prevent import XSS - Escape entity IDs in delete button onclick handlers - Escape item.type in extractor save button onclick - Use escapeAttr() for domain Visit link href attribute Bug fixes: - Fix toast race condition: clear previous timeout on rapid calls - Reuse DOM element in escapeHtml() instead of creating new one per call Optimizations: - Add debounce (200ms) to entity search and bookmark search inputs - Make header background semi-transparent so backdrop-filter blur works - Add -webkit-backdrop-filter for Safari support Improvements: - Expand entity extractor domain TLD regex from ~10 to 40+ TLDs - Add bc1 (SegWit/Bech32) Bitcoin address detection https://claude.ai/code/session_01LJNJws3Tyq7jFNGVAYBszq * Add PWA support for iOS Home Screen experience - Web App Manifest (manifest.json) with standalone display mode - Service worker (sw.js) for offline caching with stale-while-revalidate - SVG app icon with Spin branding (purple gradient + green status dot) - Apple meta tags: web-app-capable, status-bar-style, app-title - Dynamic apple-touch-icon generation via canvas (180x180 PNG) - iOS install banner with Safari/Chrome/Firefox-specific instructions - Auto-shows after 4s on iOS Safari (not in standalone mode) - Dismissable, remembers dismissal for 7 days - Standalone mode CSS: - Safe area insets for notch/Dynamic Island on all fixed elements - Overscroll prevention (no bounce/pull-to-refresh) - Tap highlight removal on interactive elements - Text selection re-enabled on content areas - Desktop App link hidden in standalone (already using app) - viewport-fit=cover for edge-to-edge rendering - -webkit-backdrop-filter prefix for Safari https://claude.ai/code/session_01LJNJws3Tyq7jFNGVAYBszq --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4a27767 commit 4afc7d7

12 files changed

Lines changed: 1000 additions & 4 deletions

File tree

app.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,11 +1054,148 @@ function runLoadingSequence() {
10541054
}, 2200);
10551055
}
10561056

1057+
/* ─── PWA: Service Worker Registration ─────────────────── */
1058+
function registerServiceWorker() {
1059+
if ('serviceWorker' in navigator) {
1060+
navigator.serviceWorker.register('./sw.js').then(function(reg) {
1061+
// Check for updates periodically
1062+
setInterval(function() { reg.update(); }, 60 * 60 * 1000);
1063+
}).catch(function() {
1064+
// SW registration failed (e.g. not HTTPS, unsupported) - app works fine without it
1065+
});
1066+
}
1067+
}
1068+
1069+
/* ─── PWA: Generate Apple Touch Icon via Canvas ────────── */
1070+
function generateAppleTouchIcon() {
1071+
try {
1072+
var canvas = document.createElement('canvas');
1073+
canvas.width = 180;
1074+
canvas.height = 180;
1075+
var ctx = canvas.getContext('2d');
1076+
if (!ctx) return;
1077+
1078+
// Background
1079+
ctx.fillStyle = '#08080d';
1080+
ctx.beginPath();
1081+
ctx.roundRect(0, 0, 180, 180, 36);
1082+
ctx.fill();
1083+
1084+
// Gradient overlay
1085+
var grad = ctx.createLinearGradient(0, 0, 180, 180);
1086+
grad.addColorStop(0, 'rgba(123, 31, 235, 0.2)');
1087+
grad.addColorStop(1, 'rgba(28, 223, 102, 0.2)');
1088+
ctx.fillStyle = grad;
1089+
ctx.beginPath();
1090+
ctx.roundRect(6, 6, 168, 168, 32);
1091+
ctx.fill();
1092+
1093+
// Letter S
1094+
ctx.fillStyle = '#ffffff';
1095+
ctx.font = '900 100px system-ui, -apple-system, sans-serif';
1096+
ctx.textAlign = 'center';
1097+
ctx.textBaseline = 'middle';
1098+
ctx.fillText('S', 90, 95);
1099+
1100+
// Green dot (surveillance indicator)
1101+
ctx.fillStyle = '#1cdf66';
1102+
ctx.beginPath();
1103+
ctx.arc(142, 142, 16, 0, Math.PI * 2);
1104+
ctx.fill();
1105+
ctx.fillStyle = '#08080d';
1106+
ctx.beginPath();
1107+
ctx.arc(142, 142, 7, 0, Math.PI * 2);
1108+
ctx.fill();
1109+
ctx.fillStyle = '#1cdf66';
1110+
ctx.beginPath();
1111+
ctx.arc(142, 142, 3, 0, Math.PI * 2);
1112+
ctx.fill();
1113+
1114+
// Create link element for apple-touch-icon
1115+
var link = document.createElement('link');
1116+
link.rel = 'apple-touch-icon';
1117+
link.href = canvas.toDataURL('image/png');
1118+
document.head.appendChild(link);
1119+
} catch (e) {
1120+
// Canvas not supported or roundRect not available - fallback gracefully
1121+
}
1122+
}
1123+
1124+
/* ─── PWA: iOS Install Banner ──────────────────────────── */
1125+
var INSTALL_DISMISSED_KEY = 'spin_install_dismissed';
1126+
1127+
function isIOS() {
1128+
return /iPad|iPhone|iPod/.test(navigator.userAgent) ||
1129+
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
1130+
}
1131+
1132+
function isStandalone() {
1133+
return window.navigator.standalone === true ||
1134+
window.matchMedia('(display-mode: standalone)').matches;
1135+
}
1136+
1137+
function showInstallBanner() {
1138+
// Only show on iOS Safari, not already installed, not dismissed recently
1139+
if (!isIOS() || isStandalone()) return;
1140+
1141+
try {
1142+
var dismissed = localStorage.getItem(INSTALL_DISMISSED_KEY);
1143+
if (dismissed) {
1144+
var dismissedAt = parseInt(dismissed, 10);
1145+
// Don't show again for 7 days
1146+
if (Date.now() - dismissedAt < 7 * 24 * 60 * 60 * 1000) return;
1147+
}
1148+
} catch (e) { /* localStorage unavailable */ }
1149+
1150+
var banner = document.getElementById('install-banner');
1151+
var steps = document.getElementById('install-steps');
1152+
if (!banner || !steps) return;
1153+
1154+
// Detect browser for correct instructions
1155+
var isChrome = /CriOS/.test(navigator.userAgent);
1156+
var isFirefox = /FxiOS/.test(navigator.userAgent);
1157+
1158+
if (isChrome) {
1159+
steps.innerHTML =
1160+
'<div class="step"><span class="step-num">1</span> Tap the share button <span class="step-icon">&#8943;</span> (three dots menu)</div>' +
1161+
'<div class="step"><span class="step-num">2</span> Scroll down and tap <strong>"Add to Home Screen"</strong></div>' +
1162+
'<div class="step"><span class="step-num">3</span> Tap <strong>"Add"</strong> to install</div>';
1163+
} else if (isFirefox) {
1164+
steps.innerHTML =
1165+
'<div class="step"><span class="step-num">1</span> Open this page in <strong>Safari</strong> for Home Screen support</div>';
1166+
} else {
1167+
// Safari (default)
1168+
steps.innerHTML =
1169+
'<div class="step"><span class="step-num">1</span> Tap the share button <span class="step-icon">&#xFEFF;&#x2B06;&#xFE0E;</span> at the bottom</div>' +
1170+
'<div class="step"><span class="step-num">2</span> Scroll down and tap <strong>"Add to Home Screen"</strong></div>' +
1171+
'<div class="step"><span class="step-num">3</span> Tap <strong>"Add"</strong> to install</div>';
1172+
}
1173+
1174+
// Show after a short delay
1175+
setTimeout(function() {
1176+
banner.classList.add('show');
1177+
}, 4000);
1178+
}
1179+
1180+
function dismissInstallBanner() {
1181+
var banner = document.getElementById('install-banner');
1182+
if (banner) banner.classList.remove('show');
1183+
try {
1184+
localStorage.setItem(INSTALL_DISMISSED_KEY, Date.now().toString());
1185+
} catch (e) { /* ignore */ }
1186+
}
1187+
10571188
/* ─── Initialize ───────────────────────────────────────── */
10581189
document.addEventListener('DOMContentLoaded', function() {
10591190
// Run loading screen
10601191
runLoadingSequence();
10611192

1193+
// Register service worker for offline/PWA
1194+
registerServiceWorker();
1195+
1196+
// Generate apple-touch-icon dynamically
1197+
generateAppleTouchIcon();
1198+
10621199
// Update entity counts
10631200
updateEntityCount();
10641201

@@ -1124,4 +1261,7 @@ document.addEventListener('DOMContentLoaded', function() {
11241261
showRandomTip();
11251262
setInterval(showRandomTip, 45000);
11261263
}, 20000);
1264+
1265+
// Show iOS install banner (after loading screen finishes)
1266+
setTimeout(showInstallBanner, 3500);
11271267
});

docs/app.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,11 +1054,148 @@ function runLoadingSequence() {
10541054
}, 2200);
10551055
}
10561056

1057+
/* ─── PWA: Service Worker Registration ─────────────────── */
1058+
function registerServiceWorker() {
1059+
if ('serviceWorker' in navigator) {
1060+
navigator.serviceWorker.register('./sw.js').then(function(reg) {
1061+
// Check for updates periodically
1062+
setInterval(function() { reg.update(); }, 60 * 60 * 1000);
1063+
}).catch(function() {
1064+
// SW registration failed (e.g. not HTTPS, unsupported) - app works fine without it
1065+
});
1066+
}
1067+
}
1068+
1069+
/* ─── PWA: Generate Apple Touch Icon via Canvas ────────── */
1070+
function generateAppleTouchIcon() {
1071+
try {
1072+
var canvas = document.createElement('canvas');
1073+
canvas.width = 180;
1074+
canvas.height = 180;
1075+
var ctx = canvas.getContext('2d');
1076+
if (!ctx) return;
1077+
1078+
// Background
1079+
ctx.fillStyle = '#08080d';
1080+
ctx.beginPath();
1081+
ctx.roundRect(0, 0, 180, 180, 36);
1082+
ctx.fill();
1083+
1084+
// Gradient overlay
1085+
var grad = ctx.createLinearGradient(0, 0, 180, 180);
1086+
grad.addColorStop(0, 'rgba(123, 31, 235, 0.2)');
1087+
grad.addColorStop(1, 'rgba(28, 223, 102, 0.2)');
1088+
ctx.fillStyle = grad;
1089+
ctx.beginPath();
1090+
ctx.roundRect(6, 6, 168, 168, 32);
1091+
ctx.fill();
1092+
1093+
// Letter S
1094+
ctx.fillStyle = '#ffffff';
1095+
ctx.font = '900 100px system-ui, -apple-system, sans-serif';
1096+
ctx.textAlign = 'center';
1097+
ctx.textBaseline = 'middle';
1098+
ctx.fillText('S', 90, 95);
1099+
1100+
// Green dot (surveillance indicator)
1101+
ctx.fillStyle = '#1cdf66';
1102+
ctx.beginPath();
1103+
ctx.arc(142, 142, 16, 0, Math.PI * 2);
1104+
ctx.fill();
1105+
ctx.fillStyle = '#08080d';
1106+
ctx.beginPath();
1107+
ctx.arc(142, 142, 7, 0, Math.PI * 2);
1108+
ctx.fill();
1109+
ctx.fillStyle = '#1cdf66';
1110+
ctx.beginPath();
1111+
ctx.arc(142, 142, 3, 0, Math.PI * 2);
1112+
ctx.fill();
1113+
1114+
// Create link element for apple-touch-icon
1115+
var link = document.createElement('link');
1116+
link.rel = 'apple-touch-icon';
1117+
link.href = canvas.toDataURL('image/png');
1118+
document.head.appendChild(link);
1119+
} catch (e) {
1120+
// Canvas not supported or roundRect not available - fallback gracefully
1121+
}
1122+
}
1123+
1124+
/* ─── PWA: iOS Install Banner ──────────────────────────── */
1125+
var INSTALL_DISMISSED_KEY = 'spin_install_dismissed';
1126+
1127+
function isIOS() {
1128+
return /iPad|iPhone|iPod/.test(navigator.userAgent) ||
1129+
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
1130+
}
1131+
1132+
function isStandalone() {
1133+
return window.navigator.standalone === true ||
1134+
window.matchMedia('(display-mode: standalone)').matches;
1135+
}
1136+
1137+
function showInstallBanner() {
1138+
// Only show on iOS Safari, not already installed, not dismissed recently
1139+
if (!isIOS() || isStandalone()) return;
1140+
1141+
try {
1142+
var dismissed = localStorage.getItem(INSTALL_DISMISSED_KEY);
1143+
if (dismissed) {
1144+
var dismissedAt = parseInt(dismissed, 10);
1145+
// Don't show again for 7 days
1146+
if (Date.now() - dismissedAt < 7 * 24 * 60 * 60 * 1000) return;
1147+
}
1148+
} catch (e) { /* localStorage unavailable */ }
1149+
1150+
var banner = document.getElementById('install-banner');
1151+
var steps = document.getElementById('install-steps');
1152+
if (!banner || !steps) return;
1153+
1154+
// Detect browser for correct instructions
1155+
var isChrome = /CriOS/.test(navigator.userAgent);
1156+
var isFirefox = /FxiOS/.test(navigator.userAgent);
1157+
1158+
if (isChrome) {
1159+
steps.innerHTML =
1160+
'<div class="step"><span class="step-num">1</span> Tap the share button <span class="step-icon">&#8943;</span> (three dots menu)</div>' +
1161+
'<div class="step"><span class="step-num">2</span> Scroll down and tap <strong>"Add to Home Screen"</strong></div>' +
1162+
'<div class="step"><span class="step-num">3</span> Tap <strong>"Add"</strong> to install</div>';
1163+
} else if (isFirefox) {
1164+
steps.innerHTML =
1165+
'<div class="step"><span class="step-num">1</span> Open this page in <strong>Safari</strong> for Home Screen support</div>';
1166+
} else {
1167+
// Safari (default)
1168+
steps.innerHTML =
1169+
'<div class="step"><span class="step-num">1</span> Tap the share button <span class="step-icon">&#xFEFF;&#x2B06;&#xFE0E;</span> at the bottom</div>' +
1170+
'<div class="step"><span class="step-num">2</span> Scroll down and tap <strong>"Add to Home Screen"</strong></div>' +
1171+
'<div class="step"><span class="step-num">3</span> Tap <strong>"Add"</strong> to install</div>';
1172+
}
1173+
1174+
// Show after a short delay
1175+
setTimeout(function() {
1176+
banner.classList.add('show');
1177+
}, 4000);
1178+
}
1179+
1180+
function dismissInstallBanner() {
1181+
var banner = document.getElementById('install-banner');
1182+
if (banner) banner.classList.remove('show');
1183+
try {
1184+
localStorage.setItem(INSTALL_DISMISSED_KEY, Date.now().toString());
1185+
} catch (e) { /* ignore */ }
1186+
}
1187+
10571188
/* ─── Initialize ───────────────────────────────────────── */
10581189
document.addEventListener('DOMContentLoaded', function() {
10591190
// Run loading screen
10601191
runLoadingSequence();
10611192

1193+
// Register service worker for offline/PWA
1194+
registerServiceWorker();
1195+
1196+
// Generate apple-touch-icon dynamically
1197+
generateAppleTouchIcon();
1198+
10621199
// Update entity counts
10631200
updateEntityCount();
10641201

@@ -1124,4 +1261,7 @@ document.addEventListener('DOMContentLoaded', function() {
11241261
showRandomTip();
11251262
setInterval(showRandomTip, 45000);
11261263
}, 20000);
1264+
1265+
// Show iOS install banner (after loading screen finishes)
1266+
setTimeout(showInstallBanner, 3500);
11271267
});

docs/icon.svg

Lines changed: 19 additions & 0 deletions
Loading

docs/index.html

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8">
5-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
66
<title>Spin Web - OSINT Mini Toolkit</title>
77
<meta name="description" content="Spin Web - A privacy-first OSINT investigation toolkit. Analyze phones, emails, usernames, and domains. 100% client-side.">
88
<meta name="theme-color" content="#7b1feb">
99
<meta property="og:title" content="Spin Web - OSINT Mini Toolkit">
1010
<meta property="og:description" content="Privacy-first OSINT investigation toolkit. All analysis runs locally in your browser.">
1111
<meta property="og:type" content="website">
12+
<!-- PWA / iOS Home Screen -->
13+
<link rel="manifest" href="manifest.json">
14+
<meta name="apple-mobile-web-app-capable" content="yes">
15+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
16+
<meta name="apple-mobile-web-app-title" content="Spin Web">
17+
<meta name="mobile-web-app-capable" content="yes">
18+
<link rel="icon" href="icon.svg" type="image/svg+xml">
19+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='16' fill='%237b1feb'/><text x='50' y='72' font-family='system-ui' font-size='60' font-weight='900' fill='white' text-anchor='middle'>S</text></svg>" type="image/svg+xml">
1220
<link rel="stylesheet" href="style.css">
13-
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='16' fill='%237b1feb'/><text x='50' y='72' font-family='system-ui' font-size='60' font-weight='900' fill='white' text-anchor='middle'>S</text></svg>">
1421
</head>
1522
<body>
1623

@@ -410,6 +417,19 @@ <h3>Hivemind</h3>
410417
<p id="tip-popup-text"></p>
411418
</div>
412419

420+
<!-- iOS Install Banner -->
421+
<div id="install-banner" class="install-banner">
422+
<div class="install-banner-content">
423+
<div class="install-banner-icon">S</div>
424+
<div class="install-banner-text">
425+
<strong>Install Spin Web</strong>
426+
<span>Add to Home Screen for the full app experience &mdash; offline access, no browser UI, instant launch.</span>
427+
</div>
428+
<button class="install-banner-close" onclick="dismissInstallBanner()" aria-label="Dismiss">&times;</button>
429+
</div>
430+
<div class="install-banner-steps" id="install-steps"></div>
431+
</div>
432+
413433
<script src="app.js"></script>
414434
</body>
415435
</html>

0 commit comments

Comments
 (0)