- 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.
202 lines
6.7 KiB
HTML
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 %}
|