Add global music player, invoicing/payment system, account subpages, mail account admin, and the qualityiron.vegas Jet 2.0 port

Jet interpreter (Tag.cpp, __call.cpp, __imap*.cpp/h, __smtpsend.cpp/h,
__imapemptyfolder.cpp/h - new): assorted parser/tag fixes found across
this work - a stray literal-tag-in-comment and backslash-quote parser
corruption class, a dangling-pointer fix, a __call.cpp fd leak, IMAP
folder-management tags, multi-account/per-host IMAP support, and a
smtpsend Message-ID fix.

barant.com site (jetc/barant-site/):
- A site-wide persistent music player (header band, playlist/queue,
  stereo FFT EQ, session-persisted state) fed by new library/
  ianslibrary/music song-list subpages.
- /account split into info/phones/invoices subpages; a full invoicing
  and Square payment system (create/edit/copy/pay/email invoice);
  admin invoice management and mail-account management subpages.
- /services split into overview/hosting/siptrunking/documents
  subpages, retiring the old standalone pages.
- SIPPHONE.tag: a Jet 2.0 WebRTC softphone tag.
- main.css/spa.js/email.js: the SPA loading indicator, the music
  player band styling, and assorted fixes accumulated alongside this
  work.

qualityiron-site/: the qualityiron.vegas job/contractor/customer/
document management app, ported from its original Jet 1.4 codebase to
Jet 2.0 as barant.coms APIServers second multi-tenant site.

wcwc/wcwc.jet: the WCWC site Jet source (wcwc.jtc build output not
included).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Brad ArantandClaude Sonnet 5 committed 2026-10-06 17:42:01 +00:00
1 parent 905fbafa5a
commit b8f7793773
406 files changed
+15803 -587

No files matched your search

