Add IMAP webmail tag, admin/opensource subpage tiers, store/cart, htmleditor, uploads

New __imap*/__smtpsend/__eval Jet tags give templates real IMAP
(list/message/flag/delete/move/search/folders/accounts) and SMTP-send
capability, backing the new /email webmail client (email.jet/
email.menu, assets/images/email.js + newemail.js) and its
mainpagetemplate.jet-based page shell. Most of this tag family's
source was already being built into the live site (Tag.cpp already
registered __imap/__imaplist/__imapmessage/__imapflag/__imapdelete/
__eval) but had never actually been committed; this also registers
and commits the remaining five (__imapmove/__imapfolders/
__imapsearch/__smtpsend/__imapaccounts).

admin.page/admin.menu gain a real subpage tier (admin-email,
admin-newemail, admin-invoices, admin-products, admin-users) replacing
several previously top-level pages, mirrored by a similar
opensource.page subpage tier (opensource-overview/asm1802/barantmail/
jet/serverframework/synthmax) - old top-level pages retired via git
rename (asm1802.page, jet.page, serverframework.page,
admin_invoices.page); barantmail.page is dropped outright, folded into
opensource-barantmail.subpage.

New storefront (store.page, store-createstore.subpage,
barantstore.page) and cart flow (cart.jet, updatecartqty.proc,
removecartitem.proc, editproduct.page/selectproduct.proc/
update_product.proc for inventory maintenance against the barant
database).

New document upload/download manager (documents.jet, documents_api.jet,
documents_upload.proc, documents_delete.proc,
assets/images/documents.js + documents-page.js) with a persistent
floating panel, and a rich-text page editor (htmleditor.jet/.page,
createhtmleditor.proc, savehtmleditor.proc, assets/images/htmleditor.js
+ its icon/logo assets).

SESSIONCTL.tag: login now redirects back to the originally-requested
page instead of always landing on /account ($[start:0] vs $[start]
variable-name mismatch).

main.css/menu.bar/invoices.menu/jet_overview.section/login.menu:
styling and navigation updates for the above; new site assets (fonts,
images, audio) referenced by them.

jetc/build-assets.sh, list_jet_files.sh, lowercase_jet.py,
strip_fragments.py: small dev tooling used while building the above.
This commit is contained in:
Brad Arant committed 2026-09-06 04:14:00 +00:00
1 parent b0cadc3f08
commit c1491b044b
115 files changed
+6174 -137

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,96 @@
(function() {
var dropzone = document.getElementById('documents-dropzone');
var fileInput = document.getElementById('documents-file-input');
var listBody = document.getElementById('documents-list-body');
if (!dropzone || !listBody)
return;
function escapeHtml(s) {
var div = document.createElement('div');
div.textContent = s == null ? '' : String(s);
return div.innerHTML;
}
function post(path, params) {
return fetch(path, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(params || {}).toString()
}).then(function(r) { return r.json(); });
}
function loadList() {
post('/documents/api/list', {}).then(function(res) {
if (!res.ok || !res.documents.length) {
listBody.innerHTML = '<div id="documents-empty">No documents yet - upload your first one above.</div>';
return;
}
var html = '<table id="documents-list"><tr><th>Name</th><th>Uploaded</th><th></th><th></th></tr>';
for (var i = 0; i < res.documents.length; i++) {
var doc = res.documents[i];
html += '<tr>' +
'<td>' + escapeHtml(doc.name) + '</td>' +
'<td>' + escapeHtml(doc.created) + '</td>' +
'<td><span class="documents-action" data-action="download" data-id="' + doc.id + '" data-name="' + escapeHtml(doc.name) + '">Download</span></td>' +
'<td><span class="documents-action" data-action="delete" data-id="' + doc.id + '">Delete</span></td>' +
'</tr>';
}
html += '</table>';
listBody.innerHTML = html;
var actions = listBody.querySelectorAll('.documents-action');
for (var j = 0; j < actions.length; j++) {
actions[j].addEventListener('click', function() {
var id = this.getAttribute('data-id');
if (this.getAttribute('data-action') === 'download') {
window.fileManagerDownload && window.fileManagerDownload(id, this.getAttribute('data-name'));
} else {
post('/documents/api/delete', { id: id }).then(function(res) {
if (res.ok)
loadList();
});
}
});
}
}).catch(function() {
listBody.innerHTML = '<div id="documents-empty">Could not load your documents.</div>';
});
}
function uploadFiles(files) {
if (!files || !files.length)
return;
if (window.fileManagerUpload)
window.fileManagerUpload(files);
}
dropzone.addEventListener('click', function() {
fileInput.click();
});
fileInput.addEventListener('change', function() {
uploadFiles(fileInput.files);
fileInput.value = '';
});
dropzone.addEventListener('dragover', function(e) {
e.preventDefault();
dropzone.classList.add('documents-dropzone-active');
});
dropzone.addEventListener('dragleave', function() {
dropzone.classList.remove('documents-dropzone-active');
});
dropzone.addEventListener('drop', function(e) {
e.preventDefault();
dropzone.classList.remove('documents-dropzone-active');
uploadFiles(e.dataTransfer.files);
});
// The site-wide file manager (documents.js, loaded once via
// mainpagetemplate.jet) dispatches this after a successful upload -
// listening here (rather than only refreshing right after our own
// uploadFiles() calls) also picks up uploads that were still running
// when this page was navigated to via spa.js.
document.addEventListener('documents:changed', loadList);
loadList();
})();
+178
View File
@@ -0,0 +1,178 @@
// Site-wide file transfer manager - loaded once via mainpagetemplate.jet
// (see its own script tag, outside #content) so the floating panel and
// any in-progress transfer survive spa.js's own #content-only swaps
// when navigating to a different page mid-upload/download.
(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 - the browser's own HTTP connection pooling
// runs several at once, tracked here as separate panel entries with
// independent progress via xhr.upload's own progress event (fetch()
// has no equivalent for the request/upload side, only the response).
window.fileManagerUpload = function(files) {
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', '/documents/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('documents: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('filedata', file);
xhr.send(fd);
})(files[i]);
}
};
// Downloads a document by id via XHR (responseType 'blob') instead
// of a plain <a href> so it gets real tracked progress in the same
// panel as uploads, then hands the fetched 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', '/documents/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();
});
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

+778
View File
@@ -0,0 +1,778 @@
(function() {
var app = document.getElementById('email-app');
if (!app) return;
// Small inline-SVG icon set for the toolbar/action buttons below -
// stroke="currentColor" so each one just inherits its <button>'s own
// text color (the site's gold theme, see .icon-btn in email.page's
// own <style> block), no separate image assets or color variants to
// keep in sync with the theme.
var ICONS = {
search:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
'<circle cx="11" cy="11" r="7"/>' +
'<path d="M21 21l-4.35-4.35"/>' +
'</svg>',
compose:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M4 6h13a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"/>' +
'<path d="M2.5 7.5 12 14l9.5-6.5"/>' +
'<path d="M18.5 3.5v5M16 6h5"/>' +
'</svg>',
refresh:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M20 11a8 8 0 0 0-14.6-4.4M4 4v5h5"/>' +
'<path d="M4 13a8 8 0 0 0 14.6 4.4M20 20v-5h-5"/>' +
'</svg>',
disconnect:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>' +
'<path d="M16 17l5-5-5-5"/>' +
'<path d="M21 12H9"/>' +
'</svg>',
flag:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M5 3v18"/>' +
'<path d="M5 4h12l-3 4.5L17 13H5"/>' +
'</svg>',
trash:
'<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="M10 11v6M14 11v6"/>' +
'</svg>'
};
// Matches the exact, case-sensitive names the mailbox itself reports
// via IMAP LIST (confirmed live: "Inbox", "Spam", "Trash" - only
// INBOX is specially case-insensitive per RFC 3501, the other two
// are not, so this has to be exact) - see email.menu's own folder
// links, which pass these same three names to emailSelectFolder().
var currentFolder = 'Inbox';
var currentUid = null;
// null = showing the normal folder listing; a non-empty string =
// showing search results instead (see performSearch()/clearSearch()
// and loadMessageList()'s own branch on this).
var currentSearchQuery = null;
// The array last handed to renderMessageList() - kept so openMessage()
// can flip a single message's .seen flag locally and recompute the
// toolbar's unread/total count without a round trip.
var currentMessages = [];
// email.menu (the site's own left-side #panelmenu, a static include
// living outside #email-app/#content entirely - see mainpagetemplate
// .jet's own panelmenu wiring) renders its folder links against this
// global rather than anything scoped to this closure, since it has
// no other way to reach into a same-page IMAP client without a full
// page navigation (which the rest of this client deliberately avoids
// for every other interaction too - see the module doc comment on
// post() below... [see loadMessageList/openMessage]). Guarded with
// `window.emailSelectFolder &&` at each call site in email.menu so a
// click before email.js has loaded (or before a mailbox is even
// connected) is a harmless no-op instead of a broken link.
window.emailSelectFolder = function(folder) {
if (!document.getElementById('email-client') || (folder === currentFolder && !currentSearchQuery))
return;
currentFolder = folder;
currentUid = null;
// A folder click always means "show me this folder, plain" - drops
// out of search mode even if the folder didn't actually change,
// which is exactly how a user would expect to back out of a
// search result view.
currentSearchQuery = null;
var searchInput = document.getElementById('email-search-input');
if (searchInput) searchInput.value = '';
syncActiveFolder();
var pane = document.getElementById('email-reading-pane');
if (pane) pane.innerHTML = '<div id="email-empty-state">Select a message to read it.</div>';
loadMessageList();
};
function syncActiveFolder() {
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);
}
function escapeHtml(s) {
var div = document.createElement('div');
div.textContent = s == null ? '' : String(s);
return div.innerHTML;
}
function post(path, params) {
return fetch(path, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(params || {}).toString()
}).then(function(r) { return r.json(); });
}
// Matches spa.js's own delay() exactly - a separate closure here
// since this file has no dependency on spa.js at all, not worth
// sharing just for this.
function delay(ms) {
return new Promise(function(resolve) { setTimeout(resolve, ms); });
}
function showConnectForm(errorText) {
app.innerHTML =
'<div id="email-connect-form">' +
'<h3>Connect to Mailbox</h3>' +
'<input type="text" id="email-user-input" placeholder="you@barant.com" autocomplete="username">' +
'<input type="password" id="email-password-input" placeholder="Password" autocomplete="current-password">' +
'<button type="button" id="email-connect-btn">Connect</button>' +
'<div id="email-connect-error">' + (errorText ? escapeHtml(errorText) : '') + '</div>' +
'</div>';
wireConnectForm();
}
function showClientView(userEmail) {
app.innerHTML =
'<div id="email-client">' +
'<div id="email-toolbar">' +
'<span id="email-connected-as">' + escapeHtml(userEmail) + '</span>' +
'<span id="email-list-count"></span>' +
'<span id="email-toolbar-actions">' +
'<span id="email-search-box">' +
'<input type="text" id="email-search-input" placeholder="Search mail&hellip;">' +
'<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-refresh-btn" class="icon-btn" title="Refresh">' + ICONS.refresh + '</button>' +
'<button type="button" id="email-disconnect-btn" class="icon-btn" title="Disconnect">' + ICONS.disconnect + '</button>' +
'</span>' +
'</div>' +
'<div id="email-layout">' +
'<div id="email-list"></div>' +
'<div id="email-reading-pane"><div id="email-empty-state">Select a message to read it.</div></div>' +
'</div>' +
'</div>';
wireClientView();
wireAddAccountForm();
syncActiveFolder();
loadMessageList();
loadAccounts();
}
function wireConnectForm() {
var btn = document.getElementById('email-connect-btn');
var userInput = document.getElementById('email-user-input');
var passInput = document.getElementById('email-password-input');
var errorDiv = document.getElementById('email-connect-error');
function submit() {
errorDiv.textContent = '';
btn.disabled = true;
post('/email/connect', { user: userInput.value, password: passInput.value })
.then(function(res) {
btn.disabled = false;
if (res.ok) {
showClientView(userInput.value);
} else {
errorDiv.textContent = res.error || 'Could not connect.';
}
})
.catch(function() {
btn.disabled = false;
errorDiv.textContent = 'Connection request failed.';
});
}
btn.addEventListener('click', submit);
passInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') submit();
});
}
function wireClientView() {
// Navigates via a synthetic <a> click rather than
// window.location.href so spa.js's own document-level click
// listener intercepts it and does its normal fade transition,
// exactly as if a real link had been clicked.
document.getElementById('email-compose-btn').addEventListener('click', function() {
var a = document.createElement('a');
a.href = '/admin/newemail';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
document.getElementById('email-refresh-btn').addEventListener('click', function() {
loadMessageList();
});
document.getElementById('email-disconnect-btn').addEventListener('click', function() {
// Disconnecting only ever drops the ACTIVE account now (see
// /email/disconnect's own doc comment in email.jet) - if another
// already-connected account remains, res.active names it and the
// view just switches there instead of falling back to the
// connect form.
post('/email/disconnect', {}).then(function(res) {
if (res.active) {
resetViewForActiveAccount(res.active);
} else {
currentFolder = 'Inbox';
currentUid = null;
syncActiveFolder();
showConnectForm();
}
});
});
// Matches sender, subject, or body content in whatever folder is
// currently open (see __imap.cpp's searchMessages()) - an empty
// query is treated as "leave search mode" rather than sent to the
// server as a real search, since an empty needle would otherwise
// just match everything anyway (see __IMAP_SEARCH.cpp's own
// containsCI()) - clearer to just fall back to the plain listing.
function runSearch() {
var input = document.getElementById('email-search-input');
var query = input.value.trim();
currentSearchQuery = query === '' ? null : query;
currentUid = null;
document.getElementById('email-reading-pane').innerHTML =
'<div id="email-empty-state">Select a message to read it.</div>';
loadMessageList();
}
document.getElementById('email-search-btn').addEventListener('click', runSearch);
document.getElementById('email-search-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter')
runSearch();
});
}
function updateListCount(messages) {
var countEl = document.getElementById('email-list-count');
if (!countEl)
return;
if (!messages.length) {
countEl.textContent = '';
return;
}
var unread = 0;
for (var i = 0; i < messages.length; i++) {
if (!messages[i].seen)
unread++;
}
countEl.textContent = '(' + unread + '/' + messages.length + ')';
}
function renderMessageList(messages) {
var list = document.getElementById('email-list');
currentMessages = messages;
updateListCount(messages);
if (!messages.length) {
list.innerHTML = '<div id="email-empty-state">No messages.</div>';
return;
}
var html = '';
for (var i = 0; i < messages.length; i++) {
var m = messages[i];
html += '<div class="email-row' + (m.seen ? '' : ' unread') +
(m.uid === currentUid ? ' selected' : '') + '" data-uid="' + m.uid + '">' +
'<div class="email-row-info">' +
'<div class="email-from">' + escapeHtml(m.from) + '</div>' +
'<div class="email-subject">' + escapeHtml(m.subject || '(no subject)') + '</div>' +
'<div class="email-date">' + escapeHtml(m.date) + '</div>' +
'</div>' +
'<button type="button" class="icon-btn email-row-trash-btn" data-uid="' + m.uid + '" title="Move to Trash">' + ICONS.trash + '</button>' +
'</div>';
}
list.innerHTML = html;
var rows = list.querySelectorAll('.email-row');
for (var j = 0; j < rows.length; j++) {
rows[j].addEventListener('click', function() {
openMessage(parseInt(this.getAttribute('data-uid'), 10));
});
}
// Each row's own trash button - stopPropagation so it doesn't also
// trigger the row's own click-to-open handler just above.
var trashBtns = list.querySelectorAll('.email-row-trash-btn');
for (var k = 0; k < trashBtns.length; k++) {
trashBtns[k].addEventListener('click', function(e) {
e.stopPropagation();
trashMessage(parseInt(this.getAttribute('data-uid'), 10));
});
}
// Whatever caused this particular render (initial connect, a
// folder switch, the refresh button with nothing open) left
// currentUid null - open the top message rather than leave the
// reading pane sitting empty. trashMessage() deliberately sets
// currentUid to the row it's already decided to select BEFORE
// reloading the list (see its own comment), specifically so this
// check sees a real, already-correct uid and stays out of the way
// instead of racing it to open something different.
if (currentUid === null)
openMessage(messages[0].uid);
}
// Moves a message into the Trash folder (server-side: COPY there,
// then \Deleted+EXPUNGE from the original folder - see __imap.cpp's
// moveMessage()) rather than permanently removing it, the same
// "delete means trash it" convention the reading pane's own Delete
// button (a real, permanent expunge) deliberately does NOT follow -
// that one stays as-is; this is a separate, softer action reachable
// straight from the list.
// Total time the .email-row-removing CSS animation takes to finish
// (see email.page's own <style> block) - the opacity fade runs with
// no delay for FADE_MS, then the height/padding/margin/border
// collapse (what actually slides the rows below it up) runs for
// COLLAPSE_MS starting COLLAPSE_DELAY_MS in. Kept here as the single
// source of truth for how long trashMessage() waits before swapping
// in the server's real, post-trash list - duplicated as literal
// numbers in the CSS transition properties themselves, since CSS has
// no way to reference a JS constant, so keep those two in sync by
// hand if either ever changes.
var REMOVE_FADE_MS = 200;
var REMOVE_COLLAPSE_DELAY_MS = 200;
var REMOVE_COLLAPSE_MS = 220;
// Plays the dissolve-then-collapse removal animation on one row in
// place, resolving once it's fully finished (safe to call with a
// null row - resolves immediately, for when the trashed uid's row
// isn't actually in the currently-rendered list for some reason).
function animateRowRemoval(row) {
if (!row)
return Promise.resolve();
var height = row.getBoundingClientRect().height;
row.style.height = height + 'px';
// Forces the browser to register the explicit pixel height above
// as a real, already-applied style before the next line changes
// it again - without this the two height changes collapse into a
// single layout pass and no transition plays at all.
row.offsetHeight;
row.classList.add('email-row-removing');
return new Promise(function(resolve) {
setTimeout(resolve, REMOVE_FADE_MS + REMOVE_COLLAPSE_DELAY_MS + REMOVE_COLLAPSE_MS);
});
}
// Only the trashed row itself animates - every other row just stays
// exactly where it is and shifts up as an ordinary side effect of
// the trashed row's own height collapsing to 0 (real document-flow
// reflow, not a separate animation applied to them). The list is
// still reloaded from the server afterward (flags/counts could have
// changed for reasons unrelated to this one trash action), but with
// loadMessageList(true) - skipping ITS OWN whole-list fade, since by
// the time it swaps in fresh markup the trashed row has already
// visually shrunk to nothing and every other row is already sitting
// in its final position; fading the whole list on top of that would
// just be a second, redundant flash.
function trashMessage(uid) {
var rows = document.querySelectorAll('.email-row');
var index = -1;
var trashedRow = null;
for (var i = 0; i < rows.length; i++) {
if (parseInt(rows[i].getAttribute('data-uid'), 10) === uid) {
index = i;
trashedRow = rows[i];
break;
}
}
var nextUid = null;
if (index !== -1) {
if (index + 1 < rows.length)
nextUid = parseInt(rows[index + 1].getAttribute('data-uid'), 10);
else if (index - 1 >= 0)
nextUid = parseInt(rows[index - 1].getAttribute('data-uid'), 10);
}
Promise.all([
animateRowRemoval(trashedRow),
post('/email/trash', { folder: currentFolder, uid: uid })
]).then(function() {
// Set BEFORE the reload, not after - renderMessageList()'s own
// auto-select-first-when-nothing's-selected check runs partway
// through loadMessageList(true) below, and needs to already see
// the real target here (or genuinely null, if the list is about
// to be empty) rather than a stale null that would make it pick
// the top row instead and race this function's own openMessage()
// call just below.
if (currentUid === uid)
currentUid = nextUid;
loadMessageList(true).then(function() {
if (nextUid !== null)
openMessage(nextUid);
else
document.getElementById('email-reading-pane').innerHTML =
'<div id="email-empty-state">Select a message to read it.</div>';
});
});
}
// 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
// of "Loading..." text followed by an abrupt content swap. Pass
// skipFade=true to skip that whole-list transition and just quietly
// re-render in place - used only right after trashMessage()'s own
// per-row removal animation has already provided the visual
// transition, so this reload doesn't also blink the entire list.
function loadMessageList(skipFade) {
var list = document.getElementById('email-list');
if (!list)
return Promise.resolve();
if (!skipFade)
list.classList.add('email-fading');
// Search results use the identical JSON shape as a normal list
// (see __imap.cpp's shared parseSummaryLines()), so this is the
// only place that needs to know search mode even exists -
// renderMessageList() below has no idea which one produced what
// it's rendering.
var request = currentSearchQuery
? post('/email/search', { folder: currentFolder, query: currentSearchQuery })
: post('/email/list', { folder: currentFolder });
return Promise.all([
request,
skipFade ? true : delay(180)
])
.then(function(results) {
var res = results[0];
if (!res.ok && res.error)
list.innerHTML = '<div id="email-empty-state">' + escapeHtml(res.error) + '</div>';
else
renderMessageList(Array.isArray(res) ? res : []);
// Every place that reloads the list (connect, folder switch,
// refresh, post-trash) is a place the unseen counts next to
// each folder name could plausibly have changed too - one
// hook here covers all of them rather than needing a separate
// call at each individual call site.
loadFolderCounts();
if (!skipFade)
list.classList.remove('email-fading');
})
.catch(function() {
list.innerHTML = '<div id="email-empty-state">Could not load messages.</div>';
if (!skipFade)
list.classList.remove('email-fading');
});
}
// Populates the unread-count badge next to each folder name in
// email.menu's own static markup (a real DOM lookup by
// data-folder-count, not anything rendered by this file - email.menu
// is a separate include living in #panelmenu, outside #content
// entirely). Uses IMAP STATUS server-side (see __imap.cpp's
// folderCounts()), not a real FETCH, so this is cheap enough to call
// on every list reload without it being a heavy extra round trip.
// Silently does nothing on error - a stale/missing count badge is a
// cosmetic problem, not worth surfacing as a user-visible failure.
function loadFolderCounts() {
post('/email/folders', {}).then(function(res) {
if (res.error)
return;
var folders = ['Inbox', 'Spam', 'Trash'];
for (var i = 0; i < folders.length; i++) {
var f = folders[i];
var span = document.querySelector('.email-folder-count[data-folder-count="' + f + '"]');
if (span && res[f])
span.textContent = res[f].messages > 0 ? (res[f].unseen + '/' + res[f].messages) : '';
}
}).catch(function() {});
}
function loadAccounts() {
post('/email/accounts', {}).then(function(res) {
var list = document.getElementById('email-account-list');
if (!list || !res.accounts)
return;
var html = '';
for (var i = 0; i < res.accounts.length; i++) {
var user = res.accounts[i];
html += '<a href="javascript:void(0);" class="email-account-link' +
(user === res.active ? ' email-account-selected' : '') + '" data-user="' +
escapeHtml(user) + '">' + escapeHtml(user) + '</a>';
}
list.innerHTML = html;
var links = list.querySelectorAll('.email-account-link');
for (var j = 0; j < links.length; j++) {
links[j].addEventListener('click', function() {
var user = this.getAttribute('data-user');
if (!this.classList.contains('email-account-selected'))
switchAccount(user);
});
}
}).catch(function() {});
}
// Shared by switchAccount() and the add-account form's own success
// path (which is already connected/active server-side by the time
// /email/connect returns ok - no reason to also round-trip through
// switchaccount right after) - resets the view to a fresh Inbox for
// whichever account just became active, same as a brand new connect.
function resetViewForActiveAccount(user) {
currentFolder = 'Inbox';
currentUid = null;
currentSearchQuery = null;
var searchInput = document.getElementById('email-search-input');
if (searchInput) searchInput.value = '';
var connectedAs = document.getElementById('email-connected-as');
if (connectedAs) connectedAs.textContent = user;
syncActiveFolder();
var pane = document.getElementById('email-reading-pane');
if (pane) pane.innerHTML = '<div id="email-empty-state">Select a message to read it.</div>';
loadMessageList();
loadAccounts();
}
function switchAccount(user) {
post('/email/switchaccount', { user: user }).then(function(res) {
if (res.ok)
resetViewForActiveAccount(user);
}).catch(function() {});
}
window.emailShowAddAccount = function() {
var form = document.getElementById('email-add-account-form');
var link = document.getElementById('email-add-account-link');
if (!form)
return;
form.style.display = 'flex';
if (link) link.style.display = 'none';
var userInput = document.getElementById('email-add-account-user');
if (userInput) userInput.focus();
};
function wireAddAccountForm() {
var btn = document.getElementById('email-add-account-btn');
var userInput = document.getElementById('email-add-account-user');
var passInput = document.getElementById('email-add-account-password');
var errorDiv = document.getElementById('email-add-account-error');
if (!btn)
return;
function submit() {
errorDiv.textContent = '';
btn.disabled = true;
post('/email/connect', { user: userInput.value, password: passInput.value })
.then(function(res) {
btn.disabled = false;
if (!res.ok) {
errorDiv.textContent = res.error || 'Could not connect.';
return;
}
var newUser = userInput.value;
userInput.value = '';
passInput.value = '';
document.getElementById('email-add-account-form').style.display = 'none';
document.getElementById('email-add-account-link').style.display = 'block';
resetViewForActiveAccount(newUser);
})
.catch(function() {
btn.disabled = false;
errorDiv.textContent = 'Connection request failed.';
});
}
btn.addEventListener('click', submit);
passInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') submit();
});
}
function renderMessage(msg) {
var pane = document.getElementById('email-reading-pane');
pane.innerHTML =
'<div class="email-header">' +
'<div><b>From:</b> ' + escapeHtml(msg.from) + '</div>' +
'<div><b>To:</b> ' + escapeHtml(msg.to) + '</div>' +
'<div><b>Subject:</b> ' + escapeHtml(msg.subject || '(no subject)') + '</div>' +
'<div><b>Date:</b> ' + escapeHtml(msg.date) + '</div>' +
'</div>' +
'<div class="email-body-container"></div>' +
'<div class="email-actions">' +
'<button type="button" id="email-flag-btn" class="icon-btn' + (msg.flagged ? ' icon-btn-active' : '') +
'" title="' + (msg.flagged ? 'Unflag' : 'Flag') + '">' + ICONS.flag + '</button>' +
'<button type="button" id="email-delete-btn" class="icon-btn" title="Delete">' + ICONS.trash + '</button>' +
'</div>';
// HTML-formatted mail renders in a sandboxed iframe, never via
// innerHTML into this page directly - the body content is whatever
// a third party sent, and this page runs as the logged-in user
// with real session cookies. `allow-same-origin` alone (no
// `allow-scripts`, no `allow-forms`, no `allow-popups`, no
// `allow-top-navigation`) still blocks every dangerous thing
// untrusted HTML could do - scripts simply do not run without
// allow-scripts, full stop, regardless of same-origin access - it
// ONLY additionally lets THIS PAGE read the iframe's own rendered
// content height back out (contentDocument), which the very first,
// fully-opaque `sandbox=""` version couldn't do at all. That's
// needed to size the iframe to its actual content below, rather
// than a fixed height with its own internal scrollbar sitting
// inside the reading pane's own scrollbar - two nested scrollbars
// for one piece of content, confusing regardless of how tall the
// fixed box happened to be. Building it via createElement + the
// .srcdoc PROPERTY (not a string-concatenated HTML attribute)
// sidesteps any need to hand-escape the content for attribute-
// safety - the DOM API does that correctly on its own. Plain-text
// mail keeps using .textContent, same reasoning.
var bodyContainer = pane.querySelector('.email-body-container');
if (msg.isHtml) {
var frame = document.createElement('iframe');
frame.className = 'email-body-frame';
frame.setAttribute('sandbox', 'allow-same-origin');
frame.setAttribute('scrolling', 'no');
frame.addEventListener('load', function() {
try {
var doc = frame.contentDocument;
if (doc && doc.documentElement)
frame.style.height = Math.max(doc.documentElement.scrollHeight, doc.body.scrollHeight) + 'px';
} catch (e) {
// Left at its CSS fallback height - still shows the content,
// just with its own scrollbar if that fallback runs short.
}
});
frame.srcdoc = msg.body;
bodyContainer.appendChild(frame);
} else {
var textDiv = document.createElement('div');
textDiv.className = 'email-body';
textDiv.textContent = msg.body;
bodyContainer.appendChild(textDiv);
}
document.getElementById('email-flag-btn').addEventListener('click', function() {
post('/email/flag', {
folder: currentFolder,
uid: msg.uid,
flag: '\\Flagged',
set: msg.flagged ? 'no' : 'yes'
}).then(function() {
loadMessageList();
openMessage(msg.uid);
});
});
document.getElementById('email-delete-btn').addEventListener('click', function() {
if (!window.confirm('Delete this message?')) return;
post('/email/delete', { folder: currentFolder, uid: msg.uid }).then(function() {
currentUid = null;
document.getElementById('email-reading-pane').innerHTML =
'<div id="email-empty-state">Select a message to read it.</div>';
loadMessageList();
});
});
}
// Opening a message is the reading pane's own concern - it fetches
// and renders itself via /email/message and nothing else. It used to
// also call loadMessageList() afterward (re-fetching and
// re-rendering the ENTIRE list, every single time a message was
// opened) just to pick up the \Seen flag the server already set as a
// side effect of fetching the body - that's a locally-known update
// (we know which uid was just opened), applied straight to the
// already-rendered row below instead of paying for a second full
// round trip and list re-render on every click.
function openMessage(uid) {
currentUid = uid;
var pane = document.getElementById('email-reading-pane');
var rows = document.querySelectorAll('.email-row');
var wasUnread = false;
for (var i = 0; i < rows.length; i++) {
var isTarget = parseInt(rows[i].getAttribute('data-uid'), 10) === uid;
rows[i].classList.toggle('selected', isTarget);
if (isTarget) {
wasUnread = rows[i].classList.contains('unread');
// Marked read the instant it's selected, not after the fetch
// resolves - fetchMessage() marks it \Seen server-side anyway
// (BODY[] rather than BODY.PEEK[]), so there's nothing
// optimistic about this beyond removing the visual lag.
rows[i].classList.remove('unread');
}
}
if (wasUnread) {
for (var k = 0; k < currentMessages.length; k++) {
if (currentMessages[k].uid === uid)
currentMessages[k].seen = true;
}
updateListCount(currentMessages);
loadFolderCounts();
}
// Same fade-out/fetch-concurrently/fade-in shape as spa.js's own
// navigate() - a minimum-duration fade (via delay()) runs alongside
// the fetch rather than after it, so a fast response still shows
// the full transition instead of a flash, and a slow one doesn't
// wait any longer than the fetch itself already takes.
pane.classList.add('email-fading');
Promise.all([
post('/email/message', { folder: currentFolder, uid: uid }),
delay(180)
])
.then(function(results) {
var res = results[0];
if (!res.ok && res.error) {
pane.innerHTML = '<div id="email-empty-state">' + escapeHtml(res.error) + '</div>';
} else {
renderMessage(res);
}
pane.classList.remove('email-fading');
})
.catch(function() {
pane.innerHTML = '<div id="email-empty-state">Could not load message.</div>';
pane.classList.remove('email-fading');
});
}
// Up/Down moves through the message list and opens whatever it
// lands on - registered once, unconditionally, at load time (same
// always-on-but-internally-guarded shape as spa.js's own document-
// level click listener) rather than added/removed each time the
// client view is (re)shown, so reconnecting never stacks up
// duplicate handlers. Bails out immediately whenever the client
// view isn't actually showing (the connect form has no .email-row
// elements at all) or the keystroke is really meant for a form
// field (a future compose/search input, say) rather than list
// navigation.
document.addEventListener('keydown', function(e) {
var active = document.activeElement;
if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA'))
return;
// Delete trashes whichever message is currently open, the same
// action (and the same dissolve-and-slide-up animation) as
// clicking that row's own trash button - see trashMessage().
if (e.key === 'Delete') {
if (!document.getElementById('email-client') || currentUid === null)
return;
e.preventDefault();
trashMessage(currentUid);
return;
}
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp')
return;
var rows = document.querySelectorAll('.email-row');
if (!rows.length)
return;
e.preventDefault();
var index = -1;
for (var i = 0; i < rows.length; i++) {
if (parseInt(rows[i].getAttribute('data-uid'), 10) === currentUid) {
index = i;
break;
}
}
if (e.key === 'ArrowDown')
index = (index === -1) ? 0 : Math.min(index + 1, rows.length - 1);
else
index = (index === -1) ? 0 : Math.max(index - 1, 0);
rows[index].scrollIntoView({ block: 'nearest' });
openMessage(parseInt(rows[index].getAttribute('data-uid'), 10));
});
if (document.getElementById('email-connect-form')) {
wireConnectForm();
} else if (document.getElementById('email-client')) {
wireClientView();
wireAddAccountForm();
syncActiveFolder();
loadMessageList();
loadAccounts();
}
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

