Files
SneakyScope/app/templates/viewer.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

144 lines
5.1 KiB
HTML

{% extends "base.html" %}
{% block title %}Source Viewer{% endblock %}
{% block content %}
<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">
<section class="bg-card border border-gray-800 rounded-xl p-4">
<header class="flex items-center justify-between gap-3 flex-wrap">
<div>
<h2 class="text-lg font-semibold">Source Viewer</h2>
<div class="text-sm text-gray-400">
<strong>File:</strong> <span id="fileName" class="break-all">{{ filename }}</span>
</div>
</div>
<div class="flex items-center gap-2">
<button id="copyBtn"
class="inline-flex items-center rounded-lg px-3 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700 text-sm">
Copy
</button>
<button id="wrapBtn"
class="inline-flex items-center rounded-lg px-3 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700 text-sm">
Toggle wrap
</button>
<a id="openRaw" href="{{ raw_url }}" target="_blank" rel="noopener"
class="inline-flex items-center rounded-lg px-3 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700 text-sm">
Open raw
</a>
<a id="downloadRaw" href="{{ raw_url }}" download
class="inline-flex items-center rounded-lg px-3 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700 text-sm">
Download
</a>
</div>
</header>
<div id="viewerStatus" class="text-sm text-gray-400 mt-2 mb-3"></div>
<div id="editor" class="h-[72vh] border border-gray-800 rounded-lg"></div>
</section>
</div>
{% endblock %}
{% block scripts %}
<!-- Monaco AMD loader (no integrity to avoid mismatch) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.49.0/min/vs/loader.min.js"
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
(function () {
const RAW_URL = "{{ raw_url }}";
const FILENAME = "{{ filename }}";
const LANGUAGE = "{{ language|default('', true) }}";
const statusEl = document.getElementById('viewerStatus');
function extToLang(name) {
if (!name) return 'plaintext';
const m = name.toLowerCase().match(/\.([a-z0-9]+)$/);
const ext = m ? m[1] : '';
const map = {
js:'javascript', mjs:'javascript', cjs:'javascript',
ts:'typescript', json:'json',
html:'html', htm:'html', css:'css',
py:'python', sh:'shell', bash:'shell',
yml:'yaml', yaml:'yaml',
md:'markdown', txt:'plaintext', log:'plaintext'
};
return map[ext] || 'plaintext';
}
// Wait until the AMD loader defines window.require
function waitForRequire(msLeft = 5000) {
return new Promise((resolve, reject) => {
const t0 = performance.now();
(function poll() {
if (window.require && typeof window.require === 'function') return resolve();
if (performance.now() - t0 > msLeft) return reject(new Error('Monaco loader not available'));
setTimeout(poll, 25);
})();
});
}
function configureMonaco() {
// Point AMD loader at the CDN
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.49.0/min/vs' } });
// Worker bootstrap
window.MonacoEnvironment = {
getWorkerUrl: function () {
const base = 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.49.0/min/';
const code = "self.MonacoEnvironment={baseUrl:'" + base + "'};importScripts('" + base + "vs/base/worker/workerMain.js');";
return 'data:text/javascript;charset=utf-8,' + encodeURIComponent(code);
}
};
}
async function main() {
try {
statusEl.textContent = 'Loading file…';
await waitForRequire();
configureMonaco();
const resp = await fetch(RAW_URL, { cache: 'no-store' });
const text = await resp.text();
require(['vs/editor/editor.main'], function () {
const editor = monaco.editor.create(document.getElementById('editor'), {
value: text,
language: LANGUAGE || extToLang(FILENAME),
readOnly: true,
automaticLayout: true,
wordWrap: 'on',
minimap: { enabled: false },
scrollBeyondLastLine: false,
theme: 'vs-dark'
});
// Buttons
document.getElementById('copyBtn')?.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(editor.getValue());
statusEl.textContent = 'Copied.';
} catch (e) {
statusEl.textContent = 'Copy failed: ' + e;
}
});
document.getElementById('wrapBtn')?.addEventListener('click', () => {
const opts = editor.getRawOptions();
editor.updateOptions({ wordWrap: opts.wordWrap === 'on' ? 'off' : 'on' });
});
// Status
const warn = resp.ok ? '' : `Warning: HTTP ${resp.status}`;
const empty = text.length ? '' : (warn ? ' · empty file' : 'Empty file');
statusEl.textContent = warn + (warn && empty ? '' : '') + (empty || '');
});
} catch (err) {
statusEl.textContent = 'Viewer error: ' + err.message;
}
}
main();
})();
</script>
{% endblock %}