Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 027ac59bc7 | |||
| 458b7897dd |
@@ -61,3 +61,16 @@ function error_out(string $message, int $status = 400): never
|
|||||||
{
|
{
|
||||||
json_out(['error' => $message], $status);
|
json_out(['error' => $message], $status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aborts with 400 if no Clickatell API key has been configured.
|
||||||
|
*/
|
||||||
|
function require_api_key(): void
|
||||||
|
{
|
||||||
|
$stmt = db()->prepare("SELECT setting_value FROM `system` WHERE setting_name = 'clickatell_api_key'");
|
||||||
|
$stmt->execute();
|
||||||
|
$key = $stmt->fetchColumn();
|
||||||
|
if ($key === false || trim((string) $key) === '') {
|
||||||
|
error_out('No Clickatell API key is configured. Set one in Admin → Clickatell Settings before sending or scheduling messages.', 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ function handle_get(): never
|
|||||||
|
|
||||||
function handle_post(): never
|
function handle_post(): never
|
||||||
{
|
{
|
||||||
|
require_api_key();
|
||||||
|
|
||||||
$body = json_body();
|
$body = json_body();
|
||||||
$templateId = (int) ($body['template_id'] ?? 0);
|
$templateId = (int) ($body['template_id'] ?? 0);
|
||||||
$mode = (string) ($body['mode'] ?? '');
|
$mode = (string) ($body['mode'] ?? '');
|
||||||
@@ -104,6 +106,8 @@ function handle_post(): never
|
|||||||
|
|
||||||
function handle_put(): never
|
function handle_put(): never
|
||||||
{
|
{
|
||||||
|
require_api_key();
|
||||||
|
|
||||||
$id = (int) ($_GET['id'] ?? 0);
|
$id = (int) ($_GET['id'] ?? 0);
|
||||||
if ($id <= 0) {
|
if ($id <= 0) {
|
||||||
error_out('Missing or invalid id');
|
error_out('Missing or invalid id');
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
require_api_key();
|
||||||
|
|
||||||
$body = json_body();
|
$body = json_body();
|
||||||
$customBody = trim((string) ($body['message'] ?? ''));
|
$customBody = trim((string) ($body['message'] ?? ''));
|
||||||
$templateId = isset($body['template_id']) ? (int) $body['template_id'] : null;
|
$templateId = isset($body['template_id']) ? (int) $body['template_id'] : null;
|
||||||
|
|||||||
+14
-1
@@ -162,6 +162,7 @@
|
|||||||
<script src="https://cdn.datatables.net/2.3.8/js/dataTables.min.js"></script>
|
<script src="https://cdn.datatables.net/2.3.8/js/dataTables.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let logTable = null;
|
let logTable = null;
|
||||||
|
let _lastDataKey = '';
|
||||||
|
|
||||||
function esc(str) {
|
function esc(str) {
|
||||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
@@ -190,7 +191,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadLog() {
|
async function loadLog(isRefresh = false) {
|
||||||
const tbody = document.getElementById('logBody');
|
const tbody = document.getElementById('logBody');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -202,6 +203,14 @@
|
|||||||
document.getElementById('stat-failed').textContent = stats.total_failed ?? '—';
|
document.getElementById('stat-failed').textContent = stats.total_failed ?? '—';
|
||||||
document.getElementById('stat-rate').textContent = stats.success_rate != null ? stats.success_rate + '%' : '—';
|
document.getElementById('stat-rate').textContent = stats.success_rate != null ? stats.success_rate + '%' : '—';
|
||||||
|
|
||||||
|
// Skip expensive DOM rebuild if nothing has changed
|
||||||
|
const dataKey = `${entries.length}:${stats.total_sent}:${stats.total_failed}`;
|
||||||
|
if (isRefresh && dataKey === _lastDataKey) return;
|
||||||
|
_lastDataKey = dataKey;
|
||||||
|
|
||||||
|
// Preserve current page position before destroying the table
|
||||||
|
const prevPage = logTable ? logTable.page() : 0;
|
||||||
|
|
||||||
// Destroy existing DataTable before rewriting DOM
|
// Destroy existing DataTable before rewriting DOM
|
||||||
if (logTable) { logTable.destroy(); logTable = null; }
|
if (logTable) { logTable.destroy(); logTable = null; }
|
||||||
|
|
||||||
@@ -237,12 +246,16 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (isRefresh && prevPage > 0 && prevPage < logTable.page.info().pages) {
|
||||||
|
logTable.page(prevPage).draw(false);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
tbody.innerHTML = `<tr><td colspan="5" class="text-danger text-center py-4">Could not load delivery log.</td></tr>`;
|
tbody.innerHTML = `<tr><td colspan="5" class="text-danger text-center py-4">Could not load delivery log.</td></tr>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadLog();
|
loadLog();
|
||||||
|
setInterval(() => loadLog(true), 10_000);
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
async function checkAuth() {
|
async function checkAuth() {
|
||||||
|
|||||||
@@ -334,6 +334,7 @@
|
|||||||
} catch { /* DB unavailable — stats stay as dashes */ }
|
} catch { /* DB unavailable — stats stay as dashes */ }
|
||||||
}
|
}
|
||||||
loadStats();
|
loadStats();
|
||||||
|
setInterval(() => loadStats(), 10_000);
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
async function checkAuth() {
|
async function checkAuth() {
|
||||||
|
|||||||
+16
-1
@@ -117,6 +117,11 @@
|
|||||||
<i class="fa-solid fa-flask me-1"></i> Sandbox mode is active — texts are tagged as a test and excluded from
|
<i class="fa-solid fa-flask me-1"></i> Sandbox mode is active — texts are tagged as a test and excluded from
|
||||||
cost calculations.
|
cost calculations.
|
||||||
</div>
|
</div>
|
||||||
|
<div id="noApiKeyBanner" class="alert alert-danger"
|
||||||
|
style="display:none;font-size:0.85rem;font-weight:600;text-align:center;">
|
||||||
|
<i class="fa-solid fa-circle-exclamation me-1"></i> No Clickatell API key configured —
|
||||||
|
<a href="index.html" class="alert-link">set one in Admin settings</a> before scheduling.
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>Schedule</h1>
|
<h1>Schedule</h1>
|
||||||
@@ -161,7 +166,7 @@
|
|||||||
<option value="" disabled selected>Loading templates…</option>
|
<option value="" disabled selected>Loading templates…</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<button class="btn btn-primary" onclick="addSchedule()">
|
<button class="btn btn-primary" id="addScheduleBtn" onclick="addSchedule()">
|
||||||
<i class="fa-solid fa-plus me-1"></i> Add to Schedule
|
<i class="fa-solid fa-plus me-1"></i> Add to Schedule
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -516,6 +521,16 @@
|
|||||||
}
|
}
|
||||||
checkAuth();
|
checkAuth();
|
||||||
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') checkAuth(); });
|
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') checkAuth(); });
|
||||||
|
|
||||||
|
async function checkApiKey() {
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/settings.php?key=clickatell_api_key').then(res => res.json());
|
||||||
|
const missing = !r.set;
|
||||||
|
document.getElementById('noApiKeyBanner').style.display = missing ? '' : 'none';
|
||||||
|
document.getElementById('addScheduleBtn').disabled = missing;
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
checkApiKey();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
+18
-2
@@ -103,6 +103,11 @@
|
|||||||
<i class="fa-solid fa-flask me-1"></i> Sandbox mode is active — texts are tagged as a test and excluded from
|
<i class="fa-solid fa-flask me-1"></i> Sandbox mode is active — texts are tagged as a test and excluded from
|
||||||
cost calculations.
|
cost calculations.
|
||||||
</div>
|
</div>
|
||||||
|
<div id="noApiKeyBanner" class="alert alert-danger"
|
||||||
|
style="display:none;font-size:0.85rem;font-weight:600;text-align:center;">
|
||||||
|
<i class="fa-solid fa-circle-exclamation me-1"></i> No Clickatell API key configured —
|
||||||
|
<a href="index.html" class="alert-link">set one in Admin settings</a> before sending.
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="page-header d-flex align-items-center gap-3">
|
<div class="page-header d-flex align-items-center gap-3">
|
||||||
<a href="index.html" class="btn btn-sm btn-secondary">
|
<a href="index.html" class="btn btn-sm btn-secondary">
|
||||||
@@ -196,6 +201,7 @@
|
|||||||
const countSpan = document.getElementById('recipientCount');
|
const countSpan = document.getElementById('recipientCount');
|
||||||
const recipientsSel = document.getElementById('recipients');
|
const recipientsSel = document.getElementById('recipients');
|
||||||
const resultEl = document.getElementById('sendResult');
|
const resultEl = document.getElementById('sendResult');
|
||||||
|
let noApiKey = false;
|
||||||
|
|
||||||
// ── Populate recipients from API ───────────────────────────
|
// ── Populate recipients from API ───────────────────────────
|
||||||
async function loadRecipients() {
|
async function loadRecipients() {
|
||||||
@@ -276,7 +282,7 @@
|
|||||||
sectionEl.style.visibility = empty ? 'hidden' : 'visible';
|
sectionEl.style.visibility = empty ? 'hidden' : 'visible';
|
||||||
placeholderEl.style.display = empty ? '' : 'none';
|
placeholderEl.style.display = empty ? '' : 'none';
|
||||||
previewEl.textContent = empty ? '' : previewText(val);
|
previewEl.textContent = empty ? '' : previewText(val);
|
||||||
sendBtn.disabled = empty;
|
sendBtn.disabled = empty || noApiKey;
|
||||||
resultEl.style.display = 'none';
|
resultEl.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -333,7 +339,7 @@
|
|||||||
<i class="fa-solid fa-circle-xmark me-2"></i>${esc(err.message)}
|
<i class="fa-solid fa-circle-xmark me-2"></i>${esc(err.message)}
|
||||||
</div>`;
|
</div>`;
|
||||||
} finally {
|
} finally {
|
||||||
sendBtn.disabled = msgEl.value.trim() === '';
|
sendBtn.disabled = msgEl.value.trim() === '' || noApiKey;
|
||||||
sendBtn.innerHTML = '<i class="fa-solid fa-paper-plane me-2"></i>Send to <span id="recipientCount">' + countSpan.textContent + '</span> attendees';
|
sendBtn.innerHTML = '<i class="fa-solid fa-paper-plane me-2"></i>Send to <span id="recipientCount">' + countSpan.textContent + '</span> attendees';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -384,6 +390,16 @@
|
|||||||
}
|
}
|
||||||
checkAuth();
|
checkAuth();
|
||||||
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') checkAuth(); });
|
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') checkAuth(); });
|
||||||
|
|
||||||
|
async function checkApiKey() {
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/settings.php?key=clickatell_api_key').then(res => res.json());
|
||||||
|
noApiKey = !r.set;
|
||||||
|
document.getElementById('noApiKeyBanner').style.display = noApiKey ? '' : 'none';
|
||||||
|
if (noApiKey) document.getElementById('sendBtn').disabled = true;
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
checkApiKey();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user