| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740 |
- <!DOCTYPE html>
- <html>
- <head>
- <title>room_cal dev</title>
- <style>
- body { font-family: monospace; font-size: 13px; max-width: 860px; margin: 20px auto; padding: 0 16px; }
- h1 { font-size: 1.1em; margin-bottom: 16px; }
- h2 { font-size: 1em; border-bottom: 1px solid #bbb; padding-bottom: 2px; margin: 20px 0 8px; }
- .row { margin: 4px 0; display: flex; align-items: center; gap: 6px; }
- label.lbl { width: 110px; flex-shrink: 0; color: #555; }
- input[type=text], input[type=number], input[type=date], select {
- font-family: monospace; font-size: 12px; padding: 2px 4px; border: 1px solid #bbb;
- }
- input[type=date] { width: 130px; }
- input[type=number] { width: 90px; }
- input[type=text] { width: 160px; }
- select { padding: 2px; }
- button {
- font-family: monospace; font-size: 12px; cursor: pointer;
- padding: 2px 8px; border: 1px solid #888; background: #f0f0f0;
- }
- button:hover { background: #e0e0e0; }
- button.del { border-color: #c00; color: #c00; }
- button.del:hover { background: #fee; }
- .result {
- background: #f7f7f7; border: 1px solid #ddd; padding: 6px 8px;
- margin-top: 6px; white-space: pre-wrap; font-size: 12px;
- max-height: 280px; overflow: auto; display: none;
- }
- .result.show { display: block; }
- .result.err { border-color: #c00; background: #fff0f0; color: #900; }
- .cb-group { display: flex; gap: 12px; flex-wrap: wrap; }
- .cb-group label { width: auto; display: flex; align-items: center; gap: 3px; cursor: pointer; }
- .dash { color: #888; }
- #base-url { width: 240px; }
- .section-note { color: #888; font-size: 11px; margin-left: 4px; }
- </style>
- </head>
- <body>
- <h1>room_cal API dev</h1>
- <div class="row">
- <label class="lbl">base URL</label>
- <input id="base-url" type="text">
- </div>
- <!-- ── SYSTEM ───────────────────────────────────── -->
- <h2>System</h2>
- <div class="row">
- <button onclick="doReload()">POST /reload</button>
- <span class="section-note">flush in-memory CalDAV event caches</span>
- </div>
- <div class="result" id="res-reload"></div>
- <!-- ── HEALTH ───────────────────────────────────── -->
- <h2>Health</h2>
- <div class="row">
- <button onclick="doHealth()">GET /health</button>
- <span class="section-note">CalDAV connection (Docker probe, unauthenticated)</span>
- </div>
- <div class="result" id="res-health"></div>
- <div class="row" style="margin-top:4px">
- <button onclick="doStatus()">GET /status</button>
- <span class="section-note">full status including credential probes (authenticated)</span>
- </div>
- <div class="result" id="res-status"></div>
- <!-- ── SERVICES ───────────────────────────────────── -->
- <h2>Services</h2>
- <div class="row">
- <button onclick="doServices()">GET /services</button>
- </div>
- <div class="result" id="res-services"></div>
- <!-- ── CALC ──────────────────────────────────────── -->
- <h2>Calc</h2>
- <div class="row">
- <label class="lbl">transform</label>
- <select id="calc-transform">
- <option>net_to_list</option><option>list_to_net</option>
- <option>list_to_retail</option><option>retail_to_list</option>
- <option>net_to_retail</option><option>retail_to_net</option>
- </select>
- </div>
- <div class="row">
- <label class="lbl">service</label>
- <select id="calc-service"></select>
- </div>
- <div class="row">
- <label class="lbl">amount</label>
- <input id="calc-amount" type="number" value="100">
- <button onclick="doCalc()">GET</button>
- </div>
- <div class="result" id="res-calc"></div>
- <!-- ── PRICES ─────────────────────────────────────── -->
- <h2>Prices</h2>
- <div class="row">
- <label class="lbl">date</label>
- <input id="price-d1" type="date">
- <span class="dash">–</span>
- <input id="price-d2" type="date" title="end date for range (leave blank for single day)">
- <button onclick="doGetPrice()">GET</button>
- <button onclick="doPostPrice()">POST</button>
- <span class="section-note">POST uses start date only</span>
- </div>
- <div class="row">
- <label class="lbl">net value</label>
- <input id="price-value" type="number" step="0.01" placeholder="e.g. 120">
- </div>
- <div class="result" id="res-prices"></div>
- <!-- ── RESERVATIONS ───────────────────────────────── -->
- <h2>Reservations</h2>
- <div class="row">
- <label class="lbl">date</label>
- <input id="rsv-d1" type="date">
- <span class="dash">–</span>
- <input id="rsv-d2" type="date" title="end date for range">
- <button onclick="doGetReservation()">GET</button>
- <span class="section-note">GET uses date range</span>
- </div>
- <div class="row">
- <label class="lbl">uid</label>
- <input id="rsv-uid" type="text" placeholder="reservation uid">
- </div>
- <div class="row">
- <label class="lbl">checkin_date</label>
- <input id="rsv-checkin" type="date" title="required for PATCH">
- </div>
- <div class="row">
- <label class="lbl">door_code</label>
- <input id="rsv-door-code" type="text" placeholder="4-digit code">
- </div>
- <div class="row">
- <label class="lbl">wifi_key</label>
- <input id="rsv-wifi-key" type="text" placeholder="wifi password">
- </div>
- <div class="row">
- <label class="lbl">wifi_ssid</label>
- <input id="rsv-wifi-ssid" type="text" placeholder="network name">
- </div>
- <div class="row">
- <button onclick="doPatchReservation()">PATCH /reservation/:uid</button>
- <span class="section-note">only non-empty fields are sent</span>
- </div>
- <div class="result" id="res-reservations"></div>
- <!-- ── CLOSED ─────────────────────────────────────── -->
- <h2>Closed</h2>
- <div class="row">
- <label class="lbl">date</label>
- <input id="cls-d1" type="date">
- <span class="dash">–</span>
- <input id="cls-d2" type="date" title="end date for range">
- <button onclick="doGetClosed()">GET</button>
- <button onclick="doPostClosed()">POST</button>
- <button class="del" onclick="doDeleteClosed()">DELETE</button>
- <span class="section-note">POST/DELETE use start date only</span>
- </div>
- <div class="row">
- <label class="lbl">services</label>
- <div class="cb-group" id="closed-checks"></div>
- </div>
- <div class="result" id="res-closed"></div>
- <!-- ── DISCOUNTS ──────────────────────────────────── -->
- <h2>Discounts</h2>
- <div class="row">
- <label class="lbl">date</label>
- <input id="disc-d1" type="date">
- <span class="dash">–</span>
- <input id="disc-d2" type="date" title="end date for range">
- <button onclick="doGetDiscount()">GET</button>
- <button onclick="doPostDiscount()">POST</button>
- <button class="del" onclick="doDeleteDiscount()">DELETE</button>
- <span class="section-note">POST/DELETE use start date only</span>
- </div>
- <div class="row">
- <label class="lbl">type</label>
- <select id="disc-type"><option value="percent">percent</option><option value="flat">flat</option></select>
- <label class="lbl" style="width:auto;margin-left:8px">value</label>
- <input id="disc-value" type="number" step="0.01" placeholder="e.g. 10">
- </div>
- <div class="result" id="res-discounts"></div>
- <!-- ── WORKFLOWS ───────────────────────────────────── -->
- <h2>Workflows <span class="section-note">(awaits local write, backgrounds push)</span></h2>
- <div class="row">
- <label class="lbl">date</label>
- <input id="wf-d1" type="date">
- <span class="dash">–</span>
- <input id="wf-d2" type="date" title="end date for range workflows">
- </div>
- <div class="row">
- <label class="lbl">price</label>
- <input id="wf-price-value" type="number" step="0.01" placeholder="net value">
- <button onclick="doWfSetPrice()">POST /price</button>
- </div>
- <div class="row">
- <label class="lbl">discount</label>
- <select id="wf-disc-type"><option value="percent">percent</option><option value="flat">flat</option></select>
- <input id="wf-disc-value" type="number" step="0.01" placeholder="value" style="margin-left:4px">
- <button onclick="doWfSetDiscount()">POST /discount</button>
- <button class="del" onclick="doWfDeleteDiscount()">DELETE /discount</button>
- </div>
- <div class="row">
- <label class="lbl">closed</label>
- <div class="cb-group" id="wf-closed-checks"></div>
- <button onclick="doWfSetClosed()">POST /closed</button>
- <button class="del" onclick="doWfDeleteClosed()">DELETE /closed</button>
- <button onclick="doWfSetClosedRange()">POST /closed range</button>
- <span class="section-note">range uses both dates</span>
- </div>
- <div class="row">
- <label class="lbl">sync range</label>
- <button onclick="doWfReservations()">POST /reservations</button>
- <button onclick="doWfIcalFeeds()">POST /ical-feeds</button>
- <button onclick="doWfUserCal()">POST /user-cal</button>
- <span class="section-note">uses date range above</span>
- </div>
- <div class="result" id="res-workflows"></div>
- <!-- ── ACTIONS ────────────────────────────────────── -->
- <h2>Actions</h2>
- <div class="row">
- <label class="lbl">action</label>
- <select id="action-name" onchange="renderActionKwargs()">
- <option>fetch-reservations</option><option>fetch-closed</option>
- <option>fetch-user-cal</option><option>push-closed</option>
- <option>fetch-feed</option><option>fetch-prices</option>
- <option>push-prices</option><option>fill-prices</option>
- <option>apply-discounts</option><option>apply-rules</option>
- <option>local-refresh</option><option>local-sync</option>
- <option>push-busy</option><option>diff</option>
- </select>
- </div>
- <div class="row">
- <label class="lbl">start date</label>
- <input id="action-d1" type="date">
- <span class="dash">–</span>
- <input id="action-d2" type="date" title="end date (optional)">
- </div>
- <div id="action-kwargs"></div>
- <div class="row">
- <button onclick="doAction()">POST</button>
- </div>
- <div class="result" id="res-actions"></div>
- <!-- ── CHAT ──────────────────────────────────────── -->
- <h2>Chat <span class="section-note">(service-specific messaging)</span></h2>
- <div class="row">
- <button onclick="doChatTemplates()">GET /chat/templates</button>
- </div>
- <div class="result" id="res-chat-templates"></div>
- <div class="row" style="margin-top:8px">
- <label class="lbl">service</label>
- <select id="chat-service"></select>
- <button onclick="doChatReady()" style="margin-left:6px">GET ready</button>
- </div>
- <div class="result" id="res-chat-ready"></div>
- <div class="row" style="margin-top:8px">
- <label class="lbl">chat_id</label>
- <input id="chat-id" type="text" placeholder="thread / reservation id">
- <button onclick="doChatGet()">GET thread</button>
- </div>
- <div class="row">
- <label class="lbl">message</label>
- <input id="chat-body" type="text" placeholder="message text" style="width:260px">
- <button onclick="doChatPost()">POST send</button>
- </div>
- <div class="result" id="res-chat"></div>
- <!-- ── SMS ──────────────────────────────────────── -->
- <h2>SMS Challenge</h2>
- <div class="row">
- <label class="lbl">code</label>
- <input id="sms-code" type="text" placeholder="e.g. 123456" style="width:120px">
- <button onclick="doSmsPost()">POST /sms/code</button>
- <span class="section-note">deliver a code to whoever is polling sms_code.txt</span>
- </div>
- <div class="result" id="res-sms"></div>
- <!-- ── CREDENTIALS ────────────────────────────────── -->
- <h2>Credentials</h2>
- <div class="row">
- <button onclick="doCredProbeAll()">GET /credentials</button>
- <span class="section-note">live session probe for all services</span>
- </div>
- <div class="result" id="res-cred-probe-all"></div>
- <div class="row" style="margin-top:8px">
- <label class="lbl">service</label>
- <select id="cred-service"></select>
- <button onclick="doCredProbe()">GET</button>
- <span class="section-note">probe single service</span>
- </div>
- <div class="result" id="res-cred-probe"></div>
- <div class="row" style="margin-top:8px">
- <button onclick="doCredRefreshAll()">POST /credentials</button>
- <span class="section-note">trigger automatic refresh for all services (may take minutes)</span>
- </div>
- <div class="row" style="margin-top:4px">
- <label class="lbl">service</label>
- <select id="cred-refresh-service"></select>
- <button onclick="doCredRefresh()">POST refresh</button>
- <span class="section-note">trigger refresh for one service</span>
- </div>
- <div class="result" id="res-cred-refresh"></div>
- <div id="cred-fields" style="margin-top:8px">
- <div class="row cred-row">
- <label class="lbl" style="width:60px">key</label>
- <input class="cred-key" type="text" placeholder="e.g. token">
- <label class="lbl" style="width:40px">val</label>
- <input class="cred-val" type="text" placeholder="value" style="width:220px">
- <button onclick="addCredRow(this)">+</button>
- </div>
- </div>
- <div class="row" style="margin-top:6px">
- <button onclick="doCredPut()">PUT</button>
- </div>
- <div class="result" id="res-credentials"></div>
- <script>
- const API_BASE = '';
- document.getElementById('base-url').value = API_BASE;
- const ACTION_KWARGS = {
- 'fetch-feed': [['feed_name','text','feed name'],['title','text','event title (optional)']],
- 'fetch-prices': [['service','text','service key (optional)']],
- 'fill-prices': [['weekday','number','weekday net price'],['weekend','number','weekend net price']],
- 'apply-rules': [['pipeline_name','text','pipeline name']],
- };
- let servicesList = [];
- function base() {
- return document.getElementById('base-url').value.replace(/\/+$/, '');
- }
- async function api(method, path, body) {
- const opts = { method, headers: {} };
- if (body !== undefined) {
- opts.headers['Content-Type'] = 'application/json';
- opts.body = JSON.stringify(body);
- }
- try {
- const r = await fetch(base() + path, opts);
- if (r.status === 204) return { ok: true, status: 204, data: '204 No Content' };
- const text = await r.text();
- let data;
- try { data = JSON.parse(text); } catch { data = text; }
- return { ok: r.ok, status: r.status, data };
- } catch (e) {
- return { ok: false, status: 0, data: String(e) };
- }
- }
- function pending(id) {
- const el = document.getElementById(id);
- el.className = 'result show';
- el.textContent = '⋯';
- }
- function show(id, result) {
- const el = document.getElementById(id);
- el.className = 'result show' + (result.ok ? '' : ' err');
- const d = result.data;
- const body = typeof d === 'string' ? d : JSON.stringify(d, null, 2);
- el.textContent = result.ok ? body : `HTTP ${result.status}: ${body}`;
- }
- function dateArg(d1id, d2id) {
- const s = document.getElementById(d1id).value;
- const e = document.getElementById(d2id).value;
- if (!s) return null;
- return (e && e !== s) ? `${s}/${e}` : s;
- }
- // ── reload ──
- async function doReload() {
- pending('res-reload');
- show('res-reload', await api('POST', '/reload'));
- }
- // ── health ──
- async function doHealth() {
- pending('res-health');
- show('res-health', await api('GET', '/health'));
- }
- async function doStatus() {
- pending('res-status');
- show('res-status', await api('GET', '/status'));
- }
- // ── services ──
- async function doServices() {
- pending('res-services');
- const r = await api('GET', '/services');
- show('res-services', r);
- if (r.ok && Array.isArray(r.data)) {
- servicesList = r.data;
- populateDropdowns();
- renderClosedChecks();
- }
- }
- function populateDropdowns() {
- ['calc-service', 'cred-service', 'cred-refresh-service', 'chat-service'].forEach(id => {
- const el = document.getElementById(id);
- const prev = el.value;
- el.innerHTML = servicesList.map(s =>
- `<option value="${s.key}">${s.key} — ${s.name}</option>`
- ).join('');
- if (prev) el.value = prev;
- });
- }
- function renderClosedChecks(checked = []) {
- ['closed-checks', 'wf-closed-checks'].forEach(groupId => {
- const el = document.getElementById(groupId);
- if (!el) return;
- el.innerHTML = servicesList.map(s =>
- `<label><input type="checkbox" value="${s.key}" id="${groupId}-${s.key}"${checked.includes(s.key) ? ' checked' : ''}> ${s.name}</label>`
- ).join('');
- });
- }
- function getCheckedServices() {
- return Array.from(document.querySelectorAll('#closed-checks input:checked')).map(c => c.value);
- }
- function getWfCheckedServices() {
- return Array.from(document.querySelectorAll('#wf-closed-checks input:checked')).map(c => c.value);
- }
- // ── calc ──
- async function doCalc() {
- const t = document.getElementById('calc-transform').value;
- const s = document.getElementById('calc-service').value;
- const a = document.getElementById('calc-amount').value;
- if (!s || !a) return;
- pending('res-calc');
- show('res-calc', await api('GET', `/calc/${t}?service=${encodeURIComponent(s)}&amount=${a}`));
- }
- // ── prices ──
- async function doGetPrice() {
- const d1 = document.getElementById('price-d1').value;
- const d2 = document.getElementById('price-d2').value || d1;
- if (!d1) return;
- pending('res-prices');
- const r = await api('GET', `/price/${d1}/${d2}`);
- show('res-prices', r);
- if (r.ok && r.data && r.data[d1] && r.data[d1].net !== undefined) {
- document.getElementById('price-value').value = r.data[d1].net;
- }
- }
- async function doPostPrice() {
- const d = document.getElementById('price-d1').value;
- if (!d) return;
- const v = document.getElementById('price-value').value;
- if (!v) return;
- pending('res-prices');
- show('res-prices', await api('POST', `/price/${d}`, { value: parseFloat(v) }));
- }
- // ── reservations ──
- async function doGetReservation() {
- const range = dateArg('rsv-d1', 'rsv-d2');
- if (!range) return;
- pending('res-reservations');
- show('res-reservations', await api('GET', `/reservation/${range}`));
- }
- async function doPatchReservation() {
- const uid = document.getElementById('rsv-uid').value.trim();
- if (!uid) return;
- const checkin_date = document.getElementById('rsv-checkin').value;
- if (!checkin_date) return;
- const body = { checkin_date };
- const door_code = document.getElementById('rsv-door-code').value.trim();
- const wifi_key = document.getElementById('rsv-wifi-key').value.trim();
- const wifi_ssid = document.getElementById('rsv-wifi-ssid').value.trim();
- if (door_code) body.door_code = door_code;
- if (wifi_key) body.wifi_key = wifi_key;
- if (wifi_ssid) body.wifi_ssid = wifi_ssid;
- pending('res-reservations');
- show('res-reservations', await api('PATCH', `/reservation/${uid}`, body));
- }
- // ── closed ──
- async function doGetClosed() {
- const range = dateArg('cls-d1', 'cls-d2');
- if (!range) return;
- pending('res-closed');
- const r = await api('GET', `/closed/${range}`);
- show('res-closed', r);
- if (r.ok && Array.isArray(r.data)) {
- renderClosedChecks(r.data);
- }
- }
- async function doPostClosed() {
- const d = document.getElementById('cls-d1').value;
- if (!d) return;
- pending('res-closed');
- show('res-closed', await api('POST', `/closed/${d}`, { services: getCheckedServices() }));
- }
- async function doDeleteClosed() {
- const d = document.getElementById('cls-d1').value;
- if (!d) return;
- pending('res-closed');
- show('res-closed', await api('DELETE', `/closed/${d}`));
- }
- // ── discounts ──
- async function doGetDiscount() {
- const range = dateArg('disc-d1', 'disc-d2');
- if (!range) return;
- pending('res-discounts');
- const r = await api('GET', `/discount/${range}`);
- show('res-discounts', r);
- if (r.ok && r.data && r.data.type) {
- document.getElementById('disc-type').value = r.data.type;
- document.getElementById('disc-value').value = r.data.value ?? '';
- }
- }
- async function doPostDiscount() {
- const d = document.getElementById('disc-d1').value;
- if (!d) return;
- const type = document.getElementById('disc-type').value;
- const value = document.getElementById('disc-value').value;
- if (!value) return;
- pending('res-discounts');
- show('res-discounts', await api('POST', `/discount/${d}`, { type, value: parseFloat(value) }));
- }
- async function doDeleteDiscount() {
- const d = document.getElementById('disc-d1').value;
- if (!d) return;
- pending('res-discounts');
- show('res-discounts', await api('DELETE', `/discount/${d}`));
- }
- // ── workflows ──
- async function doWfSetPrice() {
- const d = document.getElementById('wf-d1').value;
- if (!d) return;
- const v = document.getElementById('wf-price-value').value;
- if (!v) return;
- pending('res-workflows');
- show('res-workflows', await api('POST', `/workflow/price/${d}`, { value: parseFloat(v) }));
- }
- async function doWfSetDiscount() {
- const d = document.getElementById('wf-d1').value;
- if (!d) return;
- const type = document.getElementById('wf-disc-type').value;
- const value = document.getElementById('wf-disc-value').value;
- if (!value) return;
- pending('res-workflows');
- show('res-workflows', await api('POST', `/workflow/discount/${d}`, { type, value: parseFloat(value) }));
- }
- async function doWfDeleteDiscount() {
- const d = document.getElementById('wf-d1').value;
- if (!d) return;
- pending('res-workflows');
- show('res-workflows', await api('DELETE', `/workflow/discount/${d}`));
- }
- async function doWfSetClosed() {
- const d = document.getElementById('wf-d1').value;
- if (!d) return;
- pending('res-workflows');
- show('res-workflows', await api('POST', `/workflow/closed/${d}`, { services: getWfCheckedServices() }));
- }
- async function doWfDeleteClosed() {
- const d = document.getElementById('wf-d1').value;
- if (!d) return;
- pending('res-workflows');
- show('res-workflows', await api('DELETE', `/workflow/closed/${d}`));
- }
- async function doWfSetClosedRange() {
- const r = wfRange();
- if (!r) return;
- pending('res-workflows');
- show('res-workflows', await api('POST', `/workflow/closed/${r.d1}/${r.d2}`, { services: getWfCheckedServices() }));
- }
- function wfRange() {
- const d1 = document.getElementById('wf-d1').value;
- const d2 = document.getElementById('wf-d2').value || d1;
- return d1 ? { d1, d2 } : null;
- }
- async function doWfReservations() {
- const r = wfRange();
- if (!r) return;
- pending('res-workflows');
- show('res-workflows', await api('POST', `/workflow/reservations/${r.d1}/${r.d2}`));
- }
- async function doWfIcalFeeds() {
- const r = wfRange();
- if (!r) return;
- pending('res-workflows');
- show('res-workflows', await api('POST', `/workflow/ical-feeds/${r.d1}/${r.d2}`));
- }
- async function doWfUserCal() {
- const r = wfRange();
- if (!r) return;
- pending('res-workflows');
- show('res-workflows', await api('POST', `/workflow/user-cal/${r.d1}/${r.d2}`));
- }
- // ── actions ──
- function renderActionKwargs() {
- const action = document.getElementById('action-name').value;
- const kwargs = ACTION_KWARGS[action] || [];
- document.getElementById('action-kwargs').innerHTML = kwargs.map(([key, type, ph]) =>
- `<div class="row"><label class="lbl">${key}</label><input id="akw-${key}" type="${type}" placeholder="${ph}" style="width:200px"></div>`
- ).join('');
- }
- async function doAction() {
- const action = document.getElementById('action-name').value;
- const range = dateArg('action-d1', 'action-d2');
- if (!range) return;
- const kwargs = {};
- (ACTION_KWARGS[action] || []).forEach(([key]) => {
- const el = document.getElementById(`akw-${key}`);
- if (el && el.value) kwargs[key] = el.value;
- });
- pending('res-actions');
- show('res-actions', await api('POST', `/action/${action}/${range}`, kwargs));
- }
- // ── chat ──
- async function doChatTemplates() {
- pending('res-chat-templates');
- show('res-chat-templates', await api('GET', '/chat/templates'));
- }
- async function doChatReady() {
- const service = document.getElementById('chat-service').value;
- if (!service) return;
- pending('res-chat-ready');
- show('res-chat-ready', await api('GET', `/chat/${service}/ready`));
- }
- async function doChatGet() {
- const service = document.getElementById('chat-service').value;
- const chatId = document.getElementById('chat-id').value.trim();
- if (!service || !chatId) return;
- pending('res-chat');
- show('res-chat', await api('GET', `/chat/${service}/${chatId}`));
- }
- async function doChatPost() {
- const service = document.getElementById('chat-service').value;
- const chatId = document.getElementById('chat-id').value.trim();
- const body = document.getElementById('chat-body').value.trim();
- if (!service || !chatId || !body) return;
- pending('res-chat');
- show('res-chat', await api('POST', `/chat/${service}/${chatId}`, { body }));
- }
- // ── sms ──
- async function doSmsPost() {
- const code = document.getElementById('sms-code').value.trim();
- if (!code) return;
- pending('res-sms');
- show('res-sms', await api('POST', '/sms/code', { code }));
- }
- // ── credentials ──
- async function doCredRefreshAll() {
- pending('res-cred-refresh');
- show('res-cred-refresh', await api('POST', '/credentials', {}));
- }
- async function doCredRefresh() {
- const service = document.getElementById('cred-refresh-service').value;
- if (!service) return;
- pending('res-cred-refresh');
- show('res-cred-refresh', await api('POST', '/credentials', { service }));
- }
- async function doCredProbeAll() {
- pending('res-cred-probe-all');
- show('res-cred-probe-all', await api('GET', '/credentials'));
- }
- async function doCredProbe() {
- const service = document.getElementById('cred-service').value;
- if (!service) return;
- pending('res-cred-probe');
- show('res-cred-probe', await api('GET', `/credentials/${service}`));
- }
- function addCredRow(btn) {
- const row = document.createElement('div');
- row.className = 'row cred-row';
- row.innerHTML = `<label class="lbl" style="width:60px">key</label><input class="cred-key" type="text" placeholder="key"><label class="lbl" style="width:40px">val</label><input class="cred-val" type="text" placeholder="value" style="width:220px"><button onclick="this.parentElement.remove()">−</button>`;
- btn.closest('#cred-fields').appendChild(row);
- }
- async function doCredPut() {
- const service = document.getElementById('cred-service').value;
- if (!service) return;
- const creds = {};
- document.querySelectorAll('.cred-row').forEach(row => {
- const k = row.querySelector('.cred-key').value.trim();
- const v = row.querySelector('.cred-val').value;
- if (k) creds[k] = v;
- });
- pending('res-credentials');
- show('res-credentials', await api('PUT', `/credentials/${service}`, creds));
- }
- // ── init ──
- const today = new Date().toISOString().slice(0, 10);
- ['price-d1','rsv-d1','cls-d1','disc-d1','wf-d1','action-d1'].forEach(id => {
- document.getElementById(id).value = today;
- });
- const nextMonth = new Date(); nextMonth.setDate(nextMonth.getDate() + 30);
- document.getElementById('wf-d2').value = nextMonth.toISOString().slice(0, 10);
- renderActionKwargs();
- doServices();
- </script>
- </body>
- </html>
|