Adds a Notifications tab to Settings so an operator can pick which events reach their phone, published to a single self-hosted ntfy topic. The notifier subscribes to the realtime event hub the dashboard already consumes, so every notifiable event keeps exactly one publisher. Inbound media had no event, so `media.received` is added and published from the existing durable `gotImage` gate in the chat service — one notification per conversation, never repeated when the room re-syncs. It feeds the in-app notification bell as well. - Best-effort delivery: a rejecting or unreachable ntfy server is logged and never propagated into the path that produced the event. - Events buffered before the notifier attaches are skipped, so a restart cannot replay a backlog to somebody's phone. - Settings are applied field by field onto the live config object, so a save takes effect without a restart. - The access token is environment-owned (NTFY_AUTH_TOKEN) like the other secrets: stripped before the settings record is written, never returned by the API, which reports only `hasAuthToken`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
76 lines
4.3 KiB
CSS
76 lines
4.3 KiB
CSS
.settings-page { max-width: 72rem; }
|
|
.settings-navigation { display: grid; grid-template-columns: 1fr; gap: .5rem; margin-bottom: 1rem; }
|
|
.settings-navigation button { display: grid; gap: .15rem; padding: .8rem; color: var(--muted); text-align: left; background: var(--surface); border: 1px solid var(--border); border-radius: .75rem; }
|
|
.settings-navigation button strong { color: #e9f1ec; }
|
|
.settings-navigation button small { font-size: .72rem; }
|
|
.settings-navigation button.is-active { background: var(--accent-dark); border-color: #28593d; }
|
|
.settings-navigation button:focus-visible, .settings-field input:focus-visible, .settings-field select:focus-visible, .settings-field textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
.settings-loading { min-height: 24rem; }
|
|
.settings-section { display: grid; gap: 1rem; }
|
|
.settings-copy { color: var(--muted); }
|
|
.settings-grid { display: grid; gap: 1rem; }
|
|
.settings-field { display: grid; gap: .4rem; color: var(--muted); font-size: .8rem; font-weight: 750; }
|
|
.settings-field input, .settings-field select, .settings-field textarea { width: 100%; min-height: 2.7rem; padding: .55rem .7rem; color: #e9f1ec; background: var(--surface-raised); border: 1px solid var(--border); border-radius: .6rem; }
|
|
.settings-field textarea { resize: vertical; }
|
|
.settings-field [aria-invalid="true"] { border-color: var(--danger); }
|
|
.field-error, .save-error { color: var(--danger); }
|
|
.toggle-field { display: flex; align-items: flex-start; gap: .8rem; padding: .9rem; background: var(--surface-raised); border: 1px solid var(--border); border-radius: .75rem; }
|
|
.toggle-field input { width: 1.15rem; height: 1.15rem; margin-top: .15rem; accent-color: var(--accent); }
|
|
.toggle-field span { display: grid; gap: .2rem; }
|
|
.toggle-field small { color: var(--muted); }
|
|
.settings-events { display: grid; gap: .6rem; padding: 1rem; border: 1px solid var(--border); border-radius: .75rem; }
|
|
.settings-events legend { padding: 0 .4rem; color: var(--accent); font-weight: 800; }
|
|
|
|
.subreddit-heading { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 1rem; padding-top: .5rem; }
|
|
.subreddit-heading h3 { margin: 0 0 .25rem; }
|
|
.subreddit-heading p { margin: 0; color: var(--muted); }
|
|
.subreddit-list { display: grid; gap: .75rem; }
|
|
.subreddit-row { display: grid; gap: .75rem; min-width: 0; padding: 1rem; border: 1px solid var(--border); border-radius: .75rem; }
|
|
.subreddit-row legend { padding: 0 .4rem; color: var(--accent); font-weight: 800; }
|
|
.remove-subreddit { align-self: end; }
|
|
.settings-save-bar { position: sticky; bottom: 1rem; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1rem; padding: .9rem 1rem; margin-top: 1rem; background: rgba(23, 35, 30, .97); border: 1px solid var(--border); border-radius: .8rem; box-shadow: 0 1rem 3rem rgba(0, 0, 0, .3); }
|
|
.settings-save-bar > div:first-child { display: grid; gap: .15rem; color: var(--muted); }
|
|
.save-success { color: var(--accent); }
|
|
.save-warning { color: var(--warning); }
|
|
.risk-list { display: grid; gap: .5rem; padding-left: 1.2rem; color: var(--warning); }
|
|
.access-actions { display: flex; flex-wrap: wrap; gap: .75rem; }
|
|
.access-actions a { display: inline-flex; align-items: center; text-decoration: none; }
|
|
.settings-notice { padding: .75rem; border-radius: .65rem; }
|
|
.settings-notice.success { color: var(--success-text); background: var(--success-bg); }
|
|
.settings-notice.warning { color: var(--warning-text); background: var(--warning-bg); }
|
|
.settings-notice.error { color: var(--danger-text); background: var(--danger-bg); }
|
|
|
|
@media (min-width: 42rem) {
|
|
.settings-navigation { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
.settings-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
.subreddit-row { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
.scripted-field { grid-column: 1 / -1; }
|
|
}
|
|
|
|
@media (min-width: 70rem) {
|
|
.settings-navigation { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
|
}
|
|
.reddit-account-list {
|
|
display: grid;
|
|
gap: .65rem;
|
|
}
|
|
|
|
.reddit-account-row {
|
|
align-items: center;
|
|
border: 1px solid var(--border);
|
|
border-radius: .75rem;
|
|
display: flex;
|
|
gap: 1rem;
|
|
justify-content: space-between;
|
|
padding: .75rem;
|
|
}
|
|
|
|
.reddit-account-row.is-active {
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.reddit-account-row > div {
|
|
display: grid;
|
|
gap: .2rem;
|
|
}
|