completed SMS gateway project
This commit is contained in:
101
app/views/admin/api_keys/index.html.erb
Normal file
101
app/views/admin/api_keys/index.html.erb
Normal file
@@ -0,0 +1,101 @@
|
||||
<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">API Keys</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">Manage API keys for client access and authentication</p>
|
||||
</div>
|
||||
<%= link_to new_admin_api_key_path, class: "mt-4 sm:mt-0 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 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition-all" do %>
|
||||
<i class="fas fa-plus"></i>
|
||||
Create New API Key
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5">
|
||||
<% if @api_keys.any? %>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Name</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Key Prefix</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Permissions</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Status</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Last Used</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Created</th>
|
||||
<th scope="col" class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wide text-gray-500">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
<% @api_keys.each do |api_key| %>
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900"><%= api_key.name %></div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<code class="rounded bg-gray-100 px-2 py-1 text-xs font-mono text-gray-800"><%= api_key.key_prefix %>...</code>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<% perms = api_key.permissions || {} %>
|
||||
<% perms = {} unless perms.is_a?(Hash) %>
|
||||
<% if perms.any? %>
|
||||
<% perms.select { |_, v| v }.keys.each do |perm| %>
|
||||
<span class="inline-flex rounded-full bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">
|
||||
<%= perm.to_s.humanize %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<span class="inline-flex rounded-full bg-gray-50 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-700/10">None</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<% if api_key.active_and_valid? %>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-green-500"></span>
|
||||
Active
|
||||
</span>
|
||||
<% elsif !api_key.active %>
|
||||
<span class="inline-flex rounded-full bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-700/10">Revoked</span>
|
||||
<% else %>
|
||||
<span class="inline-flex rounded-full bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-yellow-700/10">Expired</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
<% if api_key.last_used_at %>
|
||||
<%= time_ago_in_words(api_key.last_used_at) %> ago
|
||||
<% else %>
|
||||
Never
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
<%= api_key.created_at.strftime("%Y-%m-%d") %>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right text-sm font-medium">
|
||||
<% if api_key.active %>
|
||||
<%= button_to admin_api_key_path(api_key), method: :delete, class: "inline-flex items-center gap-1 text-red-600 hover:text-red-900", data: { confirm: "Are you sure you want to revoke this API key?" } do %>
|
||||
<i class="fas fa-ban"></i>
|
||||
Revoke
|
||||
<% end %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="px-6 py-14 text-center">
|
||||
<i class="fas fa-key text-4xl text-gray-300"></i>
|
||||
<h3 class="mt-4 text-sm font-semibold text-gray-900">No API keys</h3>
|
||||
<p class="mt-2 text-sm text-gray-500">Get started by creating a new API key.</p>
|
||||
<div class="mt-6">
|
||||
<%= link_to new_admin_api_key_path, 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" do %>
|
||||
<i class="fas fa-plus"></i>
|
||||
Create API Key
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
105
app/views/admin/api_keys/new.html.erb
Normal file
105
app/views/admin/api_keys/new.html.erb
Normal file
@@ -0,0 +1,105 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Page header with back link -->
|
||||
<div class="flex items-center gap-4">
|
||||
<%= link_to admin_api_keys_path, class: "inline-flex items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors" do %>
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to API Keys
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="border-b border-gray-200 pb-5">
|
||||
<h1 class="text-3xl font-bold leading-tight tracking-tight text-gray-900">Create New API Key</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">Generate a new API key with specific permissions for your application.</p>
|
||||
</div>
|
||||
|
||||
<!-- Form card -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 px-6 py-8 max-w-2xl">
|
||||
<%= form_with url: admin_api_keys_path, method: :post, local: true, class: "space-y-6" do |f| %>
|
||||
<!-- Name field -->
|
||||
<div>
|
||||
<%= label_tag "api_key[name]", "Name", class: "block text-sm font-medium text-gray-700" %>
|
||||
<div class="mt-1 relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<i class="fas fa-tag text-gray-400"></i>
|
||||
</div>
|
||||
<%= text_field_tag "api_key[name]", nil,
|
||||
class: "block w-full pl-10 rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm py-3",
|
||||
placeholder: "My Application",
|
||||
required: true %>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-500">A descriptive name to identify this API key</p>
|
||||
</div>
|
||||
|
||||
<!-- Permissions checkboxes -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-3">Permissions</label>
|
||||
<div class="space-y-3">
|
||||
<label class="relative flex items-start">
|
||||
<div class="flex items-center h-6">
|
||||
<%= check_box_tag "api_key[send_sms]", "1", true,
|
||||
class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600" %>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-gray-900">Send SMS</span>
|
||||
<p class="text-sm text-gray-500">Allow sending outbound SMS messages</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="relative flex items-start">
|
||||
<div class="flex items-center h-6">
|
||||
<%= check_box_tag "api_key[receive_sms]", "1", true,
|
||||
class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600" %>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-gray-900">Receive SMS</span>
|
||||
<p class="text-sm text-gray-500">Allow receiving and querying inbound SMS</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="relative flex items-start">
|
||||
<div class="flex items-center h-6">
|
||||
<%= check_box_tag "api_key[manage_gateways]", "1", false,
|
||||
class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600" %>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-gray-900">Manage Gateways</span>
|
||||
<p class="text-sm text-gray-500">Allow managing gateway devices and settings</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="relative flex items-start">
|
||||
<div class="flex items-center h-6">
|
||||
<%= check_box_tag "api_key[manage_otp]", "1", true,
|
||||
class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600" %>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-gray-900">Manage OTP</span>
|
||||
<p class="text-sm text-gray-500">Allow generating and verifying OTP codes</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expiration field -->
|
||||
<div>
|
||||
<%= label_tag "api_key[expires_at]", "Expiration Date (Optional)", class: "block text-sm font-medium text-gray-700" %>
|
||||
<div class="mt-1 relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<i class="fas fa-calendar text-gray-400"></i>
|
||||
</div>
|
||||
<%= datetime_local_field_tag "api_key[expires_at]", nil,
|
||||
class: "block w-full pl-10 rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm py-3" %>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-500">Leave empty for no expiration</p>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center gap-3 pt-4">
|
||||
<%= submit_tag "Create API Key",
|
||||
class: "inline-flex justify-center items-center gap-2 rounded-lg bg-blue-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition-all duration-200" %>
|
||||
<%= link_to "Cancel", admin_api_keys_path,
|
||||
class: "inline-flex justify-center items-center gap-2 rounded-lg bg-gray-100 px-6 py-3 text-sm font-semibold text-gray-700 hover:bg-gray-200 transition-all duration-200" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
139
app/views/admin/api_keys/show.html.erb
Normal file
139
app/views/admin/api_keys/show.html.erb
Normal file
@@ -0,0 +1,139 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Page header -->
|
||||
<div class="border-b border-gray-200 pb-5">
|
||||
<h1 class="text-3xl font-bold leading-tight tracking-tight text-gray-900">API Key Created Successfully!</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">Your new API key has been generated and is ready to use.</p>
|
||||
</div>
|
||||
|
||||
<!-- Warning alert -->
|
||||
<div class="rounded-lg bg-yellow-50 px-4 py-4 ring-1 ring-yellow-600/10">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-600 text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-yellow-800">Important: Save this key now!</h3>
|
||||
<p class="mt-1 text-sm text-yellow-700">
|
||||
This is the only time you'll be able to see the full API key. Make sure to copy it and store it securely.
|
||||
If you lose it, you'll need to generate a new key.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Key display card -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 px-6 py-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Your New API Key</h3>
|
||||
<button
|
||||
onclick="copyToClipboard('<%= @raw_key %>')"
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 transition-all duration-200">
|
||||
<i class="fas fa-copy"></i>
|
||||
Copy to Clipboard
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="relative rounded-lg bg-gray-900 px-4 py-4 overflow-x-auto">
|
||||
<code class="text-sm font-mono text-green-400 break-all"><%= @raw_key %></code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Key Details card -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 px-6 py-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">API Key Details</h3>
|
||||
|
||||
<dl class="divide-y divide-gray-100">
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Name</dt>
|
||||
<dd class="mt-1 text-sm text-gray-700 sm:col-span-2 sm:mt-0"><%= @api_key.name %></dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Key Prefix</dt>
|
||||
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">
|
||||
<code class="rounded bg-gray-100 px-2 py-1 text-xs font-mono text-gray-800"><%= @api_key.key_prefix %>...</code>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Permissions</dt>
|
||||
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<% perms = @api_key.permissions || {} %>
|
||||
<% perms = {} unless perms.is_a?(Hash) %>
|
||||
<% perms.select { |_, v| v }.keys.each do |perm| %>
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">
|
||||
<i class="fas fa-check"></i>
|
||||
<%= perm.to_s.humanize %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Expiration</dt>
|
||||
<dd class="mt-1 text-sm text-gray-700 sm:col-span-2 sm:mt-0">
|
||||
<% if @api_key.expires_at %>
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fas fa-calendar text-gray-400"></i>
|
||||
<%= @api_key.expires_at.strftime("%B %d, %Y at %l:%M %p") %>
|
||||
</div>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10">
|
||||
<i class="fas fa-infinity"></i>
|
||||
Never expires
|
||||
</span>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Status</dt>
|
||||
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-green-500"></span>
|
||||
Active
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Created</dt>
|
||||
<dd class="mt-1 text-sm text-gray-700 sm:col-span-2 sm:mt-0">
|
||||
<%= @api_key.created_at.strftime("%B %d, %Y at %l:%M %p") %>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- Back link -->
|
||||
<div class="flex items-center gap-3">
|
||||
<%= link_to admin_api_keys_path,
|
||||
class: "inline-flex items-center gap-2 rounded-lg bg-gray-100 px-6 py-3 text-sm font-semibold text-gray-700 hover:bg-gray-200 transition-all duration-200" do %>
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to API Keys
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
// Show success feedback
|
||||
const button = event.target.closest('button');
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-check"></i> Copied!';
|
||||
button.classList.add('bg-green-600', 'hover:bg-green-500');
|
||||
button.classList.remove('bg-blue-600', 'hover:bg-blue-500');
|
||||
|
||||
setTimeout(function() {
|
||||
button.innerHTML = originalHTML;
|
||||
button.classList.remove('bg-green-600', 'hover:bg-green-500');
|
||||
button.classList.add('bg-blue-600', 'hover:bg-blue-500');
|
||||
}, 2000);
|
||||
}, function(err) {
|
||||
alert('Failed to copy: ' + err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
466
app/views/admin/api_tester/index.html.erb
Normal file
466
app/views/admin/api_tester/index.html.erb
Normal file
@@ -0,0 +1,466 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Page header -->
|
||||
<div class="border-b border-gray-200 pb-5">
|
||||
<h1 class="text-3xl font-bold leading-tight tracking-tight text-gray-900">API Tester</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">Test all API endpoints with interactive forms. View request/response in real-time.</p>
|
||||
</div>
|
||||
|
||||
<!-- API Key Selection Card -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 px-6 py-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Authentication</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Available API Keys (for reference) -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Available API Keys (Reference Only)</label>
|
||||
<div class="rounded-lg bg-gray-50 p-4 space-y-2">
|
||||
<% if @api_keys.any? %>
|
||||
<% @api_keys.each do |key| %>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-700">
|
||||
<i class="fas fa-key text-blue-500"></i> <%= key.name %>
|
||||
</span>
|
||||
<span class="font-mono text-xs text-gray-500"><%= key.key_prefix %>***</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Raw API keys are only shown once during creation for security. Enter your saved API key below.
|
||||
</p>
|
||||
<% else %>
|
||||
<p class="text-sm text-gray-500">No API keys found. Create one first.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Enter API Key -->
|
||||
<div>
|
||||
<label for="api-key-input" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Enter API Key <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="api-key-input"
|
||||
class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono text-xs"
|
||||
placeholder="api_live_... or gw_live_..."
|
||||
required>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Client API keys start with <code class="bg-gray-100 px-1 py-0.5 rounded">api_live_</code>,
|
||||
Gateway keys start with <code class="bg-gray-100 px-1 py-0.5 rounded">gw_live_</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Base URL -->
|
||||
<div>
|
||||
<label for="base-url" class="block text-sm font-medium text-gray-700 mb-2">Base URL</label>
|
||||
<input
|
||||
type="text"
|
||||
id="base-url"
|
||||
class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono text-xs"
|
||||
value="<%= request.base_url %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Endpoint Testing Tabs -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5">
|
||||
<!-- Tab Navigation -->
|
||||
<div class="border-b border-gray-200">
|
||||
<nav class="flex gap-4 px-6 pt-6" aria-label="Tabs">
|
||||
<button onclick="switchTab('send-sms')" id="tab-send-sms" class="tab-button active">
|
||||
<i class="fas fa-paper-plane"></i> Send SMS
|
||||
</button>
|
||||
<button onclick="switchTab('check-status')" id="tab-check-status" class="tab-button">
|
||||
<i class="fas fa-info-circle"></i> Check Status
|
||||
</button>
|
||||
<button onclick="switchTab('send-otp')" id="tab-send-otp" class="tab-button">
|
||||
<i class="fas fa-key"></i> Send OTP
|
||||
</button>
|
||||
<button onclick="switchTab('verify-otp')" id="tab-verify-otp" class="tab-button">
|
||||
<i class="fas fa-check-circle"></i> Verify OTP
|
||||
</button>
|
||||
<button onclick="switchTab('gateway-register')" id="tab-gateway-register" class="tab-button">
|
||||
<i class="fas fa-mobile-alt"></i> Register Gateway
|
||||
</button>
|
||||
<button onclick="switchTab('gateway-heartbeat')" id="tab-gateway-heartbeat" class="tab-button">
|
||||
<i class="fas fa-heartbeat"></i> Heartbeat
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="px-6 py-6">
|
||||
<!-- Send SMS Tab -->
|
||||
<div id="content-send-sms" class="tab-content active">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Send SMS</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">POST /api/v1/sms/send</p>
|
||||
|
||||
<form id="form-send-sms" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Phone Number</label>
|
||||
<input type="tel" name="phone_number" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" placeholder="+959123456789" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Message</label>
|
||||
<textarea name="message" rows="3" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" placeholder="Your message here" required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500">
|
||||
<i class="fas fa-paper-plane"></i> Send SMS
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Check Status Tab -->
|
||||
<div id="content-check-status" class="tab-content">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Check SMS Status</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">GET /api/v1/sms/status/:message_id</p>
|
||||
|
||||
<form id="form-check-status" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Message ID</label>
|
||||
<input type="text" name="message_id" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono text-xs" placeholder="msg_abc123..." required>
|
||||
</div>
|
||||
<button type="submit" class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500">
|
||||
<i class="fas fa-search"></i> Check Status
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Send OTP Tab -->
|
||||
<div id="content-send-otp" class="tab-content">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Send OTP</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">POST /api/v1/otp/send</p>
|
||||
|
||||
<form id="form-send-otp" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Phone Number</label>
|
||||
<input type="tel" name="phone_number" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" placeholder="+959123456789" required>
|
||||
</div>
|
||||
<button type="submit" class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500">
|
||||
<i class="fas fa-key"></i> Send OTP
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Verify OTP Tab -->
|
||||
<div id="content-verify-otp" class="tab-content">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Verify OTP</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">POST /api/v1/otp/verify</p>
|
||||
|
||||
<form id="form-verify-otp" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Phone Number</label>
|
||||
<input type="tel" name="phone_number" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" placeholder="+959123456789" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">OTP Code</label>
|
||||
<input type="text" name="code" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" placeholder="123456" maxlength="6" required>
|
||||
</div>
|
||||
<button type="submit" class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500">
|
||||
<i class="fas fa-check-circle"></i> Verify OTP
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Gateway Register Tab -->
|
||||
<div id="content-gateway-register" class="tab-content">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Register Gateway</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">POST /api/v1/gateway/register</p>
|
||||
|
||||
<form id="form-gateway-register" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Device ID</label>
|
||||
<input type="text" name="device_id" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" placeholder="android-001" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Device Name</label>
|
||||
<input type="text" name="name" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" placeholder="My Phone" required>
|
||||
</div>
|
||||
<button type="submit" class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500">
|
||||
<i class="fas fa-mobile-alt"></i> Register Gateway
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Gateway Heartbeat Tab -->
|
||||
<div id="content-gateway-heartbeat" class="tab-content">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Send Heartbeat</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">POST /api/v1/gateway/heartbeat</p>
|
||||
|
||||
<form id="form-gateway-heartbeat" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Battery Level (%)</label>
|
||||
<input type="number" name="battery_level" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" placeholder="85" min="0" max="100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Signal Strength (0-4)</label>
|
||||
<input type="number" name="signal_strength" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" placeholder="4" min="0" max="4">
|
||||
</div>
|
||||
<button type="submit" class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500">
|
||||
<i class="fas fa-heartbeat"></i> Send Heartbeat
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Request/Response Display -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Request -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 px-6 py-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Request</h3>
|
||||
<button onclick="copyRequest()" class="text-sm text-blue-600 hover:text-blue-500">
|
||||
<i class="fas fa-copy"></i> Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre id="request-display" class="bg-gray-50 rounded-lg p-4 text-xs font-mono text-gray-800 overflow-x-auto max-h-96 overflow-y-auto"><span class="text-gray-400">Request will appear here...</span></pre>
|
||||
</div>
|
||||
|
||||
<!-- Response -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 px-6 py-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Response</h3>
|
||||
<button onclick="copyResponse()" class="text-sm text-blue-600 hover:text-blue-500">
|
||||
<i class="fas fa-copy"></i> Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre id="response-display" class="bg-gray-50 rounded-lg p-4 text-xs font-mono text-gray-800 overflow-x-auto max-h-96 overflow-y-auto"><span class="text-gray-400">Response will appear here...</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Card -->
|
||||
<div class="rounded-lg bg-blue-50 px-4 py-4 ring-1 ring-blue-600/10">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-info-circle text-blue-600 text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-blue-800">API Testing Tips</h3>
|
||||
<ul class="mt-2 text-sm text-blue-700 list-disc list-inside space-y-1">
|
||||
<li>Select an API key from the dropdown or enter a custom key</li>
|
||||
<li>All requests use the Authorization header with Bearer token</li>
|
||||
<li>Gateway endpoints require gateway API keys (gw_live_...)</li>
|
||||
<li>Client endpoints require client API keys (api_live_...)</li>
|
||||
<li>View full request and response in real-time</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tab-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
color: #2563eb;
|
||||
border-bottom-color: #2563eb;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentRequest = '';
|
||||
let currentResponse = '';
|
||||
|
||||
// Tab switching
|
||||
function switchTab(tabName) {
|
||||
// Hide all tab contents
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
// Deactivate all tab buttons
|
||||
document.querySelectorAll('.tab-button').forEach(button => {
|
||||
button.classList.remove('active');
|
||||
});
|
||||
|
||||
// Show selected tab content
|
||||
document.getElementById('content-' + tabName).classList.add('active');
|
||||
document.getElementById('tab-' + tabName).classList.add('active');
|
||||
}
|
||||
|
||||
// Get API key
|
||||
function getApiKey() {
|
||||
const apiKey = document.getElementById('api-key-input').value.trim();
|
||||
if (!apiKey) {
|
||||
alert('Please enter an API key');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate format
|
||||
if (!apiKey.startsWith('api_live_') && !apiKey.startsWith('gw_live_')) {
|
||||
alert('Invalid API key format. Keys should start with "api_live_" or "gw_live_"');
|
||||
return null;
|
||||
}
|
||||
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
// Get base URL
|
||||
function getBaseUrl() {
|
||||
return document.getElementById('base-url').value.trim();
|
||||
}
|
||||
|
||||
// Display request
|
||||
function displayRequest(method, url, headers, body) {
|
||||
let request = `${method} ${url}\n\n`;
|
||||
|
||||
request += 'Headers:\n';
|
||||
Object.entries(headers).forEach(([key, value]) => {
|
||||
request += `${key}: ${value}\n`;
|
||||
});
|
||||
|
||||
if (body) {
|
||||
request += '\nBody:\n';
|
||||
request += JSON.stringify(body, null, 2);
|
||||
}
|
||||
|
||||
currentRequest = request;
|
||||
document.getElementById('request-display').textContent = request;
|
||||
}
|
||||
|
||||
// Display response
|
||||
function displayResponse(status, statusText, data) {
|
||||
let response = `Status: ${status} ${statusText}\n\n`;
|
||||
response += JSON.stringify(data, null, 2);
|
||||
|
||||
currentResponse = response;
|
||||
const displayElement = document.getElementById('response-display');
|
||||
displayElement.textContent = response;
|
||||
|
||||
// Color code based on status
|
||||
if (status >= 200 && status < 300) {
|
||||
displayElement.classList.add('text-green-700');
|
||||
displayElement.classList.remove('text-red-700', 'text-gray-800');
|
||||
} else {
|
||||
displayElement.classList.add('text-red-700');
|
||||
displayElement.classList.remove('text-green-700', 'text-gray-800');
|
||||
}
|
||||
}
|
||||
|
||||
// Copy functions
|
||||
function copyRequest() {
|
||||
navigator.clipboard.writeText(currentRequest);
|
||||
alert('Request copied to clipboard!');
|
||||
}
|
||||
|
||||
function copyResponse() {
|
||||
navigator.clipboard.writeText(currentResponse);
|
||||
alert('Response copied to clipboard!');
|
||||
}
|
||||
|
||||
// Generic API call
|
||||
async function makeApiCall(method, endpoint, body = null) {
|
||||
const apiKey = getApiKey();
|
||||
if (!apiKey) return;
|
||||
|
||||
const baseUrl = getBaseUrl();
|
||||
const url = `${baseUrl}${endpoint}`;
|
||||
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
displayRequest(method, endpoint, headers, body);
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: method,
|
||||
headers: headers
|
||||
};
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(url, options);
|
||||
const data = await response.json();
|
||||
|
||||
displayResponse(response.status, response.statusText, data);
|
||||
} catch (error) {
|
||||
displayResponse(0, 'Error', { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// Form handlers
|
||||
document.getElementById('form-send-sms').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const body = {
|
||||
to: formData.get('phone_number'), // API expects 'to' parameter
|
||||
message: formData.get('message')
|
||||
};
|
||||
await makeApiCall('POST', '/api/v1/sms/send', body);
|
||||
});
|
||||
|
||||
document.getElementById('form-check-status').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const messageId = formData.get('message_id');
|
||||
await makeApiCall('GET', `/api/v1/sms/status/${messageId}`);
|
||||
});
|
||||
|
||||
document.getElementById('form-send-otp').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const body = {
|
||||
phone_number: formData.get('phone_number')
|
||||
};
|
||||
await makeApiCall('POST', '/api/v1/otp/send', body);
|
||||
});
|
||||
|
||||
document.getElementById('form-verify-otp').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const body = {
|
||||
phone_number: formData.get('phone_number'),
|
||||
code: formData.get('code')
|
||||
};
|
||||
await makeApiCall('POST', '/api/v1/otp/verify', body);
|
||||
});
|
||||
|
||||
document.getElementById('form-gateway-register').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const body = {
|
||||
device_id: formData.get('device_id'),
|
||||
name: formData.get('name')
|
||||
};
|
||||
await makeApiCall('POST', '/api/v1/gateway/register', body);
|
||||
});
|
||||
|
||||
document.getElementById('form-gateway-heartbeat').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const body = {
|
||||
device_info: {
|
||||
battery_level: parseInt(formData.get('battery_level')) || 0,
|
||||
signal_strength: parseInt(formData.get('signal_strength')) || 0
|
||||
}
|
||||
};
|
||||
await makeApiCall('POST', '/api/v1/gateway/heartbeat', body);
|
||||
});
|
||||
</script>
|
||||
230
app/views/admin/dashboard/index.html.erb
Normal file
230
app/views/admin/dashboard/index.html.erb
Normal file
@@ -0,0 +1,230 @@
|
||||
<div class="space-y-8">
|
||||
<!-- Page header -->
|
||||
<div class="border-b border-gray-200 pb-5">
|
||||
<h1 class="text-3xl font-bold leading-tight tracking-tight text-gray-900">Dashboard</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">Welcome back! Here's what's happening with your SMS gateway today.</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats grid -->
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<!-- Gateways stat -->
|
||||
<div class="relative overflow-hidden rounded-xl bg-white px-4 py-5 shadow-sm ring-1 ring-gray-900/5 sm:px-6">
|
||||
<dt>
|
||||
<div class="absolute rounded-lg bg-blue-500 p-3">
|
||||
<i class="fas fa-mobile-alt text-xl text-white"></i>
|
||||
</div>
|
||||
<p class="ml-16 truncate text-sm font-medium text-gray-500">Gateways</p>
|
||||
</dt>
|
||||
<dd class="ml-16 flex items-baseline">
|
||||
<p class="text-3xl font-semibold text-gray-900"><%= @stats[:total_gateways] %></p>
|
||||
<p class="ml-2 flex items-baseline text-sm font-semibold text-green-600">
|
||||
<%= @stats[:online_gateways] %> online
|
||||
</p>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<!-- API Keys stat -->
|
||||
<div class="relative overflow-hidden rounded-xl bg-white px-4 py-5 shadow-sm ring-1 ring-gray-900/5 sm:px-6">
|
||||
<dt>
|
||||
<div class="absolute rounded-lg bg-green-500 p-3">
|
||||
<i class="fas fa-key text-xl text-white"></i>
|
||||
</div>
|
||||
<p class="ml-16 truncate text-sm font-medium text-gray-500">API Keys</p>
|
||||
</dt>
|
||||
<dd class="ml-16 flex items-baseline">
|
||||
<p class="text-3xl font-semibold text-gray-900"><%= @stats[:active_api_keys] %></p>
|
||||
<p class="ml-2 flex items-baseline text-sm text-gray-600">
|
||||
of <%= @stats[:total_api_keys] %> total
|
||||
</p>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<!-- Messages Today stat -->
|
||||
<div class="relative overflow-hidden rounded-xl bg-white px-4 py-5 shadow-sm ring-1 ring-gray-900/5 sm:px-6">
|
||||
<dt>
|
||||
<div class="absolute rounded-lg bg-yellow-500 p-3">
|
||||
<i class="fas fa-paper-plane text-xl text-white"></i>
|
||||
</div>
|
||||
<p class="ml-16 truncate text-sm font-medium text-gray-500">Messages Today</p>
|
||||
</dt>
|
||||
<dd class="ml-16 flex items-baseline">
|
||||
<p class="text-3xl font-semibold text-gray-900"><%= @stats[:messages_today] %></p>
|
||||
</dd>
|
||||
<div class="ml-16 mt-1 flex items-center gap-3 text-xs">
|
||||
<span class="inline-flex items-center gap-1 text-green-600">
|
||||
<i class="fas fa-arrow-up"></i> <%= @stats[:messages_sent_today] %> sent
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1 text-blue-600">
|
||||
<i class="fas fa-arrow-down"></i> <%= @stats[:messages_received_today] %> received
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Failed Messages stat -->
|
||||
<div class="relative overflow-hidden rounded-xl bg-white px-4 py-5 shadow-sm ring-1 ring-gray-900/5 sm:px-6">
|
||||
<dt>
|
||||
<div class="absolute rounded-lg bg-red-500 p-3">
|
||||
<i class="fas fa-exclamation-triangle text-xl text-white"></i>
|
||||
</div>
|
||||
<p class="ml-16 truncate text-sm font-medium text-gray-500">Failed Today</p>
|
||||
</dt>
|
||||
<dd class="ml-16 flex items-baseline">
|
||||
<p class="text-3xl font-semibold text-gray-900"><%= @stats[:failed_messages_today] %></p>
|
||||
<p class="ml-2 flex items-baseline text-sm text-gray-600">
|
||||
<%= @stats[:pending_messages] %> pending
|
||||
</p>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Messages -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5">
|
||||
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
|
||||
<h3 class="text-lg font-semibold leading-6 text-gray-900">Recent Messages</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Latest SMS activity across all gateways</p>
|
||||
</div>
|
||||
<div class="overflow-hidden">
|
||||
<% if @recent_messages.any? %>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Message ID</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Phone Number</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Direction</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Status</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Gateway</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
<% @recent_messages.each do |msg| %>
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
<code class="rounded bg-gray-100 px-2 py-1 text-xs font-mono text-gray-800"><%= msg.message_id[0..15] %>...</code>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900"><%= msg.phone_number %></td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
<% if msg.direction == "outbound" %>
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">
|
||||
<i class="fas fa-arrow-up"></i> Outbound
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10">
|
||||
<i class="fas fa-arrow-down"></i> Inbound
|
||||
</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
<% case msg.status %>
|
||||
<% when "delivered" %>
|
||||
<span class="inline-flex rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10">Delivered</span>
|
||||
<% when "sent" %>
|
||||
<span class="inline-flex rounded-full bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">Sent</span>
|
||||
<% when "failed" %>
|
||||
<span class="inline-flex rounded-full bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-700/10">Failed</span>
|
||||
<% when "pending" %>
|
||||
<span class="inline-flex rounded-full bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-yellow-700/10">Pending</span>
|
||||
<% else %>
|
||||
<span class="inline-flex rounded-full bg-gray-50 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-700/10"><%= msg.status.titleize %></span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500"><%= msg.gateway&.name || "-" %></td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500"><%= time_ago_in_words(msg.created_at) %> ago</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 px-6 py-4">
|
||||
<%= link_to admin_logs_path, class: "inline-flex items-center gap-2 text-sm font-semibold text-blue-600 hover:text-blue-500" do %>
|
||||
View all logs
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="px-6 py-14 text-center">
|
||||
<i class="fas fa-inbox text-4xl text-gray-300"></i>
|
||||
<p class="mt-4 text-sm text-gray-500">No messages yet</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gateway Status -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5">
|
||||
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
|
||||
<h3 class="text-lg font-semibold leading-6 text-gray-900">Gateway Status</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Active gateway devices and their performance</p>
|
||||
</div>
|
||||
<div class="overflow-hidden">
|
||||
<% if @recent_gateways.any? %>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Name</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Device ID</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Status</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Messages Today</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Last Heartbeat</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
<% @recent_gateways.each do |gateway| %>
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<td class="whitespace-nowrap px-6 py-4">
|
||||
<%= link_to gateway.name, admin_gateway_path(gateway), class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
<code class="rounded bg-gray-100 px-2 py-1 text-xs font-mono text-gray-800"><%= gateway.device_id %></code>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
<% if gateway.status == "online" %>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-green-500"></span>
|
||||
Online
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-700/10">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-red-500"></span>
|
||||
Offline
|
||||
</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||
<span class="inline-flex items-center gap-1 text-green-600">
|
||||
<i class="fas fa-arrow-up text-xs"></i> <%= gateway.messages_sent_today %>
|
||||
</span>
|
||||
<span class="mx-1 text-gray-300">|</span>
|
||||
<span class="inline-flex items-center gap-1 text-blue-600">
|
||||
<i class="fas fa-arrow-down text-xs"></i> <%= gateway.messages_received_today %>
|
||||
</span>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||
<% if gateway.last_heartbeat_at %>
|
||||
<%= time_ago_in_words(gateway.last_heartbeat_at) %> ago
|
||||
<% else %>
|
||||
Never
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 px-6 py-4">
|
||||
<%= link_to admin_gateways_path, class: "inline-flex items-center gap-2 text-sm font-semibold text-blue-600 hover:text-blue-500" do %>
|
||||
View all gateways
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="px-6 py-14 text-center">
|
||||
<i class="fas fa-mobile-alt text-4xl text-gray-300"></i>
|
||||
<p class="mt-4 text-sm text-gray-500">No gateways registered yet</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
168
app/views/admin/gateways/index.html.erb
Normal file
168
app/views/admin/gateways/index.html.erb
Normal file
@@ -0,0 +1,168 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Page header -->
|
||||
<div class="sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold leading-tight tracking-tight text-gray-900">Gateways</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">Manage your SMS gateway devices and monitor their status.</p>
|
||||
</div>
|
||||
<%= link_to new_admin_gateway_path, class: "mt-4 sm:mt-0 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 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition-all" do %>
|
||||
<i class="fas fa-plus"></i>
|
||||
Register New Gateway
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Gateways table card -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 overflow-hidden">
|
||||
<% if @gateways.any? %>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Name</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Device ID</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Status</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Active</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Priority</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Today</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Total</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Last Heartbeat</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Created</th>
|
||||
<th scope="col" class="px-6 py-3 text-center text-xs font-medium uppercase tracking-wide text-gray-500">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
<% @gateways.each do |gateway| %>
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<!-- Name -->
|
||||
<td class="whitespace-nowrap px-6 py-4">
|
||||
<%= link_to gateway.name, admin_gateway_path(gateway),
|
||||
class: "text-sm font-semibold text-blue-600 hover:text-blue-500 transition-colors" %>
|
||||
</td>
|
||||
|
||||
<!-- Device ID -->
|
||||
<td class="whitespace-nowrap px-6 py-4">
|
||||
<code class="rounded bg-gray-100 px-2 py-1 text-xs font-mono text-gray-800"><%= gateway.device_id %></code>
|
||||
</td>
|
||||
|
||||
<!-- Status with pulse indicator -->
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
<% if gateway.status == "online" %>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
||||
</span>
|
||||
Online
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-700/10">
|
||||
<span class="h-2 w-2 rounded-full bg-red-500"></span>
|
||||
Offline
|
||||
</span>
|
||||
<% end %>
|
||||
</td>
|
||||
|
||||
<!-- Active badge -->
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
<% if gateway.active %>
|
||||
<span class="inline-flex items-center rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10">
|
||||
<i class="fas fa-check mr-1"></i> Active
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center rounded-full bg-gray-50 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-700/10">
|
||||
<i class="fas fa-times mr-1"></i> Inactive
|
||||
</span>
|
||||
<% end %>
|
||||
</td>
|
||||
|
||||
<!-- Priority -->
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900 font-medium text-center">
|
||||
<%= gateway.priority %>
|
||||
</td>
|
||||
|
||||
<!-- Messages Today -->
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-flex items-center gap-1 text-green-600">
|
||||
<i class="fas fa-arrow-up text-xs"></i>
|
||||
<span class="font-medium"><%= gateway.messages_sent_today %></span>
|
||||
</span>
|
||||
<span class="text-gray-300">|</span>
|
||||
<span class="inline-flex items-center gap-1 text-blue-600">
|
||||
<i class="fas fa-arrow-down text-xs"></i>
|
||||
<span class="font-medium"><%= gateway.messages_received_today %></span>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Total Messages -->
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-flex items-center gap-1 text-green-600">
|
||||
<i class="fas fa-arrow-up text-xs"></i>
|
||||
<span><%= gateway.total_messages_sent %></span>
|
||||
</span>
|
||||
<span class="text-gray-300">|</span>
|
||||
<span class="inline-flex items-center gap-1 text-blue-600">
|
||||
<i class="fas fa-arrow-down text-xs"></i>
|
||||
<span><%= gateway.total_messages_received %></span>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Last Heartbeat -->
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||
<% if gateway.last_heartbeat_at %>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-gray-900"><%= time_ago_in_words(gateway.last_heartbeat_at) %> ago</span>
|
||||
<span class="text-xs text-gray-400"><%= gateway.last_heartbeat_at.strftime("%m/%d/%y %H:%M") %></span>
|
||||
</div>
|
||||
<% else %>
|
||||
<span class="text-gray-400 italic">Never</span>
|
||||
<% end %>
|
||||
</td>
|
||||
|
||||
<!-- Created -->
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||
<%= gateway.created_at.strftime("%m/%d/%y") %>
|
||||
</td>
|
||||
|
||||
<!-- Actions -->
|
||||
<td class="whitespace-nowrap px-6 py-4 text-center">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<%= link_to test_admin_gateway_path(gateway),
|
||||
class: "inline-flex items-center gap-1 rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-blue-500 transition-all duration-200" do %>
|
||||
<i class="fas fa-vial"></i>
|
||||
Test
|
||||
<% end %>
|
||||
<%= button_to toggle_admin_gateway_path(gateway), method: :post,
|
||||
class: gateway.active ?
|
||||
"inline-flex items-center gap-2 rounded-lg bg-red-600 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-red-500 transition-all duration-200" :
|
||||
"inline-flex items-center gap-2 rounded-lg bg-green-600 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-green-500 transition-all duration-200" do %>
|
||||
<% if gateway.active %>
|
||||
<i class="fas fa-ban"></i>
|
||||
Deactivate
|
||||
<% else %>
|
||||
<i class="fas fa-check"></i>
|
||||
Activate
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% else %>
|
||||
<!-- Empty state -->
|
||||
<div class="px-6 py-14 text-center">
|
||||
<div class="mx-auto h-20 w-20 flex items-center justify-center rounded-full bg-gray-100">
|
||||
<i class="fas fa-mobile-alt text-3xl text-gray-400"></i>
|
||||
</div>
|
||||
<p class="mt-4 text-base font-medium text-gray-900">No gateways registered yet</p>
|
||||
<p class="mt-2 text-sm text-gray-500">Gateway devices will appear here once they connect via the API.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
87
app/views/admin/gateways/new.html.erb
Normal file
87
app/views/admin/gateways/new.html.erb
Normal file
@@ -0,0 +1,87 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Page header with back link -->
|
||||
<div class="flex items-center gap-4">
|
||||
<%= link_to admin_gateways_path, class: "inline-flex items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors" do %>
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to Gateways
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="border-b border-gray-200 pb-5">
|
||||
<h1 class="text-3xl font-bold leading-tight tracking-tight text-gray-900">Register New Gateway</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">Add a new Android gateway device to your SMS system.</p>
|
||||
</div>
|
||||
|
||||
<!-- Form card -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 px-6 py-8 max-w-2xl">
|
||||
<%= form_with url: admin_gateways_path, method: :post, local: true, class: "space-y-6" do |f| %>
|
||||
<!-- Device ID field -->
|
||||
<div>
|
||||
<%= label_tag "gateway[device_id]", "Device ID", class: "block text-sm font-medium text-gray-700" %>
|
||||
<div class="mt-1 relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<i class="fas fa-mobile-alt text-gray-400"></i>
|
||||
</div>
|
||||
<%= text_field_tag "gateway[device_id]", nil,
|
||||
class: "block w-full pl-10 rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm py-3",
|
||||
placeholder: "device-001",
|
||||
required: true %>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-500">A unique identifier for this gateway device (e.g., phone model, serial number)</p>
|
||||
</div>
|
||||
|
||||
<!-- Name field -->
|
||||
<div>
|
||||
<%= label_tag "gateway[name]", "Gateway Name", class: "block text-sm font-medium text-gray-700" %>
|
||||
<div class="mt-1 relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<i class="fas fa-tag text-gray-400"></i>
|
||||
</div>
|
||||
<%= text_field_tag "gateway[name]", nil,
|
||||
class: "block w-full pl-10 rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm py-3",
|
||||
placeholder: "Office Phone",
|
||||
required: true %>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-500">A friendly name to identify this gateway</p>
|
||||
</div>
|
||||
|
||||
<!-- Priority field -->
|
||||
<div>
|
||||
<%= label_tag "gateway[priority]", "Priority", class: "block text-sm font-medium text-gray-700" %>
|
||||
<div class="mt-1 relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<i class="fas fa-sort-numeric-up text-gray-400"></i>
|
||||
</div>
|
||||
<%= number_field_tag "gateway[priority]", 1,
|
||||
min: 1,
|
||||
max: 10,
|
||||
class: "block w-full pl-10 rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm py-3" %>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-500">Priority level (1-10). Higher priority gateways are used first for sending messages.</p>
|
||||
</div>
|
||||
|
||||
<!-- Information box -->
|
||||
<div class="rounded-lg bg-blue-50 px-4 py-4 ring-1 ring-blue-600/10">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-info-circle text-blue-600 text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-blue-800">Gateway API Key</h3>
|
||||
<p class="mt-1 text-sm text-blue-700">
|
||||
After creating the gateway, you'll receive a unique API key. You'll need to configure this key in your Android gateway app to connect it to the system.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center gap-3 pt-4">
|
||||
<%= submit_tag "Register Gateway",
|
||||
class: "inline-flex justify-center items-center gap-2 rounded-lg bg-blue-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition-all duration-200" %>
|
||||
<%= link_to "Cancel", admin_gateways_path,
|
||||
class: "inline-flex justify-center items-center gap-2 rounded-lg bg-gray-100 px-6 py-3 text-sm font-semibold text-gray-700 hover:bg-gray-200 transition-all duration-200" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
565
app/views/admin/gateways/show.html.erb
Normal file
565
app/views/admin/gateways/show.html.erb
Normal file
@@ -0,0 +1,565 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Back link -->
|
||||
<div class="flex items-center gap-4">
|
||||
<%= link_to admin_gateways_path, class: "inline-flex items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors" do %>
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to Gateways
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if @is_new && @raw_key.present? %>
|
||||
<!-- New gateway created - show API key -->
|
||||
<div class="border-b border-gray-200 pb-5">
|
||||
<h1 class="text-3xl font-bold leading-tight tracking-tight text-gray-900">Gateway Created Successfully!</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">Your new gateway has been registered and is ready to connect.</p>
|
||||
</div>
|
||||
|
||||
<!-- Warning alert -->
|
||||
<div class="rounded-lg bg-yellow-50 px-4 py-4 ring-1 ring-yellow-600/10">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-600 text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-yellow-800">Important: Save this API key now!</h3>
|
||||
<p class="mt-1 text-sm text-yellow-700">
|
||||
This is the only time you'll be able to see the full API key. You need to configure this key in your Android gateway app to connect it to the system.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Code and API Key display -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- QR Code Card -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 px-6 py-6">
|
||||
<div class="text-center">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Scan QR Code</h3>
|
||||
<p class="text-sm text-gray-600 mb-6">Scan this QR code with your Android gateway app to auto-configure</p>
|
||||
|
||||
<div class="flex justify-center mb-4">
|
||||
<div class="bg-white p-4 rounded-lg shadow-inner border-2 border-gray-200">
|
||||
<%= @qr_code_data.html_safe %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg bg-blue-50 px-4 py-3 ring-1 ring-blue-600/10">
|
||||
<p class="text-xs text-blue-700">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
QR code contains API key, API base URL, and WebSocket URL
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Key Manual Entry Card -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 px-6 py-6">
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Manual Configuration</h3>
|
||||
<button
|
||||
onclick="copyAllConfig()"
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-gray-600 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-gray-500 transition-all duration-200">
|
||||
<i class="fas fa-copy"></i>
|
||||
Copy All
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">Or manually enter these details if QR scanning is unavailable</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- API Base URL -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">API Base URL</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1 relative rounded-lg bg-gray-50 px-3 py-2 border border-gray-200">
|
||||
<code class="text-xs font-mono text-gray-800 break-all" id="api-base-url"><%= request.base_url %></code>
|
||||
</div>
|
||||
<button
|
||||
onclick="copyField('api-base-url')"
|
||||
class="flex-shrink-0 inline-flex items-center gap-1 rounded-lg bg-blue-600 px-3 py-2 text-xs font-semibold text-white hover:bg-blue-500">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WebSocket URL -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">WebSocket URL</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1 relative rounded-lg bg-gray-50 px-3 py-2 border border-gray-200">
|
||||
<code class="text-xs font-mono text-gray-800 break-all" id="ws-url"><%= request.base_url.sub(/^http/, 'ws') %>/cable</code>
|
||||
</div>
|
||||
<button
|
||||
onclick="copyField('ws-url')"
|
||||
class="flex-shrink-0 inline-flex items-center gap-1 rounded-lg bg-blue-600 px-3 py-2 text-xs font-semibold text-white hover:bg-blue-500">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Key -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">API Key</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1 relative rounded-lg bg-gray-900 px-3 py-2">
|
||||
<code class="text-xs font-mono text-green-400 break-all" id="api-key"><%= @raw_key %></code>
|
||||
</div>
|
||||
<button
|
||||
onclick="copyField('api-key')"
|
||||
class="flex-shrink-0 inline-flex items-center gap-1 rounded-lg bg-blue-600 px-3 py-2 text-xs font-semibold text-white hover:bg-blue-500">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gateway Details card -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 px-6 py-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Gateway Details</h3>
|
||||
|
||||
<dl class="divide-y divide-gray-100">
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Device ID</dt>
|
||||
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">
|
||||
<code class="rounded bg-gray-100 px-2 py-1 text-xs font-mono text-gray-800"><%= @gateway.device_id %></code>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Name</dt>
|
||||
<dd class="mt-1 text-sm text-gray-700 sm:col-span-2 sm:mt-0"><%= @gateway.name %></dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Priority</dt>
|
||||
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="inline-flex items-center rounded-full bg-purple-50 px-3 py-1 text-sm font-medium text-purple-700 ring-1 ring-inset ring-purple-700/10">
|
||||
<%= @gateway.priority %>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Status</dt>
|
||||
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-700/10">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-red-500"></span>
|
||||
Offline (Waiting for connection)
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Created</dt>
|
||||
<dd class="mt-1 text-sm text-gray-700 sm:col-span-2 sm:mt-0">
|
||||
<%= @gateway.created_at.strftime("%B %d, %Y at %l:%M %p") %>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- Next steps card -->
|
||||
<div class="rounded-lg bg-blue-50 px-6 py-6 ring-1 ring-blue-600/10">
|
||||
<h3 class="text-sm font-semibold text-blue-800 mb-3">Quick Setup Guide</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<h4 class="text-xs font-semibold text-blue-800 mb-2">Option 1: QR Code (Recommended)</h4>
|
||||
<ol class="list-decimal list-inside space-y-1.5 text-xs text-blue-700 ml-2">
|
||||
<li>Install the Android SMS Gateway app on your device</li>
|
||||
<li>Open the app and look for "Scan QR Code" option</li>
|
||||
<li>Scan the QR code above - configuration will be applied automatically</li>
|
||||
<li>Start the gateway service in the app</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-xs font-semibold text-blue-800 mb-2">Option 2: Manual Entry</h4>
|
||||
<ol class="list-decimal list-inside space-y-1.5 text-xs text-blue-700 ml-2">
|
||||
<li>Install the Android SMS Gateway app on your device</li>
|
||||
<li>Open the app and navigate to Settings</li>
|
||||
<li>Copy and paste each field from the "Manual Configuration" section above</li>
|
||||
<li>Save the configuration and start the gateway service</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-4 border-t border-blue-200">
|
||||
<p class="text-xs text-blue-700">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
The gateway will appear as <span class="font-semibold">"Online"</span> once it successfully connects to the server.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyField(elementId) {
|
||||
const element = document.getElementById(elementId);
|
||||
const text = element.textContent;
|
||||
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
const button = event.target.closest('button');
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
||||
button.classList.add('bg-green-600', 'hover:bg-green-500');
|
||||
button.classList.remove('bg-blue-600', 'hover:bg-blue-500');
|
||||
|
||||
setTimeout(function() {
|
||||
button.innerHTML = originalHTML;
|
||||
button.classList.remove('bg-green-600', 'hover:bg-green-500');
|
||||
button.classList.add('bg-blue-600', 'hover:bg-blue-500');
|
||||
}, 2000);
|
||||
}, function(err) {
|
||||
alert('Failed to copy: ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function copyAllConfig() {
|
||||
const apiBaseUrl = document.getElementById('api-base-url').textContent;
|
||||
const wsUrl = document.getElementById('ws-url').textContent;
|
||||
const apiKey = document.getElementById('api-key').textContent;
|
||||
|
||||
const configText = `API Base URL: ${apiBaseUrl}\nWebSocket URL: ${wsUrl}\nAPI Key: ${apiKey}`;
|
||||
|
||||
navigator.clipboard.writeText(configText).then(function() {
|
||||
const button = event.target.closest('button');
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-check"></i> Copied!';
|
||||
button.classList.add('bg-green-600', 'hover:bg-green-500');
|
||||
button.classList.remove('bg-gray-600', 'hover:bg-gray-500');
|
||||
|
||||
setTimeout(function() {
|
||||
button.innerHTML = originalHTML;
|
||||
button.classList.remove('bg-green-600', 'hover:bg-green-500');
|
||||
button.classList.add('bg-gray-600', 'hover:bg-gray-500');
|
||||
}, 2000);
|
||||
}, function(err) {
|
||||
alert('Failed to copy: ' + err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<% else %>
|
||||
<!-- Existing gateway view -->
|
||||
<!-- Page header -->
|
||||
<div class="border-b border-gray-200 pb-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold leading-tight tracking-tight text-gray-900"><%= @gateway.name %></h1>
|
||||
<p class="mt-2 text-sm text-gray-600">Gateway device details and statistics</p>
|
||||
</div>
|
||||
<!-- Status indicator -->
|
||||
<div>
|
||||
<% if @gateway.status == "online" %>
|
||||
<span class="inline-flex items-center gap-2 rounded-full bg-green-50 px-4 py-2 text-sm font-medium text-green-700 ring-2 ring-inset ring-green-700/20">
|
||||
<span class="relative flex h-3 w-3">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
|
||||
</span>
|
||||
Online
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center gap-2 rounded-full bg-red-50 px-4 py-2 text-sm font-medium text-red-700 ring-2 ring-inset ring-red-700/20">
|
||||
<span class="h-3 w-3 rounded-full bg-red-500"></span>
|
||||
Offline
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats grid -->
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- Status card -->
|
||||
<div class="relative overflow-hidden rounded-xl <%= @gateway.status == 'online' ? 'bg-green-500' : 'bg-red-500' %> px-4 py-5 shadow-sm sm:px-6">
|
||||
<dt>
|
||||
<div class="absolute rounded-lg bg-white/20 p-3">
|
||||
<i class="fas fa-signal text-xl text-white"></i>
|
||||
</div>
|
||||
<p class="ml-16 truncate text-sm font-medium text-white/90">Connection Status</p>
|
||||
</dt>
|
||||
<dd class="ml-16 flex items-baseline">
|
||||
<p class="text-2xl font-semibold text-white"><%= @gateway.status.titleize %></p>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<!-- Active status card -->
|
||||
<div class="relative overflow-hidden rounded-xl <%= @gateway.active ? 'bg-blue-500' : 'bg-gray-400' %> px-4 py-5 shadow-sm sm:px-6">
|
||||
<dt>
|
||||
<div class="absolute rounded-lg bg-white/20 p-3">
|
||||
<i class="fas fa-power-off text-xl text-white"></i>
|
||||
</div>
|
||||
<p class="ml-16 truncate text-sm font-medium text-white/90">Active Status</p>
|
||||
</dt>
|
||||
<dd class="ml-16 flex items-baseline">
|
||||
<p class="text-2xl font-semibold text-white"><%= @gateway.active ? 'Active' : 'Inactive' %></p>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<!-- Priority card -->
|
||||
<div class="relative overflow-hidden rounded-xl bg-purple-500 px-4 py-5 shadow-sm sm:px-6">
|
||||
<dt>
|
||||
<div class="absolute rounded-lg bg-white/20 p-3">
|
||||
<i class="fas fa-sort-amount-up text-xl text-white"></i>
|
||||
</div>
|
||||
<p class="ml-16 truncate text-sm font-medium text-white/90">Priority Level</p>
|
||||
</dt>
|
||||
<dd class="ml-16 flex items-baseline">
|
||||
<p class="text-2xl font-semibold text-white"><%= @gateway.priority %></p>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<!-- Messages sent today -->
|
||||
<div class="relative overflow-hidden rounded-xl bg-white px-4 py-5 shadow-sm ring-1 ring-gray-900/5 sm:px-6">
|
||||
<dt>
|
||||
<div class="absolute rounded-lg bg-green-500 p-3">
|
||||
<i class="fas fa-arrow-up text-xl text-white"></i>
|
||||
</div>
|
||||
<p class="ml-16 truncate text-sm font-medium text-gray-500">Messages Sent Today</p>
|
||||
</dt>
|
||||
<dd class="ml-16 flex items-baseline">
|
||||
<p class="text-2xl font-semibold text-gray-900"><%= @gateway.messages_sent_today %></p>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<!-- Messages received today -->
|
||||
<div class="relative overflow-hidden rounded-xl bg-white px-4 py-5 shadow-sm ring-1 ring-gray-900/5 sm:px-6">
|
||||
<dt>
|
||||
<div class="absolute rounded-lg bg-blue-500 p-3">
|
||||
<i class="fas fa-arrow-down text-xl text-white"></i>
|
||||
</div>
|
||||
<p class="ml-16 truncate text-sm font-medium text-gray-500">Messages Received Today</p>
|
||||
</dt>
|
||||
<dd class="ml-16 flex items-baseline">
|
||||
<p class="text-2xl font-semibold text-gray-900"><%= @gateway.messages_received_today %></p>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<!-- Total messages -->
|
||||
<div class="relative overflow-hidden rounded-xl bg-white px-4 py-5 shadow-sm ring-1 ring-gray-900/5 sm:px-6">
|
||||
<dt>
|
||||
<div class="absolute rounded-lg bg-yellow-500 p-3">
|
||||
<i class="fas fa-paper-plane text-xl text-white"></i>
|
||||
</div>
|
||||
<p class="ml-16 truncate text-sm font-medium text-gray-500">Total Messages</p>
|
||||
</dt>
|
||||
<dd class="ml-16 flex items-baseline">
|
||||
<p class="text-2xl font-semibold text-gray-900"><%= @gateway.total_messages_sent + @gateway.total_messages_received %></p>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details card -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 px-6 py-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Gateway Details</h3>
|
||||
|
||||
<dl class="divide-y divide-gray-100">
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Device ID</dt>
|
||||
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">
|
||||
<code class="rounded bg-gray-100 px-3 py-1.5 text-sm font-mono text-gray-800"><%= @gateway.device_id %></code>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Name</dt>
|
||||
<dd class="mt-1 text-sm text-gray-700 sm:col-span-2 sm:mt-0"><%= @gateway.name %></dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Status</dt>
|
||||
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">
|
||||
<% if @gateway.status == "online" %>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-green-500"></span>
|
||||
Online
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-700/10">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-red-500"></span>
|
||||
Offline
|
||||
</span>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Active</dt>
|
||||
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">
|
||||
<% if @gateway.active %>
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10">
|
||||
<i class="fas fa-check"></i> Active
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-gray-50 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-700/10">
|
||||
<i class="fas fa-times"></i> Inactive
|
||||
</span>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Priority</dt>
|
||||
<dd class="mt-1 text-sm text-gray-700 sm:col-span-2 sm:mt-0">
|
||||
<span class="inline-flex items-center rounded-full bg-purple-50 px-3 py-1 text-sm font-medium text-purple-700 ring-1 ring-inset ring-purple-700/10">
|
||||
<%= @gateway.priority %>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Last Heartbeat</dt>
|
||||
<dd class="mt-1 text-sm text-gray-700 sm:col-span-2 sm:mt-0">
|
||||
<% if @gateway.last_heartbeat_at %>
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fas fa-heartbeat text-red-500"></i>
|
||||
<span><%= @gateway.last_heartbeat_at.strftime("%B %d, %Y at %l:%M:%S %p") %></span>
|
||||
<span class="text-gray-500">(<%= time_ago_in_words(@gateway.last_heartbeat_at) %> ago)</span>
|
||||
</div>
|
||||
<% else %>
|
||||
<span class="text-gray-400 italic">Never</span>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Total Messages Sent</dt>
|
||||
<dd class="mt-1 text-sm text-gray-700 sm:col-span-2 sm:mt-0">
|
||||
<span class="inline-flex items-center gap-1 text-green-600 font-semibold">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
<%= @gateway.total_messages_sent %>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Total Messages Received</dt>
|
||||
<dd class="mt-1 text-sm text-gray-700 sm:col-span-2 sm:mt-0">
|
||||
<span class="inline-flex items-center gap-1 text-blue-600 font-semibold">
|
||||
<i class="fas fa-arrow-down"></i>
|
||||
<%= @gateway.total_messages_received %>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Created</dt>
|
||||
<dd class="mt-1 text-sm text-gray-700 sm:col-span-2 sm:mt-0">
|
||||
<%= @gateway.created_at.strftime("%B %d, %Y at %l:%M %p") %>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Last Updated</dt>
|
||||
<dd class="mt-1 text-sm text-gray-700 sm:col-span-2 sm:mt-0">
|
||||
<%= @gateway.updated_at.strftime("%B %d, %Y at %l:%M %p") %>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<!-- Metadata section -->
|
||||
<% if @gateway.metadata.present? %>
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<h4 class="text-sm font-semibold text-gray-900 mb-3">Device Metadata</h4>
|
||||
<div class="rounded-lg bg-gray-900 px-4 py-4 overflow-x-auto">
|
||||
<pre class="text-xs font-mono text-green-400"><%= JSON.pretty_generate(@gateway.metadata) %></pre>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<div class="flex items-center gap-3">
|
||||
<%= link_to test_admin_gateway_path(@gateway),
|
||||
class: "inline-flex items-center gap-2 rounded-lg bg-blue-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 transition-all duration-200" do %>
|
||||
<i class="fas fa-vial"></i>
|
||||
Test Gateway
|
||||
<% end %>
|
||||
<%= button_to toggle_admin_gateway_path(@gateway), method: :post,
|
||||
class: @gateway.active ?
|
||||
"inline-flex items-center gap-2 rounded-lg bg-red-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-red-500 transition-all duration-200" :
|
||||
"inline-flex items-center gap-2 rounded-lg bg-green-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-green-500 transition-all duration-200" do %>
|
||||
<% if @gateway.active %>
|
||||
<i class="fas fa-ban"></i>
|
||||
Deactivate Gateway
|
||||
<% else %>
|
||||
<i class="fas fa-check"></i>
|
||||
Activate Gateway
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent messages card -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Recent Messages</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Last <%= @recent_messages.size %> messages from this gateway</p>
|
||||
</div>
|
||||
|
||||
<% if @recent_messages.any? %>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Message ID</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Phone Number</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Direction</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Status</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
<% @recent_messages.each do |msg| %>
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
<code class="rounded bg-gray-100 px-2 py-1 text-xs font-mono text-gray-800"><%= msg.message_id[0..15] %>...</code>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-900">
|
||||
<%= msg.phone_number %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
<% if msg.direction == "outbound" %>
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">
|
||||
<i class="fas fa-arrow-up"></i> Outbound
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10">
|
||||
<i class="fas fa-arrow-down"></i> Inbound
|
||||
</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
<% case msg.status %>
|
||||
<% when "delivered" %>
|
||||
<span class="inline-flex rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10">Delivered</span>
|
||||
<% when "sent" %>
|
||||
<span class="inline-flex rounded-full bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">Sent</span>
|
||||
<% when "failed" %>
|
||||
<span class="inline-flex rounded-full bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-700/10">Failed</span>
|
||||
<% when "pending" %>
|
||||
<span class="inline-flex rounded-full bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-yellow-700/10">Pending</span>
|
||||
<% else %>
|
||||
<span class="inline-flex rounded-full bg-gray-50 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-700/10"><%= msg.status.titleize %></span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||
<%= msg.created_at.strftime("%m/%d/%y %H:%M") %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="px-6 py-14 text-center">
|
||||
<i class="fas fa-inbox text-4xl text-gray-300"></i>
|
||||
<p class="mt-4 text-sm font-medium text-gray-900">No messages yet</p>
|
||||
<p class="mt-2 text-sm text-gray-500">Messages will appear here once this gateway starts processing SMS.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
353
app/views/admin/gateways/test.html.erb
Normal file
353
app/views/admin/gateways/test.html.erb
Normal file
@@ -0,0 +1,353 @@
|
||||
<% if @gateway.nil? %>
|
||||
<div class="rounded-lg bg-red-50 px-4 py-4 ring-1 ring-red-600/10">
|
||||
<p class="text-red-800">Error: Gateway not found</p>
|
||||
<%= link_to "Back to Gateways", admin_gateways_path, class: "text-red-600 underline" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="space-y-6">
|
||||
<!-- Back link -->
|
||||
<div class="flex items-center gap-4">
|
||||
<%= link_to admin_gateway_path(@gateway), class: "inline-flex items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors" do %>
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to Gateway Details
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Page header -->
|
||||
<div class="border-b border-gray-200 pb-5">
|
||||
<h1 class="text-3xl font-bold leading-tight tracking-tight text-gray-900">Test Gateway: <%= @gateway.name %></h1>
|
||||
<p class="mt-2 text-sm text-gray-600">Test connection and send test SMS messages to verify gateway functionality.</p>
|
||||
</div>
|
||||
|
||||
<!-- Gateway Status Card -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 px-6 py-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Gateway Status</h3>
|
||||
<button
|
||||
onclick="checkConnection()"
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 transition-all duration-200">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
Refresh Status
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="status-container" class="space-y-4">
|
||||
<!-- Status will be loaded here -->
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connection Information Card -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 px-6 py-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Connection Information</h3>
|
||||
|
||||
<dl class="divide-y divide-gray-100">
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Device ID</dt>
|
||||
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">
|
||||
<code class="rounded bg-gray-100 px-2 py-1 text-xs font-mono text-gray-800"><%= @gateway.device_id %></code>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Gateway Name</dt>
|
||||
<dd class="mt-1 text-sm text-gray-700 sm:col-span-2 sm:mt-0"><%= @gateway.name %></dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Priority</dt>
|
||||
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="inline-flex items-center rounded-full bg-purple-50 px-3 py-1 text-sm font-medium text-purple-700 ring-1 ring-inset ring-purple-700/10">
|
||||
<%= @gateway.priority %>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt class="text-sm font-medium text-gray-900">Active</dt>
|
||||
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">
|
||||
<% if @gateway.active %>
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10">
|
||||
<i class="fas fa-check"></i> Active
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-700/10">
|
||||
<i class="fas fa-times"></i> Inactive
|
||||
</span>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- Send Test SMS Card -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 px-6 py-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Send Test SMS</h3>
|
||||
<p class="text-sm text-gray-600 mb-6">Send a test SMS message through this gateway to verify it's working correctly.</p>
|
||||
|
||||
<form id="test-sms-form" class="space-y-6">
|
||||
<!-- Phone Number -->
|
||||
<div>
|
||||
<label for="phone_number" class="block text-sm font-medium text-gray-700">Phone Number</label>
|
||||
<div class="mt-1 relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<i class="fas fa-phone text-gray-400"></i>
|
||||
</div>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone_number"
|
||||
name="phone_number"
|
||||
class="block w-full pl-10 rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm py-3"
|
||||
placeholder="+959123456789"
|
||||
required>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-500">Enter phone number with country code (e.g., +959123456789)</p>
|
||||
</div>
|
||||
|
||||
<!-- Message Body -->
|
||||
<div>
|
||||
<label for="message_body" class="block text-sm font-medium text-gray-700">Message</label>
|
||||
<div class="mt-1">
|
||||
<textarea
|
||||
id="message_body"
|
||||
name="message_body"
|
||||
rows="4"
|
||||
class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm py-3"
|
||||
placeholder="This is a test message from the admin interface."
|
||||
required>This is a test message from MySMSAPio admin interface. Gateway: <%= @gateway.name %></textarea>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-500" id="char-count">160 characters remaining</p>
|
||||
</div>
|
||||
|
||||
<!-- Result Display -->
|
||||
<div id="sms-result" class="hidden">
|
||||
<!-- Success/Error message will be displayed here -->
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex justify-center items-center gap-2 rounded-lg bg-blue-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition-all duration-200">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
<span id="submit-text">Send Test SMS</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick="resetForm()"
|
||||
class="inline-flex justify-center items-center gap-2 rounded-lg bg-gray-100 px-6 py-3 text-sm font-semibold text-gray-700 hover:bg-gray-200 transition-all duration-200">
|
||||
<i class="fas fa-redo"></i>
|
||||
Reset Form
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Warning Card -->
|
||||
<div class="rounded-lg bg-yellow-50 px-4 py-4 ring-1 ring-yellow-600/10">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-600 text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-yellow-800">Important Notes</h3>
|
||||
<ul class="mt-2 text-sm text-yellow-700 list-disc list-inside space-y-1">
|
||||
<li>Test SMS messages will be sent to real phone numbers</li>
|
||||
<li>Ensure the gateway is online and connected before testing</li>
|
||||
<li>Standard SMS charges may apply to the recipient</li>
|
||||
<li>Test messages are marked with metadata for identification</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Check connection on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
checkConnection();
|
||||
updateCharCount();
|
||||
});
|
||||
|
||||
// Update character count
|
||||
const messageBody = document.getElementById('message_body');
|
||||
messageBody.addEventListener('input', updateCharCount);
|
||||
|
||||
function updateCharCount() {
|
||||
const length = messageBody.value.length;
|
||||
const remaining = 160 - length;
|
||||
const charCount = document.getElementById('char-count');
|
||||
|
||||
if (remaining < 0) {
|
||||
charCount.textContent = `${Math.abs(remaining)} characters over limit (message will be split into ${Math.ceil(length / 160)} parts)`;
|
||||
charCount.classList.add('text-red-600');
|
||||
charCount.classList.remove('text-gray-500');
|
||||
} else {
|
||||
charCount.textContent = `${remaining} characters remaining`;
|
||||
charCount.classList.remove('text-red-600');
|
||||
charCount.classList.add('text-gray-500');
|
||||
}
|
||||
}
|
||||
|
||||
// Check gateway connection
|
||||
async function checkConnection() {
|
||||
const container = document.getElementById('status-container');
|
||||
container.innerHTML = '<div class="flex items-center justify-center py-8"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div></div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('<%= check_connection_admin_gateway_path(@gateway) %>', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
container.innerHTML = `
|
||||
<div class="rounded-lg bg-green-50 px-4 py-4 ring-1 ring-green-600/10">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-check-circle text-green-600 text-2xl"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-semibold text-green-800">Gateway is Online</h3>
|
||||
<p class="mt-1 text-sm text-green-700">
|
||||
Last heartbeat: ${data.time_ago} ago
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-green-600">
|
||||
${data.last_heartbeat}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="rounded-lg bg-red-50 px-4 py-4 ring-1 ring-red-600/10">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-times-circle text-red-600 text-2xl"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-semibold text-red-800">Gateway is Offline</h3>
|
||||
<p class="mt-1 text-sm text-red-700">
|
||||
${data.message}
|
||||
</p>
|
||||
${data.last_heartbeat ? `<p class="mt-1 text-xs text-red-600">Last seen: ${data.time_ago} ago</p>` : '<p class="mt-1 text-xs text-red-600">Never connected</p>'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
container.innerHTML = `
|
||||
<div class="rounded-lg bg-red-50 px-4 py-4 ring-1 ring-red-600/10">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-exclamation-circle text-red-600 text-2xl"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-semibold text-red-800">Error Checking Status</h3>
|
||||
<p class="mt-1 text-sm text-red-700">${error.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('test-sms-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitButton = e.target.querySelector('button[type="submit"]');
|
||||
const submitText = document.getElementById('submit-text');
|
||||
const resultDiv = document.getElementById('sms-result');
|
||||
|
||||
// Disable button and show loading
|
||||
submitButton.disabled = true;
|
||||
submitText.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
|
||||
|
||||
const formData = {
|
||||
phone_number: document.getElementById('phone_number').value,
|
||||
message_body: document.getElementById('message_body').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('<%= send_test_sms_admin_gateway_path(@gateway) %>', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
resultDiv.className = 'rounded-lg bg-green-50 px-4 py-4 ring-1 ring-green-600/10';
|
||||
resultDiv.innerHTML = `
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-check-circle text-green-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-semibold text-green-800">Test SMS Sent Successfully!</h3>
|
||||
<p class="mt-1 text-sm text-green-700">${data.message}</p>
|
||||
<div class="mt-2 text-xs text-green-600">
|
||||
<p>Message ID: <code class="bg-green-100 px-2 py-1 rounded">${data.message_id}</code></p>
|
||||
<p class="mt-1">Status: <span class="font-semibold">${data.sms_status}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
resultDiv.className = 'rounded-lg bg-red-50 px-4 py-4 ring-1 ring-red-600/10';
|
||||
resultDiv.innerHTML = `
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-times-circle text-red-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-semibold text-red-800">Failed to Send Test SMS</h3>
|
||||
<p class="mt-1 text-sm text-red-700">${data.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
resultDiv.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
resultDiv.className = 'rounded-lg bg-red-50 px-4 py-4 ring-1 ring-red-600/10';
|
||||
resultDiv.innerHTML = `
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-exclamation-circle text-red-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-semibold text-red-800">Error</h3>
|
||||
<p class="mt-1 text-sm text-red-700">${error.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
resultDiv.classList.remove('hidden');
|
||||
} finally {
|
||||
// Re-enable button
|
||||
submitButton.disabled = false;
|
||||
submitText.innerHTML = '<i class="fas fa-paper-plane"></i> Send Test SMS';
|
||||
}
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
document.getElementById('test-sms-form').reset();
|
||||
document.getElementById('sms-result').classList.add('hidden');
|
||||
updateCharCount();
|
||||
}
|
||||
</script>
|
||||
<% end %>
|
||||
235
app/views/admin/logs/index.html.erb
Normal file
235
app/views/admin/logs/index.html.erb
Normal file
@@ -0,0 +1,235 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Page header -->
|
||||
<div class="border-b border-gray-200 pb-5">
|
||||
<h1 class="text-3xl font-bold leading-tight tracking-tight text-gray-900">SMS Logs</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">View and filter all SMS messages across your gateways.</p>
|
||||
</div>
|
||||
|
||||
<!-- Filters card -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 px-6 py-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Filters</h3>
|
||||
<i class="fas fa-filter text-gray-400"></i>
|
||||
</div>
|
||||
|
||||
<%= form_with url: admin_logs_path, method: :get, local: true do |f| %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Direction filter -->
|
||||
<div>
|
||||
<%= label_tag :direction, "Direction", class: "block text-sm font-medium text-gray-700 mb-1" %>
|
||||
<%= select_tag :direction,
|
||||
options_for_select([["All Directions", ""], ["Inbound", "inbound"], ["Outbound", "outbound"]], params[:direction]),
|
||||
class: "block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
</div>
|
||||
|
||||
<!-- Status filter -->
|
||||
<div>
|
||||
<%= label_tag :status, "Status", class: "block text-sm font-medium text-gray-700 mb-1" %>
|
||||
<%= select_tag :status,
|
||||
options_for_select([["All Statuses", ""], ["Pending", "pending"], ["Queued", "queued"], ["Sent", "sent"], ["Delivered", "delivered"], ["Failed", "failed"]], params[:status]),
|
||||
class: "block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
</div>
|
||||
|
||||
<!-- Phone number filter -->
|
||||
<div>
|
||||
<%= label_tag :phone_number, "Phone Number", class: "block text-sm font-medium text-gray-700 mb-1" %>
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<i class="fas fa-phone text-gray-400"></i>
|
||||
</div>
|
||||
<%= text_field_tag :phone_number, params[:phone_number],
|
||||
class: "block w-full pl-10 rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
||||
placeholder: "Search phone..." %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gateway filter -->
|
||||
<div>
|
||||
<%= label_tag :gateway_id, "Gateway", class: "block text-sm font-medium text-gray-700 mb-1" %>
|
||||
<%= select_tag :gateway_id,
|
||||
options_for_select([["All Gateways", ""]] + Gateway.order(:name).pluck(:name, :id), params[:gateway_id]),
|
||||
class: "block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
</div>
|
||||
|
||||
<!-- Start date filter -->
|
||||
<div>
|
||||
<%= label_tag :start_date, "Start Date", class: "block text-sm font-medium text-gray-700 mb-1" %>
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<i class="fas fa-calendar text-gray-400"></i>
|
||||
</div>
|
||||
<%= date_field_tag :start_date, params[:start_date],
|
||||
class: "block w-full pl-10 rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- End date filter -->
|
||||
<div>
|
||||
<%= label_tag :end_date, "End Date", class: "block text-sm font-medium text-gray-700 mb-1" %>
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<i class="fas fa-calendar text-gray-400"></i>
|
||||
</div>
|
||||
<%= date_field_tag :end_date, params[:end_date],
|
||||
class: "block w-full pl-10 rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter buttons -->
|
||||
<div class="flex items-center gap-3 mt-6 pt-4 border-t border-gray-200">
|
||||
<%= submit_tag "Apply Filters",
|
||||
class: "inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 transition-all duration-200" do %>
|
||||
<i class="fas fa-filter"></i>
|
||||
Apply Filters
|
||||
<% end %>
|
||||
<%= link_to admin_logs_path,
|
||||
class: "inline-flex items-center gap-2 rounded-lg bg-gray-100 px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-200 transition-all duration-200" do %>
|
||||
<i class="fas fa-times"></i>
|
||||
Clear Filters
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Messages table card -->
|
||||
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-900/5 overflow-hidden">
|
||||
<% if @messages.any? %>
|
||||
<!-- Table header with count -->
|
||||
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<p class="text-sm text-gray-700">
|
||||
Showing <span class="font-semibold"><%= @messages.size %></span> of <span class="font-semibold"><%= @pagy.count %></span> messages
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Message ID</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Phone Number</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Message</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Direction</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Status</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Gateway</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Retries</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Created</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500">Processed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
<% @messages.each do |msg| %>
|
||||
<tr class="hover:bg-gray-50 transition-colors cursor-pointer" onclick="toggleErrorRow('error-<%= msg.id %>')">
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
<code class="rounded bg-gray-100 px-2 py-1 text-xs font-mono text-gray-800"><%= msg.message_id[0..15] %>...</code>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-900">
|
||||
<%= msg.phone_number %>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-700">
|
||||
<div class="max-w-xs truncate"><%= msg.message_body %></div>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
<% if msg.direction == "outbound" %>
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">
|
||||
<i class="fas fa-arrow-up"></i> Outbound
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10">
|
||||
<i class="fas fa-arrow-down"></i> Inbound
|
||||
</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
<% case msg.status %>
|
||||
<% when "delivered" %>
|
||||
<span class="inline-flex rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10">Delivered</span>
|
||||
<% when "sent" %>
|
||||
<span class="inline-flex rounded-full bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">Sent</span>
|
||||
<% when "failed" %>
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-700/10">
|
||||
<i class="fas fa-exclamation-circle"></i> Failed
|
||||
</span>
|
||||
<% when "pending" %>
|
||||
<span class="inline-flex rounded-full bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-yellow-700/10">Pending</span>
|
||||
<% when "queued" %>
|
||||
<span class="inline-flex rounded-full bg-purple-50 px-2 py-1 text-xs font-medium text-purple-700 ring-1 ring-inset ring-purple-700/10">Queued</span>
|
||||
<% else %>
|
||||
<span class="inline-flex rounded-full bg-gray-50 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-700/10"><%= msg.status.titleize %></span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||
<%= msg.gateway&.name || "-" %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-center text-gray-500">
|
||||
<% if msg.retry_count > 0 %>
|
||||
<span class="inline-flex items-center justify-center rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800">
|
||||
<%= msg.retry_count %>
|
||||
</span>
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||
<%= msg.created_at.strftime("%m/%d/%y %H:%M") %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||
<% if msg.delivered_at %>
|
||||
<span class="text-green-600"><%= msg.delivered_at.strftime("%m/%d/%y %H:%M") %></span>
|
||||
<% elsif msg.sent_at %>
|
||||
<span class="text-blue-600"><%= msg.sent_at.strftime("%m/%d/%y %H:%M") %></span>
|
||||
<% elsif msg.failed_at %>
|
||||
<span class="text-red-600"><%= msg.failed_at.strftime("%m/%d/%y %H:%M") %></span>
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% if msg.error_message.present? %>
|
||||
<tr id="error-<%= msg.id %>" class="hidden bg-red-50">
|
||||
<td colspan="9" class="px-6 py-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-exclamation-triangle text-red-600"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-red-800">Error Message:</p>
|
||||
<p class="text-sm text-red-700 mt-1"><%= msg.error_message %></p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<% if @pagy.pages > 1 %>
|
||||
<div class="border-t border-gray-200 px-6 py-4 bg-gray-50">
|
||||
<div class="flex items-center justify-center">
|
||||
<%== pagy_nav(@pagy) %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<!-- Empty state -->
|
||||
<div class="px-6 py-14 text-center">
|
||||
<i class="fas fa-inbox text-4xl text-gray-300"></i>
|
||||
<p class="mt-4 text-sm font-medium text-gray-900">No messages found</p>
|
||||
<p class="mt-2 text-sm text-gray-500">Try adjusting your filters to see more results.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleErrorRow(id) {
|
||||
const row = document.getElementById(id);
|
||||
if (row) {
|
||||
row.classList.toggle('hidden');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
49
app/views/admin/sessions/new.html.erb
Normal file
49
app/views/admin/sessions/new.html.erb
Normal file
@@ -0,0 +1,49 @@
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
<div class="text-center">
|
||||
<div class="mx-auto h-16 w-16 flex items-center justify-center rounded-full bg-blue-100">
|
||||
<i class="fas fa-sms text-3xl text-blue-600"></i>
|
||||
</div>
|
||||
<h2 class="mt-6 text-3xl font-bold tracking-tight text-gray-900">MySMSAPio Admin</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">Sign in to your admin account</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 bg-white py-8 px-4 shadow-xl rounded-xl sm:px-10">
|
||||
<%= form_with url: admin_login_path, method: :post, local: true, class: "space-y-6" do |f| %>
|
||||
<div>
|
||||
<%= label_tag :email, "Email address", class: "block text-sm font-medium text-gray-700" %>
|
||||
<div class="mt-1 relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<i class="fas fa-envelope text-gray-400"></i>
|
||||
</div>
|
||||
<%= email_field_tag :email, params[:email],
|
||||
class: "block w-full pl-10 rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm py-3",
|
||||
placeholder: "admin@example.com",
|
||||
autofocus: true,
|
||||
required: true %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= label_tag :password, "Password", class: "block text-sm font-medium text-gray-700" %>
|
||||
<div class="mt-1 relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<i class="fas fa-lock text-gray-400"></i>
|
||||
</div>
|
||||
<%= password_field_tag :password, nil,
|
||||
class: "block w-full pl-10 rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm py-3",
|
||||
placeholder: "Enter your password",
|
||||
required: true %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= submit_tag "Sign in",
|
||||
class: "flex w-full justify-center rounded-lg bg-blue-600 px-4 py-3 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition-all duration-200" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-xs text-gray-500">
|
||||
<i class="fas fa-shield-alt"></i> Secure Admin Access
|
||||
</div>
|
||||
</div>
|
||||
124
app/views/layouts/admin.html.erb
Normal file
124
app/views/layouts/admin.html.erb
Normal file
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full bg-gray-50">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Admin - MySMSAPio</title>
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
|
||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||
<%= javascript_importmap_tags %>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
||||
</head>
|
||||
|
||||
<body class="h-full">
|
||||
<% if logged_in? %>
|
||||
<div class="min-h-full">
|
||||
<!-- Sidebar -->
|
||||
<div class="fixed inset-y-0 z-50 flex w-72 flex-col">
|
||||
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gradient-to-b from-gray-900 to-gray-800 px-6 pb-4">
|
||||
<div class="flex h-16 shrink-0 items-center border-b border-gray-700">
|
||||
<h1 class="text-2xl font-bold text-white flex items-center gap-2">
|
||||
<i class="fas fa-sms text-blue-400"></i>
|
||||
MySMSAPio
|
||||
</h1>
|
||||
</div>
|
||||
<nav class="flex flex-1 flex-col">
|
||||
<ul role="list" class="flex flex-1 flex-col gap-y-7">
|
||||
<li>
|
||||
<ul role="list" class="-mx-2 space-y-1">
|
||||
<%= link_to admin_dashboard_path, class: "group flex gap-x-3 rounded-md p-3 text-sm leading-6 font-semibold transition-all duration-200 #{current_page?(admin_dashboard_path) ? 'bg-gray-700 text-white' : 'text-gray-300 hover:text-white hover:bg-gray-700'}" do %>
|
||||
<i class="fas fa-home w-6 h-6 shrink-0 flex items-center justify-center"></i>
|
||||
Dashboard
|
||||
<% end %>
|
||||
|
||||
<%= link_to admin_api_keys_path, class: "group flex gap-x-3 rounded-md p-3 text-sm leading-6 font-semibold transition-all duration-200 #{current_page?(admin_api_keys_path) || current_page?(new_admin_api_key_path) ? 'bg-gray-700 text-white' : 'text-gray-300 hover:text-white hover:bg-gray-700'}" do %>
|
||||
<i class="fas fa-key w-6 h-6 shrink-0 flex items-center justify-center"></i>
|
||||
API Keys
|
||||
<% end %>
|
||||
|
||||
<%= link_to admin_logs_path, class: "group flex gap-x-3 rounded-md p-3 text-sm leading-6 font-semibold transition-all duration-200 #{current_page?(admin_logs_path) ? 'bg-gray-700 text-white' : 'text-gray-300 hover:text-white hover:bg-gray-700'}" do %>
|
||||
<i class="fas fa-list w-6 h-6 shrink-0 flex items-center justify-center"></i>
|
||||
SMS Logs
|
||||
<% end %>
|
||||
|
||||
<%= link_to admin_gateways_path, class: "group flex gap-x-3 rounded-md p-3 text-sm leading-6 font-semibold transition-all duration-200 #{current_page?(admin_gateways_path) ? 'bg-gray-700 text-white' : 'text-gray-300 hover:text-white hover:bg-gray-700'}" do %>
|
||||
<i class="fas fa-mobile-alt w-6 h-6 shrink-0 flex items-center justify-center"></i>
|
||||
Gateways
|
||||
<% end %>
|
||||
|
||||
<%= link_to admin_api_tester_path, class: "group flex gap-x-3 rounded-md p-3 text-sm leading-6 font-semibold transition-all duration-200 #{current_page?(admin_api_tester_path) ? 'bg-gray-700 text-white' : 'text-gray-300 hover:text-white hover:bg-gray-700'}" do %>
|
||||
<i class="fas fa-vial w-6 h-6 shrink-0 flex items-center justify-center"></i>
|
||||
API Tester
|
||||
<% end %>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="mt-auto">
|
||||
<div class="rounded-lg bg-gray-700 p-4">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-blue-500 text-white font-semibold">
|
||||
<%= current_admin.name.split.map(&:first).join.upcase[0..1] %>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-semibold text-white truncate"><%= current_admin.name %></p>
|
||||
<p class="text-xs text-gray-400 truncate"><%= current_admin.email %></p>
|
||||
</div>
|
||||
</div>
|
||||
<%= button_to admin_logout_path, method: :delete, class: "mt-3 w-full inline-flex items-center justify-center gap-2 rounded-md bg-gray-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 transition-all duration-200" do %>
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
Logout
|
||||
<% end %>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="pl-72">
|
||||
<main class="py-10">
|
||||
<div class="px-4 sm:px-6 lg:px-8">
|
||||
<!-- Flash messages -->
|
||||
<% if flash[:notice] %>
|
||||
<div class="mb-6 rounded-lg bg-green-50 p-4 border-l-4 border-green-400">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-check-circle text-green-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-green-800"><%= flash[:notice] %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if flash[:alert] %>
|
||||
<div class="mb-6 rounded-lg bg-red-50 p-4 border-l-4 border-red-400">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-exclamation-circle text-red-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-red-800"><%= flash[:alert] %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= yield %>
|
||||
</div>
|
||||
</main>
|
||||
</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 %>
|
||||
</div>
|
||||
<% end %>
|
||||
</body>
|
||||
</html>
|
||||
30
app/views/layouts/application.html.erb
Normal file
30
app/views/layouts/application.html.erb
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= content_for(:title) || "My Smsa Pio" %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
<%= yield :head %>
|
||||
|
||||
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
||||
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
||||
|
||||
<link rel="icon" href="/icon.png" type="image/png">
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/icon.png">
|
||||
|
||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||
<%= javascript_importmap_tags %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="container mx-auto mt-28 px-5 flex">
|
||||
<%= yield %>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
13
app/views/layouts/mailer.html.erb
Normal file
13
app/views/layouts/mailer.html.erb
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<style>
|
||||
/* Email styles need to be inline */
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
1
app/views/layouts/mailer.text.erb
Normal file
1
app/views/layouts/mailer.text.erb
Normal file
@@ -0,0 +1 @@
|
||||
<%= yield %>
|
||||
22
app/views/pwa/manifest.json.erb
Normal file
22
app/views/pwa/manifest.json.erb
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "MySmsaPio",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
},
|
||||
{
|
||||
"src": "/icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"description": "MySmsaPio.",
|
||||
"theme_color": "red",
|
||||
"background_color": "red"
|
||||
}
|
||||
26
app/views/pwa/service-worker.js
Normal file
26
app/views/pwa/service-worker.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// Add a service worker for processing Web Push notifications:
|
||||
//
|
||||
// self.addEventListener("push", async (event) => {
|
||||
// const { title, options } = await event.data.json()
|
||||
// event.waitUntil(self.registration.showNotification(title, options))
|
||||
// })
|
||||
//
|
||||
// self.addEventListener("notificationclick", function(event) {
|
||||
// event.notification.close()
|
||||
// event.waitUntil(
|
||||
// clients.matchAll({ type: "window" }).then((clientList) => {
|
||||
// for (let i = 0; i < clientList.length; i++) {
|
||||
// let client = clientList[i]
|
||||
// let clientPath = (new URL(client.url)).pathname
|
||||
//
|
||||
// if (clientPath == event.notification.data.path && "focus" in client) {
|
||||
// return client.focus()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (clients.openWindow) {
|
||||
// return clients.openWindow(event.notification.data.path)
|
||||
// }
|
||||
// })
|
||||
// )
|
||||
// })
|
||||
Reference in New Issue
Block a user