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>
266 lines
8.5 KiB
CSS
266 lines
8.5 KiB
CSS
/* Support overlay. Full-screen like chat / email / canvas. Dark chrome,
|
|
* two-column layout on desktop, stacked on narrow. Inbox left, ticket
|
|
* detail + reply composer + KB picker on the right. */
|
|
|
|
.support-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 90;
|
|
background: var(--t-bg);
|
|
display: none;
|
|
flex-direction: column;
|
|
}
|
|
.support-overlay.open { display: flex; }
|
|
|
|
.support-header {
|
|
flex: 0 0 auto;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 14px;
|
|
padding: 10px 16px;
|
|
border-bottom: 1px solid var(--t-border);
|
|
background: var(--t-surface);
|
|
}
|
|
.support-header-left, .support-header-right { display: flex; align-items: center; gap: 10px; }
|
|
.support-header i { font-size: 16px; color: var(--t-accent); }
|
|
|
|
.support-btn {
|
|
background: transparent;
|
|
border: 1px solid var(--t-border);
|
|
color: var(--t-muted);
|
|
cursor: pointer;
|
|
border-radius: 3px;
|
|
padding: 6px 10px;
|
|
font-size: 12px;
|
|
font-family: var(--t-font-pixel, 'Silkscreen', cursive);
|
|
transition: all 0.12s;
|
|
}
|
|
.support-btn:hover { border-color: var(--t-accent); color: var(--t-accent); }
|
|
.support-btn-primary {
|
|
background: var(--t-accent);
|
|
color: var(--t-bg);
|
|
border-color: var(--t-accent);
|
|
}
|
|
.support-btn-primary:hover { filter: brightness(1.08); color: var(--t-bg); }
|
|
.support-btn-danger:hover { border-color: var(--t-danger); color: var(--t-danger); }
|
|
.support-btn-ghost {
|
|
width: 30px; height: 30px; padding: 0;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.support-filter { display: flex; gap: 4px; }
|
|
.support-pill {
|
|
background: transparent;
|
|
border: 1px solid var(--t-border);
|
|
color: var(--t-muted);
|
|
font-family: var(--t-font-pixel, 'Silkscreen', cursive);
|
|
font-size: 9px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
padding: 4px 10px;
|
|
cursor: pointer;
|
|
border-radius: 3px;
|
|
transition: all 0.12s;
|
|
}
|
|
.support-pill:hover { color: var(--t-bright); }
|
|
.support-pill.active { background: var(--t-accent); border-color: var(--t-accent); color: var(--t-bg); }
|
|
|
|
.support-body {
|
|
flex: 1;
|
|
display: grid;
|
|
grid-template-columns: 320px 1fr;
|
|
min-height: 0;
|
|
}
|
|
@media (max-width: 800px) {
|
|
.support-body { grid-template-columns: 1fr; }
|
|
.support-list { max-height: 40vh; }
|
|
.support-list.hidden-mobile { display: none; }
|
|
}
|
|
|
|
.support-list {
|
|
border-right: 1px solid var(--t-border);
|
|
overflow-y: auto;
|
|
background: var(--t-bg);
|
|
}
|
|
.support-ticket {
|
|
padding: 12px 14px;
|
|
border-bottom: 1px solid var(--t-border);
|
|
cursor: pointer;
|
|
transition: background 0.1s;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
.support-ticket:hover { background: var(--t-surface); }
|
|
.support-ticket.active { background: color-mix(in srgb, var(--t-accent) 12%, transparent); border-left: 3px solid var(--t-accent); padding-left: 11px; }
|
|
.support-ticket-row { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
|
|
.support-ticket-issue {
|
|
font-family: var(--t-font-pixel, 'Silkscreen', cursive);
|
|
font-size: 9px;
|
|
color: var(--t-accent);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.support-ticket-status {
|
|
font-family: var(--t-font-pixel, 'Silkscreen', cursive);
|
|
font-size: 8px;
|
|
padding: 1px 5px;
|
|
border-radius: 2px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.support-ticket-status[data-status="open"] { background: rgba(251, 146, 60, 0.2); color: #fb923c; }
|
|
.support-ticket-status[data-status="answered"] { background: rgba(74, 222, 128, 0.15); color: #4ade80; }
|
|
.support-ticket-status[data-status="closed"] { background: rgba(100, 116, 139, 0.2); color: var(--t-dim); }
|
|
.support-ticket-title { color: var(--t-bright); font-size: 13px; line-height: 1.3; }
|
|
.support-ticket-meta { color: var(--t-dim); font-size: 11px; display: flex; gap: 8px; }
|
|
|
|
.support-detail {
|
|
overflow-y: auto;
|
|
padding: 20px 24px 40px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
}
|
|
.support-empty, .support-empty-hero {
|
|
color: var(--t-dim);
|
|
text-align: center;
|
|
padding: 40px 20px;
|
|
font-size: 13px;
|
|
}
|
|
.support-empty-hero i { font-size: 48px; display: block; margin: 0 auto 12px; color: var(--t-muted); }
|
|
|
|
.support-ticket-header h2 { font-family: var(--t-font-pixel); color: var(--t-bright); font-size: 14px; margin: 0 0 4px; }
|
|
.support-ticket-header .meta { color: var(--t-dim); font-size: 11px; display: flex; gap: 12px; flex-wrap: wrap; }
|
|
.support-ticket-header .email { color: var(--t-text); }
|
|
|
|
.support-thread { display: flex; flex-direction: column; gap: 10px; }
|
|
.support-msg {
|
|
background: var(--t-surface);
|
|
border: 1px solid var(--t-border);
|
|
border-radius: 4px;
|
|
padding: 12px 14px;
|
|
white-space: pre-wrap;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
.support-msg[data-dir="out"] {
|
|
border-left: 3px solid var(--t-accent);
|
|
padding-left: 11px;
|
|
}
|
|
.support-msg[data-dir="in"] {
|
|
border-left: 3px solid #fb923c;
|
|
padding-left: 11px;
|
|
}
|
|
.support-msg-label {
|
|
font-family: var(--t-font-pixel);
|
|
font-size: 9px;
|
|
color: var(--t-dim);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 6px;
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
.support-msg-label .tag { color: var(--t-accent); }
|
|
.support-msg-label[data-dir="in"] .tag { color: #fb923c; }
|
|
|
|
/* Composer */
|
|
.support-composer {
|
|
background: var(--t-surface);
|
|
border: 1px solid var(--t-border);
|
|
border-radius: 4px;
|
|
padding: 12px 14px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
.support-composer-head {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
.support-composer-head .font-pixel { font-family: var(--t-font-pixel); font-size: 10px; color: var(--t-accent); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
.support-kb-search {
|
|
flex: 1;
|
|
background: var(--t-bg);
|
|
border: 1px solid var(--t-border);
|
|
color: var(--t-bright);
|
|
padding: 6px 10px;
|
|
border-radius: 3px;
|
|
font-size: 12px;
|
|
font-family: var(--t-font-content, monospace);
|
|
outline: none;
|
|
}
|
|
.support-kb-search:focus { border-color: var(--t-accent); }
|
|
|
|
.support-kb-results {
|
|
display: none;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
background: var(--t-bg);
|
|
border: 1px solid var(--t-border);
|
|
border-radius: 3px;
|
|
}
|
|
.support-kb-results.open { display: block; }
|
|
.support-kb-result {
|
|
padding: 8px 12px;
|
|
border-bottom: 1px solid var(--t-border);
|
|
cursor: pointer;
|
|
transition: background 0.1s;
|
|
}
|
|
.support-kb-result:hover { background: var(--t-surface); }
|
|
.support-kb-result:last-child { border-bottom: none; }
|
|
.support-kb-result-title { color: var(--t-bright); font-size: 12px; font-weight: 600; }
|
|
.support-kb-result-preview { color: var(--t-dim); font-size: 11px; margin-top: 2px; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
|
|
|
.support-composer textarea {
|
|
background: var(--t-bg);
|
|
border: 1px solid var(--t-border);
|
|
color: var(--t-bright);
|
|
padding: 10px 12px;
|
|
border-radius: 3px;
|
|
font-family: var(--t-font-content, ui-monospace, monospace);
|
|
font-size: 13px;
|
|
min-height: 140px;
|
|
resize: vertical;
|
|
outline: none;
|
|
}
|
|
.support-composer textarea:focus { border-color: var(--t-accent); }
|
|
|
|
.support-composer-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
justify-content: flex-end;
|
|
flex-wrap: wrap;
|
|
}
|
|
.support-composer-actions .spacer { flex: 1; }
|
|
.support-composer-status { font-size: 11px; color: var(--t-dim); align-self: center; margin-right: auto; }
|
|
.support-composer-status.err { color: var(--t-danger); }
|
|
.support-composer-status.ok { color: #4ade80; }
|
|
|
|
/* Settings tab (injected into the main settings overlay) */
|
|
.support-settings-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
|
|
.support-settings-field label {
|
|
font-family: var(--t-font-pixel, 'Silkscreen', cursive);
|
|
font-size: 9px;
|
|
color: var(--t-dim);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.support-settings-field input, .support-settings-field select {
|
|
background: var(--t-bg);
|
|
border: 1px solid var(--t-border);
|
|
color: var(--t-bright);
|
|
padding: 8px 10px;
|
|
border-radius: 3px;
|
|
font-family: var(--t-font-content, monospace);
|
|
font-size: 12px;
|
|
outline: none;
|
|
}
|
|
.support-settings-field input:focus { border-color: var(--t-accent); }
|
|
.support-settings-help { font-size: 10px; color: var(--t-dim); line-height: 1.5; }
|
|
.support-settings-row { display: flex; gap: 8px; align-items: center; margin-top: 12px; }
|