Files
deepseek beaf0e1f37 Toolvana: production-ready media tools platform (CodeIgniter 4)
- 134-tool registry with programmatic SEO (unique titles/H1/descriptions,
  JSON-LD graphs, sitemap index, canonical 301 enforcement via required filter)
- DB-backed job queue (SKIP LOCKED) with drivers: Ffmpeg, Images (GD),
  Pdf (qpdf/gs/poppler), Youtube (thumbnails), Qr (server-side PNG)
- Security: SSRF guard, MIME validation, rate limits, API-key auth,
  bcrypt admin login, security headers
- Admin panel: dashboard, tools/categories/guides CRUD, SEO audit,
  analytics, job inspector with retry, system health, feature flags
- Docker deployment (nginx + web/api FPM pools + scalable workers),
  PHPUnit suite (19 tests / 1139 assertions), PWA manifest + service worker
2026-08-23 07:10:30 +00:00

99 lines
3.7 KiB
JavaScript

/* URL-tool flow: paste link → instant info → job → poll → download. */
(function () {
'use strict';
var panel = document.getElementById('tool-panel');
if (!panel) return;
var slug = panel.dataset.tool;
var form = document.getElementById('tool-form');
var urlInput = document.getElementById('url-input');
var infoPanel = document.getElementById('info-panel');
var statusFlow = document.getElementById('status-flow');
var progressFill = document.getElementById('progress-fill');
var resultPanel = document.getElementById('result-panel');
var errorBox = document.getElementById('error-box');
var pollTimer = null, currentJobId = null;
function track(name) {
try {
navigator.sendBeacon &&
navigator.sendBeacon('/api/events', new Blob([JSON.stringify({ name: name, tool: slug })], { type: 'application/json' }));
} catch (e) {}
}
function showError(msg) { errorBox.textContent = msg; errorBox.hidden = false; track('tool_failure'); }
function setStage(stage, progress) {
statusFlow.hidden = false;
document.querySelectorAll('.stages li').forEach(function (li) {
li.classList.toggle('done', li.dataset.stage !== stage);
li.classList.toggle('active', li.dataset.stage === stage);
});
if (typeof progress === 'number') progressFill.style.width = Math.max(6, progress) + '%';
}
form.addEventListener('submit', function (e) {
e.preventDefault();
hideError();
resultPanel.classList.remove('show');
fetch('/api/jobs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tool: slug, url: urlInput.value })
})
.then(function (r) { return r.json().then(function (j) { return { status: r.status, body: j }; }); })
.then(function (res) {
if (!res.ok) throw new Error(res.body.message || res.body.error || 'Request failed.');
currentJobId = res.body.job_id;
setStage('preparing', 8);
poll();
})
.catch(function (err) { showError(err.message || 'Something went wrong. Please try again.'); });
});
function hideError() { errorBox.hidden = true; }
function poll() {
if (!currentJobId) return;
fetch('/api/jobs/' + currentJobId)
.then(function (r) { return r.json(); })
.then(function (data) {
if (!data.ok) throw new Error('lost');
if (data.status === 'completed') { finish(data); return; }
if (data.status === 'failed') { showError(data.message || 'This video could not be processed.'); return; }
var stageMap = { preparing: 'preparing', fetching: 'fetching', processing: 'processing', converting: 'converting' };
setStage(stageMap[data.stage] || 'processing', data.progress);
pollTimer = setTimeout(poll, 1600);
})
.catch(function () { pollTimer = setTimeout(poll, 3200); });
}
function finish(data) {
clearTimeout(pollTimer);
setStage('complete', 100);
resultPanel.innerHTML =
'<div class="result-meta">' +
'<span class="badge">Done</span>' +
'<strong>' + escapeHtml(data.output_name || 'download') + '</strong>' +
'<span>' + humanSize(data.output_size || 0) + '</span>' +
'<a class="btn-primary" style="text-decoration:none;padding:.55rem 1.3rem;font-size:.95rem" href="' + data.download_url + '" download>Download</a>' +
'</div>';
resultPanel.classList.add('show');
}
function humanSize(bytes) {
var u = ['B', 'KB', 'MB', 'GB'], i = 0;
while (bytes >= 1024 && i < u.length - 1) { bytes /= 1024; i++; }
return (Math.round(bytes * 10) / 10) + ' ' + u[i];
}
function escapeHtml(s) {
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
})();