456 lines
17 KiB
HTML
456 lines
17 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>SMS Gateway</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||
<link rel="stylesheet" href="/css/style.css" />
|
||
</head>
|
||
<body>
|
||
|
||
<div id="app">
|
||
|
||
<!-- ── Login ───────────────────────────────────────────────────────────── -->
|
||
<div id="login-page">
|
||
<div class="login-card">
|
||
<h1>📡 SMS Gateway</h1>
|
||
<p>Sign in to access the dashboard</p>
|
||
<div id="login-error" class="alert alert-danger" style="display:none;margin-bottom:16px;"></div>
|
||
<form id="login-form" style="display:flex;flex-direction:column;gap:16px;">
|
||
<div class="form-group">
|
||
<label>Username</label>
|
||
<input id="login-username" type="text" placeholder="admin" autocomplete="username" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Password</label>
|
||
<input id="login-password" type="password" placeholder="••••••••" autocomplete="current-password" />
|
||
</div>
|
||
<button type="submit" class="btn btn-primary" style="width:100%;justify-content:center;">Sign in</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Sidebar ─────────────────────────────────────────────────────────── -->
|
||
<nav id="sidebar">
|
||
<div class="sidebar-logo">
|
||
<span>📡</span> SMS Gateway
|
||
</div>
|
||
<div class="nav-item active" data-page="dashboard">
|
||
<span class="icon">🏠</span> Dashboard
|
||
</div>
|
||
<div class="nav-item" data-page="send">
|
||
<span class="icon">✉️</span> Send SMS
|
||
</div>
|
||
<div class="nav-item" data-page="received">
|
||
<span class="icon">📥</span> Received
|
||
</div>
|
||
<div class="nav-item" data-page="sent">
|
||
<span class="icon">📤</span> Sent
|
||
</div>
|
||
<div class="nav-item" data-page="webhooks">
|
||
<span class="icon">🔗</span> Webhooks
|
||
</div>
|
||
<div class="nav-item" data-page="tokens">
|
||
<span class="icon">🔑</span> API Tokens
|
||
</div>
|
||
<div class="nav-item" data-page="settings">
|
||
<span class="icon">⚙️</span> Settings
|
||
</div>
|
||
<div class="sidebar-spacer"></div>
|
||
<div class="sidebar-user">
|
||
<div class="avatar" id="user-avatar">A</div>
|
||
<div class="user-info">
|
||
<div class="user-name" id="user-name">Admin</div>
|
||
</div>
|
||
<button class="logout-btn" id="logout-btn" title="Sign out">⏏</button>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- ── Main ────────────────────────────────────────────────────────────── -->
|
||
<main id="main">
|
||
|
||
<!-- Dashboard -->
|
||
<div class="page active" id="page-dashboard">
|
||
<div class="page-header">
|
||
<div>
|
||
<h2>Dashboard</h2>
|
||
<p>Overview of your SMS gateway</p>
|
||
</div>
|
||
<button class="btn btn-outline" onclick="App.refreshDashboard()">↻ Refresh</button>
|
||
</div>
|
||
<div class="stat-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-label">Messages Sent</div>
|
||
<div class="stat-value blue" id="stat-sent">—</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Messages Received</div>
|
||
<div class="stat-value green" id="stat-received">—</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Failed</div>
|
||
<div class="stat-value red" id="stat-failed">—</div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
|
||
<strong>Recent Messages</strong>
|
||
<button class="btn btn-sm btn-outline" onclick="App.loadDashboard()">↻</button>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th></th>
|
||
<th>Phone</th>
|
||
<th>Message</th>
|
||
<th>Status</th>
|
||
<th>Time</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="recent-messages">
|
||
<tr><td colspan="5"><div class="empty-state"><div class="spinner"></div></div></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Send SMS -->
|
||
<div class="page" id="page-send">
|
||
<div class="page-header">
|
||
<div>
|
||
<h2>Send SMS</h2>
|
||
<p>Send a message directly from this dashboard</p>
|
||
</div>
|
||
</div>
|
||
<div class="card" style="max-width:600px;">
|
||
<form class="send-form" id="send-form">
|
||
<div id="send-alert" style="display:none;"></div>
|
||
<div class="form-group">
|
||
<label>Recipient phone number</label>
|
||
<input id="send-number" type="tel" placeholder="+46701234567" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Message</label>
|
||
<textarea id="send-message" placeholder="Type your message…" rows="4"></textarea>
|
||
<small style="color:var(--muted);"><span id="char-count">0</span> / 160 characters (SMS segment 1)</small>
|
||
</div>
|
||
<div style="display:flex;gap:10px;">
|
||
<button type="submit" class="btn btn-primary" id="send-btn">
|
||
✉️ Send Message
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Received Messages -->
|
||
<div class="page" id="page-received">
|
||
<div class="page-header">
|
||
<div>
|
||
<h2>Received Messages</h2>
|
||
<p>Incoming SMS messages from your device</p>
|
||
</div>
|
||
<div style="display:flex;gap:8px;">
|
||
<button class="btn btn-outline" onclick="App.triggerPoll()">↻ Poll device</button>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>From</th>
|
||
<th>Message</th>
|
||
<th>Received at</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="received-list">
|
||
<tr><td colspan="3"><div class="empty-state"><div class="spinner"></div></div></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="pagination" id="received-pagination"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sent Messages -->
|
||
<div class="page" id="page-sent">
|
||
<div class="page-header">
|
||
<div>
|
||
<h2>Sent Messages</h2>
|
||
<p>Outgoing SMS messages</p>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>To</th>
|
||
<th>Message</th>
|
||
<th>Status</th>
|
||
<th>Sent at</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="sent-list">
|
||
<tr><td colspan="4"><div class="empty-state"><div class="spinner"></div></div></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="pagination" id="sent-pagination"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Webhooks -->
|
||
<div class="page" id="page-webhooks">
|
||
<div class="page-header">
|
||
<div>
|
||
<h2>Webhooks</h2>
|
||
<p>Forward SMS events to external services</p>
|
||
</div>
|
||
<button class="btn btn-primary" onclick="App.openWebhookModal()">+ Add Webhook</button>
|
||
</div>
|
||
<div class="card">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Phone filter</th>
|
||
<th>URL</th>
|
||
<th>Events</th>
|
||
<th>Status</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="webhook-list">
|
||
<tr><td colspan="6"><div class="empty-state"><div class="spinner"></div></div></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- API Tokens -->
|
||
<div class="page" id="page-tokens">
|
||
<div class="page-header">
|
||
<div>
|
||
<h2>API Tokens</h2>
|
||
<p>Bearer tokens for external API access</p>
|
||
</div>
|
||
<button class="btn btn-primary" onclick="App.openTokenModal()">+ Create Token</button>
|
||
</div>
|
||
<div class="card" style="margin-bottom:16px;">
|
||
<strong style="display:block;margin-bottom:8px;">API Usage</strong>
|
||
<p style="color:var(--muted);font-size:13px;margin-bottom:12px;">
|
||
Use your token in the <code style="color:var(--primary);">Authorization</code> header:
|
||
</p>
|
||
<div class="token-value">Authorization: Bearer <your-token></div>
|
||
<div style="margin-top:12px;display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--muted);">
|
||
<code>POST /api/sms/send { "number": "+46…", "message": "…" }</code>
|
||
<code>GET /api/sms/messages</code>
|
||
<code>GET /api/sms/messages/received</code>
|
||
<code>GET /api/sms/messages/sent</code>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Token</th>
|
||
<th>Status</th>
|
||
<th>Created</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="token-list">
|
||
<tr><td colspan="5"><div class="empty-state"><div class="spinner"></div></div></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Settings -->
|
||
<div class="page" id="page-settings">
|
||
<div class="page-header">
|
||
<div>
|
||
<h2>Settings</h2>
|
||
<p>Configure your Teltonika device connection</p>
|
||
</div>
|
||
</div>
|
||
<div class="card" style="max-width:600px;">
|
||
<div class="config-section">
|
||
<div>
|
||
<h3>Device Connection</h3>
|
||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||
<div id="settings-alert" style="display:none;"></div>
|
||
<div class="form-group">
|
||
<label>Device IP address</label>
|
||
<input id="cfg-ip" placeholder="192.168.1.1" />
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Username</label>
|
||
<input id="cfg-username" placeholder="admin" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Password</label>
|
||
<input id="cfg-password" type="password" placeholder="••••••••" />
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Modem ID</label>
|
||
<input id="cfg-modem" placeholder="1-1" />
|
||
<small style="color:var(--muted);">Modem identifier (e.g. 1-1, 3-1.4)</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Poll interval (seconds)</label>
|
||
<input id="cfg-poll" type="number" min="10" max="3600" placeholder="30" />
|
||
</div>
|
||
<div style="display:flex;gap:10px;">
|
||
<button class="btn btn-primary" onclick="App.saveConfig()">Save settings</button>
|
||
<button class="btn btn-outline" onclick="App.testConnection()">Test connection</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<h3>Logging</h3>
|
||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||
<div id="log-alert" style="display:none;"></div>
|
||
<div class="form-group">
|
||
<label>Log level</label>
|
||
<select id="cfg-log-level">
|
||
<option value="ERROR">ERROR — errors only</option>
|
||
<option value="WARNING">WARNING — errors + warnings</option>
|
||
<option value="INFO">INFO — normal operation (default)</option>
|
||
<option value="DEBUG">DEBUG — verbose, includes HTTP requests</option>
|
||
</select>
|
||
<small style="color:var(--muted);">Also configurable via <code>LOG_LEVEL</code> env var at startup</small>
|
||
</div>
|
||
<button class="btn btn-primary" onclick="App.saveLogLevel()" style="align-self:flex-start;">
|
||
Apply log level
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<h3>Change Password</h3>
|
||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||
<div id="pw-alert" style="display:none;"></div>
|
||
<div class="form-group">
|
||
<label>Current password</label>
|
||
<input id="pw-current" type="password" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>New password</label>
|
||
<input id="pw-new" type="password" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Confirm new password</label>
|
||
<input id="pw-confirm" type="password" />
|
||
</div>
|
||
<button class="btn btn-primary" onclick="App.changePassword()" style="align-self:flex-start;">
|
||
Update password
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</main>
|
||
</div>
|
||
|
||
<!-- ── Toast container ───────────────────────────────────────────────────── -->
|
||
<div id="toast-container"></div>
|
||
|
||
<!-- ── Webhook Modal ─────────────────────────────────────────────────────── -->
|
||
<div class="modal-backdrop" id="webhook-modal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3 id="webhook-modal-title">Add Webhook</h3>
|
||
<button class="modal-close" onclick="App.closeModal('webhook-modal')">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label>Name</label>
|
||
<input id="wh-name" placeholder="My webhook" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>URL</label>
|
||
<input id="wh-url" placeholder="https://example.com/webhook" />
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>HTTP Method</label>
|
||
<select id="wh-method">
|
||
<option value="POST">POST</option>
|
||
<option value="GET">GET</option>
|
||
<option value="PUT">PUT</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Events</label>
|
||
<select id="wh-events">
|
||
<option value="received">Received only</option>
|
||
<option value="sent">Sent only</option>
|
||
<option value="both">Both</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Phone number filter (leave empty for all)</label>
|
||
<input id="wh-phone" placeholder="+46701234567" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Extra headers (JSON object, optional)</label>
|
||
<textarea id="wh-headers" rows="3" placeholder='{"X-Api-Key": "secret"}'></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-outline" onclick="App.closeModal('webhook-modal')">Cancel</button>
|
||
<button class="btn btn-primary" onclick="App.saveWebhook()">Save</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Token Modal ───────────────────────────────────────────────────────── -->
|
||
<div class="modal-backdrop" id="token-modal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3>Create API Token</h3>
|
||
<button class="modal-close" onclick="App.closeModal('token-modal')">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div id="token-created" style="display:none;">
|
||
<div class="alert alert-success">Token created — copy it now, it will not be shown again.</div>
|
||
<div style="margin-top:12px;" class="form-group">
|
||
<label>Token value</label>
|
||
<div class="token-value" id="new-token-value"></div>
|
||
</div>
|
||
<button class="btn btn-outline btn-sm" style="margin-top:8px;" onclick="App.copyToken()">Copy</button>
|
||
</div>
|
||
<div id="token-form">
|
||
<div class="form-group">
|
||
<label>Token name</label>
|
||
<input id="tk-name" placeholder="e.g. My Application" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-outline" onclick="App.closeModal('token-modal')">Close</button>
|
||
<button class="btn btn-primary" id="token-create-btn" onclick="App.createToken()">Create</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/app.js"></script>
|
||
</body>
|
||
</html>
|