diff --git a/Tag.cpp b/Tag.cpp index a32c3eb..0975404 100644 --- a/Tag.cpp +++ b/Tag.cpp @@ -11,6 +11,7 @@ #include "__imapmessage.h" #include "__imapflag.h" #include "__imapdelete.h" +#include "__imapemptyfolder.h" #include "__imapmove.h" #include "__imapfolders.h" #include "__imapsearch.h" @@ -215,9 +216,33 @@ namespace jet { // matches the same unconsumed on every subsequent // iteration forever. Build the real expected delimiter // from splitTagName itself so it works for any split tag. - coreutils::MString splitOpenTag; - splitOpenTag << "<" << splitTagName << ">"; - in.ifNextIgnoreCase(splitOpenTag); + in.ifNext("<"); + in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"); + while(!in.eod()) { + in.skipWhitespace(); + if(in.ifNext(">")) + break; + else if(in.getTokenExclude("=").getLength() != 0) { + bool dq = in.ifNext("=\""); + bool sq = !dq && in.ifNext("='"); + if(dq || sq) { + while(1) { + if(in.ifNext(dq ? "\"" : "'")) + break; + in.nextChar(); + } + } else { + in.nextChar(); + while(!in.eod()) { + char c = in.charAt(0); + if(c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '>' || c == '/') + break; + in.nextChar(); + } + } + } else + break; + } start = in.getCursor(); } else if(ifNested(in)) { } else @@ -477,6 +502,9 @@ namespace jet { } else if(ifTagName(in, "imapdelete")) { __imapdelete _imapdelete(in, out, global, this, local); continue; + } else if(ifTagName(in, "imapemptyfolder")) { + __imapemptyfolder _imapemptyfolder(in, out, global, this, local); + continue; } else if(ifTagName(in, "imapmove")) { __imapmove _imapmove(in, out, global, this, local); continue; @@ -737,16 +765,53 @@ namespace jet { bool Tag::ifSplitTagName(coreutils::ZString &in) { in.push(); - if(in.ifNext("<")) - if(coreutils::MString(in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_")).toLower() == coreutils::MString(splitTagName).toLower()) { - if(in.ifNext(">")) - in.pop(); - return true; - } + if(in.ifNext("<")) { + if(coreutils::MString(in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_")).toLower() == coreutils::MString(splitTagName).toLower()) { + // Tolerate attributes on the split-tag marker itself (e.g. + // 's own ) rather than requiring + // '>' to land immediately after the tag name - every + // splitTagName="else" caller writes a bare so this + // never mattered for them, but is the + // universal convention everywhere it's used. Previously this + // whole check was a single unbraced + // if(...){in.pop();return true;} where the missing braces + // meant "return true" executed unconditionally, leaving the + // cursor in a half-advanced, never-popped state on the + // attributed-tag path - confirmed live via a real /email/list + // failure whose caught error text leaked + // "name=\"_imaperr\">" into the JSON response. + while(!in.eod()) { + in.skipWhitespace(); + if(in.ifNext(">")) { + in.pop(); + return true; + } else if(in.getTokenExclude("=").getLength() != 0) { + bool dq = in.ifNext("=\""); + bool sq = !dq && in.ifNext("='"); + if(dq || sq) { + while(1) { + if(in.ifNext(dq ? "\"" : "'")) + break; + in.nextChar(); + } + } else { + in.nextChar(); + while(!in.eod()) { + char c = in.charAt(0); + if(c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '>' || c == '/') + break; + in.nextChar(); + } + } + } else + break; + } + } + } in.pop(); return false; } - + int Tag::skipBlankLine(coreutils::ZString in) { ZString temp = in.getTokenInclude(" \t"); if(ifNext("\n")) diff --git a/__call.cpp b/__call.cpp index d951445..a56d2a0 100644 --- a/__call.cpp +++ b/__call.cpp @@ -7,13 +7,15 @@ #include #include #include +#include +#include #include namespace jet { - + __call::__call(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) { if(hasContainer && keywordDefined("input")) - throw coreutils::Exception("call tag cannot have both input keyword and have a container."); + throw coreutils::Exception("call tag cannot have both input keyword and have a container."); if(!keywordDefined("pgm")) throw coreutils::Exception("pgm keyword must be specified."); for(ix = 0; ix < 50; ++ix) @@ -35,48 +37,93 @@ namespace jet { } else break; } - pipe(fdo); + // pipe()/fork() both used to go unchecked here - on failure (most + // realistically pipe() hitting the process's open-file-descriptor + // limit, see below) fdo[] is left holding whatever garbage was + // already on the stack/in this member array, and every use of it + // after this point (close(), read(), and - for a failed fork() - + // waitpid(-1, ...), which has the special POSIX meaning "wait for + // ANY child" rather than "wait for none") operates on that garbage + // or on an unrelated process instead of safely no-oping. That's a + // real, confirmed mechanism for exactly the kind of sporadic + // memory corruption this codebase has seen: closing/reading a + // stale fd that coincidentally matches another subsystem's live + // socket, or reaping a concurrent request's own child out from + // under it. + if(pipe(fdo) != 0) + throw coreutils::Exception("call: pipe() failed - " + std::string(strerror(errno)), __FILE__, __LINE__, 500); pid = fork(); + if(pid < 0) { + close(fdo[0]); + close(fdo[1]); + throw coreutils::Exception("call: fork() failed - " + std::string(strerror(errno)), __FILE__, __LINE__, 500); + } if(pid == 0) { close(fdo[0]); dup2(fdo[1], 1); - if(hasContainer) { - pipe(fdi); - if(fork() == 0) { - close(fdi[0]); - write(fdi[1], container.getData(), container.getLength()); - close(fdi[1]); - exit(0); + if(hasContainer) { + if(pipe(fdi) == 0) { + pid_t innerPid = fork(); + if(innerPid == 0) { + close(fdi[0]); + write(fdi[1], container.getData(), container.getLength()); + close(fdi[1]); + _exit(0); + } else if(innerPid > 0) { + close(fdi[1]); + dup2(fdi[0], 0); + close(fdi[0]); + } else { + close(fdi[0]); + close(fdi[1]); + } } - close(fdi[1]); - dup2(fdi[0], 0); } else if(keywordDefined("input")) { coreutils::MString input(resolveKeyword("input")); - pipe(fdi); - if(fork() == 0) { - close(fdi[0]); - write(fdi[1], input.getData(), input.getLength()); - close(fdi[1]); - exit(0); + if(pipe(fdi) == 0) { + pid_t innerPid = fork(); + if(innerPid == 0) { + close(fdi[0]); + write(fdi[1], input.getData(), input.getLength()); + close(fdi[1]); + _exit(0); + } else if(innerPid > 0) { + close(fdi[1]); + dup2(fdi[0], 0); + close(fdi[0]); + } else { + close(fdi[0]); + close(fdi[1]); + } } - close(fdi[1]); - dup2(fdi[0], 0); } rc = execvpe(argv[0], argv, global.envp); close(fdo[1]); - exit(errno); + _exit(errno); } close(fdo[1]); if(keywordDefined("name")) { coreutils::MString value; value.read(fdo[0]); storeVariable(resolveKeyword("name"), value, resolveKeyword("scope")); - } else - out.read(fdo[0]); + } else + out.read(fdo[0]); + // fdo[0] (the read end, still open in this - the parent - process) + // was never closed here before, leaking one file descriptor on + // every single invocation for the life of the process. + // Harmless-looking for an occasional , but is used + // throughout this codebase (jq/curl parsing, mailcreate, this + // session's own per-line-item JS-escaping on the invoice editor, + // which alone can issue a double-digit number of calls per page + // load) - repeated, routine use was slowly accumulating leaked + // fds for as long as this process stayed up, eventually hitting + // the real open-files limit and turning into exactly the pipe() + // failures (and the undefined behavior above) this fix addresses. + close(fdo[0]); waitpid(pid, &status, 0); - if(keywordDefined("error")) { + if(keywordDefined("error")) { global.variables[resolveKeyword("error")] = (status >> 8 & 255); - } + } } - + } diff --git a/__imap.cpp b/__imap.cpp index b2506e2..ad4ebec 100644 --- a/__imap.cpp +++ b/__imap.cpp @@ -559,8 +559,15 @@ namespace jet { coreutils::MString passwordVal = resolveKeyword("password"); coreutils::JFile config(configPath); - std::string host = config.getString("host"); - long port = (long)config.getNumber("port"); + // host/port keywords override the config file's own values when + // present - lets a caller reuse one config file (still needed for + // caCertificate/smtpPort) across many accounts that each live on + // a different mail server, rather than requiring a separate config + // file per server. Every existing caller omits these keywords, so + // behavior is unchanged for them - config's own host/port is still + // the only thing read in that case. + std::string host = keywordDefined("host") ? resolveKeyword("host").str() : config.getString("host"); + long port = keywordDefined("port") ? strtol(resolveKeyword("port").str().c_str(), nullptr, 10) : (long)config.getNumber("port"); if (port <= 0) port = 993; @@ -1034,6 +1041,24 @@ namespace jet { return coreutils::MString(std::string("{\"ok\":") + (ok ? "true" : "false") + "}"); } + coreutils::MString __imap::emptyFolder(const std::string &folder) { + if (!selectFolderIfNeeded(folder)) + throw coreutils::Exception("imap: could not select folder " + folder, __FILE__, __LINE__, 502); + + std::string tag = nextTag(); + sendLine(tag + " UID STORE 1:* +FLAGS (\\Deleted)"); + auto lines = readUntilTagged(tag); + bool ok1 = taggedOk(lines, tag); + + std::string tag2 = nextTag(); + sendLine(tag2 + " EXPUNGE"); + auto lines2 = readUntilTagged(tag2); + bool ok2 = taggedOk(lines2, tag2); + + bool ok = ok1 && ok2; + return coreutils::MString(std::string("{\"ok\":") + (ok ? "true" : "false") + "}"); + } + // BARANTMail (see __IMAP_COPY.cpp) implements COPY but not the // RFC 6851 MOVE extension - not surprising, since the whole point // of that extension was standardizing what every real-world client diff --git a/__imap.h b/__imap.h index 7d396d9..3a8b953 100644 --- a/__imap.h +++ b/__imap.h @@ -63,6 +63,15 @@ namespace jet { coreutils::MString setFlag(const std::string &folder, uint32_t uid, const std::string &flag, bool add); coreutils::MString deleteMessage(const std::string &folder, uint32_t uid); coreutils::MString moveMessage(const std::string &folder, uint32_t uid, const std::string &destFolder); + // Permanently empties an entire folder - every message, not one at + // a time. "1:*" is IMAP's standard UID-range syntax for "every UID + // in the selected mailbox" (well-defined and a no-op, not an error, + // on an already-empty folder - verified directly against + // BARANTMail rather than assumed from the RFC), so this is a + // single mark-all-deleted STORE plus one EXPUNGE, not a loop over + // deleteMessage() - the whole point is one IMAP round trip instead + // of one per message. + coreutils::MString emptyFolder(const std::string &folder); coreutils::MString folderCounts(); coreutils::MString sessionId; diff --git a/__imapaccounts.cpp b/__imapaccounts.cpp index cbb16e6..9a78aa0 100644 --- a/__imapaccounts.cpp +++ b/__imapaccounts.cpp @@ -37,8 +37,8 @@ namespace jet { return s.substr(start, end - start + 1); } - std::vector> __imapaccounts::decodeList(const std::string &encodedRaw) { - std::vector> entries; + std::vector<__imapaccounts::Account> __imapaccounts::decodeList(const std::string &encodedRaw) { + std::vector entries; coreutils::Base64 base64; // The list value round-trips through / template // captures, which include whatever whitespace/newlines surround @@ -56,26 +56,35 @@ namespace jet { entry = trimWhitespace(entry); if (entry.empty()) continue; - size_t colon = entry.find(':'); - if (colon == std::string::npos) + size_t colon1 = entry.find(':'); + if (colon1 == std::string::npos) continue; - std::string userB64 = trimWhitespace(entry.substr(0, colon)); - std::string passB64 = trimWhitespace(entry.substr(colon + 1)); - std::string user = base64.decode(userB64).str(); - std::string password = base64.decode(passB64).str(); - entries.push_back(std::make_pair(user, password)); + size_t colon2 = entry.find(':', colon1 + 1); + if (colon2 == std::string::npos) + continue; + std::string hostB64 = trimWhitespace(entry.substr(0, colon1)); + std::string userB64 = trimWhitespace(entry.substr(colon1 + 1, colon2 - colon1 - 1)); + std::string passB64 = trimWhitespace(entry.substr(colon2 + 1)); + Account account; + account.host = base64.decode(hostB64).str(); + account.user = base64.decode(userB64).str(); + account.password = base64.decode(passB64).str(); + entries.push_back(account); } return entries; } - std::string __imapaccounts::encodeList(const std::vector> &entries) { + std::string __imapaccounts::encodeList(const std::vector &entries) { coreutils::Base64 base64; std::string result; for (size_t i = 0; i < entries.size(); i++) { if (i > 0) result += ";"; - std::string user = entries[i].first; - std::string password = entries[i].second; + std::string host = entries[i].host; + std::string user = entries[i].user; + std::string password = entries[i].password; + result += base64.encode(host).str(); + result += ":"; result += base64.encode(user).str(); result += ":"; result += base64.encode(password).str(); @@ -90,20 +99,25 @@ namespace jet { auto entries = decodeList(listStr); if (action == "add") { + std::string host = trimWhitespace(resolveKeyword("host").str()); std::string user = trimWhitespace(resolveKeyword("user").str()); std::string password = trimWhitespace(resolveKeyword("password").str()); for (size_t i = 0; i < entries.size(); i++) { - if (entries[i].first == user) { + if (entries[i].user == user) { entries.erase(entries.begin() + (long)i); break; } } - entries.push_back(std::make_pair(user, password)); + Account account; + account.host = host; + account.user = user; + account.password = password; + entries.push_back(account); out = coreutils::MString(encodeList(entries)); } else if (action == "remove") { std::string user = trimWhitespace(resolveKeyword("user").str()); for (size_t i = 0; i < entries.size(); i++) { - if (entries[i].first == user) { + if (entries[i].user == user) { entries.erase(entries.begin() + (long)i); break; } @@ -113,21 +127,31 @@ namespace jet { std::string user = trimWhitespace(resolveKeyword("user").str()); std::string password; for (size_t i = 0; i < entries.size(); i++) { - if (entries[i].first == user) { - password = entries[i].second; + if (entries[i].user == user) { + password = entries[i].password; break; } } out = coreutils::MString(password); + } else if (action == "gethost") { + std::string user = trimWhitespace(resolveKeyword("user").str()); + std::string host; + for (size_t i = 0; i < entries.size(); i++) { + if (entries[i].user == user) { + host = entries[i].host; + break; + } + } + out = coreutils::MString(host); } else if (action == "firstuser") { - out = coreutils::MString(entries.empty() ? std::string() : entries[0].first); + out = coreutils::MString(entries.empty() ? std::string() : entries[0].user); } else if (action == "list") { std::string active = resolveKeyword("active").str(); std::string json = "{\"accounts\":["; for (size_t i = 0; i < entries.size(); i++) { if (i > 0) json += ","; - json += "\"" + jsonEscape(entries[i].first) + "\""; + json += "\"" + jsonEscape(entries[i].user) + "\""; } json += "],\"active\":\"" + jsonEscape(active) + "\"}"; out = coreutils::MString(json); diff --git a/__imapaccounts.h b/__imapaccounts.h index 1261a87..649f80b 100644 --- a/__imapaccounts.h +++ b/__imapaccounts.h @@ -11,8 +11,8 @@ namespace jet { /// - /// + /// /// /// Pure list-transform tag (no session-variable access of its own, /// no IMAP connection) that manages the multi-account list webmail @@ -23,34 +23,45 @@ namespace jet { /// touches Global's session-variable map directly). /// /// The list's own on-the-wire encoding (semicolon-separated - /// "base64(user):base64(password)" entries) is private to this tag - - /// callers only ever pass the whole `list` value back in verbatim - /// and use the returned value the same way, so the choice of - /// delimiter can't collide with any real username or password - /// character. + /// "base64(host):base64(user):base64(password)" entries) is private + /// to this tag - callers only ever pass the whole `list` value back + /// in verbatim and use the returned value the same way, so the + /// choice of delimiter can't collide with any real username or + /// password character. Each account can live on its own IMAP server + /// (see the //etc. tags' own optional host/port + /// keywords), not just the one fixed host in email.cfg. /// - /// action="add": upserts user/password (removing any existing entry - /// for the same user first, so reconnecting with a changed password - /// updates it) - returns the updated encoded list. + /// action="add": upserts host/user/password (removing any existing + /// entry for the same user first, so reconnecting with a changed + /// password or server updates it) - returns the updated encoded list. /// action="remove": drops the entry for user, if present - returns /// the updated encoded list. /// action="getpassword": returns the stored password for user, or /// an empty string if not present. + /// action="gethost": returns the stored host for user, or an empty + /// string if not present. /// action="firstuser": returns the first entry's username, or an /// empty string if the list is empty. /// action="list": returns JSON ({"accounts":[...],"active":"..."}) /// - the one client-facing action; "active" echoes the `active` /// keyword (typically $[@_imap_user]) back verbatim so the sidebar - /// can highlight the current one, and never includes any password. + /// can highlight the current one, and never includes any password or + /// host (unchanged shape from before per-account hosts existed - + /// nothing client-side needs the host to switch between already- + /// connected accounts). /// class __imapaccounts : public Tag { + struct Account { + std::string host, user, password; + }; + public: __imapaccounts(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local); private: - static std::vector> decodeList(const std::string &encoded); - static std::string encodeList(const std::vector> &entries); + static std::vector decodeList(const std::string &encoded); + static std::string encodeList(const std::vector &entries); static std::string jsonEscape(const std::string &s); }; diff --git a/__imapemptyfolder.cpp b/__imapemptyfolder.cpp new file mode 100644 index 0000000..58daa14 --- /dev/null +++ b/__imapemptyfolder.cpp @@ -0,0 +1,26 @@ +#include "__imapemptyfolder.h" +#include "Exception.h" +#include "__imap.h" + +namespace jet { + + __imapemptyfolder::__imapemptyfolder(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) { + + coreutils::MString sessionId = resolveKeyword("sessionid"); + if (!global.imapSessionExists(sessionId)) + throw coreutils::Exception("imap sessionid does not exist.", __FILE__, __LINE__, 502); + + // Unlike __imapdelete (one message - low blast radius even on a + // wrong default), this permanently destroys an entire folder's + // contents - no silent "defaults to INBOX" here. A missing/blank + // folder attribute is refused outright rather than risked. + if (!keywordDefined("folder")) + throw coreutils::Exception("imapemptyfolder tag must have folder defined.", __FILE__, __LINE__, 400); + std::string folder = resolveKeyword("folder").str(); + if (folder.empty()) + throw coreutils::Exception("imapemptyfolder tag must have folder defined.", __FILE__, __LINE__, 400); + + out = global.getImapSession(sessionId)->emptyFolder(folder); + } + +} diff --git a/__imapemptyfolder.h b/__imapemptyfolder.h new file mode 100644 index 0000000..102d983 --- /dev/null +++ b/__imapemptyfolder.h @@ -0,0 +1,26 @@ +#ifndef ____imapemptyfolder_h__ +# define ____imapemptyfolder_h__ + +# include "Tag.h" + +namespace jet { + + /// + /// + /// + /// Self-closing tag - permanently deletes every message in the given + /// folder (mark-all-\Deleted + one EXPUNGE, not a per-message loop), + /// writes {"ok":true|false} into `out`. See __imaplist.h's doc + /// comment for how `out` reaches the real response body. + /// + + class __imapemptyfolder : public Tag { + + public: + __imapemptyfolder(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local); + + }; + +} + +#endif diff --git a/__smtpsend.cpp b/__smtpsend.cpp index de96709..50ece7e 100644 --- a/__smtpsend.cpp +++ b/__smtpsend.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include namespace jet { @@ -88,10 +90,31 @@ namespace jet { return std::string(buf); } + // RFC 5322 3.6.4: Message-ID "SHOULD be present" - in practice + // several real receivers (confirmed live: Gmail) now hard-reject + // anything missing one ("550 5.7.1 Messages missing a valid + // Message-ID header are not accepted") rather than just marking it + // down, so this isn't optional in practice. Domain part is the + // sender's own domain (everything after '@' in `from`), matching + // RFC 5322's own recommendation that it be a name under the + // generating system's control - falls back to the literal `from` + // string in the (should-never-happen) case it has no '@' at all. + std::string __smtpsend::generateMessageId(const std::string &from) { + size_t at = from.find('@'); + std::string domainPart = (at == std::string::npos) ? from : from.substr(at + 1); + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + std::ostringstream idStream; + idStream << ts.tv_sec << "." << std::setfill('0') << std::setw(9) << ts.tv_nsec + << "." << rand() << "@" << domainPart; + return idStream.str(); + } + std::string __smtpsend::buildMessage(const std::string &from, const std::string &to, const std::string &subject, const std::string &html) { std::string msg; msg += "Date: " + rfc2822Date() + "\r\n"; + msg += "Message-ID: <" + generateMessageId(from) + ">\r\n"; msg += "From: " + from + "\r\n"; msg += "To: " + to + "\r\n"; msg += "Subject: " + subject + "\r\n"; diff --git a/__smtpsend.h b/__smtpsend.h index 6438a22..5e489e9 100644 --- a/__smtpsend.h +++ b/__smtpsend.h @@ -58,6 +58,7 @@ namespace jet { static std::string base64Encode(const std::string &s); static std::string rfc2822Date(); + static std::string generateMessageId(const std::string &from); // Builds the full RFC 5322 message (headers + dot-stuffed body) // handed to SMTP's DATA command. static std::string buildMessage(const std::string &from, const std::string &to, diff --git a/jetc/barant-site/assets/images/btglogo.png b/jetc/barant-site/assets/images/btglogo.png index 43ab4af..6c37916 100644 Binary files a/jetc/barant-site/assets/images/btglogo.png and b/jetc/barant-site/assets/images/btglogo.png differ diff --git a/jetc/barant-site/assets/images/email.js b/jetc/barant-site/assets/images/email.js index f7ec8cd..31ed1d4 100644 --- a/jetc/barant-site/assets/images/email.js +++ b/jetc/barant-site/assets/images/email.js @@ -43,6 +43,13 @@ pageNext: '' + '' + + '', + emptyFolder: + '' + + '' + + '' + + '' + + '' + '' }; @@ -109,6 +116,15 @@ var links = document.querySelectorAll('.email-folder-link'); for (var i = 0; i < links.length; i++) links[i].classList.toggle('email-folder-selected', links[i].getAttribute('data-folder') === currentFolder); + // Only Spam/Trash ever offer this - emptying Inbox (or any future + // folder) as a single destructive click isn't something any real + // mail client exposes, and the server enforces the exact same + // Spam/Trash-only allowlist independently (see email.jet) - this is + // just keeping the button from ever being visible somewhere it + // would 400 anyway, not the actual safety boundary. + var emptyBtn = document.getElementById('email-empty-folder-btn'); + if (emptyBtn) + emptyBtn.hidden = (currentFolder !== 'Spam' && currentFolder !== 'Trash'); } function escapeHtml(s) { @@ -158,6 +174,7 @@ '' + '' + '' + + '' + '' + '' + '' + @@ -218,6 +235,7 @@ document.getElementById('email-refresh-btn').addEventListener('click', function() { loadMessageList(); }); + document.getElementById('email-empty-folder-btn').addEventListener('click', emptyCurrentFolder); // Matches sender, subject, or body content in whatever folder is // currently open (see __imap.cpp's searchMessages()) - an empty @@ -444,6 +462,38 @@ }); } + // Permanently deletes every message in the CURRENT folder (only ever + // reachable while viewing Spam or Trash - see syncActiveFolder()'s own + // button-visibility gate, and email.jet's independent server-side + // allowlist, which is the real enforcement). Unlike trashMessage() + // there's no undo, no "move somewhere" - a real, human-readable confirm() + // is the whole safety net here, matching how Gmail/Outlook both gate + // "Empty Trash now" behind exactly one extra click and nothing fancier. + function emptyCurrentFolder() { + if (currentFolder !== 'Spam' && currentFolder !== 'Trash') + return; + if (!window.confirm('Permanently delete every message in ' + currentFolder + '? This cannot be undone.')) + return; + var btn = document.getElementById('email-empty-folder-btn'); + if (btn) btn.disabled = true; + post('/email/emptyfolder', { folder: currentFolder }) + .then(function(res) { + if (btn) btn.disabled = false; + if (!res.ok) { + window.alert(res.error || 'Could not empty the folder.'); + return; + } + currentUid = null; + document.getElementById('email-reading-pane').innerHTML = + '
Select a message to read it.
'; + loadMessageList(); + }) + .catch(function() { + if (btn) btn.disabled = false; + window.alert('Empty-folder request failed.'); + }); + } + // Same fade-out/fetch-concurrently/fade-in shape as openMessage() and // spa.js's own navigate() - every list refresh (folder switch, // refresh button) gets the same smooth transition instead of a flash @@ -488,8 +538,10 @@ if (!skipFade) list.classList.remove('email-fading'); }) - .catch(function() { - list.innerHTML = '
Could not load messages.
'; + .catch(function(err) { + console.error('loadMessageList failed:', err); + list.innerHTML = '
Could not load messages' + + (err && err.message ? ': ' + escapeHtml(err.message) : '.') + '
'; if (!skipFade) list.classList.remove('email-fading'); }); @@ -506,8 +558,10 @@ // cosmetic problem, not worth surfacing as a user-visible failure. function loadFolderCounts() { post('/email/folders', {}).then(function(res) { - if (res.error) + if (res.error) { + console.error('loadFolderCounts server error:', res.error); return; + } var folders = ['Inbox', 'Spam', 'Trash']; for (var i = 0; i < folders.length; i++) { var f = folders[i]; @@ -515,7 +569,9 @@ if (span && res[f]) span.textContent = res[f].messages > 0 ? (res[f].unseen + '/' + res[f].messages) : ''; } - }).catch(function() {}); + }).catch(function(err) { + console.error('loadFolderCounts failed:', err); + }); } function loadAccounts() { @@ -756,8 +812,10 @@ } pane.classList.remove('email-fading'); }) - .catch(function() { - pane.innerHTML = '
Could not load message.
'; + .catch(function(err) { + console.error('openMessage failed:', err); + pane.innerHTML = '
Could not load message' + + (err && err.message ? ': ' + escapeHtml(err.message) : '.') + '
'; pane.classList.remove('email-fading'); }); } diff --git a/jetc/barant-site/assets/images/main.css b/jetc/barant-site/assets/images/main.css index b31d418..ed7222f 100644 --- a/jetc/barant-site/assets/images/main.css +++ b/jetc/barant-site/assets/images/main.css @@ -13,16 +13,16 @@ @font-face { font-family: avenir; - src: url('https://barant.com/fonts/AvenirLTStd-Light.ttf'); + src: url('https://barant.com/assets/AvenirLTStd-Light.ttf'); } @font-face { font-family: source; - src: url('https://barant.com/fonts/jackinput.TTF'); + src: url('https://barant.com/assets/jackinput.TTF'); } @font-face { font-family: exo2; - src: url('https://barant.com/fonts/Exo2-RegularCondensed.otf'); + src: url('https://barant.com/assets/Exo2-RegularCondensed.otf'); } :root { @@ -283,7 +283,7 @@ textarea.fieldinputerror { background: linear-gradient(180deg, #131318, #0d0d10); border-radius: 5px 5px 0px 0px; border-bottom: 1px solid var(--bond-line); - max-height: 80px; + max-height: 125px; } .entryform { background: #232330; @@ -365,24 +365,43 @@ textarea.fieldinputerror { transition: color 0.2s ease, border-color 0.2s ease; } -/* The currently-active entry in a page's own sidebar submenu (e.g. - opensource.menu's list of subpages) - a background highlight rather - than baritemselected's "raised tab" treatment, since these are plain - inline links stacked in a list, not tabs sitting on a bar edge. - Applied server-side (see opensource.menu) by comparing each - linkbutton's own subpage against $[SUBPAGE] - spa.js already - re-renders and swaps this menu's markup on every client-side - navigation (see its #_spa_panelmenu handling), so this stays correct - without any script of its own. */ +/* Every entry in a page's own sidebar submenu (e.g. opensource.menu's + list of subpages) shares this same box model - selected and + unselected alike - so the only thing that changes when an item + becomes active is its background/border, never its box size or the + position of its own text. Giving only the active item padding/margin + (the original approach) made its text visibly shift the moment it + was selected, since the unselected items sat flush with none. */ +#panelmenu a { + display: block; + padding: 2px 6px; + margin: 2px 0; + } + +/* The currently-active entry - a background highlight rather than + baritemselected's "raised tab" treatment, since these are plain + links stacked in a list, not tabs sitting on a bar edge. Applied + server-side (see opensource.menu) by comparing each linkbutton's own + subpage against $[SUBPAGE] - spa.js also applies it instantly client- + side on click (see its #panelmenu click listener) and then + re-confirms it from the server's own #_spa_panelmenu markup once the + fetch resolves, so this stays correct either way. */ .submenuitem-active { background: var(--bond-panel-2); color: var(--bond-gold-bright) !important; - padding: 2px 6px; - margin: -2px -6px; border-radius: 3px; box-shadow: inset 0 0 0 1px var(--bond-gold); } +/* Every menu entry's own markup is
- that +
was needed to force a line break after an inline link, but every + entry is display:block now (its own implicit line break), so the + trailing
doubles up into a blank line for every item, not just + the active one. */ +#panelmenu a + br { + display: none; + } + .panel { background: var(--bond-panel); margin: 10px; @@ -861,3 +880,499 @@ textarea.fieldinputerror { .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; } + + + + + + + + + + + + + + + + + + + +/* Fades in during an SPA navigation fetch - lives outside #content (a + content.innerHTML swap would just wipe it out otherwise), positioned + relative to the viewport rather than the resizable content column so + dragging #panelmenu-resizer narrower/wider never throws its centering + off. The transition duration alone (no separate timer) is what keeps + a fast load from ever showing it at a noticeable opacity before the + class comes back off. */ +.spa-loading-indicator { + position: fixed; + top: 90px; + left: 50%; + transform: translateX(-50%); + color: var(--bond-text-dim); + font-size: 10pt; + letter-spacing: 1px; + opacity: 0; + transition: opacity 0.6s ease; + pointer-events: none; + z-index: 500; +} +.spa-loading-indicator.spa-loading-visible { + opacity: 1; +} + + +/* Fades in during an SPA navigation fetch - lives outside #content (a + content.innerHTML swap would just wipe it out otherwise), positioned + relative to the viewport rather than the resizable content column so + dragging #panelmenu-resizer narrower/wider never throws its centering + off. The transition duration alone (no separate timer) is what keeps + a fast load from ever showing it at a noticeable opacity before the + class comes back off. */ +.spa-loading-indicator { + position: fixed; + top: 90px; + left: 50%; + transform: translateX(-50%); + color: var(--bond-text-dim); + font-size: 10pt; + letter-spacing: 1px; + opacity: 0; + transition: opacity 0.6s ease; + pointer-events: none; + z-index: 500; +} +.spa-loading-indicator.spa-loading-visible { + opacity: 1; +} + + +/* Keeps the music/library/ianslibrary subpages pinned to the browser + window's height (footer always visible, no whole-page scroll) while + the song list itself scrolls internally - the exact same pattern + already proven for the email app's own message list + (.pageclass:has(#email-app)), just scoped to this marker instead and + adapted for 's own heading-then-contents structure + (the heading needs to stay a fixed size above the scrolling list, + not get swept into the same scroll region). height:100% has to be + set explicitly at every level down to .music-song-list itself - + percentage heights only resolve against an ancestor with its own + definite height, and a plain block element's default is auto, which + does not count, so skipping any one level here would silently break + the whole chain. #main-subpage (main.page's generic per-subpage + wrapper, between #content and .sectionframe) is exactly that kind of + skip - missing it let .sectionframe's height:100% resolve to auto, + so .music-song-list sized itself to its full content height instead + of a scrollable box, and the overflow got clipped by .pageclass's + own overflow:hidden with no scrollbar to show for it. */ +.pageclass:has(.music-song-list) { + height: calc(100vh - 150px); + overflow: hidden; +} +#content:has(.music-song-list) { + height: 100%; + overflow: hidden; +} +#main-subpage:has(.music-song-list) { + height: 100%; + overflow: hidden; +} +.sectionframe:has(.music-song-list) { + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + box-sizing: border-box; +} +.sectionframe:has(.music-song-list) .sectionframecontents { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} +.music-song-list { + flex: 1; + min-height: 0; + overflow-y: auto; +} + +/* Fades in during an SPA navigation fetch - lives outside #content (a + content.innerHTML swap would just wipe it out otherwise), positioned + relative to the viewport rather than the resizable content column so + dragging #panelmenu-resizer narrower/wider never throws its centering + off. The transition duration alone (no separate timer) is what keeps + a fast load from ever showing it at a noticeable opacity before the + class comes back off. */ +.spa-loading-indicator { + position: fixed; + top: 90px; + left: 50%; + transform: translateX(-50%); + color: var(--bond-text-dim); + font-size: 10pt; + letter-spacing: 1px; + opacity: 0; + transition: opacity 0.6s ease; + pointer-events: none; + z-index: 500; +} +.spa-loading-indicator.spa-loading-visible { + opacity: 1; +} + +/* Global persistent music player band - slides open only while playing */ +.globalplayer-band { + max-height: 0; + overflow: hidden; + display: flex; + align-items: center; + gap: 10px; + background: var(--bond-panel); + border-bottom: 1pt solid var(--bond-line); + padding: 0 16px; + box-sizing: border-box; + transition: max-height 0.3s ease, padding 0.3s ease; +} +.globalplayer-band.globalplayer-open { + max-height: 54px; + padding: 10px 16px; +} +.globalplayer-title { + position: relative; + flex: 1; + /* A flex item's default min-width is its content's own min-content + size, not 0 - without this, a long song name would force the whole + LCD panel wider (squeezing the volume slider/buttons) rather than + actually truncating against the space it is given. */ + min-width: 0; + display: flex; + flex-direction: column; + justify-content: center; + background: #000; + border: 1px solid #333; + border-radius: 4px; + padding: 4px 10px; + box-sizing: border-box; + box-shadow: inset 0 0 8px rgba(26,143,196,0.12), inset 0 2px 4px rgba(0,0,0,0.8); +} +.globalplayer-duration { + position: absolute; + top: 4px; + right: 8px; + font-family: 'Consolas', 'Courier New', monospace; + font-size: 10pt; + color: #1a8fc4; + text-shadow: 0 0 4px rgba(26,143,196,0.7); + opacity: 0.85; +} +.globalplayer-random-indicator { + position: absolute; + top: 20px; + right: 8px; + font-family: 'Consolas', 'Courier New', monospace; + font-size: 7pt; + letter-spacing: 1px; + color: #1a8fc4; + text-shadow: 0 0 4px rgba(26,143,196,0.7); + opacity: 0.85; + display: none; +} +.globalplayer-random-indicator.globalplayer-random-indicator-visible { + display: block; +} +.globalplayer-title-text { + font-family: 'Consolas', 'Courier New', monospace; + font-size: 20pt; + line-height: 1.1; + color: #1a8fc4; + text-shadow: 0 0 5px rgba(26,143,196,0.8), 0 0 1px rgba(26,143,196,0.9); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.globalplayer-progress { + position: relative; + height: 8px; + width: 100%; + background: rgba(26,143,196,0.15); + border-radius: 4px; + margin-top: 4px; + box-sizing: border-box; + cursor: pointer; + touch-action: none; +} +.globalplayer-progress-fill { + height: 100%; + width: 0%; + background: #1a8fc4; + border-radius: 4px 0 0 4px; + box-shadow: 0 0 4px rgba(26,143,196,0.8); + transition: width 0.2s linear; + pointer-events: none; +} +.globalplayer-progress-handle { + position: absolute; + top: 50%; + left: 0%; + width: 12px; + height: 12px; + border-radius: 50%; + background: radial-gradient(circle at 35% 30%, #fff6d0 0%, #d4af37 55%, #6b5313 100%); + border: 1px solid #5a4410; + box-shadow: 0 1px 3px rgba(0,0,0,0.7); + transform: translate(-50%, -50%); + transition: left 0.2s linear; + pointer-events: none; +} +.globalplayer-progress.globalplayer-progress-dragging .globalplayer-progress-fill, +.globalplayer-progress.globalplayer-progress-dragging .globalplayer-progress-handle { + transition: none; +} + +/* Square, touch-tone-style buttons for the player band - same dished + gold-on-black look as the sipphone tag's dialpad keys. */ +.globalplayer-btn { + flex-shrink: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + background: radial-gradient(circle at 50% 50%, #8a6a22 0%, #b8903a 32%, #d4af37 55%, #ecce79 80%, #fff6d0 100%); + border: 1px solid #5a4410; + box-shadow: + 0 2px 0 #4a3a0f, + 0 3px 5px rgba(0,0,0,0.5), + inset 0 3px 5px rgba(0,0,0,0.45), + inset 0 -1px 1px rgba(255,255,255,0.5); + color: #3a2d05; + cursor: pointer; + transition: transform 0.06s ease, box-shadow 0.06s ease; +} +.globalplayer-btn svg { width: 16px; height: 16px; } +.globalplayer-btn:active { + transform: translateY(2px); + box-shadow: + inset 0 4px 8px rgba(0,0,0,0.6), + inset 0 -1px 1px rgba(255,255,255,0.3); +} +.globalplayer-btn-text { + width: auto; + padding: 0 12px; + font-size: 10px; + font-weight: bold; + letter-spacing: 1px; + white-space: nowrap; +} + +/* Stereo FFT-style bar graph - left channel above the midline, right + channel below it, drawn directly onto a small canvas. */ +.globalplayer-eq { + flex-shrink: 0; + width: 60px; + height: 32px; + background: #000; + border: 1px solid #333; + border-radius: 4px; +} + +/* Playlist dropdown - its own independent sliding strip directly below + the player band, NOT nested inside it: the band's own overflow:hidden + (needed for its slide animation) would otherwise clip anything + positioned to pop out below it, which is why this was invisible + before despite the open/close class toggling correctly in JS. */ +.globalplayer-queue { + max-height: 0; + overflow: hidden; + background: var(--bond-panel-2); + border-bottom: 1pt solid var(--bond-line); + transition: max-height 0.25s ease; +} +.globalplayer-queue.globalplayer-queue-open { + max-height: 240px; + overflow-y: auto; +} +.globalplayer-queue-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin: 6px 10px; + padding: 8px 14px; + border: 1px solid var(--bond-line); + border-radius: 8px; + background: rgba(255,255,255,0.03); + color: var(--bond-text-dim); +} +.globalplayer-queue-name { + font-size: 18pt; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.globalplayer-queue-left { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 0; +} +.globalplayer-queue-handle { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + color: var(--bond-text-dim); + cursor: grab; +} +.globalplayer-queue-handle svg { + width: 14px; + height: 14px; +} +.globalplayer-queue-item.globalplayer-queue-dragging { + opacity: 0.4; +} +.globalplayer-queue-item.globalplayer-queue-dragover { + border-top: 2px solid var(--bond-gold-bright); +} +.globalplayer-queue-right { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 8px; +} +.globalplayer-queue-duration { + flex-shrink: 0; + font-size: 9pt; + opacity: 0.75; +} +.globalplayer-queue-remove { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + margin: 0; + border-radius: 6px; + border: 1px solid var(--bond-line); + background: linear-gradient(180deg, #1c1c24, #131318); + color: var(--bond-text-dim); + cursor: pointer; + transition: box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease, color 0.2s ease; +} +.globalplayer-queue-remove svg { + width: 12px; + height: 12px; + display: block; +} +.globalplayer-queue-remove:hover { + border-color: var(--bond-red); + color: var(--bond-red); + box-shadow: 0 0 0.5em rgba(161,58,58,0.35); + background: linear-gradient(180deg, #24242e, #1a1a20); +} +.globalplayer-queue-item.globalplayer-queue-current { + color: var(--bond-gold-bright); + font-weight: 600; + border-color: var(--bond-gold); + background: rgba(212,175,55,0.08); +} + +/* Tactile feedback for clicking a song to queue it: an immediate flash + on the row, plus a lasting checkmark so it is clear at a glance which + songs are already in the queue. */ +/* Scoped to the name span, not the row itself: the row is a flex + container with justify-content:space-between, and a ::before on it + directly becomes its own flex item - with 3 items instead of 2, the + name ends up looking centered between the checkmark and the duration/ + remove group instead of staying left-aligned. Putting it on the name + span keeps it as ordinary inline content inside that one flex item. */ +.song-queued .music-song-name::before { + content: "\2713 "; + color: var(--bond-gold-bright); +} +.song-queued-flash { + animation: songQueuedFlash 0.6s ease; +} +@keyframes songQueuedFlash { + 0% { background: var(--bond-gold-bright); color: var(--bond-bg); } + 100% { background: transparent; } +} + +/* Song-picker rows (music/jukebox subpages) - same boxed, rounded-corner + look as the playlist dropdown's own entries, for visual consistency + between "songs you can pick" and "what's in the queue". */ +.music-song-row { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 10px; + margin: 4px 0; + padding: 5px 10px; + border: 1px solid var(--bond-line); + border-radius: 8px; + background: rgba(255,255,255,0.03); + color: var(--bond-text-dim); + cursor: pointer; +} +.music-song-row:hover { + border-color: var(--bond-gold); + background: rgba(255,255,255,0.06); +} +.music-song-name { + font-size: 12pt; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.music-song-duration { + flex-shrink: 0; + font-size: 8pt; + opacity: 0.75; +} + +/* Volume slider - a small gold knob riding a thin gold-tinted track */ +.globalplayer-volume { + -webkit-appearance: none; + appearance: none; + flex-shrink: 0; + width: 70px; + height: 4px; + border-radius: 2px; + background: var(--bond-line); + outline: none; + cursor: pointer; +} +.globalplayer-volume::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 14px; + height: 14px; + border-radius: 50%; + background: radial-gradient(circle at 35% 30%, #fff6d0 0%, #d4af37 55%, #6b5313 100%); + border: 1px solid #5a4410; + box-shadow: 0 1px 2px rgba(0,0,0,0.6); + cursor: pointer; +} +.globalplayer-volume::-moz-range-track { + height: 4px; + border-radius: 2px; + background: var(--bond-line); +} +.globalplayer-volume::-moz-range-thumb { + width: 14px; + height: 14px; + border-radius: 50%; + background: radial-gradient(circle at 35% 30%, #fff6d0 0%, #d4af37 55%, #6b5313 100%); + border: 1px solid #5a4410; + box-shadow: 0 1px 2px rgba(0,0,0,0.6); + cursor: pointer; +} diff --git a/jetc/barant-site/assets/images/spa.js b/jetc/barant-site/assets/images/spa.js index 9778162..98dd678 100644 --- a/jetc/barant-site/assets/images/spa.js +++ b/jetc/barant-site/assets/images/spa.js @@ -1,6 +1,7 @@ (function() { var content = document.getElementById('content'); var form = document.getElementById('mainform'); + var loadingIndicator = document.getElementById('spa-loading-indicator'); // 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 @@ -220,6 +221,12 @@ function navigate(url, push) { var sep = url.indexOf('?') === -1 ? '?' : '&'; content.classList.add('spa-fading'); + // Opacity transition alone (see the CSS) means a fetch that resolves + // quickly never gets the indicator past a barely-visible opacity + // before this removes the class again - exactly the "do not flash + // this for a fast load" behavior wanted, with no separate delay + // timer needed to achieve it. + if (loadingIndicator) loadingIndicator.classList.add('spa-loading-visible'); Promise.all([ fetch(url + sep + '_fragment=1', { credentials: 'same-origin' }).then(function(r) { return r.text(); }), delay(180) @@ -233,6 +240,7 @@ syncActiveTab(finalUrl.split('?')[0]); } content.classList.remove('spa-fading'); + if (loadingIndicator) loadingIndicator.classList.remove('spa-loading-visible'); }); } @@ -254,6 +262,7 @@ var subpage = fd.get('SUBPAGE') || ''; var url = formPrefix + '/' + page + (subpage ? '/' + subpage : ''); content.classList.add('spa-fading'); + if (loadingIndicator) loadingIndicator.classList.add('spa-loading-visible'); Promise.all([ fetch(url + '?_fragment=1', { method: 'POST', @@ -271,6 +280,7 @@ syncActiveTab(finalUrl.split('?')[0]); } content.classList.remove('spa-fading'); + if (loadingIndicator) loadingIndicator.classList.remove('spa-loading-visible'); }); }); } @@ -279,6 +289,24 @@ navigate(window.location.pathname + window.location.search, false); }); + // Instant visual feedback on a side-menu click - without this, the + // active-item highlight only updated once applyResponse() swapped in + // the new #_spa_panelmenu markup after the fetch finished, which read + // as unresponsive (the click did nothing visible until the whole + // subpage had already loaded). That server-rendered swap is still the + // authoritative source of truth and will correct this on arrival (e.g. + // if a click ends up redirected somewhere else entirely), so this is + // purely an optimistic bridge, not a second source of truth. + if (panelMenu) { + panelMenu.addEventListener('click', function(e) { + var a = e.target.closest('a'); + if (!a || !panelMenu.contains(a)) return; + var current = panelMenu.querySelector('.submenuitem-active'); + if (current) current.classList.remove('submenuitem-active'); + a.classList.add('submenuitem-active'); + }); + } + // 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 diff --git a/jetc/barant-site/src/SIPPHONE.tag b/jetc/barant-site/src/SIPPHONE.tag new file mode 100644 index 0000000..2baf8c5 --- /dev/null +++ b/jetc/barant-site/src/SIPPHONE.tag @@ -0,0 +1,421 @@ + + + + +select id, exten, secret from phones.extensions where user_id=$[_login_id] and secret is not null + + +
+ +
◆ B · A · R · A · N · T ◆
+ +
+
$[.exten]
+
OFFLINE
+
+ +
+ + +
+ +
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
*
+
0
+
#
+
+ +
+
CALL
+
VIDEO: OFF
+
+ +
+
+ + +
+ +
diff --git a/jetc/barant-site/src/account-info.subpage b/jetc/barant-site/src/account-info.subpage new file mode 100644 index 0000000..16b5749 --- /dev/null +++ b/jetc/barant-site/src/account-info.subpage @@ -0,0 +1,83 @@ + + + select * from users where id=$[_login_id] + + + + + + +
Name:$[.name]
Company Name:$[.company_name]
E-Mail:$[.email]
+ +
+ +select * from user_messages where user_id=$[_login_id]$[sqlerror] + + + + + + + + + +
$[.message]$[.link]
+
+
+ +select * + from virtual_machines + join products on virtual_machines.sku=products.sku + where user_id=$[_login_id] + + + + + +
+
+
Ubuntu Server 22.04 LTE +
barant.com +
IP: 74.80.233.78
+ +
+
+
3 days 2:10:33

34% of 40.2GB
+
+
+ +
+ +
+
+ +select * + from files + join products on files.sku=products.sku + where user_id=$[_login_id] + + +

Content that has been purchased and can be downloaded is displayed +below. Click on the Download button to begin downloading your files.

+ + + + + + + + + +
+ + + $[.description] + +
$[license_name]
$[license_key]
+
+
Download
+
+
+ +
+
diff --git a/jetc/barant-site/src/account-invoices.subpage b/jetc/barant-site/src/account-invoices.subpage new file mode 100644 index 0000000..c45eef6 --- /dev/null +++ b/jetc/barant-site/src/account-invoices.subpage @@ -0,0 +1,18 @@ + + +select user_id, status from invoices where id=$[@_invoice_id] + + +
+ + +
+
+
+
+
+ + select id as X,id as 'Invoice ID',date_format(created, '%b %D %Y') as 'Date',amount as 'Amount',status as 'Status' from invoices where user_id=$[_login_id] order by created desc + +
+
diff --git a/jetc/barant-site/src/account-phones.subpage b/jetc/barant-site/src/account-phones.subpage new file mode 100644 index 0000000..84fd9ac --- /dev/null +++ b/jetc/barant-site/src/account-phones.subpage @@ -0,0 +1,4 @@ + + + + diff --git a/jetc/barant-site/src/account.menu b/jetc/barant-site/src/account.menu new file mode 100644 index 0000000..8cb4cdb --- /dev/null +++ b/jetc/barant-site/src/account.menu @@ -0,0 +1,17 @@ + + + ACCOUNT INFORMATION
+ + ACCOUNT INFORMATION
+
+ + PHONES
+ + PHONES
+
+ + INVOICES
+ + INVOICES
+
+
diff --git a/jetc/barant-site/src/account.page b/jetc/barant-site/src/account.page index 1689e47..e432b8a 100755 --- a/jetc/barant-site/src/account.page +++ b/jetc/barant-site/src/account.page @@ -4,108 +4,16 @@ - - - select * from users where id=$[_login_id] - - - - - - -
Name:$[.name]
Company Name:$[.company_name]
E-Mail:$[.email]
- -
- -select * from user_messages where user_id=$[_login_id]$[sqlerror] - - - - - - - - - -
$[.message]$[.link]
-
-
- - select * - from virtual_machines - join products on virtual_machines.sku=products.sku - where user_id=$[_login_id] - - - - - -
-
-
Ubuntu Server 22.04 LTE -
barant.com -
IP: 74.80.233.78
- -
-
-
3 days 2:10:33

34% of 40.2GB
-
-
- -
- -
-
- - - - select * - from files - join products on files.sku=products.sku - where user_id=$[_login_id] - - -

Content that has been purchased and can be downloaded is displayed -below. Click on the Download button to begin downloading your files.

- - - - - - - - - -
- - - $[.description] - -
$[license_name]
$[license_key]
-
-
Download
-
-
- -
-
- - -
-
- - select id as X,id as 'Invoice ID',date_format(created, '%b %D %Y') as 'Date',amount as 'Amount',status as 'Status' from invoices where user_id=$[_login_id] - -
- -
- - -
-
-
-
- - +
+ + + + + + +

That account section doesn't exist.

+
+
+
diff --git a/jetc/barant-site/src/admin-email.subpage b/jetc/barant-site/src/admin-email.subpage index 5c95d9c..949d811 100644 --- a/jetc/barant-site/src/admin-email.subpage +++ b/jetc/barant-site/src/admin-email.subpage @@ -93,6 +93,16 @@ 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's own display:inline-flex is a plain class selector - + same specificity as the browser's own [hidden]{display:none} UA + default, and an author rule always wins that tie regardless of + source order - confirmed as a real, previously-shipped bug on a + different site this same session (a hidden modal that stayed + visible for the exact same reason). Without this, the empty-folder + button's `hidden` attribute would be set correctly but silently + ignored, leaving it visible on every folder instead of only + Spam/Trash. */ + .icon-btn[hidden] { display: none; } .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); @@ -118,12 +128,14 @@
$[@_imap_user] + + @@ -135,4 +147,4 @@
- + diff --git a/jetc/barant-site/src/admin-invoice.subpage b/jetc/barant-site/src/admin-invoice.subpage new file mode 100644 index 0000000..bc6d628 --- /dev/null +++ b/jetc/barant-site/src/admin-invoice.subpage @@ -0,0 +1,46 @@ + +
+
Edit Invoice
+
+ + + +
+ diff --git a/jetc/barant-site/src/admin-invoices.subpage b/jetc/barant-site/src/admin-invoices.subpage index d11d89b..8d1f514 100644 --- a/jetc/barant-site/src/admin-invoices.subpage +++ b/jetc/barant-site/src/admin-invoices.subpage @@ -1,5 +1,120 @@

Invoices

- -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 - +
+ + +
+ + + + + + + + + + + + + select invoices.id,user_id,name,invoices.created,amount,invoices.status from invoices join users on invoices.user_id=users.id + + + + + + + + + + + + +
iduser_idnamecreatedamountstatus
Open$[.id]$[.user_id]$[.name]$[.created]$[.amount]$[.status]
+ diff --git a/jetc/barant-site/src/admin-mailaccounts.subpage b/jetc/barant-site/src/admin-mailaccounts.subpage new file mode 100644 index 0000000..7728820 --- /dev/null +++ b/jetc/barant-site/src/admin-mailaccounts.subpage @@ -0,0 +1,98 @@ +

Mail Accounts

+
+ + +
+ + + + + + + + select id,email,host,created from email_accounts where user_id=$[@_login_id] order by email + + + + + + + + +
E-MailServerAdded
$[.email]$[.host]$[.created]
+

Saved accounts automatically reconnect the next time you log in.

+ diff --git a/jetc/barant-site/src/admin.menu b/jetc/barant-site/src/admin.menu index 332172d..c99e760 100755 --- a/jetc/barant-site/src/admin.menu +++ b/jetc/barant-site/src/admin.menu @@ -1,8 +1,29 @@ - EMAIL
- INVOICES
- PRODUCTS
- USERS
+ + EMAIL
+ + EMAIL
+
+ + INVOICES
+ + INVOICES
+
+ + PRODUCTS
+ + PRODUCTS
+
+ + USERS
+ + USERS
+
+ + MAIL ACCOUNTS
+ + MAIL ACCOUNTS
+
diff --git a/jetc/barant-site/src/admin.page b/jetc/barant-site/src/admin.page index ec239ab..cf96f5b 100755 --- a/jetc/barant-site/src/admin.page +++ b/jetc/barant-site/src/admin.page @@ -2,10 +2,18 @@ ADMINISTRATION - + +

Requested page is not found.

+ + +
- + +

Requested page is not found.

+ + +
@@ -14,6 +22,13 @@ + + +

Requested page is not found.

+ + +
+

Requested page is not found.

@@ -28,18 +43,27 @@
- + +

Requested page is not found.

+ + +
+ +

Requested page is not found.

+

Select an item from the menu.

+
+
diff --git a/jetc/barant-site/src/admin_invoice.page b/jetc/barant-site/src/admin_invoice.page deleted file mode 100755 index 84e791e..0000000 --- a/jetc/barant-site/src/admin_invoice.page +++ /dev/null @@ -1,11 +0,0 @@ - -INVOICE - - - - - -
-
Edit Invoice
-
-
\ No newline at end of file diff --git a/jetc/barant-site/src/adminmenu.section b/jetc/barant-site/src/adminmenu.section index b50b93a..3a0434d 100755 --- a/jetc/barant-site/src/adminmenu.section +++ b/jetc/barant-site/src/adminmenu.section @@ -1,4 +1,4 @@
Modular 1 Presets
-
Invoices
+
Invoices
\ No newline at end of file diff --git a/jetc/barant-site/src/barant.tag b/jetc/barant-site/src/barant.tag index 75766b9..b6796cf 100755 --- a/jetc/barant-site/src/barant.tag +++ b/jetc/barant-site/src/barant.tag @@ -95,7 +95,7 @@
- + $[sqlerror] @@ -117,7 +117,7 @@ diff --git a/jetc/barant-site/src/copy_invoice.jet b/jetc/barant-site/src/copy_invoice.jet new file mode 100644 index 0000000..54754c7 --- /dev/null +++ b/jetc/barant-site/src/copy_invoice.jet @@ -0,0 +1,29 @@ +#api POST /admin/copy-invoice + + + + + +
+ +{"ok":false,"error":"Not authorized."} + + +{"ok":false,"error":"Please select an invoice to copy."} + +select * from invoices where id=$[source_invoice_id] + +insert into invoices set user_id=$[.user_id], created=now(), amount=$[.amount], status="OPEN" +select last_insert_id() as new_invoice_id + +insert into invoices_detail (invoice_id,sku,sku_suffix,quantity,description,text,price,total,tax_amount,type) + select $[new_invoice_id],sku,sku_suffix,quantity,description,text,price,total,tax_amount,type + from invoices_detail where invoice_id=$[source_invoice_id] + +{"ok":true,"invoice_id":$[new_invoice_id]} + +{"ok":false,"error":"Invoice not found."} + + + + diff --git a/jetc/barant-site/src/create_invoice.jet b/jetc/barant-site/src/create_invoice.jet new file mode 100644 index 0000000..93b4d63 --- /dev/null +++ b/jetc/barant-site/src/create_invoice.jet @@ -0,0 +1,21 @@ +#api POST /admin/create-invoice + + + + + +
+ +{"ok":false,"error":"Not authorized."} + + +{"ok":false,"error":"Please select a user."} + +insert into invoices set user_id=$[user_id], created=now(), amount=0.00, status="OPEN" +select last_insert_id() as new_invoice_id + + +{"ok":true,"invoice_id":$[new_invoice_id]} + + + diff --git a/jetc/barant-site/src/editinvoice.page b/jetc/barant-site/src/editinvoice.page old mode 100755 new mode 100644 index 6bc73de..7e35c4c --- a/jetc/barant-site/src/editinvoice.page +++ b/jetc/barant-site/src/editinvoice.page @@ -3,9 +3,6 @@ @@ -169,37 +177,83 @@ function addLine(qty,skuid,descr,text,prc,tax,tot,typ) { -

Invoice $[_invoice_id]

-select * from invoices where id=$[_invoice_id] - -
- -
- Open + Open Account - -
-
- select * from invoices_detail where invoice_id=$[_invoice_id] +select i.id as invnumber, + u.name as name, + u.company_name as company_name, + u.email as email, + date_format(i.created,'%m/%e/%Y') as invdate, + i.status as status, + i.user_id as user_id + from invoices as i + join users as u on i.user_id=u.id + where i.id=$[@_invoice_id] + +
+ + + +
+
Invoice: $[.invnumber]
+
$[.status]
+
+
+ + + + + + +
Account: + +
Company Name:$[.company_name]
E-Mail:$[.email]
Invoice Date:$[.invdate]
+ + + + + + + + + + + + + + + + + +
QtyItem / DescriptionUnit PriceTaxAmount
Invoice Total:
+ select * from invoices_detail where invoice_id=$[@_invoice_id] + + + + + addLine('$[.quantity]',$[detail_sku_js],$[detail_description_js],$[detail_text_js],'$[.price]','$[.tax_amount]','$[.total]',$[detail_type_js]); + - -
+
+ +
- +
Update Invoice
- +
+ +
Invoice not found.
+

diff --git a/jetc/barant-site/src/email.jet b/jetc/barant-site/src/email.jet index 8bc4209..a577e43 100644 --- a/jetc/barant-site/src/email.jet +++ b/jetc/barant-site/src/email.jet @@ -4,6 +4,7 @@ #api POST /email/message #api POST /email/flag #api POST /email/delete +#api POST /email/emptyfolder #api POST /email/trash #api POST /email/folders #api POST /email/search @@ -12,10 +13,15 @@ #api POST /email/switchaccount
+ + + - - + + + + @@ -24,7 +30,6 @@ {"ok":false,"error":"$[_imaperr]"} - @@ -36,22 +41,25 @@ + {"ok":true,"active":""} + - + + {"ok":true,"active":"$[_next_user]"} + {"ok":true,"active":""} - @@ -63,7 +71,7 @@ - + @@ -71,67 +79,82 @@ {"ok":false,"error":"$[_imaperr]"} - - + {"ok":false,"error":"$[_imaperr]"} - - + {"ok":false,"error":"$[_imaperr]"} - - + {"ok":false,"error":"$[_imaperr]"} - + + + + + + + + + + + + + + + + +{"ok":false,"error":"$[_imaperr]"} + + +{"ok":false,"error":"Only the Spam and Trash folders can be emptied."} + + - + {"ok":false,"error":"$[_imaperr]"} - - + {"ok":false,"error":"$[_imaperr]"} - - + {"ok":false,"error":"$[_imaperr]"} - @@ -139,7 +162,6 @@ {"ok":false,"error":"$[_smtperr]"} - @@ -147,11 +169,13 @@ + {"ok":false,"error":"That account is not connected."} - + + {"ok":true} @@ -159,7 +183,6 @@ {"ok":false,"error":"$[_switcherr]"} - {"ok":false,"error":"Unknown route."} @@ -170,6 +193,7 @@ + diff --git a/jetc/barant-site/src/invoice.method b/jetc/barant-site/src/invoice.method index 7f20c6d..29d58d2 100755 --- a/jetc/barant-site/src/invoice.method +++ b/jetc/barant-site/src/invoice.method @@ -5,54 +5,53 @@ date_format(i.created,'%m/%e/%Y') as invdate, i.status as status, i.amount as amount - from invoices as i + from invoices as i join users as u on i.user_id=u.id - where i.id=$[_invoice_id] + where i.id=$[@_invoice_id] -
-
-
- Invoice: $[.invnumber] $[.status] -
-
-
-
Name: $[.name]
-
Company Name: $[.company_name]
-
E-Mail $[.email]
-
Invoice Date: $[.invdate]
-
-
-select * from invoices_detail where invoice_id=$[_invoice_id] - - - - - + +
+
QtyDescriptionUnit PriceAmount
+ + +
+
Invoice: $[.invnumber]
+
$[.status]
+
+
+ + + + + +
Name:$[.name]
Company Name:$[.company_name]
E-Mail:$[.email]
Invoice Date:$[.invdate]
+select * from invoices_detail where invoice_id=$[@_invoice_id] + + + + + - - - - + - - - + + + - - - select * from payments where invoice_id=$[_invoice_id] - - - - - - -
QtyDescriptionUnit PriceAmount
+
$[quantity] + $[description]
$[text]
+ $[price]$[total]$[total]
Invoice Total:$[amount]
Invoice Total:$[amount]

Credit Card: $[.card_brand] ending in $[.last_4_digits] expires on $[.exp_month]/$[.exp_year]$[.amount]
-
+
+select * from payments where invoice_id=$[@_invoice_id] + +
+ Credit Card: $[.card_brand] ending in $[.last_4_digits] expires $[.exp_month]/$[.exp_year] +
+
diff --git a/jetc/barant-site/src/jukebox.page b/jetc/barant-site/src/jukebox.page deleted file mode 100755 index d3013f5..0000000 --- a/jetc/barant-site/src/jukebox.page +++ /dev/null @@ -1,67 +0,0 @@ - -JUKEBOX - - - -
- -
-
Click on a title below
- -
- - -
$[file]
- -
-
-
- -
- - -
- - -
$[file]
- -
-
-
-
- - -
-
diff --git a/jetc/barant-site/src/login.method b/jetc/barant-site/src/login.method index 66753b9..33731de 100755 --- a/jetc/barant-site/src/login.method +++ b/jetc/barant-site/src/login.method @@ -14,7 +14,20 @@ $[login_type] $[login_uuid] update users set last_login=now() where id=$[login_id] - + select group_concat(to_base64(host),':',to_base64(email),':',to_base64(password) order by id separator ';') as accounts_list, + (select host from email_accounts where user_id=$[login_id] order by id limit 1) as first_host, + (select email from email_accounts where user_id=$[login_id] order by id limit 1) as first_email, + (select password from email_accounts where user_id=$[login_id] order by id limit 1) as first_password + from email_accounts where user_id=$[login_id] + + + + + + + + + diff --git a/jetc/barant-site/src/mail_invoice.jet b/jetc/barant-site/src/mail_invoice.jet new file mode 100644 index 0000000..eecc433 --- /dev/null +++ b/jetc/barant-site/src/mail_invoice.jet @@ -0,0 +1,26 @@ +#api POST /admin/mail-invoice + + + + + +
+ +{"ok":false,"error":"Not authorized."} + + +{"ok":false,"error":"No invoice selected."} + + +{"ok":false,"error":"Please enter an e-mail address."} + + + +{"ok":true} + +{"ok":false,"error":"$[_mailerr]"} + + + + + diff --git a/jetc/barant-site/src/mailaccounts.jet b/jetc/barant-site/src/mailaccounts.jet new file mode 100644 index 0000000..87b3670 --- /dev/null +++ b/jetc/barant-site/src/mailaccounts.jet @@ -0,0 +1,43 @@ +#api POST /admin/add-mail-account +#api POST /admin/remove-mail-account + + + + + +
+ +{"ok":false,"error":"Not authorized."} + + + +{"ok":false,"error":"Please enter a mail server."} + + +{"ok":false,"error":"Please enter an e-mail address."} + + +{"ok":false,"error":"Please enter a password."} + + + + +insert into email_accounts (user_id,email,host,password) values ($[@_login_id],'$[mail_email]','$[mail_host]','$[mail_password]') + on duplicate key update host='$[mail_host]', password='$[mail_password]' +{"ok":true} + +{"ok":false,"error":"Could not connect with those credentials: $[_imaperr]"} + + + + + + +{"ok":false,"error":"No account specified."} + +delete from email_accounts where id=$[account_id] and user_id=$[@_login_id] +{"ok":true} + + + + diff --git a/jetc/barant-site/src/main-ianslibrary.subpage b/jetc/barant-site/src/main-ianslibrary.subpage new file mode 100644 index 0000000..0784fb1 --- /dev/null +++ b/jetc/barant-site/src/main-ianslibrary.subpage @@ -0,0 +1,7 @@ + + +
+
+ diff --git a/jetc/barant-site/src/main-library.subpage b/jetc/barant-site/src/main-library.subpage new file mode 100644 index 0000000..855479a --- /dev/null +++ b/jetc/barant-site/src/main-library.subpage @@ -0,0 +1,7 @@ + + +
+
+ diff --git a/jetc/barant-site/src/main-music.subpage b/jetc/barant-site/src/main-music.subpage new file mode 100644 index 0000000..7af539f --- /dev/null +++ b/jetc/barant-site/src/main-music.subpage @@ -0,0 +1,7 @@ + + +
+
+ diff --git a/jetc/barant-site/src/main-overview.subpage b/jetc/barant-site/src/main-overview.subpage new file mode 100644 index 0000000..4dfcbb8 --- /dev/null +++ b/jetc/barant-site/src/main-overview.subpage @@ -0,0 +1,23 @@ + + +
+

We are BARANT Technology Group. Contact us for information on how we can + help you with your technical objectives.

+ +

Modular1 VST Synthesizer

+

Check out our Modular1 VST3 +Synthesizer If you appreciate the art of modular analog +synthesizers then you will really enjoy the Modular1 with all the +features of an analog modular synthesizer put into a single VST3 +instrument. With the added features of polyphony and patches that +change instantaneously this synthesizer is truly a powerhouse in the +audio toolbox.

+ +

Excellent quality and technical performance provide a performance level +instrument for musicians or students who wish to learn sound wave +synthesis techniques or for musicians to enable that old analog sound +in their DAW recording system..

+

Checkout the product page for more information on the capabilities +of the Modular1 synthesizer.

+
+
diff --git a/jetc/barant-site/src/main.menu b/jetc/barant-site/src/main.menu index b2afa6e..6b9cfdc 100644 --- a/jetc/barant-site/src/main.menu +++ b/jetc/barant-site/src/main.menu @@ -1,7 +1,29 @@ + + + OVERVIEW
+ + OVERVIEW
+
+ + MUSIC
+ + MUSIC
+
+ + MATT'S LIBRARY
+ + MATT'S LIBRARY
+
+ + IAN'S LIBRARY
+ + IAN'S LIBRARY
+
+
SERVICES
- HOSTING
- SIP TRUNKING
+ HOSTING
+ SIP TRUNKING
OPEN SOURCE
CONTACT
diff --git a/jetc/barant-site/src/main.page b/jetc/barant-site/src/main.page index 6a9bcc3..92c7788 100755 --- a/jetc/barant-site/src/main.page +++ b/jetc/barant-site/src/main.page @@ -2,28 +2,16 @@ BARANT Technology Group - - -
-

We are BARANT Technology Group. Contact us for information on how we can - help you with your technical objectives.

- -

Modular1 VST Synthesizer

-

Check out our Modular1 VST3 -Synthesizer If you appreciate the art of modular analog -synthesizers then you will really enjoy the Modular1 with all the -features of an analog modular synthesizer put into a single VST3 -instrument. With the added features of polyphony and patches that -change instantaneously this synthesizer is truly a powerhouse in the -audio toolbox.

- -

Excellent quality and technical performance provide a performance level -instrument for musicians or students who wish to learn sound wave -synthesis techniques or for musicians to enable that old analog sound -in their DAW recording system..

-

Checkout the product page for more information on the capabilities -of the Modular1 synthesizer.

-
-
+
+ + + + + + +

That page doesn't exist.

+
+
+
diff --git a/jetc/barant-site/src/mainpagetemplate.jet b/jetc/barant-site/src/mainpagetemplate.jet index 4f7c2f4..3ce69f6 100644 --- a/jetc/barant-site/src/mainpagetemplate.jet +++ b/jetc/barant-site/src/mainpagetemplate.jet @@ -72,7 +72,7 @@ $[_BODY] BARANT Technology Group - + @@ -88,13 +88,753 @@ $[_BODY]
- -
-
BARANT TECHNOLOGY GROUP
-
+
+
+
+ + +
+
+ +
+
+ +
+
+
+
RANDOM
+
Not Playing
+
+
+ 100$[@_player_volume]" title="Volume" oninput="__playerSetVolume(this.value);" onchange="__playerPersistVolume(this.value);"> + +
RANDOM
+ +
+ +
+ @@ -115,6 +855,7 @@ $[_BODY]
+
Retrieving content...
@@ -131,7 +872,7 @@ $[_BODY]
No transfers yet.
- + diff --git a/jetc/barant-site/src/menu.bar b/jetc/barant-site/src/menu.bar index 59b768d..ca1406f 100755 --- a/jetc/barant-site/src/menu.bar +++ b/jetc/barant-site/src/menu.bar @@ -9,10 +9,7 @@   HOME SERVICES - HOSTING CREATE STORE - DOCUMENT MGMT - SIP TRUNKING VST WORDPRESS OPEN SOURCE @@ -21,16 +18,18 @@ select count(*) as count from cart_item where session_uuid='$[_session_id]' - CART + CART - ACCOUNT - ADMIN + ACCOUNT + + ADMIN + - LOGIN + LOGIN -   +   diff --git a/jetc/barant-site/src/modular1_patches.page b/jetc/barant-site/src/modular1_patches.page index b321db7..52e29e2 100755 --- a/jetc/barant-site/src/modular1_patches.page +++ b/jetc/barant-site/src/modular1_patches.page @@ -3,8 +3,8 @@ - -
- -
-
Click on a title below
- -
- - -
$[file]
- -
-
-
- -
- -
- diff --git a/jetc/barant-site/src/payinvoice.page b/jetc/barant-site/src/payinvoice.page index b0a5942..49db784 100755 --- a/jetc/barant-site/src/payinvoice.page +++ b/jetc/barant-site/src/payinvoice.page @@ -1,7 +1,7 @@ PAY INVOICE - select * from invoices where id=$[_invoice_id] @@ -10,8 +10,8 @@
diff --git a/jetc/barant-site/src/payment.method b/jetc/barant-site/src/payment.method index 7929de1..cea47d3 100755 --- a/jetc/barant-site/src/payment.method +++ b/jetc/barant-site/src/payment.method @@ -1,5 +1,5 @@ - -
Please enter your payment information:
-
-
- -
+
+
Card Details
+
+ +
Processing Payment — Please wait…
+
+
diff --git a/jetc/barant-site/src/phones.section b/jetc/barant-site/src/phones.section index eadd9e0..70edfcb 100755 --- a/jetc/barant-site/src/phones.section +++ b/jetc/barant-site/src/phones.section @@ -1,3 +1,5 @@ +select extensions.id as 'X', extensions.exten as 'Phone Number' from phones.extensions where extensions.user_id=$[@_login_id] + + + + diff --git a/jetc/qualityiron-site/src/job.page b/jetc/qualityiron-site/src/job.page new file mode 100644 index 0000000..620c823 --- /dev/null +++ b/jetc/qualityiron-site/src/job.page @@ -0,0 +1,268 @@ + +Quality Iron - Job + + + + +
+$[message] +
+
+ +select *, + date_format(convert_tz(jobs.created, 'UTC','America/Los_Angeles'), "%W, %M %D, %Y %l:%i%p") as created_date, + status.description as status, + customers.name as customer_name, + contractors.name as contractor_name + from jobs + left outer join customers on jobs.customer_id=customers.id + left outer join contractors on jobs.contractor_id=contractors.id + join status on status_id=status.id + where jobs.id=$[@_selected_job_id] +$[sqlerror] + +
JOB ($[.job_name] $[.job_number])
+
DOCUMENTS
+ + + +
+ + +
+ + + + + + + + + + + + + + +
+ + +
+

Job Addresses

+
+select * from address_types + + + +select *, + ja.id as job_address_id + from addresses a + join job_addresses ja on ja.address_id=a.id + join address_types at on ja.address_type=at.id + where ja.job_id=$[@_selected_job_id] + + + +
+ + + + + + + +
Address Type:$[.description]
Address 1:$[.address1]
Address 2:$[.address2]
City:$[.city]
State:$[.state]
Zip Code:$[.zip_code]
+
+
+ +

No address exists for this job.

+
+
+
+ +
+ + +
+

Job Contacts

+
+select * from contact_types + + + +select *, + jc.id as job_contact_id + from contacts c + join job_contacts jc on jc.contact_id=c.id + join contact_types ct on jc.contact_type=ct.id + where job_id=$[@_selected_job_id] + + + +
+ + + + + +
Contact Type:$[.description]
Name:$[.name]
Phone:$[.phone]
Email:$[.email]
+
+
+ +

No contact exists for this job.

+
+
+
+ +
+ +
+

Job Dates

+select *, + dt.id as date_type_id + from date_types dt + left outer join job_dates jd on jd.type=dt.id and jd.job_id=$[@_selected_job_id] + order by description$[sqlerror] + + + + + +
$[.description]:
Update Dates
+
+ +
+

Reviews

+select * + from job_reviews + where owner=$[@_login_id] + and job_id=$[@_selected_job_id] + order by review_date + + + + +
Review Date:$[.review_date]
Reason:$[.notation]
+
+ + + + +
Review Date:
Reason:
Set Review
+
+ +
+ + + +
+

Job Activity

+select *, + date_format(convert_tz(job_activity_log.created,'UTC','America/Los_Angeles'),'%W, %M %D, %Y at %l:%i:%s %p') as log_date, + users.name as user_name, + jobs.job_name as job_name, + jobs.id as job_id + from job_activity_log + join users on job_activity_log.user_id=users.id + join jobs on job_activity_log.job_id=jobs.id + where jobs.id = $[@_selected_job_id] + order by job_activity_log.created desc limit 15 +$[sqlerror] + + + + + + + + + + + + + + + +
DateUserJob NameActivity
$[.log_date]$[.user_name]$[.job_name]$[.text]
+
+ +
+ +
+
diff --git a/jetc/qualityiron-site/src/jobdocs.jet b/jetc/qualityiron-site/src/jobdocs.jet new file mode 100644 index 0000000..784ed36 --- /dev/null +++ b/jetc/qualityiron-site/src/jobdocs.jet @@ -0,0 +1,2 @@ +#api GET /jobdocs/download +Not authorized.select id,name,filename,mimetype from documents where id=$[id] and status='ACTIVE'
$[_filedata]Document not found.Unknown route. \ No newline at end of file diff --git a/jetc/qualityiron-site/src/jobdocs.page b/jetc/qualityiron-site/src/jobdocs.page new file mode 100644 index 0000000..4ec0e51 --- /dev/null +++ b/jetc/qualityiron-site/src/jobdocs.page @@ -0,0 +1,133 @@ + +Quality Iron - Job Documents + +select job_number, job_name from jobs where id=$[@_selected_job_id]$[sqlerror] +
JOB ($[.job_name] $[.job_number])
+
DOCUMENTS
+ +
+

Upload Document

+
+Category: + +
+ +
Drop files here, or click to choose files to upload
+
Loading…
+
+ + + +
+
diff --git a/jetc/qualityiron-site/src/jobdocs_api.jet b/jetc/qualityiron-site/src/jobdocs_api.jet new file mode 100644 index 0000000..bb15bef --- /dev/null +++ b/jetc/qualityiron-site/src/jobdocs_api.jet @@ -0,0 +1,84 @@ +#api POST /jobdocs/api/upload +#api POST /jobdocs/api/delete +#api POST /jobdocs/api/list + + + + + + +
+ +{"ok":false,"error":"Not authorized."} + + +{"ok":false,"error":"Name is required."} + + +{"ok":false,"error":"job_id is required."} + + + + select uuid() as u$[sqlerror] + + insert into documents + set name='$[x_name]', + subtype_id=$[subtype_id], + owner=$[@_login_id], + submitted_by=$[@_login_id], + submitted_date=now(), + job_id=$[job_id], + filename='$[storage_name]', + mimetype='$[x_mimetype]', + file_size=$[file_size], + status='ACTIVE'$[sqlerror] + select last_insert_id() as document_id$[sqlerror] + + +{"ok":true,"id":$[document_id],"name":"$[x_name]"} + + + + + +
+ +{"ok":false,"error":"Not authorized."} + + select id from documents where id=$[id] and status='ACTIVE'$[sqlerror] + + update documents set status='DELETED' where id=$[id]$[sqlerror] +{"ok":true} + +{"ok":false,"error":"Document not found."} + + + + +
+ +{"ok":false,"error":"Not authorized."} + + +{"ok":false,"error":"job_id is required."} + + select d.id, d.name, d.submitted_date, d.mimetype, d.file_size, + u.name as submitted_by_name, + t.description as type_description, + s.description as subtype_description + from documents d + join users u on d.submitted_by=u.id + left join subtype s on d.subtype_id=s.id + left join type t on s.type_id=t.id + where d.job_id=$[job_id] and d.status='ACTIVE' + order by t.description, s.description, d.submitted_date desc$[sqlerror] + +{"ok":true,"documents":[,{"id":$[.id],"name":"$[.name]","submitted_date":"$[.submitted_date]","mimetype":"$[.mimetype]","file_size":$[.file_size],"submitted_by":"$[.submitted_by_name]","type":"$[.type_description]","subtype":"$[.subtype_description]"}]} + + + +{"ok":false,"error":"Unknown route."} + + + + diff --git a/jetc/qualityiron-site/src/jobfolder.form b/jetc/qualityiron-site/src/jobfolder.form new file mode 100644 index 0000000..e862e3c --- /dev/null +++ b/jetc/qualityiron-site/src/jobfolder.form @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + +
Job Number: +style="background-color: #fcc;" + +type="text" name="jobnumber" value="$[jobnumber]" oninput="activate();">
Job Name:
Customer: + + + + + + +select * from customers order by name + + +
Contractor: + + + + + +select *,trim(name) as tname from contractors order by name + + + +
Contractor Job Id:
Assessor Parcel Number:
Insurance:
Status: + +
+ diff --git a/jetc/qualityiron-site/src/jobs.list b/jetc/qualityiron-site/src/jobs.list new file mode 100644 index 0000000..0038a8b --- /dev/null +++ b/jetc/qualityiron-site/src/jobs.list @@ -0,0 +1,104 @@ + + +
+

JOBS

+
+ + select *, + cu.name as customer, + co.name as contractor, + a.address1 as address, + c1.name as repname, + c2.name as supervisor, + c2.phone as supervisor_phone, + s.id as status_id + from jobs j + join status_users su on su.status_id=j.status_id and su.user_id=$[_login_id] + left outer join customers cu on j.customer_id=cu.id + left outer join contractors co on j.contractor_id=co.id + left outer join job_addresses ja on ja.job_id=j.id and ja.address_type=1 + left outer join addresses a on ja.address_id=a.id + left outer join job_contacts jc1 on jc1.job_id=j.id and jc1.contact_type=3 + left outer join contacts c1 on jc1.contact_id=c1.id + left outer join job_contacts jc2 on jc2.job_id=j.id and jc2.contact_type=1 + left outer join contacts c2 on jc2.contact_id=c2.id + join status s on j.status_id=s.id + where (job_number is not null or job_name is not null) + order by s.sequence,job_name$[sqlerror] + +
Jobs
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Job NumberJob NameContractorJob Site AddressQI RepSupervisorSupervisor Phone
$[.description]
$[.job_number]$[.job_name]$[.contractor]$[.address]$[.repname]$[.supervisor]$[.supervisor_phone]
+
+
+
+
diff --git a/jetc/qualityiron-site/src/jobs.page b/jetc/qualityiron-site/src/jobs.page new file mode 100644 index 0000000..4934050 --- /dev/null +++ b/jetc/qualityiron-site/src/jobs.page @@ -0,0 +1,102 @@ + +Quality Iron + + + +
+

JOBS

+
+ + select *, + cu.name as customer, + co.name as contractor, + a.address1 as address, + c1.name as repname, + c2.name as supervisor, + c2.phone as supervisor_phone + from jobs j + left outer join customers cu on j.customer_id=cu.id + left outer join contractors co on j.contractor_id=co.id + left outer join job_addresses ja on ja.job_id=j.id and ja.address_type=1 + left outer join addresses a on ja.address_id=a.id + left outer join job_contacts jc1 on jc1.job_id=j.id and jc1.contact_type=3 + left outer join contacts c1 on jc1.contact_id=c1.id + left outer join job_contacts jc2 on jc2.job_id=j.id and jc2.contact_type=1 + left outer join contacts c2 on jc2.contact_id=c2.id + join status on j.status_id=status.id + where (job_number is not null or job_name is not null) + order by job_name$[sqlerror] + +
Jobs
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Job NumberJob NameContractorJob Site AddressQI RepSupervisorSupervisor PhoneStatus
$[.job_number]$[.job_name]$[.contractor]$[.address]$[.repname]$[.supervisor]$[.supervisor_phone]$[.description]
+
+ +
+
+
diff --git a/jetc/qualityiron-site/src/login.bar b/jetc/qualityiron-site/src/login.bar new file mode 100644 index 0000000..2f828c5 --- /dev/null +++ b/jetc/qualityiron-site/src/login.bar @@ -0,0 +1,4 @@ + +
Welcome, $[_login_name]
+ Logout +
diff --git a/jetc/qualityiron-site/src/login.method b/jetc/qualityiron-site/src/login.method new file mode 100644 index 0000000..9dd2051 --- /dev/null +++ b/jetc/qualityiron-site/src/login.method @@ -0,0 +1,14 @@ +select * from users where name="$[username]" and secret="$[secret]" + + + + + + + + $[_login_id] + $[_login_name] + $[_login_email] + update users set last_login=now() where id=$[_login_id] + + diff --git a/jetc/qualityiron-site/src/login.page b/jetc/qualityiron-site/src/login.page new file mode 100644 index 0000000..5f9b6b0 --- /dev/null +++ b/jetc/qualityiron-site/src/login.page @@ -0,0 +1,20 @@ + +Quality Iron Login + +
+
Login
+ +
+
User Name: 
+
+
+
+
Password: 
+
+
+
+
Login
+
+
+
+
diff --git a/jetc/qualityiron-site/src/login.proc b/jetc/qualityiron-site/src/login.proc new file mode 100644 index 0000000..0f16ac3 --- /dev/null +++ b/jetc/qualityiron-site/src/login.proc @@ -0,0 +1,3 @@ + + + diff --git a/jetc/qualityiron-site/src/logout.proc b/jetc/qualityiron-site/src/logout.proc new file mode 100644 index 0000000..5ef7851 --- /dev/null +++ b/jetc/qualityiron-site/src/logout.proc @@ -0,0 +1,6 @@ + + + + + + diff --git a/jetc/qualityiron-site/src/mainpagetemplate.jet b/jetc/qualityiron-site/src/mainpagetemplate.jet new file mode 100644 index 0000000..4c96acd --- /dev/null +++ b/jetc/qualityiron-site/src/mainpagetemplate.jet @@ -0,0 +1,127 @@ +#api GET / +#api GET /{page} +#api GET /{page}/{subpage} +#api POST / +#api POST /{page} +#api POST /{page}/{subpage} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +$[_BODY] + + + + + + + + + + + + + +
+
+ + + + + + +Quality Iron Works + + + +
/$[SUBPAGE]" enctype="multipart/form-data"> + + + + + + + + +
+
+
+ +
+
+
+ +
+
+ + + +

Page not found.

+
+
+
+ +
+
+
+
+ + +
+
+
+ Transfers + Clear +
+
No transfers yet.
+
+
+ + + + + +
+ + + + + +

Page not found.

+
+
+ +
+
diff --git a/jetc/qualityiron-site/src/profile.page b/jetc/qualityiron-site/src/profile.page new file mode 100644 index 0000000..1bb3e75 --- /dev/null +++ b/jetc/qualityiron-site/src/profile.page @@ -0,0 +1,61 @@ + +Quality Iron - My Profile + +
+

MY PROFILE

+select * from users where id=$[@_login_id]$[sqlerror] + +
+ + + + + + + + + + + + + + + +
NameE-MailPhoneLast Login
Edit$[.name]$[.email]$[.phone]$[.last_login]
+
+
+
+ +
+

STATUS VALUES

+ select s.id as status_id, + s.description as description, + s.comment as comment, + su.id as status_user_id + from status s +left outer join status_users su on s.id=su.status_id and su.user_id=$[@_login_id] + where (su.user_id=$[@_login_id] or su.user_id is null) + order by sequence$[sqlerror] +
+ + + + + + + + + + + + + +
DescriptionComment
checked +>$[.description]$[.comment]
+
+Update +
+ +
+
diff --git a/jetc/qualityiron-site/src/roles.page b/jetc/qualityiron-site/src/roles.page new file mode 100644 index 0000000..8705790 --- /dev/null +++ b/jetc/qualityiron-site/src/roles.page @@ -0,0 +1,36 @@ + +Quality Iron - Roles + + +
+

USER ROLES

+select r.*, + (select count(*) from user_roles ur where ur.role_id=r.id) as count + from roles r + order by description$[sqlerror] + + + + + + + + + + + + + + + +
DescriptionUsed
+ + + +$[.count]Save
+

Add Role

+ +Add +
+
+
diff --git a/jetc/qualityiron-site/src/selectcontractor.proc b/jetc/qualityiron-site/src/selectcontractor.proc new file mode 100644 index 0000000..0d45284 --- /dev/null +++ b/jetc/qualityiron-site/src/selectcontractor.proc @@ -0,0 +1,3 @@ + +$[selected_contractor_id] + diff --git a/jetc/qualityiron-site/src/selectcustomer.proc b/jetc/qualityiron-site/src/selectcustomer.proc new file mode 100644 index 0000000..e82ebb1 --- /dev/null +++ b/jetc/qualityiron-site/src/selectcustomer.proc @@ -0,0 +1,3 @@ + +$[selected_customer_id] + diff --git a/jetc/qualityiron-site/src/selectjob.proc b/jetc/qualityiron-site/src/selectjob.proc new file mode 100644 index 0000000..b31369e --- /dev/null +++ b/jetc/qualityiron-site/src/selectjob.proc @@ -0,0 +1,3 @@ + +$[selected_job_id] + diff --git a/jetc/qualityiron-site/src/selectuser.proc b/jetc/qualityiron-site/src/selectuser.proc new file mode 100644 index 0000000..8484a5f --- /dev/null +++ b/jetc/qualityiron-site/src/selectuser.proc @@ -0,0 +1,3 @@ + +$[selected_user_id] + diff --git a/jetc/qualityiron-site/src/setreview.proc b/jetc/qualityiron-site/src/setreview.proc new file mode 100644 index 0000000..93da014 --- /dev/null +++ b/jetc/qualityiron-site/src/setreview.proc @@ -0,0 +1,9 @@ + + +insert into job_reviews + set job_id=$[@_selected_job_id], + review_date='$[review_date]', + notation='$[x_notation]', + owner=$[@_login_id] +$[sqlerror] + diff --git a/jetc/qualityiron-site/src/updatecontractor.proc b/jetc/qualityiron-site/src/updatecontractor.proc new file mode 100644 index 0000000..5d2496a --- /dev/null +++ b/jetc/qualityiron-site/src/updatecontractor.proc @@ -0,0 +1,18 @@ + + + + + +update contractors + set name='$[x_name]', + address1='$[x_address1]', + address2='$[x_address2]', + city='$[city]', + state='$[state]', + zip_code='$[zip_code]', + contact='$[contact]', + contact_phone='$[contact_phone]', + contact_email='$[contact_email]' + where id=$[@_selected_contractor_id] +$[sqlerror] + diff --git a/jetc/qualityiron-site/src/updatecustomer.proc b/jetc/qualityiron-site/src/updatecustomer.proc new file mode 100644 index 0000000..fb8ee47 --- /dev/null +++ b/jetc/qualityiron-site/src/updatecustomer.proc @@ -0,0 +1,8 @@ + + + +update customers + set name='$[x_name]' + where id=$[@_selected_customer_id] +$[sqlerror] + diff --git a/jetc/qualityiron-site/src/updatejobdates.proc b/jetc/qualityiron-site/src/updatejobdates.proc new file mode 100644 index 0000000..3fa07b9 --- /dev/null +++ b/jetc/qualityiron-site/src/updatejobdates.proc @@ -0,0 +1,18 @@ + + +select *, + dt.id as date_type_id + from date_types dt + left outer join job_dates jd on jd.type=dt.id and jd.job_id=$[@_selected_job_id] + + + + +delete from job_dates where job_id=$[@_selected_job_id] and type=$[.date_type_id] + +replace into job_dates set job_id=$[@_selected_job_id],type=$[.date_type_id],date='$[date$[.date_type_id]]',owner=$[@_login_id] + +$[sqlerror] + + + diff --git a/jetc/qualityiron-site/src/updatejobfolder.proc b/jetc/qualityiron-site/src/updatejobfolder.proc new file mode 100644 index 0000000..89b17be --- /dev/null +++ b/jetc/qualityiron-site/src/updatejobfolder.proc @@ -0,0 +1,173 @@ + + +select * from jobs where id=$[@_selected_job_id] + + + + + + + + + + + + +select * from jobs where job_number='$[x_jobnumber]' + + + + + + + + + + + + + + + + + + + + +insert into customers set name=trim("$[x_customer_name]"), created=now(),owner=$[@_login_id]$[sqlerror] +select last_insert_id() as customer_id$[sqlerror] + + + + + +insert into contractors set name=trim("$[x_contractor_name]"), created=now(),owner=$[@_login_id]$[sqlerror] +select last_insert_id() as contractor_id$[sqlerror] + + + +update jobs + set job_number=null'$[x_jobnumber]', + job_name=trim('$[x_job_name]'), + customer_id=$[customer_id], + created=now(), + status_id=$[status_id], + contractor_id=$[contractor_id], + contractor_job_id='$[x_contractor_job_id]', + apn='$[apn]', + insurance='$[insurance]' + where id=$[@_selected_job_id] +$[sqlerror] + + +insert into job_activity_log + set created=now(), + job_id=$[@_selected_job_id], + user_id=$[@_login_id], + message_id='JOB0002', + text='Job Number updated from "$[ox_job_number]" to "$[x_jobnumber]".', + url='/job?id=$[@_selected_job_id]' +$[sqlerror] + + + +insert into job_activity_log + set created=now(), + job_id=$[@_selected_job_id], + user_id=$[@_login_id], + message_id='JOB0003', + text='Job Name updated from "$[ox_job_name]" to "$[x_job_name]".', + url='/job?id=$[@_selected_job_id]' +$[sqlerror] + + + + + + +select * from customers where id=$[customer_id] + + + + + + +select * from customers where id=$[o_customer_id] + + + + + + + + +insert into job_activity_log + set created=now(), + job_id=$[@_selected_job_id], + user_id=$[@_login_id], + message_id='JOB0004', + text='Job Customer updated from "$[ox_customer_name]" to "$[x_customer_name]".', + url='/job?id=$[@_selected_job_id]' +$[sqlerror] + + +select * from contractors where id=$[contractor_id] + +select * from contractors where id=$[o_contractor_id] + +insert into job_activity_log + set created=now(), + job_id=$[@_selected_job_id], + user_id=$[@_login_id], + message_id='JOB0005', + text='Job Contractor updated from "$[ox_contractor_name]" to "$[x_contractor_name]".', + url='/job?id=$[@_selected_job_id]' +$[sqlerror] + + +insert into job_activity_log + set created=now(), + job_id=$[@_selected_job_id], + user_id=$[@_login_id], + message_id='JOB0006', + text='Job Contractor Job Id updated from "$[o_contractor_job_id]" to "$[x_contractor_job_id]".', + url='/job?id=$[@_selected_job_id]' +$[sqlerror] + + +insert into job_activity_log + set created=now(), + job_id=$[@_selected_job_id], + user_id=$[@_login_id], + message_id='JOB0009', + text='Assessor Parcel Number updated from "$[o_apn]" to "$[apn]".', + url='/job?id=$[@_selected_job_id]' +$[sqlerror] + + +insert into job_activity_log + set created=now(), + job_id=$[@_selected_job_id], + user_id=$[@_login_id], + message_id='JOB0007', + text='Job Insurance updated from "$[o_insurance]" to "$[insurance]".', + url='/job?id=$[@_selected_job_id]' +$[sqlerror] + + +select * from status where id=$[status_id] + +select * from status where id=$[o_status_id] + +insert into job_activity_log + set created=now(), + job_id=$[@_selected_job_id], + user_id=$[@_login_id], + message_id='JOB0008', + text='Job Status updated from "$[o_status]" to "$[x_status]".', + url='/job?id=$[@_selected_job_id]' +$[sqlerror] + + + + diff --git a/jetc/qualityiron-site/src/updaterole.proc b/jetc/qualityiron-site/src/updaterole.proc new file mode 100644 index 0000000..8f01860 --- /dev/null +++ b/jetc/qualityiron-site/src/updaterole.proc @@ -0,0 +1,4 @@ + + +update roles set description='$[x_description]' where id=$[selected_role_id]$[sqlerror] + diff --git a/jetc/qualityiron-site/src/updatestatus.proc b/jetc/qualityiron-site/src/updatestatus.proc new file mode 100644 index 0000000..2529997 --- /dev/null +++ b/jetc/qualityiron-site/src/updatestatus.proc @@ -0,0 +1,6 @@ + + + + +update status set description='$[x_description]', comment='$[x_comment]', sequence=$[x_sequence] where id=$[selected_status_id]$[sqlerror] + diff --git a/jetc/qualityiron-site/src/updatestatususer.proc b/jetc/qualityiron-site/src/updatestatususer.proc new file mode 100644 index 0000000..c697484 --- /dev/null +++ b/jetc/qualityiron-site/src/updatestatususer.proc @@ -0,0 +1,11 @@ + + +delete from status_users where user_id=$[@_login_id]$[sqlerror] +select * from status$[sqlerror] + + +insert into status_users set status_id=$[.id],user_id=$[@_login_id]$[sqlerror] + + + + diff --git a/jetc/qualityiron-site/src/updatesubtype.proc b/jetc/qualityiron-site/src/updatesubtype.proc new file mode 100644 index 0000000..0a940cb --- /dev/null +++ b/jetc/qualityiron-site/src/updatesubtype.proc @@ -0,0 +1,4 @@ + + +update subtype set description='$[x_description]' where id=$[selected_subtype_id]$[sqlerror] + diff --git a/jetc/qualityiron-site/src/updatetype.proc b/jetc/qualityiron-site/src/updatetype.proc new file mode 100644 index 0000000..bc37646 --- /dev/null +++ b/jetc/qualityiron-site/src/updatetype.proc @@ -0,0 +1,4 @@ + + +update type set description='$[x_description]' where id=$[selected_type_id]$[sqlerror] + diff --git a/jetc/qualityiron-site/src/updateuser.proc b/jetc/qualityiron-site/src/updateuser.proc new file mode 100644 index 0000000..9197fc7 --- /dev/null +++ b/jetc/qualityiron-site/src/updateuser.proc @@ -0,0 +1,13 @@ + + + + + update users + set name='$[x_username]', + secret='$[secret]', + email='$[x_email]', + phone='$[phone]', + allow_user_maint=if('$[allow_user_maint]'='Y','Y','N') + where id=$[@_selected_user_id] +$[sqlerror] + diff --git a/jetc/qualityiron-site/src/user.form b/jetc/qualityiron-site/src/user.form new file mode 100644 index 0000000..9a19786 --- /dev/null +++ b/jetc/qualityiron-site/src/user.form @@ -0,0 +1,14 @@ +User Name: + +Password: + +Verify Password: + +E-Mail: + +Phone: + +Grant Admin rights?: +checked +> diff --git a/jetc/qualityiron-site/src/users.page b/jetc/qualityiron-site/src/users.page new file mode 100644 index 0000000..9d4e413 --- /dev/null +++ b/jetc/qualityiron-site/src/users.page @@ -0,0 +1,36 @@ + +Quality Iron - Users + + +
+

USERS

+Create User +select *, + date_format(convert_tz(last_login,'UTC','America/Los_Angeles'), "%W, %M %D, %Y %l:%i%p") as last_login_date + from users order by name$[sqlerror] + +
Users
+
+ + + + + + + + + + + + + + + + + +
NameE-MailPhoneLast Login
Edit$[.name]$[.email]$[.phone]$[.last_login_date]
+
+
+
+
+
diff --git a/jetc/wcwc/wcwc.jet b/jetc/wcwc/wcwc.jet new file mode 100644 index 0000000..3b449c3 --- /dev/null +++ b/jetc/wcwc/wcwc.jet @@ -0,0 +1,1790 @@ +#api GET /wcwc +$[jetc_doctype] + + + + + + + + + + + + + WCWC Homepage » WEST COAST WING CHUN + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+
+
+
+
+
WEST COAST WING CHUN
+
+
+
+

Ip Man Wing Chun

+
+
+
+

Commit yourself to a higher standard.

+
+
+
+

What you’re taught—and how you’re taught—matters.

The effectiveness of your Wing Chun is shaped by the quality of your instruction.

 

+
+ +
+
+
+
+
+
+ Ip Man WIng Chun application training demonstrating structure, control, and practical technique at West Coast Wing Chun
+
+
+
+
+
+
+
+
+
+
+
+

Become a Student

+
+
+
+

Develop confidence, practical skill, and a deeper understanding of authentic Ip Man Wing Chun through structured, purposeful training.

+
+ +
+
+
+
+
+
+

Become an Instructor

+
+
+
+

Deepen your understanding of the art while developing the knowledge, skill, and confidence to teach others.
 

+
+ +
+
+
+
+
+
+

Become an Affiliate School

+
+
+
+

Join a network committed to quality instruction, professional development, and the long-term success of every affiliated school.

+
+ +
+
+
+
+
+
+
+
+
+
+ A serious Wing Chun Sifu watches his wing chun class
+
+
+
+
WEST COAST WING CHUN
+
+
+
+

What Makes WCWC Different

+
+
+
+

WCWC is not built around random techniques or disconnected drills. The system is taught through a structured curriculum that connects lineage, forms, principles, concepts, techniques, and practical application.

+
+
+
+
+
+
+
+
+ +
+ + +
+ +
+ +

+ + Lineage with Perspective +

+ +

+ Master Bryan Talbot’s training gives WCWC a rare depth of understanding across multiple expressions of Wing Chun, while preserving the sources and principles behind the art. +

+ +
+ +
+
+
+
+
+
+ +
+ + +
+ +
+ +

+ + Curriculum with Structure +

+ +

+ Students follow a clear progression from fundamentals through advanced training, with each level building on the last.

+ +
+ +
+
+
+
+
+
+ +
+ + +
+ +
+ +

+ + Application with Purpose +

+ +

+ Students learn how each movement connects to the forms, the principles behind it, and the way it is applied under pressure.

+ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+

TRAIN WITH WCWC

+
+
+
+
+
+
+
+
+

Find a West Coast Wing Chun School

+
+
+
+

Official WCWC schools follow the same structured curriculum and training standard under the direction of Master Bryan Talbot.

+
+
+
+
+
+
+
+

Long Beach, Ca

+
+
+
+

Located in Long Beach. Serving Lakewood, Signal Hill, Seal Beach, Los Alamitos, and the South Bay.

+
+ +
+ +
+
+
+
+
+
+
+

San Diego, Ca

+
+
+
+

Located near Clairemont and Kearny Mesa. Serving San Diego and surrounding communities.

+
+ +
+ +
+
+
+
+
+
+
+

Houston, TX

+
+
+
+

Located in Spring. Serving The Woodlands, Tomball, Klein, Conroe, and North Houston.

+
+ +
+ +
+
+
+
+
+
+
+

Phoenix, AZ

+
+
+
+

Located in GIlbert. Serving Phoenix, Mesa, Chandler, Queen Creek, and the East Valley.

+
+ +
+ +
+
+
+
+
+
+
+

Can’t find a school near you?

+

Train online or start a training group.

+
+
+
+
+
+
+
+
+
+
+
+

ONE WCWC STANDARD

+
+
+
+

Same System.
Same Standard. +

+
+
+
+

WCWC schools are connected by one organized curriculum, one training standard, and one shared goal: helping students understand Wing Chun as a complete, functional martial art.

+
+
+
+
+
+
+
+
+ +
+ + +
+ +
+ +

+ + One Curriculum +

+ +

+ Students progress through the same organized WCWC material, from fundamentals through advanced training.

+ +
+ +
+
+
+
+
+
+ +
+ + +
+ +
+ +

+ + One Standard +

+ +

+ Official WCWC schools follow a shared training standard under the direction of Master Bryan Talbot. +

+ +
+ +
+
+
+
+
+
+ +
+ + +
+ +
+ +

+ + One Path Forward +

+ +

+ Whether a student trains for self-defense, deeper skill, or future instructor development, the path is clear and structured. +

+ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+

TRAINING WITH A FUTURE

+
+
+
+

A Path for Serious Students and Future Instructors + +

+
+
+
+

WCWC is looking for serious students who want to help preserve, practice, and carry the art forward. New students build strong fundamentals, experienced practitioners can deepen their understanding, and committed students may continue into advanced training and instructor development. 

+
+
+
+

Interested in helping carry WCWC forward? Tell us about your training background, location, and goals.

+
+
+
+
+
+
+
+

The future of WCWC depends on students who are willing to train seriously, understand the system deeply, and help carry the art forward with integrity. 

+
+ +
+
+
+
+
+
+
+
+
+
+

IP MAN FAMILY LINEAGE

+
+
+
+

A Lineage with Depth +

+
+
+
+

WCWC preserves Ip Man family Wing Chun through a formal lineage connected to Ip Chun, Ip Ching, Ron Heimberger & Samuel Kwok. Bryan Talbot’s depth also comes from earlier lineage training under Ron Heimberger, whose Wing Chun background included Leung Ting and later Ip Ching family Wing Chun.

+
+
+
+

Ip Man → Ip Chun / Ip Ching →  Ron Heimberger/Samuel Kwok → Bryan Talbot → WCWC Instructors

+
+
+
+

After Ron Heimberger’s passing, Bryan continued deeper into Ip Man family Wing Chun through Samuel Kwok. This gave him rare experience with three complete versions of the Wing Chun system and shaped WCWC’s ability to teach the art with clarity, structure, and respect for its sources.

+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + +
+ +
+ +

+ + Form as Foundation +

+ +

+ Students learn the forms as the source of structure, position, movement, and principle.

+ +
+ +
+
+
+
+
+
+ +
+ + +
+ +
+ +

+ + Function Gives Meaning +

+ +

+ Movements are not practiced as empty shapes. Students learn what each action is for and how it works. +

+ +
+ +
+
+
+
+
+
+ +
+ + +
+ +
+ +

+ + Application with Understanding +

+ +

+

Students learn not only what to do, but why it works and when to apply it.

+ +
+ +
+
+
+
+
+
+
+
+
+
STRUCTURED TRAINING
+
+
+
+

A Curriculum Built from the Ground Up

+
+
+
+

As Master Bryan Talbot often says: form follows function. WCWC students do not learn disconnected techniques. Each level connects forms, principles, concepts, drills, partner work, and application so students understand where each movement comes from, what it is meant to do, and how to use it.

+
+
+
+
+
+
+
+
+
+
+
+
FUNCTIONAL WING CHUN
+
+
+
+

Wing Chun That Works

+
+
+
+

WCWC is built on the belief that Wing Chun works when it is understood and trained properly. Students are taught to develop structure, timing, sensitivity, control, and practical skill — not just memorized techniques.

+
+
+
+

Understanding matters because survival is not choreography.

+
+
+
+
+
+
+
+
+
+
+
+

BEYOND TECHNIQUE

+
+
+
+

Wing Chun Beyond Technique

+
+
+
+

In WCWC, Wing Chun is more than a set of techniques. Its concepts and principles reveal patterns that apply under pressure, in conflict, in decision-making, and in daily life. Structure, centerline, economy, timing, sensitivity, and balance are trained physically — but they also become ways to understand how we move through the world.

+
+
+
+
+
+
+
+
+ + +
+ +

+ + Structure +

+ +

+ Learning how to stand, move, and respond from a stable center — physically, mentally and emotionally.

+ +
+ +
+
+
+
+
+
+ + +
+ +

+ + Centerline +

+ +

+ Training attention, intention, and direction so energy is not wasted on what does not matter.

+ +
+ +
+
+
+
+
+
+ + +
+ +

+ + Sensitivity +

+ +

+ Developing the ability to feel pressure, adapt to change, and respond without becoming rigid or overwhelmed.

+ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
START TRAINING WITH WCWC
+
+
+
+

Find your Path into West Coast Wing Chun

+
+
+
+

Whether you are new to Wing Chun, returning to training, looking for a WCWC school, or interested in future instructor development, start with one inquiry. We’ll help direct you to the right next step.

+
+ +
+
+

Tell us where you are, your experience level, and your training goals.
.

+
+
+
+
+
+
+ + +
+ + +
+ + + + + +
+
+
+
+
+
+
+
+

Start Training with WCWC

+
+
+
+

Tell us where you are, your experience level, and your training goals. We’ll help direct you to the right WCWC school, training location, or next step.

+
+
+
+
+ + + + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +