Files
sso-manager-node/nodejs/views/notifications.ejs
T
wmantly f72e8881cf Fix mobile layout: wrap unresponsive tables, let the group filter bar wrap (#42)
Auditing mobile/responsive views turned up two classes of bug:

1. Missing .table-responsive wrapper (token.ejs and profile.ejs's other
   table already had it): admin.ejs (2 tables), invites.ejs (6 columns),
   notifications.ejs (5 columns), users.ejs (7 columns — the worst case).
   Without it these overflow the page horizontally on narrow viewports
   instead of scrolling within the table.

2. groups.ejs's search/sort/count toolbar was a plain `d-flex` (no
   flex-wrap) containing a search input, a <select> with a hard
   min-width:175px, and a nowrap count span — on a narrow viewport
   there's nowhere for that content to go but off-screen. Added
   flex-wrap and gave the search input a flex-basis so it wraps onto
   its own line first when space is tight.

Verified: EJS compiles for all five templates, npm test 192/192 pass,
and fetched each route from a running instance to confirm the fixes
are present in the served HTML.
2026-07-14 21:18:01 -04:00

193 lines
7.4 KiB
Plaintext

<%- include('top') %>
<script type="text/javascript">
app.auth.forceLogin('app_sso_admin');
async function loadHistory() {
try {
const data = await app.api.get('notification');
const list = data.results || [];
list.forEach(function(n) {
n.created_on_fmt = moment(n.created_on).fromNow();
n.filter_label = formatFilterLabel(n.filter_type, n.filter_value, n.active_only);
});
$.scope.notificationHistory.push(...list);
} catch(e) {
if (e && e.status === 401) location.replace('/');
else console.error('Failed to load notification history:', e);
}
}
function formatFilterLabel(type, value, active_only) {
const suffix = active_only ? ' (active only)' : '';
if (type === 'group') return 'Groups: ' + value + suffix;
if (type === 'users') return 'Specific users';
if (type === 'all_active') return 'All active users';
if (type === 'all') return 'All users';
return type;
}
function toggleFilterInputs() {
const type = document.querySelector('input[name="notif-filter"]:checked').value;
document.getElementById('notif-group-row').style.display = type === 'group' ? '' : 'none';
document.getElementById('notif-users-row').style.display = type === 'users' ? '' : 'none';
document.getElementById('notif-active-row').style.display = (type === 'group' || type === 'all') ? '' : 'none';
}
async function sendNotification() {
const subject = document.getElementById('notif-subject').value.trim();
const message = document.getElementById('notif-message').value.trim();
const filterType = document.querySelector('input[name="notif-filter"]:checked').value;
const groupValue = document.getElementById('notif-group').value.trim();
const usersValue = document.getElementById('notif-users').value.trim();
const activeOnly = document.getElementById('notif-active-only').checked;
const msgEl = document.getElementById('notif-result');
if (!subject || !message) { alert('Subject and message are required.'); return; }
let filter_value = '';
if (filterType === 'group') filter_value = groupValue;
if (filterType === 'users') filter_value = JSON.stringify(usersValue.split(',').map(s => s.trim()).filter(Boolean));
msgEl.className = 'alert alert-info mt-2';
msgEl.textContent = 'Sending…';
msgEl.style.display = '';
try {
const result = await app.api.post('notification', {
subject,
message,
filter_type: filterType,
filter_value,
active_only: activeOnly,
});
msgEl.className = 'alert alert-success mt-2';
msgEl.textContent = `Sent to ${result.results.sent_count} recipient(s). ${result.results.failed_count} failed.`;
const n = result.results;
n.created_on_fmt = 'just now';
n.filter_label = formatFilterLabel(n.filter_type, n.filter_value, n.active_only);
$.scope.notificationHistory.unshift([n]);
// Reset form
document.getElementById('notif-subject').value = '';
document.getElementById('notif-message').value = '';
} catch(e) {
msgEl.className = 'alert alert-danger mt-2';
msgEl.textContent = 'Failed: ' + ((e.responseJSON && e.responseJSON.message) || e.message || 'Unknown error');
}
}
$(document).ready(function() {
loadHistory();
toggleFilterInputs();
});
</script>
<div class="row">
<div class="col-12">
<div class="d-flex align-items-center mb-3 mt-2">
<h4 class="mb-0"><i class="fa-solid fa-paper-plane"></i> Notifications</h4>
</div>
<div class="row g-3">
<!-- Compose -->
<div class="col-lg-6">
<div class="card shadow-lg">
<div class="card-header shadow">
<i class="fa-solid fa-pencil"></i> Compose
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Subject</label>
<input type="text" class="form-control shadow" id="notif-subject" placeholder="Maintenance window tonight" />
</div>
<div class="mb-3">
<label class="form-label">Message <small class="text-muted">(HTML allowed)</small></label>
<textarea class="form-control shadow" id="notif-message" rows="6" placeholder="<p>Hello, we will be performing maintenance...</p>"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Send to</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="notif-filter" id="f-all-active" value="all_active" checked onchange="toggleFilterInputs()">
<label class="form-check-label" for="f-all-active">All active users</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="notif-filter" id="f-all" value="all" onchange="toggleFilterInputs()">
<label class="form-check-label" for="f-all">All users (including inactive)</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="notif-filter" id="f-group" value="group" onchange="toggleFilterInputs()">
<label class="form-check-label" for="f-group">Group members</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="notif-filter" id="f-users" value="users" onchange="toggleFilterInputs()">
<label class="form-check-label" for="f-users">Specific users</label>
</div>
</div>
<div id="notif-group-row" class="mb-3" style="display:none">
<label class="form-label">Groups <small class="text-muted">(comma-separated)</small></label>
<input type="text" class="form-control shadow" id="notif-group" placeholder="host_hec-bot_admin, app_sso_admin" />
</div>
<div id="notif-users-row" class="mb-3" style="display:none">
<label class="form-label">UIDs <small class="text-muted">(comma-separated)</small></label>
<input type="text" class="form-control shadow" id="notif-users" placeholder="wmantly, jsmith" />
</div>
<div id="notif-active-row" class="mb-3" style="display:none">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="notif-active-only" checked>
<label class="form-check-label" for="notif-active-only">Active members only</label>
</div>
</div>
<button class="btn btn-primary shadow" onclick="sendNotification()">
<i class="fa-solid fa-paper-plane"></i> Send
</button>
<div id="notif-result" style="display:none" class="mt-2"></div>
</div>
</div>
</div>
<!-- History -->
<div class="col-lg-6">
<div class="card shadow-lg">
<div class="card-header shadow">
<i class="fa-solid fa-clock-rotate-left"></i> History
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm mb-0">
<thead>
<tr>
<th>Sent</th>
<th>Subject</th>
<th>Filter</th>
<th class="text-center">✓</th>
<th class="text-center">✗</th>
</tr>
</thead>
<tbody jq-repeat="notificationHistory" jq-index-key="notification_id">
<tr>
<td><small class="text-muted">{{created_on_fmt}}</small></td>
<td><small>{{subject}}</small></td>
<td><small class="text-muted">{{filter_label}}</small></td>
<td class="text-center text-success"><small>{{sent_count}}</small></td>
<td class="text-center text-danger"><small>{{failed_count}}</small></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('impersonate_modal') %>
<%- include('bottom') %>