@@ -0,0 +1,678 @@
function b64encode(str) {
return btoa(unescape(encodeURIComponent(str)));
}
function submitNewLetter(form) {
var name = document.getElementById('filename_display').value;
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
alert('File name may only contain letters, numbers, underscores and hyphens.');
return false;
}
form.filename.value = name;
return true;
}
var mainpage;
var control;
var result;
var mousedownx;
var mousedowny;
var dragHint = "move";
var data;
var showGrid = false;
var snapToGrid = false;
var gridSize = 10;
var selected;
var firstSelected;
var itemparameters;
var grid;
var context;
var draggingHTML;
var window_dragging = false;
const undo = [];
var undoIndex = 0;
var lastRedo;
var mode = 'MOUSEUP';
var textobj;
var lastdragover;
var pendingDragover;
var dropIndicator;
var sequence = 0;
function showDropIndicator(target) {
if(!dropIndicator) {
dropIndicator = document.createElement('div');
dropIndicator.style.position = 'fixed';
dropIndicator.style.height = '4px';
dropIndicator.style.background = '#0088ff';
dropIndicator.style.zIndex = '99999';
dropIndicator.style.pointerEvents = 'none';
document.body.appendChild(dropIndicator);
}
let rect = target.getBoundingClientRect();
dropIndicator.style.left = rect.left + 'px';
dropIndicator.style.width = rect.width + 'px';
dropIndicator.style.top = (rect.top - 2) + 'px';
dropIndicator.style.display = 'block';
}
function hideDropIndicator() {
if(dropIndicator) {
dropIndicator.style.display = 'none';
}
}
function saveUndo() {
undo[undoIndex++] = mainpage.innerHTML;
}
function do_undo() {
if(undoIndex > 0) {
mainpage.innerHTML = undo[--undoIndex];
}
return true;
}
function do_redo() {
return true;
}
function init() {
mainpage = document.getElementById("mainpage");
data = document.getElementById("data");
itemparameters = document.getElementById("itemparameters");
var gridsize = document.getElementById("gridsize");
gridsize.value = gridSize;
grid = document.getElementById("grid");
context = grid.getContext("2d");
drawGrid();
buildToolbar();
document.designMode = 'off';
}
function getLayerMap(element) {
var map = '';
if(element.id != 'mainpage') {
var curelem = element;
while(curelem.id != 'mainpage') {
map = '&lt;' + curelem.nodeName + '&gt;' + map;
curelem = curelem.parentNode;
}
}
document.getElementById('map').innerHTML = map;
}
function jetEscape(text) {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function buildToolbar() {
let tbitems = [
{
"label": "&lt;div>",
"html": "<div id=\"newitem\" contenteditable=\"false\" style=\"width: 300px; height: 200px; background-color: #789;\"></div>"
},
{
"label": "&lt;input>",
"html": "<input id=\"newitem\" type=\"text\">"
},
{
"label": "&lt;button>",
"html": "<button id=\"newitem\" type=\"button\">Label</button>"
},
{
"label": "&lt;password>",
"html": "<input id=\"newitem\" type=\"password\">"
},
{
"label": "&lt;checkbox>",
"html": "<input id=\"newitem\" type=\"checkbox\">"
},
{
"label": "&lt;label>",
"html": "<label style=\"color: #000f;\" id=\"newitem\">Label text</label>"
},
{
"label": "&lt;p>",
"html": "<p style=\"color: #000f;\" id=\"newitem\">Type Here</p>"
},
{
"label": "&lt;hr>",
"html": "<hr id=\"newitem\">"
},
{
"label": "Jet: " + "$" + "[var]",
"html": "<code id=\"newitem\" contenteditable=\"true\" style=\"display:inline-block; background:#eef; border:1px dashed #99c; padding:4px; font-family:monospace; white-space:pre;\">" + jetEscape("$" + "[varname]") + "</code>"
},
{
"label": "Jet: &lt;set&gt;",
"html": "<code id=\"newitem\" contenteditable=\"true\" style=\"display:inline-block; background:#eef; border:1px dashed #99c; padding:4px; font-family:monospace; white-space:pre;\">" + jetEscape("<set name=\"varname\" value=\"\">") + "</code>"
},
{
"label": "Jet: &lt;if&gt;",
"html": "<code id=\"newitem\" contenteditable=\"true\" style=\"display:inline-block; background:#eef; border:1px dashed #99c; padding:4px; font-family:monospace; white-space:pre-wrap;\">" + jetEscape("<if value1=\"" + "$" + "[varname]\" value2=\"\" type=\"eq\">\ncontent\n</if>") + "</code>"
}
];
tbitems.forEach(doit);
function doit(currentValue, index) {
let index2 = index + 1;
let toolbar = document.getElementById('toolbar');
var itemHTML = '<button type="button" id="button' + index2;
itemHTML += '" style="width: 100%; height: 40px; border: 2px solid green;"';
itemHTML += '>' + currentValue.label + '</button>';
toolbar.insertAdjacentHTML("beforeend", itemHTML);
let id = 'button' + index2;
let button = document.getElementById(id);
button.html = currentValue.html;
button.addEventListener("mousedown",
(e) => {
e.preventDefault();
saveUndo();
draggingHTML = currentValue.html;
mainpage.insertAdjacentHTML('beforeend', draggingHTML);
let newEl = mainpage.lastElementChild;
newEl.style.position = 'absolute';
newEl.style.zIndex = '10000';
newEl.style.left = '0px';
newEl.style.top = '0px';
startNewItemDrag(newEl);
}
, false);
}
}
function startNewItemDrag(el) {
let rect = mainpage.getBoundingClientRect();
function ndMove(e) {
el.style.left = (e.clientX - rect.left - el.offsetWidth / 2) + 'px';
el.style.top = (e.clientY - rect.top - el.offsetHeight / 2) + 'px';
}
function ndUp(e) {
document.removeEventListener('mousemove', ndMove);
document.removeEventListener('mouseup', ndUp);
draggingHTML = null;
if((typeof selected !== 'undefined') && selected) {
selected.style.outline = '';
}
selected = el;
selected.style.outline = '2px solid #0088ff';
getLayerMap(selected);
displayParameters();
mode = "MOUSEUP";
}
document.addEventListener('mousemove', ndMove);
document.addEventListener('mouseup', ndUp);
}
function window_mousedown(win, e) {
mousedownx = e.offsetX;
mousedowny = e.offsetY;
if(e.offsetX >= (win.clientWidth - 3)) {
win.style.cursor = 'ew-resize';
window_dragging = true;
}
else {
win.style.cursor = 'default';
window_dragging = false;
}
}
function window_mousemove(win, e) {
}
function window_mouseup(win, e) {
}
function drawGrid() {
if(showGrid == true) {
context.clearRect(0,0,grid.width,grid.height);
context.lineWidth = 0.5;
context.strokeStyle = "#808080";
for(var ix = 0; ix < grid.width; ix += gridSize) {
context.beginPath();
context.moveTo(ix, 0);
context.lineTo(ix, grid.height);
context.stroke();
context.beginPath();
context.moveTo(0, ix);
context.lineTo(grid.width, ix);
context.stroke();
}
} else {
context.clearRect(0,0,grid.width,grid.height);
}
}
function resetDragHint(e) {
if(!selected) {
return;
}
if(selected.style.position == 'absolute') {
if((e.offsetX >= (parseFloat(selected.style.width) - 10)) && (e.offsetY >= (parseFloat(selected.style.height) - 10))) {
selected.style.cursor = "nwse-resize";
dragHint = "rightbottomresize";
} else if((e.offsetY < 10) && (e.offsetX >= (parseFloat(selected.style.width) - 10))) {
selected.style.cursor = "nesw-resize";
dragHint = "righttopresize";
} else if((e.offsetX <= 10) && (e.offsetY >= (parseFloat(selected.style.height) - 10))) {
selected.style.cursor = "nesw-resize";
dragHint = "leftbottomresize";
} else if((e.offsetX <= 10) && (e.offsetY <= 10)) {
selected.style.cursor = "nwse-resize";
dragHint = "lefttopresize";
} else if(e.offsetX >= (parseFloat(selected.style.width) - 10)) {
selected.style.cursor = "ew-resize";
dragHint = "rightresize";
} else if(e.offsetY >= (parseFloat(selected.style.height) - 10)) {
selected.style.cursor = "ns-resize";
dragHint = "bottomresize";
} else if(e.offsetX <= 10) {
selected.style.cursor = "ew-resize";
dragHint = "leftresize";
} else if(e.offsetY <= 10) {
selected.style.cursor = "ns-resize";
dragHint = "topresize";
} else {
selected.style.cursor = "move";
dragHint = "move";
}
} else {
if((e.offsetX >= (parseFloat(selected.style.width) - 10)) && (e.offsetY >= (parseFloat(selected.style.height) - 10))) {
selected.style.cursor = "nwse-resize";
dragHint = "rightbottomresize";
} else if(e.offsetX >= (parseFloat(selected.style.width) - 10)) {
selected.style.cursor = "ew-resize";
dragHint = "rightresize";
} else if(e.offsetY >= (parseFloat(selected.style.height) - 10)) {
selected.style.cursor = "ns-resize";
dragHint = "bottomresize";
} else {
selected.style.cursor = "move";
dragHint = "move";
}
}
}
function mousedown(e) {
let mouseabove = document.elementFromPoint(e.clientX, e.clientY);
if((typeof selected === 'undefined') || (selected != mouseabove)) {
mode = 'NEWSELECTION';
if((typeof selected !== 'undefined') && (selected.nodeName == "DIV")) {
document.designMode = 'off';
selected.contentEditable = false;
selected.blur();
selected.style.userSelect = 'none';
}
if((typeof selected !== 'undefined') && selected) {
selected.style.outline = '';
}
selected = mouseabove;
selected.style.outline = '2px solid #0088ff';
getLayerMap(selected);
} else {
if(mode != "TEXT") {
mode = "DOWNONSELECTED";
}
}
if(mode != 'TEXT') {
resetDragHint(e);
}
mousedownx = e.clientX;
mousedowny = e.clientY;
displayParameters();
return true;
}
function mouseup(e) {
if(mode == "TEXT") {
return true;
}
if(mode == "DOWNONSELECTED") {
if((selected.nodeName == "DIV") || (selected.nodeName == "CODE")) {
document.designMode = 'on';
selected.style.userSelect = 'auto';
selected.contentEditable = true;
selected.style.cursor = 'text';
selected.focus();
mode = "TEXT";
return true;
}
}
if(draggingHTML) {
draggingHTML = null;
}
if(selected && (mode == 'MOVED') && (selected.style.position == 'absolute')) {
selected.style.pointerEvents = 'none';
let dragover = document.elementFromPoint(e.clientX, e.clientY);
selected.style.pointerEvents = 'auto';
if(dragover.nodeName == 'DIV') {
if(dragover.id != selected.parentElement.id) {
dragover.appendChild(selected);
selected.style.left = ((selected.getBoundingClientRect().x + e.offsetX - mousedownx) - dragover.getBoundingClientRect().x) + 'px';
selected.style.top = ((selected.getBoundingClientRect().y + e.offsetY - mousedowny) - dragover.getBoundingClientRect().y) + 'px';
}
}
} else if(selected && (mode == 'MOVED') && pendingDragover) {
pendingDragover.prepend(selected);
selected.style.left = parseFloat(pendingDragover.style.left) + "px";
selected.style.top = parseFloat(pendingDragover.style.top) + "px";
}
pendingDragover = null;
hideDropIndicator();
mode = "MOUSEUP";
return true;
}
function mousemove(e) {
if(mode == "MOUSEUP") {
resetDragHint(e);
} else {
if(mode == "TEXT") {
return true;
}
if((selected.style.position == 'static') ||
(typeof selected.style.position === 'undefined') ||
(selected.style.position == '')) {
if(dragHint == "move") {
if(mode != "MOVED") {
mode = "MOVED";
}
selected.style.pointerEvents = 'none';
let dragover = document.elementFromPoint(e.clientX, e.clientY);
selected.style.pointerEvents = 'auto';
if(dragover.nodeName == 'DIV') {
pendingDragover = dragover;
showDropIndicator(dragover);
}
return true;
} else if(dragHint == "rightbottomresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.width = (parseFloat(selected.style.width) + e.movementX) + "px";
selected.style.height = (parseFloat(selected.style.height) + e.movementY) + "px";
} else if(dragHint == "rightresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.width = (parseFloat(selected.style.width) + e.movementX) + "px";
} else if(dragHint == "bottomresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.height = (parseFloat(selected.style.height) + e.movementY) + "px";
}
} else if(selected.style.position == 'absolute') {
if(dragHint == "move") {
if(mode != "MOVED") {
saveUndo();
mode = "MOVED";
}
selected.style.left = (parseFloat(selected.style.left) + e.movementX) + "px";
selected.style.top = (parseFloat(selected.style.top) + e.movementY) + "px";
} else if(dragHint == "rightbottomresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.width = (parseFloat(selected.style.width) + e.movementX) + "px";
selected.style.height = (parseFloat(selected.style.height) + e.movementY) + "px";
} else if(dragHint == "lefttopresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.left = (parseFloat(selected.style.left) + e.movementX) + "px";
selected.style.top = (parseFloat(selected.style.top) + e.movementY) + "px";
selected.style.width = (parseFloat(selected.style.width) - e.movementX) + "px";
selected.style.height = (parseFloat(selected.style.height) - e.movementY) + "px";
} else if(dragHint == "leftbottomresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.left = (parseFloat(selected.style.left) + e.movementX) + "px";
selected.style.width = (parseFloat(selected.style.width) - e.movementX) + "px";
selected.style.height = (parseFloat(selected.style.height) + e.movementY) + "px";
} else if(dragHint == "righttopresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.top = (parseFloat(selected.style.top) + e.movementY) + "px";
selected.style.height = (parseFloat(selected.style.height) - e.movementY) + "px";
selected.style.width = (parseFloat(selected.style.width) + e.movementX) + "px";
} else if(dragHint == "rightresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.width = (parseFloat(selected.style.width) + e.movementX) + "px";
} else if(dragHint == "bottomresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.height = (parseFloat(selected.style.height) + e.movementY) + "px";
} else if(dragHint == "leftresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.left = (parseFloat(selected.style.left) + e.movementX) + "px";
selected.style.width = (parseFloat(selected.style.width) - e.movementX) + "px";
} else if(dragHint == "topresize") {
if(mode != "RESIZED") {
saveUndo();
mode = "RESIZED";
}
selected.style.top = (parseFloat(selected.style.top) + e.movementY) + "px";
selected.style.height = (parseFloat(selected.style.height) - e.movementY) + "px";
}
displayParameters();
}
return true;
}
}
function heading(name) {
let properties = document.getElementById('properties');
properties.insertAdjacentHTML('beforeend', "<tr><td colspan=\"2\" style=\"background-color: #abc; padding: 4px; font-weight: bold;\">" + name + ":</td></tr>");
}
function setColorProperty(event) {
selected.style.setProperty(event.target.id, document.getElementById(event.target.id).value);
}
function getColorProperty(cssprop) {
let value = selected.style.getPropertyValue(cssprop);
if(value.substring(0, 4) == "rgb(") {
document.getElementById(cssprop).value = value;
}
else {
selected.style.removeProperty(cssprop);
}
}
function setSizeProperty(event) {
var value = document.getElementById(event.target.id);
var units = document.getElementById(event.target.id + "Units");
if(value.value == "") {
units.value = "";
} else if(units && units.value == "") {
units.value = "px";
}
if(units) {
selected.style.setProperty(event.target.id, value.value + units.value);
}
}
function getSizeProperty(cssprop) {
let size = selected.style.getPropertyValue(cssprop);
if(size) {
document.getElementById(cssprop).value = parseInt(size);
document.getElementById(cssprop + "Units").value = size.replace(/[0-9]/g, '');
}
}
function property(name, payload, cssprop, initfun, changefun) {
let properties = document.getElementById('properties');
properties.insertAdjacentHTML('beforeend', "<tr><td style=\"background-color: #cdf; padding: 3px 4px; vertical-align: middle; white-space: nowrap;\">" + name + ":</td><td style=\"background-color: #cdf; padding: 3px 4px; vertical-align: middle;\">" + payload + "</td></tr>");
if(initfun) {
initfun(cssprop);
}
if(cssprop) {
document.getElementById(cssprop).addEventListener('input', changefun);
let prop = document.getElementById(cssprop + "Units");
if(prop) {
prop.addEventListener('change', changefun);
}
}
}
function propsize(title, cssprop) {
property(title,
"<div style=\"display: flex; align-items: center; gap: 4px;\">" +
"<input type=\"number\" id=\"" + cssprop + "\" style=\"flex: 1 1 auto; min-width: 0; box-sizing: border-box;\">" +
"<select id=\"" + cssprop + "Units\" style=\"flex: 0 0 auto;\">" +
" <option value=\"\"></option>" +
" <option value=\"px\">px</option>" +
" <option value=\"%\">%</option>" +
"</select></div>",
cssprop,
getSizeProperty,
setSizeProperty);
}
function propcolor(title, cssprop) {
property(title,
"<input type=\"color\" id=\"" + cssprop + "\" style=\"height: 18px; width: 50px; margin: 0px; padding: 0px; border: none; width:85%;\">",
cssprop,
getColorProperty,
setColorProperty);
}
function displayParameters() {
if(!selected) {
itemparameters.innerHTML = "";
return;
}
itemparameters.innerHTML = "<div style=\"width: 300px; max-width: 300px;\"><table id=\"properties\" style=\"width: 300px; table-layout: fixed;\"><colgroup><col style=\"width: 35%;\"><col style=\"width: 65%;\"></colgroup></table></div>";
property("Type", selected.nodeName);
property("Id", selected.id);
property("Display",
"<select id=\"display\" style=\"width: 100%; float: right;\">" +
" <option value=\"inline\">Inline</option>" +
" <option value=\"block\">Block</option>" +
" <option value=\"inline-block\">Inline Block</option>" +
" <option value=\"none\">None</option>" +
"</select>",
"display",
() => { document.getElementById('display').value = selected.style.display; },
() => { selected.style.display = document.getElementById('display').value; }
);
if(selected.id != "mainpage") {
heading("Geometry");
property("Position",
"<select id=\"position\" style=\"width: 100%; float: right;\">" +
" <option value=\"absolute\">Absolute</option>" +
" <option value=\"static\">Static</option>" +
" <option value=\"relative\">Relative</option>" +
"</select>",
"position",
() => { document.getElementById('position').value = selected.style.position; },
() => { saveUndo();
selected.style.position = document.getElementById('position').value;
if(document.getElementById('position').value == 'absolute') {
selected.style.left = selected.getBoundingClientRect().x + 'px';
selected.style.top = selected.getBoundingClientRect().y + 'px';
}
displayParameters();
}
);
if(selected.style.position == 'absolute') {
propsize("Left", "left");
propsize("Top", "top");
}
propsize("Width", "width");
propsize("Height", "height");
}
if(selected.nodeName != 'IMG') {
heading("Font");
propcolor("Color", "color");
}
heading("Margins");
propsize("Left", "margin-left");
propsize("Top", "margin-top");
propsize("Right", "margin-right");
propsize("Bottom", "margin-bottom");
if(selected.id != "mainpage") {
heading("Border");
propsize("Width", "border-width");
propcolor("Color", "border-color");
}
heading("Background");
propcolor("Color", "background-color");
heading("Padding");
propsize("Left", "padding-left");
propsize("Top", "padding-top");
propsize("Right", "padding-right");
propsize("Bottom", "padding-bottom");
}
function newImage(filename, e) {
saveUndo();
let img = '<img id="newitem" src="/assets/' + filename + '" style="width: 150px;">';
mainpage.insertAdjacentHTML('beforeend', img);
}
function handleFiles(files) {
}
function b64encode(str) {
return btoa(unescape(encodeURIComponent(str)));
}
function save(form) {
form.html.value = b64encode(document.getElementById('mainpage').innerHTML);
form.exit.value = '';
form.submit();
}
function savenexit(form) {
form.html.value = b64encode(document.getElementById('mainpage').innerHTML);
form.exit.value = 'yes';
form.submit();
}
function exitnosave(form) {
form.html.value = '';
form.exit.value = 'yes';
form.submit();
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.
+156 -1
View File
@@ -42,6 +42,31 @@
* { box-sizing: border-box; }
* {
scrollbar-width: thin;
scrollbar-color: var(--bond-gold) var(--bond-bg);
}
::-webkit-scrollbar {
width: 11px;
height: 11px;
}
::-webkit-scrollbar-track {
background: var(--bond-bg);
}
::-webkit-scrollbar-thumb {
background-color: var(--bond-gold);
background-image: linear-gradient(180deg, var(--bond-gold-bright), var(--bond-gold));
background-clip: padding-box;
border: 2px solid var(--bond-bg);
border-radius: 8px;
}
::-webkit-scrollbar-thumb:hover {
background-image: linear-gradient(180deg, var(--bond-gold-bright), var(--bond-gold-bright));
}
::-webkit-scrollbar-corner {
background: var(--bond-bg);
}
body {
background: radial-gradient(ellipse at top, #1a1a20 0%, #0a0a0d 60%, #060607 100%);
background-attachment: fixed;
@@ -99,6 +124,10 @@ li { font-family: 8pt exo2; color: var(--bond-text); }
padding: 0px;
max-width: 1050px;
min-width: 1050px;
min-height: calc(100vh - 150px);
box-sizing: border-box;
display: flex;
flex-direction: column;
margin: auto;
font-family: exo2;
font-size: 10pt;
@@ -106,6 +135,18 @@ li { font-family: 8pt exo2; color: var(--bond-text); }
overflow: hidden;
}
/* The email client is the one page whose own content (a long message
list) can outgrow the viewport - everywhere else .pageclass is meant
to grow taller than the viewport for long content (normal document
scroll). Scoped to only this page via :has() so no other page's
layout changes: caps .pageclass at the same height it already uses
as a floor, so #email-list/#email-reading-pane's own overflow-y:auto
scrolls instead of the whole page growing past the viewport. */
.pageclass:has(#email-app) {
height: calc(100vh - 150px);
overflow: hidden;
}
input {
background: #101014;
color: var(--bond-text);
@@ -194,6 +235,19 @@ textarea.fieldinputerror {
background: var(--bond-line);
}
.user-badge {
display: inline-block;
padding: 3px 12px;
margin-bottom: 4px;
border-radius: 6px;
border: 1px solid var(--bond-gold);
background: linear-gradient(180deg, rgba(197, 164, 90, 0.28), rgba(197, 164, 90, 0.12));
box-shadow: 0 0 0.5em rgba(197, 164, 90, 0.25);
color: var(--bond-gold-bright);
font-weight: 600;
letter-spacing: 0.03em;
}
.sectionframecontents {
padding: 10px;
text-align: justify;
@@ -319,6 +373,28 @@ textarea.fieldinputerror {
border-radius: 3px;
}
#panelmenu-resizer {
flex-shrink: 0;
width: 9px;
cursor: col-resize;
position: relative;
background: var(--bond-line-soft);
border-radius: 3px;
}
#panelmenu-resizer:hover, #panelmenu-resizer.resizing {
background: var(--bond-gold);
}
#panelmenu-resizer svg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 12px;
height: 12px;
color: var(--bond-gold-bright);
pointer-events: none;
}
.panel table {
font: 12px exo2;
padding: 5px;
@@ -398,11 +474,12 @@ textarea.fieldinputerror {
padding: 2px 10px 3px 10px;
margin: 0px;
border-radius: 3px;
box-shadow: 0 0 0.5em rgba(197, 164, 90, 0.3), 0 0 0.15em rgba(227, 196, 126, 0.4);
transition: box-shadow 0.2s ease, background 0.2s ease;
}
.button:hover {
box-shadow: 0 0 0.6em rgba(197, 164, 90, 0.35);
box-shadow: 0 0 0.9em rgba(197, 164, 90, 0.55), 0 0 0.3em rgba(227, 196, 126, 0.6);
background: linear-gradient(180deg, #24242e, #1a1a20);
}
@@ -688,3 +765,81 @@ textarea.fieldinputerror {
margin-top: 12px;
min-height: 1.2em;
}
/* Site-wide floating file transfer manager (see documents.js) - lives
outside #content in mainpagetemplate.jet so it survives SPA
navigation, letting an upload/download keep running while browsing
to a different page. */
#filemgr-panel {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9000;
font-family: exo2;
font-size: 9pt;
}
#filemgr-toggle {
width: 44px; height: 44px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
position: relative;
cursor: pointer;
border: 1px solid var(--bond-gold);
background: linear-gradient(180deg, #1c1c24, #131318);
color: var(--bond-gold-bright);
box-shadow: 0 0 0.6em rgba(197, 164, 90, 0.35);
transition: box-shadow 0.2s ease, background 0.2s ease;
margin-left: auto;
}
#filemgr-toggle:hover {
box-shadow: 0 0 1em rgba(197, 164, 90, 0.6), 0 0 0.3em rgba(227, 196, 126, 0.6);
background: linear-gradient(180deg, #24242e, #1a1a20);
}
#filemgr-toggle svg { width: 20px; height: 20px; }
#filemgr-badge {
position: absolute; top: -4px; right: -4px;
min-width: 16px; height: 16px; padding: 0 3px;
border-radius: 8px;
background: var(--bond-gold-bright);
color: #131318;
font-size: 8pt; font-weight: bold;
display: flex; align-items: center; justify-content: center;
}
#filemgr-badge:empty { display: none; }
#filemgr-body {
display: none;
width: 320px;
max-height: 400px;
margin-bottom: 10px;
margin-left: auto;
border-radius: 8px;
border: 1px solid var(--bond-line);
background: var(--bond-bg-2);
box-shadow: 0 10px 40px rgba(0,0,0,0.6);
overflow: hidden;
display: flex;
flex-direction: column;
}
#filemgr-panel.filemgr-expanded #filemgr-body { display: flex; }
#filemgr-header {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 12px;
border-bottom: 1pt solid var(--bond-line-soft);
color: var(--bond-gold-bright);
font-weight: 600;
}
#filemgr-clear { font-weight: normal; font-size: 8pt; color: var(--bond-text-dim); cursor: pointer; }
#filemgr-clear:hover { color: var(--bond-text); }
#filemgr-list { overflow-y: auto; padding: 6px; }
#filemgr-empty { color: var(--bond-text-dim); padding: 10px; text-align: center; }
.filemgr-item { padding: 8px; border-radius: 6px; margin-bottom: 4px; background: rgba(255,255,255,0.03); }
.filemgr-item-row { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
.filemgr-item-icon { flex-shrink: 0; width: 14px; height: 14px; color: var(--bond-gold-bright); }
.filemgr-item-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filemgr-item-status { flex-shrink: 0; font-size: 8pt; color: var(--bond-text-dim); }
.filemgr-item.filemgr-failed .filemgr-item-status { color: #c66; }
.filemgr-item.filemgr-done .filemgr-item-status { color: var(--bond-gold-bright); }
.filemgr-progress-track { height: 4px; border-radius: 2px; background: rgba(255,255,255,0.08); overflow: hidden; }
.filemgr-progress-fill { height: 100%; width: 0%; background: var(--bond-gold-bright); transition: width 0.15s ease; }
.filemgr-item.filemgr-done .filemgr-progress-fill { background: #6c6; }
.filemgr-item.filemgr-failed .filemgr-progress-fill { background: #c66; }
Binary file not shown.

After

Width:  |  Height:  |  Size: 815 KiB

@@ -0,0 +1,76 @@
(function() {
var form = document.getElementById('newemail-form');
if (!form)
return;
function post(path, params) {
return fetch(path, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(params || {}).toString()
}).then(function(r) { return r.json(); });
}
function navigateTo(url) {
var a = document.createElement('a');
a.href = url;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
var body = document.getElementById('newemail-body');
var toolbar = document.getElementById('newemail-toolbar');
toolbar.addEventListener('click', function(e) {
var btn = e.target.closest('button');
if (!btn)
return;
body.focus();
var cmd = btn.getAttribute('data-cmd');
if (cmd === 'createLink') {
var url = window.prompt('Link URL:');
if (url)
document.execCommand('createLink', false, url);
} else {
document.execCommand(cmd, false, null);
}
});
var errorDiv = document.getElementById('newemail-error');
var sendBtn = document.getElementById('newemail-send-btn');
var cancelBtn = document.getElementById('newemail-cancel-btn');
cancelBtn.addEventListener('click', function() {
navigateTo('/admin/email');
});
sendBtn.addEventListener('click', function() {
var to = document.getElementById('newemail-to').value.trim();
var subject = document.getElementById('newemail-subject').value.trim();
var html = body.innerHTML.trim();
errorDiv.textContent = '';
if (!to) {
errorDiv.textContent = 'A recipient address is required.';
return;
}
if (!html) {
errorDiv.textContent = 'The message body is empty.';
return;
}
sendBtn.classList.add('newemail-sending');
post('/email/send', { to: to, subject: subject, body: html })
.then(function(res) {
sendBtn.classList.remove('newemail-sending');
if (!res.ok) {
errorDiv.textContent = res.error || 'Could not send the message.';
return;
}
navigateTo('/admin/email');
})
.catch(function() {
sendBtn.classList.remove('newemail-sending');
errorDiv.textContent = 'Could not send the message.';
});
});
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+322
View File
@@ -0,0 +1,322 @@
(function() {
var content = document.getElementById('content');
var form = document.getElementById('mainform');
// Lives outside #content (a sibling in the persistent shell, next to
// the menu bar/form) - a fragment swap never touches it on its own,
// so a per-page side menu would otherwise stay frozen on whichever
// page's menu happened to be there when the shell first loaded. See
// applyResponse()'s own handling of the #_spa_panelmenu marker below.
var panelMenu = document.getElementById('panelmenu');
// Same story as panelMenu just above, for the top-of-page tab bar and
// the upper-right "Welcome X / Logout" box - both live outside
// #content too, and both depend on server-side login state
// ($[_login_id], see menu.bar's own ACCOUNT/EMAIL/ADMIN tabs and
// login.menu's Welcome/Logout block) that only the server can know.
// Without this, logging in via a SPA form submit left the tab bar
// still showing LOGIN and the header box empty, even though the
// account page's own content came back correctly - only a full page
// reload picked up the new state, since only the non-fragment branch
// ever rendered these two includes at all.
var menuBar = document.getElementById('menubar');
var loginMenu = document.getElementById('loginmenu');
// The form's own initial action="<prefix>/<currentPage>" (server-
// rendered per-request - "/spa/$[PAGE]" during the 8081 test-path
// phase, "/$[PAGE]" once a port is cut over to the real path) tells
// us the route prefix this specific deployment is using, captured
// once here since the form itself lives outside #content and never
// gets re-rendered by a client-side navigation - only the initial
// page load sets it. Used to build the target for every subsequent
// submit (the PAGE hidden field's own value changes per navigation,
// this prefix does not) - keeps this one shared spa.js correct across
// every phase of the rollout without needing a matching edit each
// time a port's routing changes.
var formPrefix = '';
if (form) {
var initialAction = form.getAttribute('action') || '';
var initialPage = (form.elements['PAGE'] && form.elements['PAGE'].value) || '';
// action is "<prefix>/<page>" normally, or "<prefix>/<page>/<subpage>"
// when the page loaded directly on a URL that already has one (e.g.
// a bookmark to /opensource/barantmail) - suffix has to account for
// both or this initial load's own subpage would get mistaken for
// part of the prefix.
var initialSubpage = (form.elements['SUBPAGE'] && form.elements['SUBPAGE'].value) || '';
var initialSuffix = initialPage + (initialSubpage ? '/' + initialSubpage : '');
if (initialSuffix && initialAction.slice(-initialSuffix.length) === initialSuffix)
formPrefix = initialAction.slice(0, initialAction.length - initialSuffix.length - 1);
}
// The nav bar (menu.bar) lives in the persistent shell, outside
// #content - a fetch-and-swap navigation never re-renders it, so
// without this the "selected" tab highlight would just stay frozen
// on whichever page the shell happened to load with. Each tab's own
// <a href="/x"> (the real, un-prefixed target path - see menu.bar's
// own <tab page="x"><a href="/x">...</a></tab> shape) is what's used
// to identify it; toggling classes here keeps it in sync with every
// client-side navigation without needing the server to resend the
// whole menu bar on every fragment response.
function syncActiveTab(targetPath) {
if (formPrefix && targetPath.indexOf(formPrefix) === 0)
targetPath = targetPath.slice(formPrefix.length) || '/';
var tabs = document.querySelectorAll('.baritem, .baritemselected');
for (var i = 0; i < tabs.length; i++) {
// Each tab wraps TWO anchors: the outer one linkbutton generates
// (href="javascript:...", drives the actual click) and a real
// <a href="/x"> nested inside it as the visible link text/label
// (see menu.bar's own <tab page="x"><a href="/x">...</a></tab>
// shape). Both match a[href] - only the second, real-path one
// identifies the page, so the javascript: one has to be skipped
// explicitly rather than just taking the first match.
var links = tabs[i].querySelectorAll('a[href]');
var isActive = false;
for (var j = 0; j < links.length; j++) {
var href = links[j].getAttribute('href');
// Exact match covers a plain page; a subpage URL (e.g.
// /admin/users, /opensource/barantmail) never equals its own
// tab's bare /admin or /opensource href, so the tab lost its
// highlight the moment a subpage was selected. Matching on
// "targetPath starts with href, then either ends or continues
// with a /" keeps the tab highlighted for any of its own
// subpages without also matching an unrelated page whose name
// merely starts with the same characters (e.g. a hypothetical
// /administration would not match /admin).
if (href && href.indexOf('javascript:') !== 0 && href !== '/' &&
(href === targetPath ||
(targetPath.indexOf(href) === 0 &&
(targetPath.charAt(href.length) === '/' || targetPath.charAt(href.length) === '')))) {
isActive = true;
break;
}
}
tabs[i].classList.toggle('baritemselected', isActive);
tabs[i].classList.toggle('baritem', !isActive);
}
}
// Returns false if the response was a full-navigation redirect
// (already handled here, caller does nothing further); true if no
// data-spa-page marker was present at all (caller keeps using the
// URL it already requested); otherwise swaps in the content and
// returns {page, subpage} - the AUTHORITATIVE page/subpage the
// server actually rendered. mainpagetemplate.jet emits
// <div data-spa-page="$[PAGE]" data-spa-subpage="$[SUBPAGE]"> right
// before the fragment body, reflecting whatever a <process> may have
// changed PAGE to via <setpage> while handling the request (e.g. a
// successful login handing back the account page, not the login page
// the form was actually submitted to). Callers must use THIS value
// for history.pushState()/the active-tab highlight, not whatever
// page they originally requested - using the pre-request page here
// was exactly why a successful login left the address bar (and the
// tab highlight) stuck on /login even though the account page's
// content had correctly come back. subpage is the optional second
// URL segment (e.g. /opensource/barantmail) a page can use to pick
// what to render in its own second-tier content area.
function applyResponse(html) {
var redirect = /<div\s+data-spa-redirect="([^"]*)"/i.exec(html);
if (redirect) {
window.location.href = redirect[1];
return false;
}
content.innerHTML = html;
// <script> elements set via innerHTML are inert - the browser
// deliberately never executes them (both inline scripts and
// src="..." ones), only ones present when the HTML parser itself
// builds the DOM (a full page load). Pages whose own content
// depends on a <script> running - checkout's Square card-form
// wiring, email's UI logic - silently did nothing when reached via
// a client-side SPA swap (e.g. clicking "Check Out") instead of a
// full reload; only worked by accident on a fresh /checkout load.
// Re-creating each script element makes the browser actually run
// it. async=false on src="" ones preserves normal in-order
// execution (dynamically-inserted external scripts default to
// async otherwise), matching how a real page load would run them.
var scripts = content.querySelectorAll('script');
for (var i = 0; i < scripts.length; i++) {
var old = scripts[i];
var fresh = document.createElement('script');
for (var j = 0; j < old.attributes.length; j++)
fresh.setAttribute(old.attributes[j].name, old.attributes[j].value);
if (fresh.src) fresh.async = false;
fresh.textContent = old.textContent;
old.parentNode.replaceChild(fresh, old);
}
// #_spa_panelmenu carries the CURRENT page's own side-menu markup
// (mainpagetemplate.jet renders it into every fragment response
// specifically so this can pull it out) - extracted from the
// parsed DOM (not the raw html string) so nested tags inside a
// page's own .menu content can't break a naive string search.
// Removed from #content afterward since it only ever belonged in
// #panelmenu - it was just riding along in the fragment payload to
// get there.
var panelMenuMarker = content.querySelector('#_spa_panelmenu');
if (panelMenuMarker) {
if (panelMenu) panelMenu.innerHTML = panelMenuMarker.innerHTML;
panelMenuMarker.remove();
}
// #_spa_menubar/#_spa_loginmenu carry the CURRENT request's own
// login-state-dependent header markup, the same riding-along
// pattern as #_spa_panelmenu above - see the comment on
// menuBar/loginMenu's declaration for why this is needed at all.
var menuBarMarker = content.querySelector('#_spa_menubar');
if (menuBarMarker) {
if (menuBar) menuBar.innerHTML = menuBarMarker.innerHTML;
menuBarMarker.remove();
}
var loginMenuMarker = content.querySelector('#_spa_loginmenu');
if (loginMenuMarker) {
if (loginMenu) loginMenu.innerHTML = loginMenuMarker.innerHTML;
loginMenuMarker.remove();
}
var pageMarker = /data-spa-page="([^"]*)"/i.exec(html);
if (!pageMarker) return true;
var subpageMarker = /data-spa-subpage="([^"]*)"/i.exec(html);
return { page: pageMarker[1], subpage: subpageMarker ? subpageMarker[1] : '' };
}
// Shared by navigate() and the form submit handler: turns
// applyResponse()'s return value into the URL that should replace
// the address bar. `applied === true` (no marker in the response)
// keeps whatever URL was actually requested; an {page, subpage}
// object builds the authoritative one instead.
function resolvedUrl(applied, requestedUrl) {
if (applied === true) return requestedUrl;
return formPrefix + '/' + applied.page + (applied.subpage ? '/' + applied.subpage : '');
}
function delay(ms) {
return new Promise(function(resolve) { setTimeout(resolve, ms); });
}
// Fade-out/fade-in on every content swap (see the matching #content
// CSS transition/`.spa-fading` rule in main.css). The 180ms delay is
// paired with Promise.all so the fetch and the minimum fade-out time
// run concurrently, not back-to-back - a slow fetch just means the
// content stays faded a little longer, a fast one still shows the
// full fade rather than snapping instantly.
function navigate(url, push) {
var sep = url.indexOf('?') === -1 ? '?' : '&';
content.classList.add('spa-fading');
Promise.all([
fetch(url + sep + '_fragment=1', { credentials: 'same-origin' }).then(function(r) { return r.text(); }),
delay(180)
])
.then(function(results) {
var html = results[0];
var applied = applyResponse(html);
if (applied) {
var finalUrl = resolvedUrl(applied, url);
if (push) history.pushState({ spa: true }, '', finalUrl);
syncActiveTab(finalUrl.split('?')[0]);
}
content.classList.remove('spa-fading');
});
}
document.addEventListener('click', function(e) {
var a = e.target.closest('a');
if (!a) return;
var href = a.getAttribute('href');
if (!href || href.charAt(0) !== '/' || href.indexOf('javascript:') === 0) return;
if (a.target && a.target !== '' && a.target !== '_self') return;
e.preventDefault();
navigate(formPrefix + href, true);
});
if (form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
var fd = new FormData(form);
var page = fd.get('PAGE') || '';
var subpage = fd.get('SUBPAGE') || '';
var url = formPrefix + '/' + page + (subpage ? '/' + subpage : '');
content.classList.add('spa-fading');
Promise.all([
fetch(url + '?_fragment=1', {
method: 'POST',
body: fd,
credentials: 'same-origin'
}).then(function(r) { return r.text(); }),
delay(180)
])
.then(function(results) {
var html = results[0];
var applied = applyResponse(html);
if (applied) {
var finalUrl = resolvedUrl(applied, url);
history.pushState({ spa: true }, '', finalUrl);
syncActiveTab(finalUrl.split('?')[0]);
}
content.classList.remove('spa-fading');
});
});
}
window.addEventListener('popstate', function() {
navigate(window.location.pathname + window.location.search, false);
});
// Drag-to-resize for the side menu (#panelmenu) against the main
// content column - the resizer bar (#panelmenu-resizer) lives in the
// persistent shell right alongside panelMenu itself (see its own
// declaration above), never touched by a fragment swap, so this only
// needs to be wired up once here, not re-attached per navigation.
// #content deliberately carries no fixed width of its own (see
// mainpagetemplate.jet) - it's a table-cell with nothing set, so it
// automatically takes up whatever's left in the row, meaning only
// panelMenu's own width ever needs to change by hand.
(function initPanelMenuResize() {
var resizer = document.getElementById('panelmenu-resizer');
if (!resizer || !panelMenu)
return;
var STORAGE_KEY = 'barant_panelmenu_width';
var MIN_WIDTH = 120;
var MAX_WIDTH = 400;
try {
var saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
var savedWidth = parseInt(saved, 10);
if (savedWidth >= MIN_WIDTH && savedWidth <= MAX_WIDTH)
panelMenu.style.width = savedWidth + 'px';
}
} catch (e) {
// Private-browsing/storage-disabled - just keep the default width.
}
var dragging = false;
var startX = 0;
var startWidth = 0;
resizer.addEventListener('mousedown', function(e) {
dragging = true;
startX = e.clientX;
startWidth = panelMenu.getBoundingClientRect().width;
resizer.classList.add('resizing');
document.body.style.userSelect = 'none';
e.preventDefault();
});
document.addEventListener('mousemove', function(e) {
if (!dragging)
return;
var newWidth = startWidth + (e.clientX - startX);
newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, newWidth));
panelMenu.style.width = newWidth + 'px';
});
document.addEventListener('mouseup', function() {
if (!dragging)
return;
dragging = false;
resizer.classList.remove('resizing');
document.body.style.userSelect = '';
try {
localStorage.setItem(STORAGE_KEY, Math.round(panelMenu.getBoundingClientRect().width).toString());
} catch (e) {
// Private-browsing/storage-disabled - the width still applies
// for the rest of this page view, just doesn't persist.
}
});
})();
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.
Binary file not shown.
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
for f in /home/barant/servercore-analysis/JetCore/jetc/barant-site/src/*.jet /home/barant/servercore-analysis/JetCore/jetc/barant-site/jet/*.jet; do
n=$(basename "$f")
case "$n" in
login_test*.jet|pagetest.jet) continue ;;
# These embed a full <!DOCTYPE html>...<body> document directly in
# their own raw source (mainpagetemplate.jet is the SPA shell;
# main.jet is a fully standalone page) - loading that raw source
# into htmleditor's contentEditable canvas via <read> means the
# browser's own HTML parser sees a real nested <html>/<body>,
# silently drops/merges those tags per normal parsing rules, and
# renders everything inside them as live chrome instead of showing
# it as editable source text. Every other .jet file here is a thin
# routing wrapper with no embedded document structure, so this
# only needs to exclude files that actually have one.
mainpagetemplate.jet|main.jet) continue ;;
esac
d=$(basename "$(dirname "$f")")
printf '<div style="padding:3px 0;"><a href="?dir=%s&amp;file=%s">%s/%s</a></div>\n' "$d" "$n" "$d" "$n"
done
+7 -1
View File
@@ -29,7 +29,7 @@
<tag name="linkbutton" keywords="pagedir page process name name2 value value2 target class multipart">
<tag name="linkbutton" keywords="pagedir page subpage process name name2 value value2 target class multipart">
<A HREF="javascript:
@@ -48,6 +48,12 @@
document.forms[0].PAGE.value='#[PAGE]';
</IF>
<IF VALUE1="#[SUBPAGE]" VALUE2="" TYPE="EQ">
document.forms[0].SUBPAGE.value='';
<ELSE>
document.forms[0].SUBPAGE.value='#[SUBPAGE]';
</IF>
<IF VALUE1="#[NAME]" VALUE2="" TYPE="NE">
document.forms[0].#[NAME].value='#[VALUE]';
</IF>
+16
View File
@@ -0,0 +1,16 @@
<include file="SESSIONCTL.tag">
<include file="barant.tag">
<include file="project.tag">
<set name="host" value="localhost">
<set name="database" value="barant">
<set name="user" value="barant">
<set name="password" value="uversa1960">
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
<sessionctl page="account">
<defaultnav>
<setpage name="account">
</defaultnav>
<link rel="stylesheet" href="/assets/main.css">
<link rel="stylesheet" href="/assets/control.css">
</sessionctl>
</database>
+138
View File
@@ -0,0 +1,138 @@
<style>
#email-connect-form { max-width: 360px; margin: 40px auto; }
#email-connect-form input { display: block; width: 100%; box-sizing: border-box; margin-bottom: 10px; padding: 6px; }
#email-connect-error { color: #a00; min-height: 1.2em; margin-bottom: 10px; }
#email-app { height: 100%; }
#email-client { display: flex; flex-direction: column; max-height: 100%; }
#email-toolbar { display: flex; align-items: center; justify-content: space-between; padding: 6px 0; border-bottom: 1pt solid var(--bond-line-soft); margin-bottom: 6px; }
#email-toolbar-actions { display: flex; align-items: center; gap: 6px; }
#email-search-box { display: flex; align-items: center; gap: 4px; margin-right: 6px; }
#email-search-input {
background: #101014; color: var(--bond-text);
border: 1px solid var(--bond-line-soft); border-radius: 4px;
padding: 4px 8px; font-size: 9pt; width: 150px;
}
#email-search-input:focus {
outline: none; border-color: var(--bond-gold-bright);
box-shadow: 0 0 0.6em rgba(197, 164, 90, 0.5), 0 0 0.2em rgba(227, 196, 126, 0.55);
}
#email-list-count { color: var(--bond-text-dim); font-size: 9pt; margin-left: 6px; }
#email-layout { display: flex; flex: 1; min-height: 0; }
#email-list { width: 340px; flex-shrink: 0; overflow-y: auto; border-right: 1pt solid var(--bond-line-soft); transition: opacity 0.18s ease; }
#email-reading-pane { flex: 1; min-width: 0; overflow-y: auto; padding: 0 12px; transition: opacity 0.18s ease; }
.email-fading { opacity: 0; }
.email-row {
display: flex; align-items: center; gap: 6px; padding: 8px 10px 8px 13px;
border-bottom: 1pt solid var(--bond-line-soft); border-left: 3px solid transparent; cursor: pointer;
overflow: hidden;
transition: opacity 0.2s ease,
height 0.22s ease 0.2s, padding-top 0.22s ease 0.2s, padding-bottom 0.22s ease 0.2s,
margin-top 0.22s ease 0.2s, margin-bottom 0.22s ease 0.2s, border-width 0.22s ease 0.2s;
}
.email-row:hover { background: rgba(255, 255, 255, 0.05); }
.email-row.unread {
font-weight: bold;
background: rgba(197, 164, 90, 0.12);
}
.email-row.selected { background: rgba(197, 164, 90, 0.16); border-left-color: var(--bond-gold); }
.email-row.selected:hover { background: rgba(197, 164, 90, 0.22); }
/* Per-row removal animation for the trash button (see trashMessage()
in email.js) - dissolves in place first (the opacity transition,
no delay), then collapses its own box to nothing (the delayed
height/padding/margin/border transitions kick in once the fade is
mostly done) so every row below it slides up smoothly to close the
gap, purely as a side effect of ordinary document-flow reflow - no
other row is touched, faded, or re-rendered to make that happen. */
.email-row.email-row-removing {
opacity: 0;
height: 0 !important;
padding-top: 0; padding-bottom: 0;
margin-top: 0; margin-bottom: 0;
border-width: 0;
}
.email-row-info { flex: 1; min-width: 0; }
.email-row .email-from { font-size: 10pt; }
.email-row .email-subject { font-size: 9pt; color: var(--bond-text-dim); }
.email-row .email-date { font-size: 8pt; color: var(--bond-text-dim); }
.email-row-trash-btn { flex-shrink: 0; width: 24px; height: 24px; opacity: 0.55; }
.email-row-trash-btn svg { width: 13px; height: 13px; }
.email-row-trash-btn:hover { opacity: 1; }
#email-reading-pane .email-header { border-bottom: 1pt solid var(--bond-line-soft); margin-bottom: 10px; padding-bottom: 10px; }
#email-reading-pane .email-body { white-space: pre-wrap; overflow-wrap: break-word; word-break: break-word; font-family: monospace; font-size: 9pt; }
#email-reading-pane .email-body-frame { width: 100%; height: 520px; border: none; border-radius: 4px; background: #fff; }
#email-reading-pane .email-actions { margin-top: 10px; }
#email-empty-state { color: var(--bond-text-dim); padding: 20px; }
#email-folder-list { display: flex; flex-direction: column; }
.email-folder-link { display: flex; align-items: center; justify-content: space-between; padding: 6px 8px; margin-bottom: 2px; border-radius: 4px; text-decoration: none; color: inherit; }
.email-folder-link:hover { background: rgba(255,255,255,0.08); }
.email-folder-link.email-folder-selected { background: rgba(255,255,255,0.14); font-weight: bold; }
.email-folder-count { font-size: 8pt; padding: 1px 6px; border-radius: 8px; background: rgba(197, 164, 90, 0.25); color: var(--bond-gold-bright); }
.email-folder-count:empty { display: none; }
.email-account-link { display: block; padding: 6px 8px; margin-bottom: 2px; border-radius: 4px; text-decoration: none; color: inherit; font-size: 9pt; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
.email-account-link:hover { background: rgba(255,255,255,0.08); }
.email-account-link.email-account-selected { background: rgba(197, 164, 90, 0.16); color: var(--bond-gold-bright); font-weight: bold; }
#email-add-account-link { display: block; padding: 6px 8px; font-size: 9pt; color: var(--bond-text-dim); text-decoration: none; cursor: pointer; }
#email-add-account-link:hover { color: var(--bond-text); }
#email-add-account-form { display: flex; flex-direction: column; gap: 6px; margin-bottom: 4px; }
#email-add-account-form input {
background: #101014; color: var(--bond-text);
border: 1px solid var(--bond-line-soft); border-radius: 4px;
padding: 5px 7px; font-size: 9pt; box-sizing: border-box; width: 100%;
}
#email-add-account-form input:focus {
outline: none; border-color: var(--bond-gold-bright);
box-shadow: 0 0 0.6em rgba(197, 164, 90, 0.5), 0 0 0.2em rgba(227, 196, 126, 0.55);
}
#email-add-account-error { color: #a00; font-size: 8pt; min-height: 1.1em; }
.icon-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 30px; height: 30px; padding: 0; margin: 0; cursor: pointer;
border-radius: 6px; border: 1px solid var(--bond-gold);
background: linear-gradient(180deg, #1c1c24, #131318);
color: var(--bond-gold-bright);
box-shadow: 0 0 0.4em rgba(197, 164, 90, 0.25);
transition: box-shadow 0.2s ease, background 0.2s ease, transform 0.1s ease;
}
.icon-btn svg { width: 16px; height: 16px; display: block; }
.icon-btn:hover {
box-shadow: 0 0 0.8em rgba(197, 164, 90, 0.5), 0 0 0.25em rgba(227, 196, 126, 0.55);
background: linear-gradient(180deg, #24242e, #1a1a20);
}
.icon-btn:active { transform: scale(0.94); }
.icon-btn.icon-btn-active {
color: #131318;
background: linear-gradient(180deg, var(--bond-gold-bright), var(--bond-gold));
box-shadow: 0 0 0.7em rgba(197, 164, 90, 0.6);
}
</style>
<div id="email-app">
<if value1="$[@_imap_user]" value2="" type="eq">
<div id="email-connect-form">
<h3>Connect to Mailbox</h3>
<input type="text" id="email-user-input" placeholder="you@barant.com" autocomplete="username">
<input type="password" id="email-password-input" placeholder="Password" autocomplete="current-password">
<button type="button" id="email-connect-btn">Connect</button>
<div id="email-connect-error"></div>
</div>
<else>
<div id="email-client">
<div id="email-toolbar">
<span id="email-connected-as">$[@_imap_user]</span><span id="email-list-count"></span>
<span id="email-toolbar-actions">
<span id="email-search-box">
<input type="text" id="email-search-input" placeholder="Search mail&hellip;">
<button type="button" id="email-search-btn" class="icon-btn" title="Search"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.35-4.35"/></svg></button>
</span>
<button type="button" id="email-compose-btn" class="icon-btn" title="New Mail"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h13a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"/><path d="M2.5 7.5 12 14l9.5-6.5"/><path d="M18.5 3.5v5M16 6h5"/></svg></button>
<button type="button" id="email-refresh-btn" class="icon-btn" title="Refresh"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 11a8 8 0 0 0-14.6-4.4M4 4v5h5"/><path d="M4 13a8 8 0 0 0 14.6 4.4M20 20v-5h-5"/></svg></button>
<button type="button" id="email-disconnect-btn" class="icon-btn" title="Disconnect"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5"/><path d="M21 12H9"/></svg></button>
</span>
</div>
<div id="email-layout">
<div id="email-list"></div>
<div id="email-reading-pane"><div id="email-empty-state">Select a message to read it.</div></div>
</div>
</div>
</if>
</div>
<script src="/assets/email.js?v=20260905o"></script>
@@ -1,14 +1,5 @@
<page secure="yes" loggedin="yes" admin="true">
<title>ADMIN INVOICES</title>
<contents>
<eval name="_menu">
<include file="adminmenu.section">
</eval>
<retain name="invoice_id">
<h3>Invoices</h3>
<showtable page="admin_invoice" process="selectinvoice" name="invoice_id" value="X">
select invoices.id as X,invoices.id,user_id,name,invoices.created,amount,invoices.status from invoices join users on invoices.user_id=users.id
</showtable>
<br>
</contents>
</page>
@@ -0,0 +1,81 @@
<style>
#newemail-app { max-width: 720px; margin: 0 auto; }
#newemail-notconnected { color: var(--bond-text-dim); padding: 20px; }
#newemail-form { display: flex; flex-direction: column; gap: 10px; }
.newemail-field { display: flex; align-items: center; gap: 8px; }
.newemail-field label { width: 70px; flex-shrink: 0; color: var(--bond-text-dim); font-size: 9pt; }
.newemail-field input {
flex: 1; background: #101014; color: var(--bond-text);
border: 1px solid var(--bond-line-soft); border-radius: 4px;
padding: 6px 8px; font-size: 10pt; box-sizing: border-box;
}
.newemail-field input:focus {
outline: none; border-color: var(--bond-gold-bright);
box-shadow: 0 0 0.6em rgba(197, 164, 90, 0.5), 0 0 0.2em rgba(227, 196, 126, 0.55);
}
#newemail-from { color: var(--bond-text-dim); font-size: 9pt; }
#newemail-toolbar { display: flex; gap: 4px; margin-top: 4px; }
#newemail-toolbar button {
width: 30px; height: 30px; padding: 0; cursor: pointer;
border-radius: 6px; border: 1px solid var(--bond-gold);
background: linear-gradient(180deg, #1c1c24, #131318);
color: var(--bond-gold-bright);
font-weight: bold; font-size: 11pt;
box-shadow: 0 0 0.4em rgba(197, 164, 90, 0.25);
transition: box-shadow 0.2s ease, background 0.2s ease, transform 0.1s ease;
}
#newemail-toolbar button:hover {
box-shadow: 0 0 0.8em rgba(197, 164, 90, 0.5), 0 0 0.25em rgba(227, 196, 126, 0.55);
background: linear-gradient(180deg, #24242e, #1a1a20);
}
#newemail-toolbar button:active { transform: scale(0.94); }
#newemail-body {
min-height: 260px; background: #fff; color: #000;
border: 1px solid var(--bond-line-soft); border-radius: 4px;
padding: 10px; font-size: 10pt; box-sizing: border-box;
overflow-y: auto;
}
#newemail-body:focus { outline: none; border-color: var(--bond-gold-bright); }
#newemail-error { color: #a00; min-height: 1.2em; }
#newemail-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 6px; }
#newemail-actions button {
padding: 8px 16px; cursor: pointer; border-radius: 6px;
border: 1px solid var(--bond-gold);
background: linear-gradient(180deg, #1c1c24, #131318);
color: var(--bond-gold-bright);
box-shadow: 0 0 0.4em rgba(197, 164, 90, 0.25);
transition: box-shadow 0.2s ease, background 0.2s ease, transform 0.1s ease;
}
#newemail-actions button:hover {
box-shadow: 0 0 0.8em rgba(197, 164, 90, 0.5), 0 0 0.25em rgba(227, 196, 126, 0.55);
background: linear-gradient(180deg, #24242e, #1a1a20);
}
#newemail-actions button:active { transform: scale(0.97); }
#newemail-send-btn.newemail-sending { opacity: 0.6; pointer-events: none; }
</style>
<div id="newemail-app">
<if value1="$[@_imap_user]" value2="" type="eq">
<div id="newemail-notconnected">Connect to your mailbox first (see the Email section) before composing a message.</div>
<else>
<h3>New Message</h3>
<div id="newemail-form">
<div class="newemail-field"><label>From</label><span id="newemail-from">$[@_imap_user]</span></div>
<div class="newemail-field"><label>To</label><input type="text" id="newemail-to" placeholder="recipient@example.com"></div>
<div class="newemail-field"><label>Subject</label><input type="text" id="newemail-subject" placeholder="Subject"></div>
<div id="newemail-toolbar">
<button type="button" data-cmd="bold" title="Bold"><b>B</b></button>
<button type="button" data-cmd="italic" title="Italic"><i>I</i></button>
<button type="button" data-cmd="underline" title="Underline"><u>U</u></button>
<button type="button" data-cmd="insertUnorderedList" title="Bulleted list">&#8226;</button>
<button type="button" data-cmd="createLink" title="Link">&#128279;</button>
</div>
<div id="newemail-body" contenteditable="true"></div>
<div id="newemail-error"></div>
<div id="newemail-actions">
<button type="button" id="newemail-cancel-btn">Cancel</button>
<button type="button" id="newemail-send-btn">Send</button>
</div>
</div>
</if>
</div>
<script src="/assets/newemail.js?v=20260905a"></script>
@@ -0,0 +1,4 @@
<h3>Products</h3>
<showtable page="editproduct" process="selectproduct" name="sku" value="X">
select sku as X,sku,type,description from products
</showtable>
+4
View File
@@ -0,0 +1,4 @@
<h3>Users</h3>
<showtable page="edituser" process="selectuser" name="selected_user_id" value="X">
select id as X,email,name,type,created,status from users
</showtable>
+16
View File
@@ -0,0 +1,16 @@
<include file="SESSIONCTL.tag">
<include file="barant.tag">
<include file="project.tag">
<set name="host" value="localhost">
<set name="database" value="barant">
<set name="user" value="barant">
<set name="password" value="uversa1960">
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
<sessionctl page="admin">
<defaultnav>
<setpage name="admin">
</defaultnav>
<link rel="stylesheet" href="/assets/main.css">
<link rel="stylesheet" href="/assets/control.css">
</sessionctl>
</database>
+9
View File
@@ -0,0 +1,9 @@
<sectionframe title="ADMINISTRATIVE SUPPORT">
<linkbutton page="admin" subpage="email">EMAIL</linkbutton><br>
<linkbutton page="admin" subpage="invoices">INVOICES</linkbutton><br>
<linkbutton page="admin" subpage="products">PRODUCTS</linkbutton><br>
<linkbutton page="admin" subpage="users">USERS</linkbutton><br>
</sectionframe>
<if value1="$[SUBPAGE]" value2="email" type="eq">
<include file="email.menu">
</if>
+38 -10
View File
@@ -1,17 +1,45 @@
<page secure="yes" loggedin="yes" admin="true">
<page secure="yes" loggedin="yes">
<title>ADMINISTRATION</title>
<contents>
<if value1="$[SUBPAGE]" value2="email" type="eq">
<include file="admin-email.subpage">
<else>
<if value1="$[SUBPAGE]" value2="newemail" type="eq">
<include file="admin-newemail.subpage">
<else>
<if value1="$[SUBPAGE]" value2="invoices" type="eq">
<if expr="('$[_login_type]'='user')">
<p>Requested page is not found.</p>
<else>
<include file="admin-invoices.subpage">
</if>
<else>
<if value1="$[SUBPAGE]" value2="users" type="eq">
<if expr="('$[_login_type]'='user')">
<p>Requested page is not found.</p>
<else>
<include file="admin-users.subpage">
</if>
<else>
<if value1="$[SUBPAGE]" value2="products" type="eq">
<if expr="('$[_login_type]'='user')">
<p>Requested page is not found.</p>
<else>
<include file="admin-products.subpage">
</if>
<else>
<if expr="('$[_login_type]'='user')">
<p>Requested page is not found.</p>
<else>
<eval name="_menu">
<include file="adminmenu.section">
</eval>
<h3>Users</h3>
<showtable page="edituser" process="selectuser" name="selected_user_id" value="X">
select id as X,email,name,type,created,status from users
</showtable>
<h3>Products</h3>
<showtable page="editproduct" process="selectproduct" name="sku" value="X">
select sku as X,sku,type,description from products
</showtable>
<br>
<p>Select an item from the menu.</p>
</if>
</if>
</if>
</if>
</if>
</if>
</contents>
</page>
-23
View File
@@ -1,23 +0,0 @@
<page>
<title>BARANTMAIL OPEN SOURCE PROJECT</title>
<contents>
<sectionframe title="BARANTMAIL OPEN SOURCE PROJECT">
<p>In the year 2000 I was extremely frustrated with the state of e-amil
systems available to Linux. Sendmail and q-mail were pretty much the two
mainstays. sendmail was prohibitively complex for the newly emerging
Internet method of using e-mail and q-mail was poorly maintained.</p>
<p>As a result, I embarked on a mission to write my own e-mail system that used
mysql as it's storage medium. I wanted to keep it simple and support
multiple users and domains with the simplest of configurations imaginable.
I also wanted a scalable multi-server capability.</p>
<p>The result is BARANTMAIL.</p>
<h2>Features</h2>
<p>Features of BARANTMAIL are:</p>
<ul>
<li>Fully compliant e-mail system.</li>
<li>Support for multiple domains.</li>
</ul>
<p></p>
</sectionframe>
</contents>
</page>
+42
View File
@@ -0,0 +1,42 @@
<page>
<title>BARANTSTORE</title>
<style>
#storefront-grid { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 14px; }
.storefront-item { width: 220px; border: 1px solid var(--bond-line); border-radius: 8px; padding: 12px; background: var(--bond-bg-2); }
.storefront-item img { width: 100%; height: 140px; object-fit: contain; background: #fff; border-radius: 4px; }
.storefront-item h4 { margin: 8px 0 4px 0; font-size: 10pt; min-height: 26px; }
.storefront-price { color: var(--bond-gold-bright); font-weight: bold; margin-bottom: 8px; }
</style>
<contents>
<sectionframe title="BarantStore">
<h1>BarantStore</h1>
<p>A demonstration storefront built on BARANT's own e-commerce
platform - browse and purchase real products below.</p>
<sql sessionid="">
select product_list.sku as sku, product_list.sku_prefix as sku_prefix,
product_list.description as description, product_list.price as price,
products.image as image
from product_list
join products on products.sku=product_list.sku_prefix
order by product_list.category, product_list.description
</sql>$[sqlerror]
<ifrow sessionid="">
<div id="storefront-grid">
<whilerow sessionid="">
<div class="storefront-item">
<if value1="$[.image]" value2="" type="ne">
<img src="/assets/$[.image]">
</if>
<h4>$[.description]</h4>
<div class="storefront-price">$$[.price]</div>
<linkbutton process="addtocart" name="sku" value="$[.sku_prefix]"><div class="button">Add to Cart</div></linkbutton>
</div>
</whilerow>
</div>
<else>
<p>No products available right now.</p>
</ifrow>
</sectionframe>
</contents>
</page>
+16
View File
@@ -0,0 +1,16 @@
<include file="SESSIONCTL.tag">
<include file="barant.tag">
<include file="project.tag">
<set name="host" value="localhost">
<set name="database" value="barant">
<set name="user" value="barant">
<set name="password" value="uversa1960">
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
<sessionctl page="cart">
<defaultnav>
<setpage name="cart">
</defaultnav>
<link rel="stylesheet" href="/assets/main.css">
<link rel="stylesheet" href="/assets/control.css">
</sessionctl>
</database>
@@ -0,0 +1,8 @@
<set name="lettersdir" value="/home/barant/servercore-analysis/JetCore/jetc/barant-site/letters">
<if value1="$[filename]" value2="" type="ne">
<if expr="(locate('/','$[filename]') = 0) and (locate('..','$[filename]') = 0)">
<write file="$[lettersdir]/$[filename].jet" mode="overwrite" value="">
<set name="id" value="$[filename]">
</if>
</if>
<setpage name="htmleditor">
+2
View File
@@ -0,0 +1,2 @@
#api GET /documents/download
<if expr="locate('/documents/download','$[REQUEST_URI]') = 1"><if value1="$[@_login_id]" value2="" type="eq">Not authorized.<else><set name="host" value="localhost"><set name="database" value="barant"><set name="user" value="barant"><set name="password" value="uversa1960"><database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid=""><sql sessionid="">select id,name from documents where id=$[id] and user_id=$[@_login_id]</sql><ifrow sessionid=""><header name="Content-Type" value="application/octet-stream"><header name="Content-Disposition" value="attachment; filename=$[.name]"><read file="./documents-barant-site/$[@_login_id]_$[id]" name="_filedata">$[_filedata]<else>Document not found.</ifrow></database></if><else>Unknown route.</if>
+32 -3
View File
@@ -2,15 +2,35 @@
<title>Document Management</title>
<style>
.price {display: table-cell;
background-color: #fff;
background-color: #fff;
width: 100px;
height: 100px;
height: 100px;
text-align: center;
vertical-align: middle;
border: 1px solid #000;
border-radius: 20px;
}
.number {font-size: 30px;}
#documents-dropzone {
border: 2px dashed var(--bond-line);
border-radius: 8px;
padding: 24px;
text-align: center;
color: var(--bond-text-dim);
margin-top: 10px;
cursor: pointer;
transition: border-color 0.15s ease, background 0.15s ease;
}
#documents-dropzone:hover, #documents-dropzone.documents-dropzone-active {
border-color: var(--bond-gold);
background: rgba(197, 164, 90, 0.06);
}
#documents-list { width: 100%; border-collapse: collapse; margin-top: 14px; }
#documents-list th, #documents-list td { padding: 6px 8px; border-bottom: 1pt solid var(--bond-line-soft); text-align: left; }
#documents-list th { color: var(--bond-text-dim); font-weight: normal; font-size: 9pt; }
#documents-list td a, #documents-list td .documents-action { color: var(--bond-gold-bright); cursor: pointer; text-decoration: none; }
#documents-list td a:hover, #documents-list td .documents-action:hover { text-decoration: underline; }
#documents-empty { color: var(--bond-text-dim); padding: 10px 0; }
</style>
<contents>
<sectionframe title="Document Management">
@@ -23,5 +43,14 @@ upon department for internal documents use. Or if you are a service
group you can establish accounts and store correspondence and
documents for any activity with that account.</p>
</sectionframe>
<if value1="$[@_login_id]" value2="" type="ne">
<sectionframe title="Your Documents">
<input type="file" id="documents-file-input" multiple style="display:none;">
<div id="documents-dropzone">Drop files here, or click to choose files to upload</div>
<div id="documents-list-body"><div id="documents-empty">Loading&hellip;</div></div>
</sectionframe>
<script src="/assets/documents-page.js?v=20260906a"></script>
</if>
</contents>
</page>
</page>
+52
View File
@@ -0,0 +1,52 @@
#api POST /documents/api/upload
#api POST /documents/api/delete
#api POST /documents/api/list
<set name="host" value="localhost">
<set name="database" value="barant">
<set name="user" value="barant">
<set name="password" value="uversa1960">
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
<if value1="$[REQUEST_URI]" value2="/documents/api/upload" type="eq">
<header name="Content-Type" value="application/json">
<if value1="$[@_login_id]" value2="" type="eq">
{"ok":false,"error":"Not authorized."}
<else>
<if value1="$[name]" value2="" type="eq">
{"ok":false,"error":"Name is required."}
<else>
<sql sessionid="">insert into documents set user_id=$[@_login_id], name="$[name]", created=now()</sql>
<sql sessionid="">select last_insert_id() as document_id</sql>
<write file="./documents-barant-site/$[@_login_id]_$[document_id]" mode="overwrite" value="$[filedata]">
{"ok":true,"id":$[document_id],"name":"$[name]"}
</if>
</if>
<else>
<if value1="$[REQUEST_URI]" value2="/documents/api/delete" type="eq">
<header name="Content-Type" value="application/json">
<if value1="$[@_login_id]" value2="" type="eq">
{"ok":false,"error":"Not authorized."}
<else>
<sql sessionid="">select id from documents where id=$[id] and user_id=$[@_login_id]</sql>
<ifrow sessionid="">
<sql sessionid="">delete from documents where id=$[id] and user_id=$[@_login_id]</sql>
{"ok":true}
<else>
{"ok":false,"error":"Document not found."}
</ifrow>
</if>
<else>
<if value1="$[REQUEST_URI]" value2="/documents/api/list" type="eq">
<header name="Content-Type" value="application/json">
<if value1="$[@_login_id]" value2="" type="eq">
{"ok":false,"error":"Not authorized."}
<else>
<sql sessionid="">select id,name,created from documents where user_id=$[@_login_id] order by created desc</sql>
<set name="_doc_count" value="0">
{"ok":true,"documents":[<whilerow sessionid=""><if value1="$[_doc_count]" value2="0" type="ne">,</if>{"id":$[.id],"name":"$[.name]","created":"$[.created]"}<set name="_doc_count" value="1"></whilerow>]}
</if>
<else>
{"ok":false,"error":"Unknown route."}
</if>
</if>
</if>
</database>
@@ -0,0 +1,11 @@
<process>
<if value1="$[@_login_id]" value2="" type="ne">
<if value1="$[delete_id]" value2="" type="ne">
<sql sessionid="">select id from documents where id=$[delete_id] and user_id=$[@_login_id]</sql>$[sqlerror]
<ifrow sessionid="">
<sql sessionid="">delete from documents where id=$[delete_id] and user_id=$[@_login_id]</sql>$[sqlerror]
</ifrow>
</if>
</if>
<setpage name="documents">
</process>
@@ -0,0 +1,10 @@
<process>
<if value1="$[@_login_id]" value2="" type="ne">
<if value1="$[name]" value2="" type="ne">
<sql sessionid="">insert into documents set user_id=$[@_login_id], name="$[name]", created=now()</sql>$[sqlerror]
<sql sessionid="">select last_insert_id() as document_id</sql>
<write file="./documents-barant-site/$[@_login_id]_$[document_id]" mode="overwrite" value="$[filedata]">
</if>
</if>
<setpage name="documents">
</process>
+31
View File
@@ -0,0 +1,31 @@
<page secure="yes" loggedin="yes" admin="true">
<title>EDIT PRODUCT</title>
<contents>
<database host="$[host]" name="$[database]" user="$[user]" password="$[password]" sessionid="1">
<eval name="_menu">
<include file="adminmenu.section">
</eval>
<sql sessionid="1">select * from products where sku='$[_product_sku]' and sku_suffix=''</sql>$[sqlerror]
<ifrow sessionid="1">
<h3>Product $[1.sku]</h3>
<entryform>
<tr><td class="fieldprompt">Type:</td><td>
<select name="type">
<option value="PHYSICAL" <if value1="$[1.type]" value2="PHYSICAL" type="eq">selected</if>>PHYSICAL</option>
<option value="STREAMING" <if value1="$[1.type]" value2="STREAMING" type="eq">selected</if>>STREAMING</option>
<option value="SUBSCRIPTION" <if value1="$[1.type]" value2="SUBSCRIPTION" type="eq">selected</if>>SUBSCRIPTION</option>
<option value="FILE" <if value1="$[1.type]" value2="FILE" type="eq">selected</if>>FILE</option>
</select>
</td></tr>
<field prompt="Description" type="text" name="description" size="50" maxlength="50" value="$[1.description]">
<field prompt="Details" type="multiline" name="text" value="$[1.text]">
<field prompt="Price" type="text" name="price" size="15" maxlength="15" value="$[1.price]">
<field prompt="Image" type="text" name="image" size="50" maxlength="255" value="$[1.image]">
<buttonbay>
<linkbutton process="update_product"><div class="button">Update Product</div></linkbutton>
</buttonbay>
</entryform>
</ifrow>
</database>
</contents>
</page>
+178
View File
@@ -0,0 +1,178 @@
#api POST /email/connect
#api POST /email/disconnect
#api POST /email/list
#api POST /email/message
#api POST /email/flag
#api POST /email/delete
#api POST /email/trash
#api POST /email/folders
#api POST /email/search
#api POST /email/send
#api POST /email/accounts
#api POST /email/switchaccount
<header name="Content-Type" value="application/json">
<if value1="$[REQUEST_URI]" value2="/email/connect" type="eq">
<try>
<imap config="./email.cfg" user="$[user]" password="$[password]" sessionid="">
<eval name="_imap_accounts"><imapaccounts action="add" list="$[@_imap_accounts]" user="$[user]" password="$[password]"></imapaccounts></eval>
<set name="@_imap_accounts" value="$[_imap_accounts]">
<set name="_imap_user" value="$[user]">
<set name="@_imap_user" value="$[user]">
<set name="_imap_password" value="$[password]">
<set name="@_imap_password" value="$[password]">
{"ok":true}
</imap>
<catch name="_imaperr">
{"ok":false,"error":"$[_imaperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/disconnect" type="eq">
<if value1="$[@_imap_user]" value2="" type="ne">
<imap config="./email.cfg" user="$[@_imap_user]" disconnect="yes" sessionid="">
</imap>
<eval name="_imap_accounts"><imapaccounts action="remove" list="$[@_imap_accounts]" user="$[@_imap_user]"></imapaccounts></eval>
<set name="@_imap_accounts" value="$[_imap_accounts]">
</if>
<eval name="_next_user"><imapaccounts action="firstuser" list="$[@_imap_accounts]"></imapaccounts></eval>
<if value1="$[_next_user]" value2="" type="eq">
<set name="@_imap_user" value="">
<set name="@_imap_password" value="">
{"ok":true,"active":""}
<else>
<try>
<eval name="_next_password"><imapaccounts action="getpassword" list="$[@_imap_accounts]" user="$[_next_user]"></imapaccounts></eval>
<imap config="./email.cfg" user="$[_next_user]" password="$[_next_password]" sessionid="">
<set name="@_imap_user" value="$[_next_user]">
<set name="@_imap_password" value="$[_next_password]">
{"ok":true,"active":"$[_next_user]"}
</imap>
<catch name="_disconnecterr">
<set name="@_imap_user" value="">
<set name="@_imap_password" value="">
{"ok":true,"active":""}
</catch>
</try>
</if>
<else>
<if value1="$[@_imap_user]" value2="" type="eq">
{"ok":false,"error":"Not connected to a mailbox."}
<else>
<if value1="$[folder]" value2="" type="eq">
<set name="folder" value="INBOX">
</if>
<if value1="$[REQUEST_URI]" value2="/email/list" type="eq">
<try>
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
<if value1="$[limit]" value2="" type="eq">
<set name="limit" value="50">
</if>
<imaplist sessionid="" folder="$[folder]" limit="$[limit]">
</imap>
<catch name="_imaperr">
{"ok":false,"error":"$[_imaperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/message" type="eq">
<try>
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
<imapmessage sessionid="" folder="$[folder]" uid="$[uid]">
</imap>
<catch name="_imaperr">
{"ok":false,"error":"$[_imaperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/flag" type="eq">
<try>
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
<imapflag sessionid="" folder="$[folder]" uid="$[uid]" flag="$[flag]" set="$[set]">
</imap>
<catch name="_imaperr">
{"ok":false,"error":"$[_imaperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/delete" type="eq">
<try>
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
<imapdelete sessionid="" folder="$[folder]" uid="$[uid]">
</imap>
<catch name="_imaperr">
{"ok":false,"error":"$[_imaperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/trash" type="eq">
<try>
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
<imapmove sessionid="" folder="$[folder]" uid="$[uid]" destfolder="Trash">
</imap>
<catch name="_imaperr">
{"ok":false,"error":"$[_imaperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/folders" type="eq">
<try>
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
<imapfolders sessionid="">
</imap>
<catch name="_imaperr">
{"ok":false,"error":"$[_imaperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/search" type="eq">
<try>
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
<imapsearch sessionid="" folder="$[folder]" query="$[query]">
</imap>
<catch name="_imaperr">
{"ok":false,"error":"$[_imaperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/send" type="eq">
<try>
<smtpsend config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" to="$[to]" subject="$[subject]" html="$[body]">
<catch name="_smtperr">
{"ok":false,"error":"$[_smtperr]"}
</catch>
</try>
<else>
<if value1="$[REQUEST_URI]" value2="/email/accounts" type="eq">
<imapaccounts action="list" list="$[@_imap_accounts]" active="$[@_imap_user]"></imapaccounts>
<else>
<if value1="$[REQUEST_URI]" value2="/email/switchaccount" type="eq">
<try>
<eval name="_switch_password"><imapaccounts action="getpassword" list="$[@_imap_accounts]" user="$[user]"></imapaccounts></eval>
<if value1="$[_switch_password]" value2="" type="eq">
{"ok":false,"error":"That account is not connected."}
<else>
<imap config="./email.cfg" user="$[user]" password="$[_switch_password]" sessionid="">
<set name="@_imap_user" value="$[user]">
<set name="@_imap_password" value="$[_switch_password]">
{"ok":true}
</imap>
</if>
<catch name="_switcherr">
{"ok":false,"error":"$[_switcherr]"}
</catch>
</try>
<else>
{"ok":false,"error":"Unknown route."}
</if>
</if>
</if>
</if>
</if>
</if>
</if>
</if>
</if>
</if>
</if>
</if>
</if>
+17
View File
@@ -0,0 +1,17 @@
<sectionframe title="MAILBOX">
<div id="email-folder-list">
<a href="javascript:void(0);" class="email-folder-link" data-folder="Inbox" onclick="window.emailSelectFolder && window.emailSelectFolder('Inbox'); return false;"><span>Inbox</span><span class="email-folder-count" data-folder-count="Inbox"></span></a>
<a href="javascript:void(0);" class="email-folder-link" data-folder="Spam" onclick="window.emailSelectFolder && window.emailSelectFolder('Spam'); return false;"><span>Spam</span><span class="email-folder-count" data-folder-count="Spam"></span></a>
<a href="javascript:void(0);" class="email-folder-link" data-folder="Trash" onclick="window.emailSelectFolder && window.emailSelectFolder('Trash'); return false;"><span>Trash</span><span class="email-folder-count" data-folder-count="Trash"></span></a>
</div>
</sectionframe>
<sectionframe title="ACCOUNTS">
<div id="email-account-list"></div>
<div id="email-add-account-form" style="display:none;">
<input type="text" id="email-add-account-user" placeholder="you@barant.com" autocomplete="username">
<input type="password" id="email-add-account-password" placeholder="Password" autocomplete="current-password">
<button type="button" id="email-add-account-btn">Connect</button>
<div id="email-add-account-error"></div>
</div>
<a href="javascript:void(0);" id="email-add-account-link" onclick="window.emailShowAddAccount && window.emailShowAddAccount(); return false;">+ Add Account</a>
</sectionframe>
+16
View File
@@ -0,0 +1,16 @@
<include file="SESSIONCTL.tag">
<include file="barant.tag">
<include file="project.tag">
<set name="host" value="localhost">
<set name="database" value="barant">
<set name="user" value="barant">
<set name="password" value="uversa1960">
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
<sessionctl page="htmleditor">
<defaultnav>
<setpage name="htmleditor">
</defaultnav>
<link rel="stylesheet" href="/assets/main.css">
<link rel="stylesheet" href="/assets/control.css">
</sessionctl>
</database>
+154
View File
@@ -0,0 +1,154 @@
<page secure="yes" loggedin="yes">
<title>HTML EDITOR</title>
<contents>
<script src="/assets/htmleditor.js"></script>
<set name="sitedir" value="/home/barant/servercore-analysis/JetCore/jetc/barant-site">
<if value1="$[dir]" value2="" type="ne">
<if value1="$[file]" value2="" type="ne">
<if expr="(('$[dir]'='src') or ('$[dir]'='jet')) and (locate('/','$[file]') = 0) and (locate('..','$[file]') = 0)">
<try>
<read file="$[sitedir]/$[dir]/$[file]" name="mhtml">
<set name="letter_dir" value="$[dir]">
<set name="letter_file" value="$[file]">
<catch>
<set name="letter_file" value="">
</try>
<else>
<set name="letter_file" value="">
</if>
<else>
<set name="letter_file" value="">
</if>
<else>
<set name="letter_file" value="">
</if>
<if value1="$[letter_file]" value2="" type="eq">
<div style="max-width: 700px; margin: 40px auto; font-family: Helvetica;">
<h1>SITE .JET FILES</h1>
<p>These are the real source files compiled into this site's own routes. Editing and saving one overwrites it directly - a timestamped backup is made automatically before each save.</p>
<call pgm="$[sitedir]/list_jet_files.sh" name="filelist">
$[filelist]
</div>
<else>
<body ondragstart="return false;" ondrop="return false;" onload="init(); return true;">
<form method="post" action="$[REQUEST_URI]">
<input type="hidden" name="PROCESS" value="savehtmleditor">
<input type="hidden" name="html" value="">
<input type="hidden" name="exit" value="">
<input type="hidden" name="letter_dir" value="$[letter_dir]">
<input type="hidden" name="letter_file" value="$[letter_file]">
<div style="position: relative;">
<div id="window1" style="position: absolute; left: 0px; top: 0px; border: 1pt solid black; display: inline-block; padding: 2px; background: #808080; color: #ffffff;"
onmousemove="window_mousemove(this, event); return true;"
onmousedown="window_mousedown(this, event); return true;"
onmouseup="window_mouseup(this, event); return true;">
<div style="background-color: #678; height: 21px; padding: 2px 2px 2px 2px;">
<div style="float: left;"><b style="padding: 2px 8px;">$[letter_dir]/$[letter_file]</b></div>
<div style="float: left;"><button type="button" onclick="save(this.form);">SAVE</button></div>
<div style="float: left;"><button type="button" onclick="savenexit(this.form);">SAVE AND EXIT</button></div>
<div style="float: right;"><button type="button" onclick="exitnosave(this.form);">EXIT NO SAVE</button></div>
</div>
<div style="background-color: #abc; padding: 0px 2px 6px 2px;">
<div style="float: left; display: inline-block; margin: 2px 2px 2px 0px; border-radius: 3px; width: 18px; height: 18px; border: 1px solid #012; background-color: #def; cursor: pointer;" onclick="return do_undo();"><img width="18" src="/assets/htmleditor_undo.png"></div>
<div style="float: left; display: inline-block; margin: 2px 2px 2px 0px; border-radius: 3px; width: 18px; height: 18px; border: 1px solid #012; background-color: #def; cursor: pointer;" onclick="return do_redo();"><img width="18" src="/assets/htmleditor_redo.png"></div>
&nbsp;
<div style="display: inline-block; float: left; margin: 2px 0px 0px 0px;">
<select style="border: 1pt solid #123; border-radius: 3px; background-color: #def;" onchange="document.execCommand('formatBlock',false,this.value);return true;">
<option value="p">Paragraph</option>
<option value="h1">Heading 1</option>
<option value="h2">Heading 2</option>
<option value="h3">Heading 3</option>
<option value="pre">Monospace</option>
</select>
</div>
<div style="float: left; display: inline-block; margin: 2px 2px 2px 5px; border-radius: 3px; width: 18px; height: 18px; border: 1px solid #012; background-color: #def; cursor: pointer;" onclick="document.execCommand('bold');return true;"><img width="14" style="padding: 2px;" src="/assets/htmleditor_bold.png"></div>
<div style="float: left; display: inline-block; margin: 2px 2px 2px 0px; border-radius: 3px; width: 18px; height: 18px; border: 1px solid #012; background-color: #def; cursor: pointer;" onclick="document.execCommand('underline');return true;"><img width="14" style="padding: 2px;" src="/assets/htmleditor_underline.png"></div>
<div style="float: left; display: inline-block; margin: 2px 2px 2px 5px; border-radius: 3px; width: 18px; height: 18px; border: 1px solid #012; background-color: #def; cursor: pointer;" onclick="document.execCommand('justifyLeft');return true;"><img width="14" style="padding: 2px;" src="/assets/htmleditor_jl.png"></div>
<div style="float: left; display: inline-block; margin: 2px 2px 2px 0px; border-radius: 3px; width: 18px; height: 18px; border: 1px solid #012; background-color: #def; cursor: pointer;" onclick="document.execCommand('justifyCenter');return true;"><img width="14" style="padding: 2px;" src="/assets/htmleditor_jc.png"></div>
<div style="float: left; display: inline-block; margin: 2px 2px 2px 0px; border-radius: 3px; width: 18px; height: 18px; border: 1px solid #012; background-color: #def; cursor: pointer;" onclick="document.execCommand('justifyRight');return true;"><img width="14" style="padding: 2px;" src="/assets/htmleditor_jr.png"></div>
<div style="float: left; display: inline-block; margin: 2px 2px 2px 0px; border-radius: 3px; width: 18px; height: 18px; border: 1px solid #012; background-color: #def; cursor: pointer;" onclick="document.execCommand('justifyFull');return true;"><img width="14" style="padding: 2px;" src="/assets/htmleditor_jj.png"></div>
<div id="map" style="display: inline-block; color: #012; font-size: 10px;"></div>
</div>
<div id="mainpage"
style="width: 1000px; height: 800px; border: 1px solid black; position: relative; background: #ffffffff; overflow: scroll; max-height: 800px; max-width: 1000px; cursor: crosshair;"
onmousedown="return mousedown(event);"
onmousemove="return mousemove(event);"
onmouseup="return mouseup(event); return true;">
$[mhtml]
</div>
<canvas id="grid" width="1000px" height="800px" style="position: absolute; left: 0px; top: 0px; alpha: 0.5; cursor: normal; pointer-events: none;"></canvas>
</div>
<div id="window3" style="position:absolute; top: 235px; left: 1010px;
border: 1pt solid black; display: inline-block; padding: 2px; background: #808080; color: #ffffff;">
<span>Options</span>
<div style="width: 320px; color: #000000; border: 1px solid black; position: relative; background: #ffffff;">
<div id="controls">
<input type="checkbox" name="grid" onchange="showGrid = this.checked; drawGrid(); return true;"><span style="font-size: 12px; margin-bottom: 2px;">Show Grid</span><br>
<input type="checkbox" name="snaptogrid" onchange="snapToGrid = this.checked; return true;"><span style="font-size: 12px; margin-bottom: 2px;">Snap To Grid</span><br>
<span style="font-size: 12px; margin-bottom: 2px;">Grid Size: </span><input type="text" name="gridsize" id="gridsize" style="width: 20px;" size="3" onchange="gridSize = parseInt(this.value); drawGrid(); return true;"><br>
</div>
<div id="itemparameters" style="font-size: 12px;"></div>
<div id="data" style="font-size: 12px;">
<p>NO DATA</p>
</div>
</div>
</div>
<div id="window2" style="position:absolute; top: 0px; left: 1010px; border: 1pt solid black; display: inline-block; padding: 2px; background: #808080; color: #ffffff;">
<span>Toolbar</span>
<div id="toolbar" style="overflow-y: auto; width: 200px; height: 300px; border: 1px solid black; position: relative; background: #ffffff;">
</div>
</div>
<div id="window4" style="position:absolute; top: 860px; left: 0px;
border: 1pt solid black; display: inline-block; padding: 2px; background: #808080; color: #ffffff;">
<span>Images</span>
<div style="width: 1000px; height: 300px; color: #000000; border: 1px solid black; position: relative; background: #ffffff;">
<div id="controls" style="overflow-y: scroll; max-height: 275px; padding: 3px;">
<div style="display: inline-block;">
<div style="width: 100px; height: 100px; border: 1px solid #000; margin: 5px 0px 5px 5px; background-image: url('/assets/htmleditor_drinkwise_logo.png'); background-size: contain; background-repeat: no-repeat; cursor: pointer;" onmousedown="newImage('htmleditor_drinkwise_logo.png', event); return true;"></div>
<div style="font-size: 8px;">drinkwise_logo.png</div>
</div>
<div style="display: inline-block;">
<div style="width: 100px; height: 100px; border: 1px solid #000; margin: 5px 0px 5px 5px; background-image: url('/assets/htmleditor_dwbackground.png'); background-size: contain; background-repeat: no-repeat; cursor: pointer;" onmousedown="newImage('htmleditor_dwbackground.png', event); return true;"></div>
<div style="font-size: 8px;">dwbackground.png</div>
</div>
</div>
</div>
<div id="itemparameters" style="font-size: 12px;"></div>
</div>
</div>
</form>
</body>
</if>
</contents>
</page>
+1 -1
View File
@@ -1,6 +1,6 @@
<sectionframe title="INVOICE HISTORY">
<sql>select * from invoices where user_id=$[_user_id]</sql>$[sqlerror]
<whilerow>
<linkbutton page="barantmail">|$[created]|</linkbutton><br>
<linkbutton process="selectinvoice" page="account" name="invoice_id" value="$[.id]">|$[.created]|</linkbutton><br>
</whilerow>
</sectionframe>
+50 -10
View File
@@ -27,17 +27,57 @@ to.</p>
many expression handling functions are dependant on the mysql library
functions. Any expression that works in mysql will work in JET.</p>
<h3>JET Libraries</h3>
<p>You can make JET scripts and compile them into super fast
libraries. Scripts can be anything from web page functions and
concealers to session control, credit card processing and just about
anything. Libraries add some performance enhancements by pre parsing
many of the elements in the script.</p>
<h3>One Language, Two Speeds: The JET 2.0 Interpreter and Compiler</h3>
<p>Libraries allow you to make your own tags and implement them in
the scripts. JET does not have JSON or XML parsing but using a
libraries you can develop an XML parser operation that usually
suffices for most script processing.
<p>Every JET script runs two ways, and you never have to choose in
advance. Drop a file on disk and JET interprets it on the spot &mdash;
edit it, refresh, see the change. No build step, no deploy pipeline,
no waiting. That's the interpreter, and it's what makes JET feel less
like "compiling a website" and more like sketching one.</p>
<p>Then, when a page is ready to carry real traffic, the exact same
source compiles straight down to native machine code with the JET
compiler, <b>jetc</b>. Not bytecode. Not a virtual machine sitting
in the middle slowing things down. Real, compiled C++ &mdash; the
tags you wrote resolve once, at build time, instead of being
re-parsed on every single request.</p>
<p>That's the pitch in one line: write it fast, then make it fast.</p>
<p><b>What jetc gives you:</b></p>
<ul>
<li><b>Genuine compiled-code speed.</b> A compiled JET module isn't
an interpreted script pretending to be quick &mdash; it's a shared
library. Tag logic that would normally be walked and evaluated on
every hit is already resolved into executable code before the first
request ever arrives.</li>
<li><b>Zero-downtime deploys, by design.</b> Compiled modules are
loaded live and swapped out while the server keeps running. A
request already in flight finishes on the version it started with;
the very next request lands on the new one. No restart, no dropped
connections, no maintenance window.</li>
<li><b>Routes that declare themselves.</b> A compiled module carries
its own API routes right inside the source file. Drop it in the
plugin directory and it's live &mdash; there's no central routing
table to hand-edit and no separate registration step to forget.</li>
<li><b>Builds that only do the work that changed.</b> The compiler
toolchain tracks what's actually been touched and skips
recompiling everything else, so a large site with dozens of modules
still builds in seconds, not minutes.</li>
<li><b>Shared code costs nothing at runtime.</b> Common tag
definitions and includes get inlined directly into the compiled
output at build time &mdash; you write clean, reusable JET source,
and the compiler makes sure reusing it is free.</li>
</ul>
<p>The interpreter and the compiler aren't two different products
you have to pick between &mdash; they're two gears on the same
engine. Prototype in the interpreter, ship in the compiler, and
never rewrite a single tag to get there.</p>
<h3>Session Control (SESSIONCTL) Library</h3>
<p>For web sites, a session control library is provided with JET and
+16
View File
@@ -0,0 +1,16 @@
<include file="SESSIONCTL.tag">
<include file="barant.tag">
<include file="project.tag">
<set name="host" value="localhost">
<set name="database" value="barant">
<set name="user" value="barant">
<set name="password" value="uversa1960">
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
<sessionctl page="login">
<defaultnav>
<setpage name="login">
</defaultnav>
<link rel="stylesheet" href="/assets/main.css">
<link rel="stylesheet" href="/assets/control.css">
</sessionctl>
</database>
+1 -1
View File
@@ -1,5 +1,5 @@
<if value1="$[_login_id]" value2="" type="ne">
<div style="color: #444; margin-bottom: 3px;">Welcome $[_login_name]</div>
<div class="user-badge">Welcome $[_login_name]</div>
<div>
<linkbutton page="cart"><img style="margin-top: 0px; margin-right: 10px;" src="/assets/shopping_cart.png" width="30px" align="center"></linkbutton>
<linkbutton process="logout" class="button">Logout</linkbutton>
+7
View File
@@ -0,0 +1,7 @@
<sectionframe title="QUICK LINKS">
<linkbutton page="services">SERVICES</linkbutton><br>
<linkbutton page="hosting">HOSTING</linkbutton><br>
<linkbutton page="siptrunk">SIP TRUNKING</linkbutton><br>
<linkbutton page="opensource">OPEN SOURCE</linkbutton><br>
<linkbutton page="contact">CONTACT</linkbutton>
</sectionframe>
+149
View File
@@ -0,0 +1,149 @@
#api GET /
#api GET /{page}
#api GET /{page}/{subpage}
#api POST /
#api POST /{page}
#api POST /{page}/{subpage}
<include file="SESSIONCTL.tag">
<include file="barant.tag">
<include file="project.tag">
<set name="host" value="localhost">
<set name="database" value="barant">
<set name="user" value="barant">
<set name="password" value="uversa1960">
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
<if value1="$[PROCESS]" value2="" type="ne">
<tag name="process" scope="local"><eval><container></eval></tag>
<eval name="_procout"><include file="./$[DIR]/$[PROCESS].proc"></eval>
</if>
<checksession>
<if value1="$[PAGE]" value2="" type="eq">
<if value1="$[page]" value2="" type="eq">
<set name="PAGE" value="main">
<else>
<set name="PAGE" value="$[page]">
</if>
</if>
<if value1="$[DIR]" value2="" type="eq">
<set name="DIR" value="">
</if>
<if value1="$[SUBPAGE]" value2="" type="eq">
<if value1="$[subpage]" value2="" type="ne">
<set name="SUBPAGE" value="$[subpage]">
</if>
</if>
<tag name="page" keywords="class onload secure loggedin admin" scope="local">
<tag name="contents" scope="local"><eval name="_BODY"><container></eval></tag>
<eval name="_leftover"><container></eval>
<if expr="('$[HTTPS]'!='on') and ('#[secure]'='yes')">
<if value1="$[_fragment]" value2="" type="eq">
<html><head><meta http-equiv="refresh" content="0; url=https://$[HTTP_HOST]$[REQUEST_URI]"></head></html>
<else>
<div data-spa-redirect="https://$[HTTP_HOST]$[REQUEST_URI]"></div>
</if>
<else>
<if expr="('$[_login_type]'='user') and ('#[admin]'='true')">
<p>Page not found.</p>
<else>
<if expr="('$[_login_id]'='') and ('#[loggedin]'='yes')">
<if value1="$[_fragment]" value2="" type="eq">
<html><head><meta http-equiv="refresh" content="0; url=https://$[HTTP_HOST]/login?start=$[PAGE]"></head></html>
<else>
<div data-spa-redirect="/login?start=$[PAGE]"></div>
</if>
<else>
<if expr="('$[_login_type]'='user') and ('#[admin]'='yes')">
<p>Page not found.</p>
<else>
$[_BODY]
</if>
</if>
</if>
</if>
</tag>
<if value1="$[_fragment]" value2="" type="eq">
<!DOCTYPE html>
<html lang="en">
<head>
<title>BARANT Technology Group</title>
<link rel="stylesheet" href="/assets/main.css?v=20260906a">
<link rel="stylesheet" href="/assets/control.css?v=20260902">
</head>
<body>
<form id="mainform" method="post" action="/$[PAGE]<if value1="$[SUBPAGE]" value2="" type="ne">/$[SUBPAGE]</if>" enctype="multipart/form-data">
<input type="hidden" name="_LASTPAGE" value="$[PAGE]">
<input type="hidden" name="PROCESS" value="">
<input type="hidden" name="EXITPROCESS" value="">
<input type="hidden" name="DIR" value="">
<input type="hidden" name="_CTLDIR" value="">
<input type="hidden" name="PAGE" value="$[PAGE]">
<input type="hidden" name="SUBPAGE" value="$[SUBPAGE]">
<input type="hidden" name="_TIMESTAMP" value="">
<div class="pageclass">
<div class="heading">
<div style="display: inline-block; width: 500px;">
<a href="https://barant.com"><img style="width: 60px; height: 60px; margin: 10px; background-color: #000; border-radius: 6px; padding: 4px; box-sizing: border-box;" align="left" src="/assets/btglogo.png"></a>
<div style="padding-top: 15px;">
<div style="left: 80px; color: #c5a45a; font-size: 20pt; letter-spacing: 0.08em; text-transform: uppercase; font-weight: 600;">BARANT TECHNOLOGY GROUP</div>
</div>
</div>
<div id="loginmenu" style="float: right; text-align: right; margin-right: 10px; margin-top: 5px;"><include file="login.menu"></div>
</div>
<div class="menubar" id="menubar">
<include file="menu.bar">
</div>
<div style="display: flex; flex: 1; width: 100%; min-height: 0;">
<div style="display: flex; width: 100%;">
<div id="panelmenu" class="panel" style="width: 200px; flex-shrink: 0;">
<try><include file="$[PAGE].menu"><catch></try>
<include file="cart.menu">
</div>
<div id="panelmenu-resizer" title="Drag to resize"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 5 L3 12 L8 19"/><path d="M16 5 L21 12 L16 19"/></svg></div>
<div class="panel" id="content" style="flex: 1; min-width: 0;">
<try>
<eval><include file="./$[DIR]/$[PAGE].page"></eval>
<catch>
<p>Page not found.</p>
</try>
</div>
<div style="clear: both;"></div>
</div>
</div>
<div class="footer" style="clear: both;">&copy; 1999-2026 BARANT Systems, All Rights Reserved</div>
</div>
</form>
<div id="filemgr-panel" class="filemgr-collapsed">
<div id="filemgr-toggle" title="File Transfers">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V6M5 12l7-7 7 7"/><path d="M5 21h14"/></svg>
<span id="filemgr-badge"></span>
</div>
<div id="filemgr-body">
<div id="filemgr-header">
<span>Transfers</span>
<span id="filemgr-clear" title="Clear completed">Clear</span>
</div>
<div id="filemgr-list"><div id="filemgr-empty">No transfers yet.</div></div>
</div>
</div>
<script src="/assets/spa.js?v=20260906a"></script>
<script src="/assets/documents.js?v=20260906a"></script>
</body>
</html>
<else>
<div data-spa-page="$[PAGE]" data-spa-subpage="$[SUBPAGE]" style="display:none"></div>
<div id="_spa_panelmenu" style="display:none"><try><include file="$[PAGE].menu"><catch></try><include file="cart.menu"></div>
<div id="_spa_menubar" style="display:none"><include file="menu.bar"></div>
<div id="_spa_loginmenu" style="display:none"><include file="login.menu"></div>
<try>
<eval><include file="./$[DIR]/$[PAGE].page"></eval>
<catch>
<p>Page not found.</p>
</try>
</if>
</database>
+2 -4
View File
@@ -10,6 +10,7 @@
<tab page="main"><a href="/main">HOME</a></tab>
<tab page="services"><a href="/services">SERVICES</a></tab>
<tab page="hosting"><a href="/hosting">HOSTING</a></tab>
<tab page="store"><a href="/store/createstore">CREATE STORE</a></tab>
<tab page="documents"><a href="/documents">DOCUMENT MGMT</a></tab>
<tab page="siptrunk"><a href="/siptrunk">SIP TRUNKING</a></tab>
<tab page="vst"><a href="/vst">VST</a></tab>
@@ -26,10 +27,7 @@
</if>
<if value1="$[_login_id]" value2="" type="ne">
<tab page="account"><a href="/account">ACCOUNT</a></tab>
<tab page="email"><a href="/email">EMAIL</a></tab>
<if value1="$[_login_type]" value2="admin" type="eq">
<tab page="admin"><a href="/admin">ADMIN</a></tab>
</if>
<tab page="admin"><a href="/admin">ADMIN</a></tab>
<else>
<tab page="login"><a href="/login">LOGIN</a></tab>
</if>
+27
View File
@@ -0,0 +1,27 @@
<page>
<title>LOADED MODULES</title>
<contents>
<sectionframe title="LIVE JTC MODULES">
<p>This APIServer process is running as a small core loader with the
real functionality split into separately compiled <b>.jtc</b> modules.
Each one is dlopen()'d straight off disk by ApiModuleWatcher, and
watched live &mdash; drop a new build into the plugin directory and it's
serving requests within moments, with zero downtime and no restart.</p>
<p>Below is the actual, live list of every compiled module currently
attached to this process, read directly off disk at the moment this
page rendered.</p>
<table>
<tr><th>Module</th><th>File</th></tr>
<whiledir dir="/home/barant/servercore-analysis/APIServer/plugins-barant-site" name="filename" filenamenoextension="modname" sort="true">
<if expr="right('$[filename]',4) = '.jtc'">
<tr><td>$[modname]</td><td>$[filename]</td></tr>
</if>
</whiledir>
</table>
</sectionframe>
</contents>
</page>
@@ -1,6 +1,3 @@
<page>
<title>RCA1802 Micro-Controller Assembler/Emulator</title>
<contents>
<sectionframe title="RCA1802">
<h1>The RCA1802 Micro-Controller</h1>
<img src="/assets/rca1802chip.jpeg" align="right">
@@ -42,11 +39,8 @@ visibility and manipulation.</p>
</sectionframe>
<sectionframe title="RCA1802 Computer Device">
We are working to provide a hardware kit for the controller device.
We are working to provide a hardware kit for the controller device.
Current schematics are being layed out using Kicad. I have a full git
change environment going for the Kicad project as well as the source
code for the assembler and emulator components.
</sectionframe>
</contents>
</page>
@@ -0,0 +1,53 @@
<sectionframe title="BARANTMAIL OPEN SOURCE PROJECT">
<p>In the year 2000 I was extremely frustrated with the state of e-amil
systems available to Linux. Sendmail and q-mail were pretty much the two
mainstays. sendmail was prohibitively complex for the newly emerging
Internet method of using e-mail and q-mail was poorly maintained.</p>
<p>As a result, I embarked on a mission to write my own e-mail system that used
mysql as it's storage medium. I wanted to keep it simple and support
multiple users and domains with the simplest of configurations imaginable.
I also wanted a scalable multi-server capability.</p>
<p>The result is BARANTMAIL.</p>
<h2>Features</h2>
<p>BARANTMAIL is a complete mail server implementation, written from
scratch in C++ - no dependency on Sendmail, Postfix, Dovecot, or any
other existing mail server software.</p>
<h3>Protocols</h3>
<ul>
<li>SMTP (port 25) plus authenticated SMTP-over-TLS submission (port 465).</li>
<li>IMAP4rev1 over TLS (port 993) - LOGIN, SELECT, FETCH/UID FETCH, STORE/UID STORE, COPY/UID COPY, EXPUNGE, SEARCH/UID SEARCH, SUBSCRIBE, NOOP and more.</li>
<li>POP3 over TLS (port 995) - USER/PASS, STAT, LIST, RETR, DELE, TOP, UIDL.</li>
</ul>
<h3>Security</h3>
<ul>
<li>TLS/SSL on every protocol, backed by OpenSSL and real CA-issued certificates.</li>
<li>DKIM signing on outbound mail.</li>
<li>SPF verification on inbound mail.</li>
</ul>
<h3>Spam Filtering</h3>
<ul>
<li>Every inbound message is scanned by SpamAssassin (spoken directly over the spamd protocol, not shelled out to a binary) on the way into a mailbox.</li>
<li>Messages scoring above the configured threshold are automatically filed into a dedicated Spam folder rather than the Inbox - not just tagged and left mixed in with everything else.</li>
</ul>
<h3>Multi-Tenancy</h3>
<ul>
<li>Multiple domains on one server (virtual hosting).</li>
<li>Multiple independent mailboxes per domain, each with its own Inbox, Spam, and Trash folders, plus support for any additional folder a client creates.</li>
</ul>
<h3>Architecture</h3>
<ul>
<li>Asynchronous, EPoll-based I/O - handles many concurrent connections without a thread per connection.</li>
<li>Queue-based SMTP delivery - inbound mail is queued to disk, then either filed locally or relayed onward over SMTP on a background thread, so a slow remote host never blocks the server.</li>
</ul>
<h3>Companion Webmail Client</h3>
<p>A full browser-based email client (see <a href="/admin/email">Email</a>
under the Admin section) is built directly against BARANTMAIL's own IMAP
implementation - real proof the protocol support is complete enough to
drive a real application, not just a demonstration.</p>
</sectionframe>
@@ -1,13 +1,6 @@
<page>
<title>barant.com - JET Extension Tags</title>
<contents>
<if value1="$[ARGS]" value2="" type="eq">
<include file="jet_overview.section">
<else>
<set name="subpage" expr="substring('$[ARGS]', 2)">
<include file="jet_$[subpage].section">
</if>
</contents>
</page>
@@ -0,0 +1,49 @@
<sectionframe title="Open Source Projects">
<h1>Open Source Projects</h1>
<p>We have several open source projects that we have created.</p>
<ul>
<li><linkbutton page="opensource" subpage="jet">JET</linkbutton> - JET Extension Tags provides an amazingly versatile and
simple mechanism for generating output for muliple uses. Originally
designed in the early days of the Internet for generating web pages
containing dynamic data it has been adapted to many uses from Asterisk
dial plans to other generated output that requires templated protocols
with dynamic data from a mysql database. Jeff Gonzalez added the
ability to manage web site sessions and pretty URLs to JET with his
session manager library.</li><br>
<li><linkbutton page="opensource" subpage="barantmail">BARANTMAIL</linkbutton> - Designed out of frustration with qmail a decade ago
BARANTMAIL was developed on top of C++ and mySQL to provide an
extremely super simple e-mail server for the open-source
community.</li><br>
<li><linkbutton page="opensource" subpage="synthmax">SYNTHMAX</linkbutton> - I have been developing a fully modular synthesizer for
use in Linux. This is not your usual plaything and maybe someday will
be used professionally in my keyboard products.</li><br>
<li><linkbutton page="opensource" subpage="serverframework">C++ SERVER FRAMEWORK</linkbutton> - A core epoll server code for developing
all sorts of high performance TCP and UDP servers. Generic socket
objects allow tying into epoll handling for any socket based
communications including timers, files and network sockets.
Basic functional server frameworks are provided for:
<br><br>
<ul>
<li>HTTP(S) Server</li>
<li>Stream Server</li>
<li>E-Mail Server</li>
<li>Chat Server</li>
</ul>
</li><br>
<li><linkbutton page="opensource" subpage="asm1802">RCA1802 Micro-Controller
Emulator</linkbutton> - A fully operational assembler
and emulation environment for the development and deployment of
firmware code utilizing the RCA1802 micro-controller chip.</li>
</ul>
Check out our git server at <a href="https://git.barant.com">git.barant.com</a>.
</sectionframe>
@@ -1,6 +1,3 @@
<page>
<title>Server Core Framework</title>
<contents>
<sectionframe title="Server Core Framework">
<p>It is finally here! The Server Core Framework is now available. The
framework offers support for just about any server you can devise.</p>
@@ -8,7 +5,7 @@ framework offers support for just about any server you can devise.</p>
<p>Now why would I want to go and build a server when there are all
sorts of frameworks out there for supporting server activity? All I
can say is top notch performance. The Server Core is all C++ and is
built on the epoll structure of the Linux operating system.</p>
built on the epoll structure of the Linux operating system.</p>
<ul>
<li>TCP and/or UDP server capabilities.</li>
@@ -40,5 +37,3 @@ git clone https://barant.com/git@ServerCore.git
</code>
</sectionframe>
</contents>
</page>
@@ -0,0 +1,5 @@
<sectionframe title="SYNTHMAX">
<p>I have been developing a fully modular synthesizer for
use in Linux. This is not your usual plaything and maybe someday will
be used professionally in my keyboard products.</p>
</sectionframe>
+6 -5
View File
@@ -1,7 +1,8 @@
<sectionframe title="OPEN SOURCE PROJECTS">
<linkbutton page="barantmail">BARANTMAIL</linkbutton><br>
<linkbutton page="jet">JET</linkbutton><br>
<linkbutton page="synthmax">SYNTHMAX</linkbutton><br>
<linkbutton page="serverframework">SERVER CORE FRAMEWORK</linkbutton>
<linkbutton page="asm1802">ASM/MCH 1802 SIMULATOR</linkbutton><br>
<linkbutton page="opensource">OVERVIEW</linkbutton><br>
<linkbutton page="opensource" subpage="barantmail">BARANTMAIL</linkbutton><br>
<linkbutton page="opensource" subpage="jet">JET</linkbutton><br>
<linkbutton page="opensource" subpage="synthmax">SYNTHMAX</linkbutton><br>
<linkbutton page="opensource" subpage="serverframework">SERVER CORE FRAMEWORK</linkbutton>
<linkbutton page="opensource" subpage="asm1802">ASM/MCH 1802 SIMULATOR</linkbutton><br>
</sectionframe>
+11 -49
View File
@@ -1,54 +1,16 @@
<page>
<title>OPEN SOURCE</title>
<contents>
<sectionframe title="Open Source Projects">
<h1>Open Source Projects</h1>
<p>We have several open source projects that we have created.</p>
<ul>
<li><linkbutton page="jet">JET</linkbutton> - JET Extension Tags provides an amazingly versatile and
simple mechanism for generating output for muliple uses. Originally
designed in the early days of the Internet for generating web pages
containing dynamic data it has been adapted to many uses from Asterisk
dial plans to other generated output that requires templated protocols
with dynamic data from a mysql database. Jeff Gonzalez added the
ability to manage web site sessions and pretty URLs to JET with his
session manager library.</li><br>
<li>BARANTMAIL - Designed out of frustration with qmail a decade ago
BARANTMAIL was developed on top of C++ and mySQL to provide an
extremely super simple e-mail server for the open-source
community.</li><br>
<li>SYNTHMAX - I have been developing a fully modular synthesizer for
use in Linux. This is not your usual plaything and maybe someday will
be used professionally in my keyboard products.</li><br>
<li>C++ SERVER FRAMEWORK - A core epoll server code for developing
all sorts of high performance TCP and UDP servers. Generic socket
objects allow tying into epoll handling for any socket based
communications including timers, files and network sockets.
Basic functional server frameworks are provided for:
<br><br>
<ul>
<li>HTTP(S) Server</li>
<li>Stream Server</li>
<li>E-Mail Server</li>
<li>Chat Server</li>
</ul>
</li><br>
<li><linkbutton page="asm1802">RCA1802 Micro-Controller
Emulator</linkbutton> - A fully operational assembler
and emulation environment for the development and deployment of
firmware code utilizing the RCA1802 micro-controller chip.</li>
</ul>
Check out our git server at <a href="https://git.barant.com">git.barant.com</a>.
</sectionframe>
<div id="opensource-subpage">
<if value1="$[SUBPAGE]" value2="" type="eq">
<include file="opensource-overview.subpage">
<else>
<try>
<include file="opensource-$[SUBPAGE].subpage">
<catch>
<p>That open source category doesn't exist.</p>
</try>
</if>
</div>
</contents>
</page>
+1
View File
@@ -0,0 +1 @@
<sql>delete from cart_item where session_uuid='$[_session_id]' and sku='$[sku]'</sql>$[sqlerror]
+15
View File
@@ -0,0 +1,15 @@
<set name="sitedir" value="/home/barant/servercore-analysis/JetCore/jetc/barant-site">
<if value1="$[html]" value2="" type="ne">
<if expr="(('$[letter_dir]'='src') or ('$[letter_dir]'='jet')) and (locate('/','$[letter_file]') = 0) and (locate('..','$[letter_file]') = 0)">
<call pgm="/bin/date" arg1="+%Y%m%d%H%M%S" name="backupstamp">
<call pgm="/bin/cp" arg1="$[sitedir]/$[letter_dir]/$[letter_file]" arg2="$[sitedir]/$[letter_dir]/$[letter_file].pre-htmleditor-$[backupstamp]">
<write file="$[sitedir]/$[letter_dir]/$[letter_file]" mode="overwrite" value="$[html;frombase64]">
<set name="dir" value="$[letter_dir]">
<set name="file" value="$[letter_file]">
</if>
</if>
<if value1="$[exit]" value2="" type="ne">
<set name="dir" value="">
<set name="file" value="">
</if>
<setpage name="htmleditor">
+3
View File
@@ -0,0 +1,3 @@
<process>
<setenv name="_product_sku">$[sku]</setenv>
</process>
@@ -0,0 +1,12 @@
<sectionframe title="Build Your Own Online Store">
<h1>Create Your Own Store</h1>
<p>BARANT Technology Group's e-commerce platform lets you build and run
your own online store right here on this site - list physical
products, digital downloads, subscriptions, or streaming content, and
we handle checkout, payment processing, and order management for you.</p>
<p>Whether you're selling a handful of items or running a full catalog,
your store lives alongside everything else here - the same account,
document storage, and support tools you already use.</p>
<p>Want to see it in action? Take a look at <a href="/barantstore">BarantStore</a>,
our own storefront, built on the same platform we're offering you.</p>
</sectionframe>
+10
View File
@@ -0,0 +1,10 @@
<page>
<title>STORE</title>
<contents>
<if value1="$[SUBPAGE]" value2="createstore" type="eq">
<include file="store-createstore.subpage">
<else>
<p>Page not found.</p>
</if>
</contents>
</page>
+5
View File
@@ -0,0 +1,5 @@
<process>
<sql>update products set type='$[type]', description='$[description]', text='$[text]', price=$[price], image='$[image]' where sku='$[_product_sku]' and sku_suffix=''</sql>$[sqlerror]
<setpage name="admin">
<set name="SUBPAGE" value="products">
</process>
+5
View File
@@ -0,0 +1,5 @@
<if expr="$[qty] <= 0">
<sql>delete from cart_item where session_uuid='$[_session_id]' and sku='$[sku]'</sql>$[sqlerror]
<else>
<sql>update cart_item set quantity=$[qty] where session_uuid='$[_session_id]' and sku='$[sku]'</sql>$[sqlerror]
</if>
+52
View File
@@ -0,0 +1,52 @@
#!/bin/bash
# Builds the hand-written assets.compiled.cpp module into assets.jtc.
# Mirrors build-shared.sh's compile/link tail exactly (same object cache,
# same libCoreUtils_pic.a, same link line) but skips the jetc --shared
# codegen step since there is no .jet source for this module.
set -e
JC="$(cd "/home/barant/servercore-analysis/JetCore" && pwd)"
CU="$JC/../CoreUtils"
HERE="$JC/jetc"
STEM="assets"
OUTPUT="$1"
if [ -z "$OUTPUT" ]; then
echo "usage: $0 <output.jtc>"
exit 1
fi
echo "Building JetCore runtime objects as position-independent code..."
list=""
for f in "$JC"/*.cpp; do
base=$(basename "$f")
if [ "$base" = "jet-2.0.cpp" ]; then continue; fi
obj="$HERE/${base%.cpp}.pic.o"
if [ ! -f "$obj" ] || [ "$f" -nt "$obj" ]; then
g++ -g -fPIC -c -std=c++23 -I"$CU" -I"$JC" "$f" -o "$obj" &
fi
list="$list $obj"
done
cu_pic_list=""
cu_rebuilt=false
for f in "$CU"/*.cpp; do
base=$(basename "$f")
obj="$HERE/cu_${base%.cpp}.pic.o"
if [ ! -f "$obj" ] || [ "$f" -nt "$obj" ]; then
g++ -g -fPIC -c -std=c++23 -I"$CU" "$f" -o "$obj" &
cu_rebuilt=true
fi
cu_pic_list="$cu_pic_list $obj"
done
wait
if [ "$cu_rebuilt" = true ] || [ ! -f "$HERE/libCoreUtils_pic.a" ]; then
ar rcs "$HERE/libCoreUtils_pic.a" $cu_pic_list
fi
g++ -g -fPIC -c -std=c++23 -I"$CU" -I"$JC" "$HERE/$STEM.compiled.cpp" -o "$HERE/$STEM.compiled.pic.o"
echo "Linking $OUTPUT..."
g++ -shared -fPIC -o "$OUTPUT" "$HERE/$STEM.compiled.pic.o" $list -std=c++23 \
-L"$HERE" -lCoreUtils_pic -lmysqlclient -lcrypto
echo "Built $OUTPUT"
+261
View File
@@ -0,0 +1,261 @@
"""
Lowercases jet tag NAMES (<TagName> / </TagName>) and attribute NAMES
(ATTR=) throughout a jet source file, WITHOUT touching:
- attribute VALUES (quoted or unquoted)
- $[...]/#[...] variable references anywhere (including nested ones,
and ones that appear inside attribute values)
- literal HTML/text content outside of tags
Handles the same real-content wrinkles this session's own JetParser.cpp
fixes had to: tags embedded inside a quoted attribute value (e.g.
linkbutton's own HREF="javascript: <IF ...>...<ELSE>...</IF>..."), and
both single and double quoted attribute values.
This is a straightforward character-by-character scanner, not a regex -
regex can't reliably track the nested quote/tag structure real content
uses.
"""
import sys
IDENT_CHARS = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_")
def skip_variable_ref(s, i, out):
"""s[i] is '$' or '#', s[i+1] is '['. Copies the whole $[...]/#[...]
reference verbatim (including nested [...] refs inside it) to out,
returns the index just past the closing ']'."""
out.append(s[i])
out.append(s[i + 1])
i += 2
depth = 1
while i < len(s) and depth > 0:
if s[i] == '[':
depth += 1
out.append(s[i])
i += 1
elif s[i] == ']':
depth -= 1
out.append(s[i])
i += 1
else:
out.append(s[i])
i += 1
return i
def is_var_start(s, i):
return i + 1 < len(s) and s[i] in '$#' and s[i + 1] == '['
def looks_like_tag(s, i):
"""s[i] is '<'. Peeks (without consuming) to decide whether this
really starts a jet tag, using the same rule Tag.cpp's own
constructor uses at runtime (Tag.cpp:50): read an identifier-char
run right after '<' (or after '</' for a close tag), and only
treat it as a real tag if that run is immediately followed by
whitespace, '/' or '>'. A literal '<' embedded in JS/text (e.g.
console.log("<")) fails this and must be left untouched - real
content relies on exactly this rule to disambiguate."""
n = len(s)
j = i + 1
if j < n and s[j] == '/':
k = j + 1
while k < n and s[k] in IDENT_CHARS:
k += 1
if k == j + 1:
return False
return k < n and s[k] == '>'
if j >= n or not s[j].isalpha():
return False
k = j
while k < n and s[k] in IDENT_CHARS:
k += 1
return k < n and s[k] in ' \t\r\n/>'
def skip_quoted_value(s, i, quote_char, out):
"""s[i] is the opening quote character (already consumed by caller,
i.e. this points at the FIRST character of the value). Copies the
value verbatim, including any embedded <tag>...</tag> constructs and
variable refs, stopping at (and consuming) the matching closing
quote. Returns the index just past the closing quote."""
while i < len(s):
if s[i] == quote_char:
out.append(s[i])
return i + 1
if is_var_start(s, i):
i = skip_variable_ref(s, i, out)
continue
if s[i] == '<' and looks_like_tag(s, i):
i = skip_embedded_tag(s, i, out)
continue
out.append(s[i])
i += 1
return i
def skip_embedded_tag(s, i, out):
"""s[i] is '<', starting a tag embedded inside a quoted attribute
value (or, via lowercase_source's own top-level loop, an ordinary
top-level tag). Lowercases its name and attribute names the same
way the top-level scanner does, then either stops after a
self-closing '/>' or, if it looks like it has its own body, copies
through to its own matching close tag verbatim (rare inside a
quoted value in real content; the void-tag list below covers the
common built-ins that never do). Returns the index just past this
tag's own closing '>' (and, for a real container tag, past its
matching close tag too)."""
out.append(s[i]) # '<'
i += 1
closing = False
if i < len(s) and s[i] == '/':
closing = True
out.append(s[i])
i += 1
name_start = i
while i < len(s) and s[i] in IDENT_CHARS:
i += 1
name = s[name_start:i]
out.append(name.lower())
if closing:
# just the name, then skip to '>'
while i < len(s) and s[i] != '>':
out.append(s[i])
i += 1
if i < len(s):
out.append(s[i])
i += 1
return i
# attribute region
self_closed = False
while i < len(s):
if s[i] in ' \t\r\n':
out.append(s[i])
i += 1
continue
if s[i] == '/' and i + 1 < len(s) and s[i + 1] == '>':
out.append('/>')
i += 2
self_closed = True
break
if s[i] == '>':
out.append('>')
i += 1
break
if s[i] in IDENT_CHARS:
attr_start = i
while i < len(s) and s[i] in IDENT_CHARS:
i += 1
attr_name = s[attr_start:i]
out.append(attr_name.lower())
# optional whitespace before '='
j = i
while j < len(s) and s[j] in ' \t\r\n':
j += 1
if j < len(s) and s[j] == '=':
out.append(s[i:j])
i = j
out.append('=')
i += 1
# optional whitespace after '='
while i < len(s) and s[i] in ' \t\r\n':
out.append(s[i])
i += 1
if i < len(s) and s[i] in ('"', "'"):
q = s[i]
out.append(q)
i += 1
i = skip_quoted_value(s, i, q, out)
else:
# unquoted value - copy verbatim to next
# whitespace/'>'//'
while i < len(s) and s[i] not in ' \t\r\n>/':
if is_var_start(s, i):
i = skip_variable_ref(s, i, out)
else:
out.append(s[i])
i += 1
continue
# anything unexpected - copy through defensively
out.append(s[i])
i += 1
void_tags = {
"set", "include", "cookie", "header", "read", "write", "call",
"system", "dump", "br", "img", "input", "hr", "meta", "link",
"area", "base", "col", "embed", "source", "track", "wbr",
"else",
}
if self_closed or name.lower() in void_tags:
return i
# real container tag embedded in a quoted value - copy through to
# its own matching close, tracking nesting of the same tag name.
depth = 1
nlower = name.lower()
while i < len(s) and depth > 0:
if is_var_start(s, i):
i = skip_variable_ref(s, i, out)
continue
if s[i] == '<':
# peek for open/close of the same tag name
j = i + 1
is_close = False
if j < len(s) and s[j] == '/':
is_close = True
j += 1
k = j
while k < len(s) and s[k] in IDENT_CHARS:
k += 1
token = s[j:k]
if not looks_like_tag(s, i):
out.append(s[i])
i += 1
continue
if token.lower() == nlower:
if is_close:
depth -= 1
else:
depth += 1
i = skip_embedded_tag(s, i, out)
continue
i = skip_embedded_tag(s, i, out)
continue
out.append(s[i])
i += 1
return i
def lowercase_source(s):
out = []
i = 0
n = len(s)
while i < n:
if is_var_start(s, i):
i = skip_variable_ref(s, i, out)
continue
if s[i] == '<' and looks_like_tag(s, i):
i = skip_embedded_tag(s, i, out)
continue
out.append(s[i])
i += 1
return ''.join(out)
def main():
if len(sys.argv) != 3:
print("usage: lowercase_jet.py <input> <output>")
sys.exit(1)
with open(sys.argv[1], encoding="utf-8", errors="surrogateescape", newline="") as f:
content = f.read()
result = lowercase_source(content)
with open(sys.argv[2], "w", encoding="utf-8", errors="surrogateescape", newline="") as f:
f.write(result)
print("wrote", sys.argv[2], "(%d bytes -> %d bytes)" % (len(content), len(result)))
if __name__ == "__main__":
main()
+96
View File
@@ -0,0 +1,96 @@
"""
Strips the dead SPA-fragment dual-render wrapper from a barant-site/jet/*.jet
route file, leaving just the real page content (the former "_full_NAME" tag
body), unwrapped, with no fragment-flag try/catch, no _frag_NAME tag
definition, no dispatch <if>, and no dead client-side SPA navigation script.
Structure being removed (present in all 20 files, verified consistent):
#api GET /NAME
<try name="_fragerr">...<catch>...</try><tag name="_frag_NAME">
... fragment-only content, NEVER actually invoked any more since the
client-side SPA click handlers were already disabled earlier this
session ...
</tag><tag name="_full_NAME">
... the REAL page content - full doctype/head/style/body wrapper ...
<script>
(function() {
var FRAGMENT_EXCLUDED = [...];
function pageNameFromPath(...) {...}
function loadPage(...) {...}
// SPA click/popstate interception disabled for now ...
})();
</script></body></html>
</tag><if value1="$[fragflag]" value2="1" type="eq"><_frag_NAME /><else><_full_NAME /></if>
Kept: "#api GET /NAME\n" plus everything between "<tag name=\"_full_NAME\">"
and its own matching close, MINUS the dead <script> block.
"""
import re
import sys
def strip_dead_spa_script(content):
pattern = re.compile(
r"<script>\s*\(function\(\)\s*\{.*?SPA click/popstate interception disabled.*?\}\)\(\);\s*</script>",
re.DOTALL,
)
new_content, n = pattern.subn("", content, count=1)
return new_content, n
def strip_fragment_wrapper(content, page_name):
lines = content.split("\n", 1)
assert lines[0].startswith("#api GET /"), f"unexpected first line: {lines[0]!r}"
api_line = lines[0]
rest = lines[1] if len(lines) > 1 else ""
start_marker = (
'<try name="_fragerr"><set name="fragflag" expr="$[fragment]" />'
'<catch><set name="fragflag" value="" /></try>'
f'<tag name="_frag_{page_name}">'
)
if start_marker not in rest:
return None, f"start marker not found"
idx = rest.index(start_marker)
prefix_extra = rest[:idx] # should just be leading whitespace/newline, kept as-is
rest = rest[idx + len(start_marker):]
mid_marker = f'</tag><tag name="_full_{page_name}">'
if mid_marker not in rest:
return None, f"mid marker not found"
mid_idx = rest.index(mid_marker)
rest = rest[mid_idx + len(mid_marker):]
end_pattern = re.compile(
r'</tag><if value1="\$\[fragflag\]" value2="1" type="eq">'
rf'<_frag_{re.escape(page_name)} /><else><_full_{re.escape(page_name)} /></if>\s*$'
)
m = end_pattern.search(rest)
if not m:
return None, f"end marker not found"
full_content = rest[:m.start()]
full_content, nscript = strip_dead_spa_script(full_content)
result = api_line + "\n" + prefix_extra + full_content
return result, f"ok (removed {nscript} dead script block(s))"
def main():
if len(sys.argv) != 4:
print("usage: strip_fragments.py <input> <output> <page_name>")
sys.exit(1)
with open(sys.argv[1], encoding="utf-8", newline="") as f:
content = f.read()
result, status = strip_fragment_wrapper(content, sys.argv[3])
if result is None:
print(f"FAILED for {sys.argv[3]}: {status}")
sys.exit(1)
with open(sys.argv[2], "w", encoding="utf-8", newline="") as f:
f.write(result)
print(f"{sys.argv[3]}: {status} ({len(content)} -> {len(result)} bytes)")
if __name__ == "__main__":
main()