Dynamically update page title emoji when device/alert status changes:
- Add PubSub broadcasts when alerts are created/resolved
- Create StatusTitleComponent LiveComponent to subscribe to alert changes
- Add StatusTitle JavaScript hook to update document.title in real-time
- Subscribe to organization:alerts channel on LiveView mount
- Push emoji update events when alert status changes
This provides immediate visual feedback in browser tabs when
critical/warning/healthy status changes without requiring page refresh.
When the Oban queue backs up, the 60-second uniqueness window expires
and duplicate jobs stack up per device. Switch to period: :infinity so
only one poll/monitor job exists per device at any time. Add replace
option to supersede stale scheduled jobs with updated scheduled_at.
Remove :executing from unique states so self-scheduling works while
the current job runs. Set max_attempts: 1 since retrying stale polls
is pointless.
Also fix all credo --strict issues across the codebase.
New role system:
- owner: full access + financials
- admin: full access + financials (except org deletion)
- executive: read-only + full financial visibility (MRR, revenue)
- technician: field ops (devices, sites, alerts) without financials
- member: legacy alias for technician (migrated)
- viewer: read-only, no financials
Financial data (MRR, revenue, billing) gated behind can_view_financials
assign in all LiveView templates: dashboard, alerts, trace, site show.
Includes migration to rename existing member roles to technician.
CRITICAL fixes:
- Mask SNMP community string in agent channel logs (CRITICAL-1)
- Remove snmpv3 passwords from REST API responses, return _set booleans (CRITICAL-2)
- Replace snmp_community with snmp_community_set in GraphQL type (CRITICAL-3)
HIGH fixes:
- Fix cookie same_site from invalid 'Towerops' to 'Lax' (HIGH-4)
- Remove unsafe-eval from CSP script-src (HIGH-6)
- Block GraphQL introspection queries in production (HIGH-7)
- Sanitize LIKE wildcards in SNMP device name search (HIGH-8)
- Reject webhooks when no secret configured instead of accepting (HIGH-9)
MEDIUM fixes:
- Hash mobile session tokens (SHA-256) before DB storage (MEDIUM-10)
- Apply security headers in all environments, not just prod (MEDIUM-14)
- Add GraphQL query complexity limit (500) in production (MEDIUM-16)
- Fix X-Frame-Options to DENY to match frame-ancestors 'none' (MEDIUM-13)
Previously redirected to /dashboard or /orgs based on whether the
superuser had organizations. Now always goes to /devices for
consistency.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Migrates all admin-related flash messages to gettext for internationalization:
- Admin.UserLive.Index: user deletion messages
- Admin.OrgLive.Index: organization deletion messages
- UserAuth: impersonation start/stop messages
Adds Gettext backend and helpers import to UserAuth module to support translations.
All admin and auth tests passing (87 tests).
- Add UserSudoController with GET and POST /users/sudo/verify routes
- Create verify.html.heex template for TOTP verification form
- Only accept TOTP codes (6 numeric digits), reject recovery codes
- Update grant_sudo_mode to set authenticated_at virtual field
- Exclude /users/sudo paths from return_to overwriting
- Add comprehensive controller tests (12 test cases)
- Verify redirect behavior, error handling, and session management
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Created new on_mount hook :load_default_organization that automatically loads user's first organization
- Moved sites and devices routes to root path (/ sites, /devices) instead of /orgs/:org_slug
- Updated all navigation links and route references throughout the app
- Dashboard, alerts, and agents still use org-specific routes (/orgs/:org_slug)
- Users can still switch organizations via org selector for other pages
- Add timezone to socket assigns in mount_current_scope
- Detect user timezone from browser using Intl.DateTimeFormat
- Auto-save detected timezone to user profile on first connection
- Update all templates to use TimeHelpers with timezone parameter
- Update agent helper functions to accept timezone parameter
- Templates updated:
- Alert list (triggered, acknowledged, resolved)
- Device list (last checked)
- Agent list and show (created, last seen, updated)
- Admin dashboard (impersonation logs)
- Admin org/user lists (created/joined dates)
- Main dashboard (alert times)
- Update user settings page to use Tailwind Plus 3-column grid layout
- Add API token management section to user settings
- Implement organization name display in navigation menu
- Update table component to use Tailwind Plus styling across all tables
- Update flash notifications with Tailwind Plus design
- Update dropdown menus and select inputs with custom chevron styling
- Fix authentication redirect to return to original page after login
- Preserve user_return_to session key across session renewal
- Add store_return_to_for_liveview plug to browser pipeline
- Add profiles.json to gitignore
- Create UserSettingsLive to replace UserSettingsController
- Convert email/password forms to LiveView with proper form handling
- Add mobile session management (toggle alerts, revoke devices)
- Add require_sudo_mode on_mount hook to UserAuth
- Create dedicated live_session for sudo mode routes in router
- Keep UserSettingsController for email confirmation via token only
- Add MobileSessions.get_session/1 function for test support
- Update tests to match LiveView behavior (password update redirects to login)
- Remove old controller tests for email/password updates (now in LiveView)
Login flow improvements:
- Remove "Welcome back!" flash message on successful login
- Auto-login users clicking magic links without confirmation prompt
- Always use "remember me" for magic link logins
- Fix Accounts.login_user_by_magic_link/1 to handle invalid token format
Test fixes:
- Add @tag :integration to Ping tests (require actual system ping)
- Add halt() calls to UserAuth.start_impersonation error paths
Users now go directly to their intended destination after clicking a
magic link, with no interruption for "log in and stay logged in" choice.
When exiting impersonation, now redirects to the superuser's default
organization's equipment list page instead of /admin. Falls back to /orgs
if the superuser has no organizations.
Critical bug fix: fetch_current_scope_for_user and mount_current_scope
were calling Accounts.get_user(nil) when impersonating session flag was
true but superuser_id or target_user_id were nil.
This caused FunctionClauseError crashes for all logged-out users if they
had stale impersonation session data.
Changes:
- Check if both superuser_id and target_user_id exist before calling get_user
- Clear invalid impersonation state if IDs are missing
- Apply fix to both fetch_current_scope_for_user (controllers) and
mount_current_scope (LiveViews)
This ensures graceful handling of corrupted/partial session state.
Changed signed_in_path to redirect users to their default organization's
equipment page instead of the organization list.
Behavior:
- If user has organizations, redirect to first org's equipment page
- First org is determined by most recently joined (membership.inserted_at)
- If user has no organizations, redirect to /orgs to create one
This provides a better UX by landing users directly at their equipment
list instead of requiring an extra click through the org selector.
Security improvements to prevent potential issues:
1. Port Validation (normalize_port):
- Validate port range is 1-65535
- Use Integer.parse instead of String.to_integer to prevent crashes
- Return default port 161 for any invalid input
2. SNMP Test Validation (validate_test_snmp_input):
- Validate IP address format before allowing SNMP tests
- Require non-empty community string
- Prevent network scanning with invalid/missing IPs
- Return clear error messages for validation failures
These changes ensure user input is properly validated and prevent:
- Integer overflow/underflow with ports
- Process crashes from invalid input
- Unauthorized network scanning via SNMP test feature
- SNMP requests with missing credentials