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

+64 -6
View File
@@ -43,6 +43,13 @@
pageNext:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M9 18l6-6-6-6"/>' +
'</svg>',
emptyFolder:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M4 7h16"/>' +
'<path d="M9 7V4h6v3"/>' +
'<path d="M6 7l1 13a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-13"/>' +
'<path d="M9.5 12.5l5 5M14.5 12.5l-5 5"/>' +
'</svg>'
};
@@ -109,6 +116,15 @@
var links = document.querySelectorAll('.email-folder-link');
for (var i = 0; i < links.length; i++)
links[i].classList.toggle('email-folder-selected', links[i].getAttribute('data-folder') === currentFolder);
// Only Spam/Trash ever offer this - emptying Inbox (or any future
// folder) as a single destructive click isn't something any real
// mail client exposes, and the server enforces the exact same
// Spam/Trash-only allowlist independently (see email.jet) - this is
// just keeping the button from ever being visible somewhere it
// would 400 anyway, not the actual safety boundary.
var emptyBtn = document.getElementById('email-empty-folder-btn');
if (emptyBtn)
emptyBtn.hidden = (currentFolder !== 'Spam' && currentFolder !== 'Trash');
}
function escapeHtml(s) {
@@ -158,6 +174,7 @@
'<button type="button" id="email-search-btn" class="icon-btn" title="Search">' + ICONS.search + '</button>' +
'</span>' +
'<button type="button" id="email-compose-btn" class="icon-btn" title="New Mail">' + ICONS.compose + '</button>' +
'<button type="button" id="email-empty-folder-btn" class="icon-btn" title="Permanently empty this folder" hidden>' + ICONS.emptyFolder + '</button>' +
'<button type="button" id="email-refresh-btn" class="icon-btn" title="Refresh">' + ICONS.refresh + '</button>' +
'</span>' +
'</div>' +
@@ -218,6 +235,7 @@
document.getElementById('email-refresh-btn').addEventListener('click', function() {
loadMessageList();
});
document.getElementById('email-empty-folder-btn').addEventListener('click', emptyCurrentFolder);
// Matches sender, subject, or body content in whatever folder is
// currently open (see __imap.cpp's searchMessages()) - an empty
@@ -444,6 +462,38 @@
});
}
// Permanently deletes every message in the CURRENT folder (only ever
// reachable while viewing Spam or Trash - see syncActiveFolder()'s own
// button-visibility gate, and email.jet's independent server-side
// allowlist, which is the real enforcement). Unlike trashMessage()
// there's no undo, no "move somewhere" - a real, human-readable confirm()
// is the whole safety net here, matching how Gmail/Outlook both gate
// "Empty Trash now" behind exactly one extra click and nothing fancier.
function emptyCurrentFolder() {
if (currentFolder !== 'Spam' && currentFolder !== 'Trash')
return;
if (!window.confirm('Permanently delete every message in ' + currentFolder + '? This cannot be undone.'))
return;
var btn = document.getElementById('email-empty-folder-btn');
if (btn) btn.disabled = true;
post('/email/emptyfolder', { folder: currentFolder })
.then(function(res) {
if (btn) btn.disabled = false;
if (!res.ok) {
window.alert(res.error || 'Could not empty the folder.');
return;
}
currentUid = null;
document.getElementById('email-reading-pane').innerHTML =
'<div id="email-empty-state">Select a message to read it.</div>';
loadMessageList();
})
.catch(function() {
if (btn) btn.disabled = false;
window.alert('Empty-folder request failed.');
});
}
// Same fade-out/fetch-concurrently/fade-in shape as openMessage() and
// spa.js's own navigate() - every list refresh (folder switch,
// refresh button) gets the same smooth transition instead of a flash
@@ -488,8 +538,10 @@
if (!skipFade)
list.classList.remove('email-fading');
})
.catch(function() {
list.innerHTML = '<div id="email-empty-state">Could not load messages.</div>';
.catch(function(err) {
console.error('loadMessageList failed:', err);
list.innerHTML = '<div id="email-empty-state">Could not load messages' +
(err && err.message ? ': ' + escapeHtml(err.message) : '.') + '</div>';
if (!skipFade)
list.classList.remove('email-fading');
});
@@ -506,8 +558,10 @@
// cosmetic problem, not worth surfacing as a user-visible failure.
function loadFolderCounts() {
post('/email/folders', {}).then(function(res) {
if (res.error)
if (res.error) {
console.error('loadFolderCounts server error:', res.error);
return;
}
var folders = ['Inbox', 'Spam', 'Trash'];
for (var i = 0; i < folders.length; i++) {
var f = folders[i];
@@ -515,7 +569,9 @@
if (span && res[f])
span.textContent = res[f].messages > 0 ? (res[f].unseen + '/' + res[f].messages) : '';
}
}).catch(function() {});
}).catch(function(err) {
console.error('loadFolderCounts failed:', err);
});
}
function loadAccounts() {
@@ -756,8 +812,10 @@
}
pane.classList.remove('email-fading');
})
.catch(function() {
pane.innerHTML = '<div id="email-empty-state">Could not load message.</div>';
.catch(function(err) {
console.error('openMessage failed:', err);
pane.innerHTML = '<div id="email-empty-state">Could not load message' +
(err && err.message ? ': ' + escapeHtml(err.message) : '.') + '</div>';
pane.classList.remove('email-fading');
});
}