@@ -0,0 +1,184 @@
// Site-wide file transfer manager for qualityiron.vegas - loaded once
// via mainpagetemplate.jet (outside #content) so the floating panel and
// any in-progress transfer survive spa.js's own #content-only swaps.
// Adapted from barant.com's own documents.js (same proven mechanism),
// just pointed at qualityiron's own /jobdocs/* routes instead of
// barant.com's /documents/*.
(function() {
var panel = document.getElementById('filemgr-panel');
if (!panel)
return;
var toggle = document.getElementById('filemgr-toggle');
var badge = document.getElementById('filemgr-badge');
var list = document.getElementById('filemgr-list');
var clearBtn = document.getElementById('filemgr-clear');
var transfers = {};
var nextId = 1;
function escapeHtml(s) {
var div = document.createElement('div');
div.textContent = s == null ? '' : String(s);
return div.innerHTML;
}
function updateBadge() {
var activeCount = 0;
for (var k in transfers) {
if (transfers[k].status === 'active')
activeCount++;
}
badge.textContent = activeCount > 0 ? String(activeCount) : '';
}
function removeEmptyPlaceholder() {
var empty = document.getElementById('filemgr-empty');
if (empty)
empty.remove();
}
function showEmptyPlaceholderIfNeeded() {
if (Object.keys(transfers).length === 0 && !document.getElementById('filemgr-empty'))
list.innerHTML = '<div id="filemgr-empty">No transfers yet.</div>';
}
var UPLOAD_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V6M5 12l7-7 7 7"/></svg>';
var DOWNLOAD_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v13M5 12l7 7 7-7"/></svg>';
function addTransfer(name, direction) {
var id = 'xfer' + (nextId++);
removeEmptyPlaceholder();
var el = document.createElement('div');
el.className = 'filemgr-item';
el.innerHTML =
'<div class="filemgr-item-row">' +
'<span class="filemgr-item-icon">' + (direction === 'upload' ? UPLOAD_ICON : DOWNLOAD_ICON) + '</span>' +
'<span class="filemgr-item-name">' + escapeHtml(name) + '</span>' +
'<span class="filemgr-item-status">0%</span>' +
'</div>' +
'<div class="filemgr-progress-track"><div class="filemgr-progress-fill"></div></div>';
list.insertBefore(el, list.firstChild);
transfers[id] = { el: el, status: 'active' };
panel.classList.add('filemgr-expanded');
updateBadge();
return id;
}
function updateProgress(id, percent) {
var t = transfers[id];
if (!t)
return;
t.el.querySelector('.filemgr-progress-fill').style.width = percent + '%';
t.el.querySelector('.filemgr-item-status').textContent = percent + '%';
}
function markDone(id, statusText) {
var t = transfers[id];
if (!t)
return;
t.status = 'done';
t.el.classList.add('filemgr-done');
t.el.querySelector('.filemgr-progress-fill').style.width = '100%';
t.el.querySelector('.filemgr-item-status').textContent = statusText || 'Done';
updateBadge();
}
function markFailed(id, statusText) {
var t = transfers[id];
if (!t)
return;
t.status = 'failed';
t.el.classList.add('filemgr-failed');
t.el.querySelector('.filemgr-item-status').textContent = statusText || 'Failed';
updateBadge();
}
// Uploads a FileList/array of File objects, each as its own
// concurrent XHR request, tracked here as separate panel entries.
// job_id/subtype_id come from data attributes on the dropzone element
// itself (set by jobdocs.page per the job/category being uploaded
// into) rather than being hardcoded here, since this same script is
// shared across every job's documents page.
window.fileManagerUpload = function(files, jobId, subtypeId) {
for (var i = 0; i < files.length; i++) {
(function(file) {
var id = addTransfer(file.name, 'upload');
var xhr = new XMLHttpRequest();
transfers[id].xhr = xhr;
xhr.open('POST', '/jobdocs/api/upload');
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable)
updateProgress(id, Math.round((e.loaded / e.total) * 100));
});
xhr.addEventListener('load', function() {
var res = null;
try { res = JSON.parse(xhr.responseText); } catch (e) {}
if (xhr.status === 200 && res && res.ok) {
markDone(id);
document.dispatchEvent(new CustomEvent('jobdocs:changed'));
} else {
markFailed(id, (res && res.error) || 'Failed');
}
});
xhr.addEventListener('error', function() { markFailed(id); });
var fd = new FormData();
fd.append('name', file.name);
fd.append('job_id', jobId);
fd.append('subtype_id', subtypeId);
fd.append('mimetype', file.type || 'application/octet-stream');
fd.append('file_size', file.size);
fd.append('filedata', file);
xhr.send(fd);
})(files[i]);
}
};
// Downloads a document by id via XHR (responseType 'blob') so it gets
// real tracked progress, then hands the bytes to the browser's normal
// save flow via a Blob URL + synthetic <a download> click.
window.fileManagerDownload = function(id, name) {
var xferId = addTransfer(name, 'download');
var xhr = new XMLHttpRequest();
transfers[xferId].xhr = xhr;
xhr.open('GET', '/jobdocs/download?id=' + encodeURIComponent(id));
xhr.responseType = 'blob';
xhr.addEventListener('progress', function(e) {
if (e.lengthComputable)
updateProgress(xferId, Math.round((e.loaded / e.total) * 100));
});
xhr.addEventListener('load', function() {
if (xhr.status !== 200) {
markFailed(xferId);
return;
}
var blobUrl = URL.createObjectURL(xhr.response);
var a = document.createElement('a');
a.href = blobUrl;
a.download = name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function() { URL.revokeObjectURL(blobUrl); }, 30000);
markDone(xferId);
});
xhr.addEventListener('error', function() { markFailed(xferId); });
xhr.send();
};
toggle.addEventListener('click', function() {
panel.classList.toggle('filemgr-expanded');
});
clearBtn.addEventListener('click', function() {
for (var k in transfers) {
if (transfers[k].status !== 'active') {
transfers[k].el.remove();
delete transfers[k];
}
}
showEmptyPlaceholderIfNeeded();
});
})();