Central support desk for the Oumta project. Tickets arrive through a
self-hostable support-relay (public form, SMTP out), hyprcrd pulls
them in as cards, replies go back out through the relay, and KB
fragments are plain hyprcrd cards so the board + search + tags
machinery handles them for free.
support-relay/ (standalone PHP/SQLite service)
- Public form + submit endpoint with IP rate-limit, honeypot, and an
email-matched reply path so users can follow up by typing their
OUMTA-n issue id — mismatched email falls back to a fresh ticket
so a hostile submitter can't hijack someone else's thread.
- One-shot setup wizard configures branding, issue-id prefix, public
URL, mail footer template, SMTP creds (stored AES-256-GCM at rest),
and generates the X-Support-Key API token shown exactly once.
- Hyprcrd-facing API: GET /api/hyprcrd/tickets?since=<iso>, POST
…/reply (sends via SMTP, flips status to answered), PATCH
…/status, DELETE, GET /ping, plus GET/POST /api/hyprcrd/settings
exposing brand/public_url/mail_footer for post-setup edits.
- Minimal dependency-free SMTP client covering STARTTLS/TLS/none,
AUTH LOGIN, RFC 2047 subject encoding, dot-stuffing. Extended to
emit multipart/alternative (plain+HTML) + multipart/related so
outbound mails carry the branded logo via Content-ID embedding;
plain-text fallback stays intact for older clients.
- Mail footer template supports {brand}, {issue_id}, {public_url}
placeholders. Lines with an empty placeholder are dropped, so the
"Reply via …" line vanishes cleanly when no public URL is set.
- Favicon on all public pages. Logo (SVG by default, PNG preferred
when both exist) picked up from logo2026col.* at send time.
- Docker: Dockerfile + docker-compose.yml for a stand-alone test
container, plus a support-relay service in the main compose
(profile "support") so hyprcrd can reach it at http://support-relay
without host-networking shenanigans.
public/plugins/support (hyprcrd client)
- Full-screen Support overlay: ticket list (status-filterable),
active ticket thread, reply composer with KB-fragment search +
click-to-insert, "Save as fragment" button, "Close ticket" shortcut.
- KB fragments are regular cards in a configurable bucket. The
composer just adds a fast search-and-insert path on top.
- Sync-tick hook auto-polls the relay alongside the existing CalDAV
+ relay-mail cycle.
- Settings tab in the main overlay: relay URL, API key, inbox / KB
bucket pickers, connection test, and a "Relay config" subsection
that round-trips to /api/hyprcrd/settings so the mail footer and
branding can be edited without touching the relay's DB.
- Local cache tables support_tickets + support_replies mirror the
relay's authoritative data keyed by relay_id.
Tooling
- dev.sh now opts into both relay + support profiles.
- dev-down.sh wipes support-relay volumes and bind-mount data for a
clean re-setup. Hyprcrd + relay data is preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
126 lines
5.4 KiB
PHP
126 lines
5.4 KiB
PHP
<?php if (!defined('SUPPORT_RELAY_BOOTSTRAPPED')) { http_response_code(404); exit; } ?><!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Support relay — setup</title>
|
|
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
<link rel="stylesheet" href="ui/style.css">
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>Relay setup</h1>
|
|
<p class="lede">First-run configuration. Once you save, this page disappears and the relay starts accepting tickets.</p>
|
|
|
|
<div id="banner"></div>
|
|
|
|
<form id="setup-form" novalidate>
|
|
<h2>Branding</h2>
|
|
<div class="field">
|
|
<label for="brand">Display name</label>
|
|
<input id="brand" type="text" value="Oumta Support" required>
|
|
<span class="hint">Shown on the public form, in email subjects, and at the bottom of replies.</span>
|
|
</div>
|
|
<div class="field">
|
|
<label for="issue_prefix">Issue-id prefix</label>
|
|
<input id="issue_prefix" type="text" value="OUMTA" pattern="[A-Za-z0-9]{1,12}" required>
|
|
<span class="hint">Tickets will look like <span class="issue-id">OUMTA-42</span>.</span>
|
|
</div>
|
|
<div class="field">
|
|
<label for="public_url">Public form URL (optional)</label>
|
|
<input id="public_url" type="url" placeholder="https://support.oumta.de">
|
|
<span class="hint">Shown in outbound mail footers so users can reply via the form with their issue id.</span>
|
|
</div>
|
|
<div class="field">
|
|
<label for="mail_footer">Mail footer (optional)</label>
|
|
<textarea id="mail_footer" rows="4" placeholder="--- {brand} · Ticket {issue_id} Reply via {public_url} (issue id: {issue_id})"></textarea>
|
|
<span class="hint">Appended to every outbound mail (confirmations + replies). Supports placeholders <code>{brand}</code>, <code>{issue_id}</code>, <code>{public_url}</code>. Lines with an empty placeholder are dropped. Leave empty to use the default.</span>
|
|
</div>
|
|
|
|
<h2>SMTP</h2>
|
|
<div class="field">
|
|
<label for="smtp_host">Host</label>
|
|
<input id="smtp_host" type="text" placeholder="smtp.example.com" required>
|
|
</div>
|
|
<div class="field">
|
|
<label for="smtp_port">Port</label>
|
|
<input id="smtp_port" type="number" value="587">
|
|
</div>
|
|
<div class="field">
|
|
<label for="smtp_encryption">Encryption</label>
|
|
<select id="smtp_encryption">
|
|
<option value="starttls" selected>STARTTLS (587)</option>
|
|
<option value="tls">Implicit TLS (465)</option>
|
|
<option value="none">None (25, not recommended)</option>
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label for="smtp_user">Username</label>
|
|
<input id="smtp_user" type="text" autocomplete="off">
|
|
</div>
|
|
<div class="field">
|
|
<label for="smtp_password">Password</label>
|
|
<input id="smtp_password" type="password" autocomplete="off">
|
|
</div>
|
|
<div class="field">
|
|
<label for="smtp_from">From address</label>
|
|
<input id="smtp_from" type="email" placeholder="support@oumta.de" required>
|
|
</div>
|
|
<div class="field">
|
|
<label for="smtp_from_name">From name</label>
|
|
<input id="smtp_from_name" type="text" placeholder="Oumta Support">
|
|
</div>
|
|
|
|
<button type="submit">Save & generate API key</button>
|
|
</form>
|
|
|
|
<div id="done" style="display:none;">
|
|
<h2>Relay is configured.</h2>
|
|
<p class="lede">Paste this API key into the hyprcrd <strong>support</strong> plugin settings, next to the relay URL. It's shown once — copy it now.</p>
|
|
<div class="setup-note" id="apikey"></div>
|
|
<p class="lede" style="margin-top:20px;">Once hyprcrd is connected, the public form is live at this URL.</p>
|
|
</div>
|
|
</main>
|
|
<script>
|
|
const $ = (id) => document.getElementById(id);
|
|
const banner = $('banner');
|
|
function setBanner(text, kind = 'err') {
|
|
banner.className = 'banner ' + kind;
|
|
banner.textContent = text;
|
|
}
|
|
|
|
$('setup-form').addEventListener('submit', async e => {
|
|
e.preventDefault();
|
|
const payload = {
|
|
brand: $('brand').value.trim(),
|
|
issue_prefix: $('issue_prefix').value.trim().toUpperCase(),
|
|
public_url: $('public_url').value.trim(),
|
|
mail_footer: $('mail_footer').value,
|
|
smtp_host: $('smtp_host').value.trim(),
|
|
smtp_port: parseInt($('smtp_port').value) || 587,
|
|
smtp_encryption: $('smtp_encryption').value,
|
|
smtp_user: $('smtp_user').value.trim(),
|
|
smtp_password: $('smtp_password').value,
|
|
smtp_from: $('smtp_from').value.trim(),
|
|
smtp_from_name: $('smtp_from_name').value.trim() || $('brand').value.trim(),
|
|
};
|
|
try {
|
|
const res = await fetch('api/setup', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Setup failed.');
|
|
$('setup-form').style.display = 'none';
|
|
$('apikey').textContent = data.api_key;
|
|
$('done').style.display = '';
|
|
banner.textContent = '';
|
|
} catch (err) {
|
|
setBanner(err.message);
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|