Files
SneakyScope/app/templates/index.html
Phillip Tarrant 469334d137 feat(ui): migrate to Tailwind (compiled) + Flowbite JS; new navbar/layout; Docker CSS build
- Add multi-stage CSS build that compiles Tailwind into app/static/tw.css
- Add Tailwind config with dark tokens (bg/nav/card) and purge globs
- Add assets/input.css (@tailwind base/components/utilities + small utilities)
- Replace Tailwind CDN + REMOVE Flowbite CSS (keep Flowbite JS only)
- New base_tailwind.html (top navbar, responsive container, {%- block scripts -%})
- Port pages to Tailwind look/feel with wider content column:
  - index: single-column form + recent results, fullscreen spinner overlay, copy-UUID
  - result: sticky jump list, Tailwind tables/badges, Suspicious Scripts/Forms sections
  - viewer: Monaco-based code viewer in Tailwind card, actions (copy/wrap/raw)
  - ssl_tls macro: rewritten with Tailwind (details/summary for raw JSON)
- Dockerfile: add css-builder stage and copy built tw.css into /app/app/static
- Remove Flowbite stylesheet to avoid overrides; Flowbite JS loaded with defer

BREAKING CHANGE:
Legacy CSS classes/components (.card, .badge, etc.) are replaced by Tailwind utilities.
All templates now expect tw.css to be served from /static.
2025-08-22 10:36:10 -05:00

202 lines
6.7 KiB
HTML

{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block content %}
<!-- Single-column stack, centered and comfortably narrow -->
<div class="max-w-xl md:max-w-3xl lg:max-w-5xl xl:max-w-6xl 2xl:max-w-7xl mx-auto space-y-4">
<!-- Start a New Analysis -->
<section class="bg-card border border-gray-800 rounded-xl p-4">
<h2 class="text-lg font-semibold mb-3">Start a New Analysis</h2>
<form id="analyze-form" action="{{ url_for('main.analyze') }}" method="post" class="space-y-3">
<div>
<label for="url" class="block text-sm text-gray-400 mb-1">Target URL or Domain</label>
<input
id="url"
name="url"
type="text"
required
class="w-full bg-[#0b0f14] border border-gray-700 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="https://example.com"
>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center">
<button
type="submit"
class="inline-flex items-center gap-2 rounded-lg px-3 py-2 bg-blue-600 hover:bg-blue-500 text-white disabled:opacity-60"
>
<span id="btn-spinner" class="hidden animate-spin inline-block h-4 w-4 rounded-full border-2 border-white/40 border-t-white"></span>
Analyse
</button>
<!-- toggle for pulling ssl/cert data -->
<label class="flex items-center gap-2 text-sm">
<input
type="checkbox"
name="fetch_ssl"
value="1"
class="rounded border-gray-600 bg-[#0b0f14]"
>
<span>
Pull SSL/TLS data (crt.sh + version probe)
<span class="text-gray-400">— crt.sh can be <b>very slow</b> at times</span>
</span>
</label>
</div>
</form>
</section>
<!-- Recent Results -->
{% if recent_results %}
<section class="bg-card border border-gray-800 rounded-xl p-4" id="recent-results">
<h2 class="text-base font-semibold mb-3">Recent Results</h2>
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<thead class="text-gray-400 border-b border-gray-800">
<tr>
<th class="text-left py-2 pr-4">Timestamp</th>
<th class="text-left py-2 pr-4">URL</th>
<th class="text-left py-2 pr-4">UUID</th>
</tr>
</thead>
<tbody>
{% for r in recent_results %}
<tr class="border-b border-gray-900">
<td class="py-2 pr-4 whitespace-nowrap">
{% if r.timestamp %}{{ r.timestamp }}{% else %}N/A{% endif %}
</td>
<td class="py-2 pr-4">
<a class="hover:text-blue-400" href="{{ url_for('main.view_result', run_uuid=r.uuid) }}">
{{ r.final_url or r.submitted_url }}
</a>
</td>
<td class="py-2 pr-4">
<div class="flex items-center gap-2">
<code id="uuid-{{ loop.index }}" class="text-gray-300">{{ r.uuid }}</code>
<button
type="button"
class="copy-btn inline-flex items-center justify-center rounded-md border border-gray-700 px-2 py-1 text-xs hover:bg-gray-800"
data-target="uuid-{{ loop.index }}"
title="Copy UUID"
>
📋
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
{% else %}
<section class="bg-card border border-gray-800 rounded-xl p-4">
<h2 class="text-base font-semibold mb-2">Recent Results</h2>
<p class="text-sm text-gray-500">No recent scans.</p>
</section>
{% endif %}
</div>
<!-- Fullscreen spinner overlay -->
<div
id="spinner-modal"
class="fixed inset-0 hidden opacity-0 transition-opacity duration-300 bg-black/70 z-50"
role="dialog"
aria-modal="true"
aria-label="Analyzing website"
>
<div class="min-h-screen flex items-center justify-center p-4 text-center">
<div class="bg-card border border-gray-800 rounded-xl px-6 py-5 shadow">
<div class="mx-auto mb-3 h-12 w-12 rounded-full border-4 border-white/30 border-t-white animate-spin"></div>
<div class="text-base">Analyzing website…</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
/**
* Show the fullscreen spinner overlay.
*/
function showSpinner() {
const modal = document.getElementById('spinner-modal');
if (!modal) return;
modal.classList.remove('hidden');
// allow reflow so opacity transition runs
requestAnimationFrame(() => modal.classList.remove('opacity-0'));
}
/**
* Hide the fullscreen spinner overlay.
*/
function hideSpinner() {
const modal = document.getElementById('spinner-modal');
if (!modal) return;
modal.classList.add('opacity-0');
modal.addEventListener('transitionend', () => {
modal.classList.add('hidden');
}, { once: true });
}
/**
* Initialize form submit handling:
* - shows overlay spinner
* - disables submit button
* - shows small spinner inside button
* - lets the browser continue with POST
*/
(function initAnalyzeForm() {
const form = document.getElementById('analyze-form');
if (!form) return;
const submitBtn = form.querySelector('button[type="submit"]');
const btnSpinner = document.getElementById('btn-spinner');
// Hide spinner overlay if arriving from bfcache/back
window.addEventListener('pageshow', () => {
hideSpinner();
if (submitBtn) submitBtn.disabled = false;
if (btnSpinner) btnSpinner.classList.add('hidden');
});
form.addEventListener('submit', (e) => {
// prevent immediate submit so UI can paint spinner first
e.preventDefault();
if (submitBtn) submitBtn.disabled = true;
if (btnSpinner) btnSpinner.classList.remove('hidden');
showSpinner();
// allow a frame so spinner paints, then submit
requestAnimationFrame(() => form.submit());
});
})();
/**
* Copy UUID buttons
*/
(function initCopyButtons() {
document.querySelectorAll('.copy-btn').forEach((btn) => {
btn.addEventListener('click', async () => {
const targetId = btn.getAttribute('data-target');
const el = document.getElementById(targetId);
if (!el) return;
try {
await navigator.clipboard.writeText(el.textContent.trim());
const prev = btn.textContent;
btn.textContent = '✅';
setTimeout(() => { btn.textContent = prev; }, 1200);
} catch (err) {
console.error('Failed to copy UUID:', err);
}
});
});
})();
</script>
{% endblock %}