feat(ntfy): add admin Notifications settings page with test button
This commit is contained in:
89
app/views/admin/notifications/show.html.erb
Normal file
89
app/views/admin/notifications/show.html.erb
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user