feat(ntfy): add admin Notifications settings page with test button

This commit is contained in:
Min Zeya Phyo
2026-07-28 02:25:50 +08:00
parent 4236d12a84
commit 9fa6c37297
5 changed files with 302 additions and 4 deletions

View File

@@ -0,0 +1,89 @@
<div class="space-y-6">
<div class="sm:flex sm:items-center sm:justify-between">
<div>
<h1 class="text-3xl font-bold leading-tight tracking-tight text-gray-900">Notification Settings</h1>
<p class="mt-2 text-sm text-gray-600">Configure ntfy push notifications for your phone.</p>
</div>
</div>
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 overflow-hidden">
<div class="px-6 py-5 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900 flex items-center gap-2">
<i class="fas fa-bell text-blue-500"></i>
ntfy Configuration
</h2>
<p class="mt-1 text-sm text-gray-500">
Install the <a href="https://ntfy.sh" class="text-blue-600 hover:underline" target="_blank" rel="noopener">ntfy app</a>
on your phone, subscribe to your topic, and enter the details below.
</p>
</div>
<div class="px-6 py-6">
<%= form_with model: @admin, url: admin_notifications_path, method: :patch, local: true, class: "space-y-6" do |f| %>
<div class="flex items-center gap-3">
<%= f.check_box :ntfy_enabled, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
<%= f.label :ntfy_enabled, "Enable ntfy notifications", class: "text-sm font-medium text-gray-700" %>
</div>
<div>
<%= f.label :ntfy_topic, "ntfy Topic", class: "block text-sm font-medium text-gray-700" %>
<p class="mt-1 text-xs text-gray-500">The topic name you subscribe to in the ntfy app. Treat this like a password.</p>
<%= f.text_field :ntfy_topic,
class: "mt-2 block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm py-2.5",
placeholder: "mysmsa-pio-your-secret-topic" %>
</div>
<div>
<%= f.label :ntfy_token, "Access Token (optional)", class: "block text-sm font-medium text-gray-700" %>
<p class="mt-1 text-xs text-gray-500">Required only if your ntfy server has access control enabled.</p>
<%= f.text_field :ntfy_token,
class: "mt-2 block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm py-2.5",
placeholder: "tk_..." %>
</div>
<div>
<%= f.label :ntfy_server_url, "ntfy Server URL", class: "block text-sm font-medium text-gray-700" %>
<p class="mt-1 text-xs text-gray-500">Leave blank to use the default (from NTFY_SERVER_URL env var or ntfy.sh).</p>
<%= f.text_field :ntfy_server_url,
class: "mt-2 block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm py-2.5",
placeholder: "https://ntfy.yourdomain.com" %>
</div>
<div class="pt-2">
<%= f.submit "Save Settings",
class: "inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 transition-all" %>
</div>
<% end %>
</div>
</div>
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 overflow-hidden">
<div class="px-6 py-5 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900 flex items-center gap-2">
<i class="fas fa-paper-plane text-green-500"></i>
Send Test Notification
</h2>
<p class="mt-1 text-sm text-gray-500">Sends a test push to your configured ntfy topic right now.</p>
</div>
<div class="px-6 py-6">
<%= button_to test_admin_notifications_path, method: :post,
class: "inline-flex items-center gap-2 rounded-lg bg-green-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-green-500 transition-all" do %>
<i class="fas fa-paper-plane"></i>
Send Test Notification
<% end %>
</div>
</div>
<div class="rounded-xl bg-blue-50 p-6 ring-1 ring-blue-900/5">
<h3 class="text-sm font-semibold text-blue-900 flex items-center gap-2">
<i class="fas fa-info-circle"></i>
How ntfy works
</h3>
<ul class="mt-3 space-y-2 text-sm text-blue-800">
<li class="flex gap-2"><i class="fas fa-check mt-1 text-xs"></i> Self-hosted ntfy server runs alongside this app in production.</li>
<li class="flex gap-2"><i class="fas fa-check mt-1 text-xs"></i> Each admin gets their own topic — subscribe in the ntfy app to receive pushes.</li>
<li class="flex gap-2"><i class="fas fa-check mt-1 text-xs"></i> Events: gateway offline/online, SMS failed/delivered, API key revoked.</li>
<li class="flex gap-2"><i class="fas fa-check mt-1 text-xs"></i> Notifications are sent asynchronously and retried on failure.</li>
</ul>
</div>
</div>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" class="h-full bg-gray-50">
<html lang="en" class="h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
@@ -12,6 +12,101 @@
<%= javascript_importmap_tags %>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
<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=Syne:wght@600;700;800&family=Outfit:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--accent: #38bdf8;
--accent-deep: #0ea5e9;
}
.font-display { font-family: 'Syne', sans-serif; }
.font-body { font-family: 'Outfit', sans-serif; }
.font-mono-tech { font-family: 'JetBrains Mono', ui-monospace, monospace; }
body { font-family: 'Outfit', sans-serif; }
/* Login immersive background */
.login-stage {
position: relative;
background: radial-gradient(ellipse 80% 60% at 50% 0%, #0f172a 0%, #020617 55%, #000 100%);
overflow: hidden;
}
.login-stage::before {
content: "";
position: absolute; inset: 0;
background-image:
linear-gradient(rgba(56, 189, 248, 0.07) 1px, transparent 1px),
linear-gradient(90deg, rgba(56, 189, 248, 0.07) 1px, transparent 1px);
background-size: 46px 46px;
-webkit-mask-image: radial-gradient(ellipse at center, black 20%, transparent 72%);
mask-image: radial-gradient(ellipse at center, black 20%, transparent 72%);
pointer-events: none;
}
.login-stage::after {
content: "";
position: absolute; inset: 0;
background:
radial-gradient(circle at 12% 18%, rgba(56, 189, 248, 0.14), transparent 42%),
radial-gradient(circle at 88% 82%, rgba(14, 165, 233, 0.11), transparent 42%);
pointer-events: none;
}
.login-card {
background: rgba(255, 255, 255, 0.035);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.09);
box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.08), 0 30px 80px -20px rgba(0, 0, 0, 0.7);
}
.beacon-ring {
animation: beacon-pulse 2.4s ease-out infinite;
}
.beacon-ring.delay-1 { animation-delay: 0.8s; }
.beacon-ring.delay-2 { animation-delay: 1.6s; }
@keyframes beacon-pulse {
0% { transform: scale(0.6); opacity: 0.55; }
80% { opacity: 0; }
100% { transform: scale(1.8); opacity: 0; }
}
.field-input {
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(148, 163, 184, 0.18);
color: #e2e8f0;
transition: border-color .18s ease, box-shadow .18s ease, background .18s ease;
}
.field-input::placeholder { color: #64748b; }
.field-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.18);
background: rgba(15, 23, 42, 0.85);
}
.btn-accent {
background: linear-gradient(135deg, #38bdf8 0%, #0ea5e9 100%);
box-shadow: 0 10px 30px -10px rgba(56, 189, 248, 0.55), inset 0 1px 0 rgba(255,255,255,0.25);
transition: transform .15s ease, box-shadow .2s ease, filter .2s ease;
}
.btn-accent:hover { transform: translateY(-1px); filter: brightness(1.08); box-shadow: 0 16px 40px -10px rgba(56, 189, 248, 0.7), inset 0 1px 0 rgba(255,255,255,0.3); }
.btn-accent:active { transform: translateY(0); }
@keyframes fade-up {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-up { animation: fade-up .5s cubic-bezier(0.16, 1, 0.3, 1) both; }
.fade-up.d-1 { animation-delay: .06s; }
.fade-up.d-2 { animation-delay: .14s; }
.fade-up.d-3 { animation-delay: .22s; }
.fade-up.d-4 { animation-delay: .30s; }
@media (prefers-reduced-motion: reduce) {
.beacon-ring, .fade-up { animation: none; }
}
</style>
</head>
<body class="h-full">
@@ -54,6 +149,11 @@
<i class="fas fa-vial w-6 h-6 shrink-0 flex items-center justify-center"></i>
API Tester
<% end %>
<%= link_to admin_notifications_path, class: "group flex gap-x-3 rounded-md p-3 text-sm leading-6 font-semibold transition-all duration-200 #{current_page?(admin_notifications_path) ? 'bg-gray-700 text-white' : 'text-gray-300 hover:text-white hover:bg-gray-700'}" do %>
<i class="fas fa-bell w-6 h-6 shrink-0 flex items-center justify-center"></i>
Notifications
<% end %>
</ul>
</li>
<li class="mt-auto">
@@ -115,9 +215,24 @@
</div>
</div>
<% else %>
<!-- Login page without sidebar -->
<div class="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<%= yield %>
<!-- Login stage (not logged in) — flash IS rendered here -->
<div class="login-stage min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="relative w-full max-w-md">
<% if flash[:alert] %>
<div class="fade-up mb-6 flex items-start gap-3 rounded-xl border border-red-400/30 bg-red-500/10 p-4 backdrop-blur-md">
<i class="fas fa-exclamation-circle mt-0.5 text-red-400"></i>
<p class="text-sm font-medium text-red-100"><%= flash[:alert] %></p>
</div>
<% end %>
<% if flash[:notice] %>
<div class="fade-up mb-6 flex items-start gap-3 rounded-xl border border-emerald-400/30 bg-emerald-500/10 p-4 backdrop-blur-md">
<i class="fas fa-check-circle mt-0.5 text-emerald-400"></i>
<p class="text-sm font-medium text-emerald-100"><%= flash[:notice] %></p>
</div>
<% end %>
<%= yield %>
</div>
</div>
<% end %>
</body>