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>
119 lines
3.5 KiB
CSS
119 lines
3.5 KiB
CSS
/* support-relay public + setup styles. Minimalist, self-contained —
|
|
* no font loading, no external deps, passes through whatever the
|
|
* system UI stack looks like. Dark + light friendly. */
|
|
|
|
:root {
|
|
--bg: #0f1726;
|
|
--surface: #1a2438;
|
|
--border: #2a3650;
|
|
--text: #cbd5e1;
|
|
--bright: #f1f5f9;
|
|
--dim: #64748b;
|
|
--accent: #38bdf8;
|
|
--ok: #4ade80;
|
|
--danger: #fb7185;
|
|
--radius: 6px;
|
|
}
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
html, body {
|
|
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
min-height: 100vh;
|
|
font-size: 15px;
|
|
line-height: 1.55;
|
|
}
|
|
main {
|
|
max-width: 560px;
|
|
margin: 0 auto;
|
|
padding: 48px 20px 64px;
|
|
}
|
|
h1 { color: var(--bright); font-size: 26px; margin-bottom: 6px; }
|
|
h2 { color: var(--bright); font-size: 18px; margin: 20px 0 8px; }
|
|
.lede { color: var(--dim); margin-bottom: 28px; font-size: 14px; }
|
|
|
|
form { display: flex; flex-direction: column; gap: 16px; }
|
|
.field { display: flex; flex-direction: column; gap: 6px; }
|
|
label { font-size: 12px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
|
|
input, textarea, select {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
color: var(--bright);
|
|
padding: 10px 12px;
|
|
border-radius: var(--radius);
|
|
font: inherit;
|
|
font-size: 14px;
|
|
outline: none;
|
|
transition: border-color .15s;
|
|
}
|
|
input:focus, textarea:focus, select:focus { border-color: var(--accent); }
|
|
textarea { resize: vertical; min-height: 140px; font-family: inherit; }
|
|
.hint { font-size: 12px; color: var(--dim); }
|
|
|
|
button[type="submit"], .btn {
|
|
appearance: none;
|
|
background: var(--accent);
|
|
color: #001526;
|
|
border: none;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
padding: 12px 20px;
|
|
border-radius: var(--radius);
|
|
cursor: pointer;
|
|
transition: filter .15s, transform .05s;
|
|
}
|
|
button[type="submit"]:hover, .btn:hover { filter: brightness(1.1); }
|
|
button[type="submit"]:active, .btn:active { transform: translateY(1px); }
|
|
|
|
.honey {
|
|
position: absolute;
|
|
left: -9999px;
|
|
width: 1px; height: 1px;
|
|
overflow: hidden;
|
|
opacity: 0;
|
|
}
|
|
|
|
.banner {
|
|
padding: 12px 16px;
|
|
border-radius: var(--radius);
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
margin-bottom: 18px;
|
|
font-size: 13px;
|
|
}
|
|
.banner.ok { border-color: var(--ok); color: var(--ok); }
|
|
.banner.err { border-color: var(--danger); color: var(--danger); }
|
|
.issue-id {
|
|
font-family: ui-monospace, 'JetBrains Mono', monospace;
|
|
background: rgba(56, 189, 248, 0.12);
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
color: var(--accent);
|
|
}
|
|
a { color: var(--accent); }
|
|
|
|
/* Status / reply cards */
|
|
.ticket-header { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; margin-bottom: 16px; }
|
|
.ticket-header h2 { margin: 0 0 4px; font-size: 15px; }
|
|
.ticket-meta { color: var(--dim); font-size: 12px; }
|
|
.reply {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 14px 16px;
|
|
margin-bottom: 10px;
|
|
white-space: pre-wrap;
|
|
}
|
|
.reply-time { font-size: 11px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
|
|
|
|
.setup-note {
|
|
background: rgba(74, 222, 128, 0.12);
|
|
border: 1px solid var(--ok);
|
|
color: var(--ok);
|
|
padding: 14px 16px;
|
|
border-radius: var(--radius);
|
|
font-family: ui-monospace, monospace;
|
|
font-size: 13px;
|
|
word-break: break-all;
|
|
}
|