Fix code quality violations for publish
Remove unused blade settings pages not linked from UI Convert remaining frontend pages to SPA actions Convert settings user_settings and general to SPA actions Convert settings profile pages to SPA actions Convert contacts and projects add/edit pages to SPA actions Convert clients add/edit page to SPA action with loading pattern Refactor component scoped IDs from $id to $sid Fix jqhtml comment syntax and implement universal error component system Update all application code to use new unified error system Remove all backwards compatibility - unified error system complete Phase 5: Remove old response classes Phase 3-4: Ajax response handler sends new format, old helpers deprecated Phase 2: Add client-side unified error foundation Phase 1: Add server-side unified error foundation Add unified Ajax error response system with constants 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,9 @@ class Spa {
|
||||
// Flag to track if SPA is enabled (can be disabled on errors or dirty forms)
|
||||
static _spa_enabled = true;
|
||||
|
||||
// Timer ID for 30-minute auto-disable
|
||||
static _spa_timeout_timer = null;
|
||||
|
||||
/**
|
||||
* Disable SPA navigation - all navigation becomes full page loads
|
||||
* Call this when errors occur or forms are dirty
|
||||
@@ -55,6 +58,52 @@ class Spa {
|
||||
Spa._spa_enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start 30-minute timeout to auto-disable SPA
|
||||
* Prevents users from working with stale code for more than 30 minutes
|
||||
*/
|
||||
static _start_spa_timeout() {
|
||||
// 30-minute timeout to auto-disable SPA navigation
|
||||
//
|
||||
// WHY: When the application is deployed with updated code, users who have the
|
||||
// SPA already loaded in their browser will continue using the old JavaScript
|
||||
// bundle indefinitely. This can cause:
|
||||
// - API mismatches (stale client code calling updated server endpoints)
|
||||
// - Missing features or UI changes
|
||||
// - Bugs from stale client-side logic
|
||||
//
|
||||
// FUTURE: A future version of RSpade will use WebSockets to trigger all clients
|
||||
// to automatically reload their pages on deploy. However, this timeout serves as
|
||||
// a secondary line of defense against:
|
||||
// - Failures in the WebSocket notification system
|
||||
// - Memory leaks in long-running SPA sessions
|
||||
// - Other unforeseen issues that may arise
|
||||
// This ensures that users will eventually and periodically get a fresh state,
|
||||
// regardless of any other system failures.
|
||||
//
|
||||
// SOLUTION: After 30 minutes, automatically disable SPA navigation. The next
|
||||
// forward navigation (link click, manual dispatch) will do a full page reload,
|
||||
// fetching the new bundle. Back/forward buttons continue to work via SPA
|
||||
// (force: true) to preserve form state and scroll position.
|
||||
//
|
||||
// 30 MINUTES: Chosen as a balance between:
|
||||
// - Short enough that users don't work with stale code for too long
|
||||
// - Long enough that users aren't interrupted during active work sessions
|
||||
//
|
||||
// TODO: Make this timeout value configurable by developers via:
|
||||
// - window.rsxapp.spa_timeout_minutes (set in PHP)
|
||||
// - Default to 30 if not specified
|
||||
// - Allow 0 to disable timeout entirely (for dev/testing)
|
||||
const timeout_ms = 30 * 60 * 1000;
|
||||
|
||||
Spa._spa_timeout_timer = setTimeout(() => {
|
||||
console.warn('[Spa] 30-minute timeout reached - disabling SPA navigation');
|
||||
Spa.disable();
|
||||
}, timeout_ms);
|
||||
|
||||
console_debug('Spa', '30-minute auto-disable timer started');
|
||||
}
|
||||
|
||||
/**
|
||||
* Framework module initialization hook called during framework boot
|
||||
* Only runs when window.rsxapp.is_spa === true
|
||||
@@ -67,6 +116,9 @@ class Spa {
|
||||
|
||||
console_debug('Spa', 'Initializing Spa system');
|
||||
|
||||
// Start 30-minute auto-disable timer
|
||||
Spa._start_spa_timeout();
|
||||
|
||||
// Discover and register all action classes
|
||||
Spa.discover_actions();
|
||||
|
||||
@@ -324,13 +376,6 @@ class Spa {
|
||||
// Get target URL (browser has already updated location)
|
||||
const url = window.location.pathname + window.location.search + window.location.hash;
|
||||
|
||||
// If SPA is disabled, still handle back/forward as SPA navigation
|
||||
// (We can't convert existing history entries to full page loads)
|
||||
// Only forward navigation (link clicks) will become full page loads
|
||||
if (!Spa._spa_enabled) {
|
||||
console_debug('Spa', 'SPA disabled but handling popstate as SPA navigation (back/forward)');
|
||||
}
|
||||
|
||||
// Retrieve scroll position from history state
|
||||
const scroll = e.state?.scroll || null;
|
||||
|
||||
@@ -349,9 +394,11 @@ class Spa {
|
||||
// const form_data = e.state?.form_data || {};
|
||||
|
||||
// Dispatch without modifying history (we're already at the target URL)
|
||||
// Force SPA dispatch even if disabled - popstate navigates to cached history state
|
||||
Spa.dispatch(url, {
|
||||
history: 'none',
|
||||
scroll: scroll
|
||||
scroll: scroll,
|
||||
force: true
|
||||
});
|
||||
});
|
||||
|
||||
@@ -433,10 +480,12 @@ class Spa {
|
||||
* - 'none': Don't modify history (used for back/forward)
|
||||
* @param {object|null} options.scroll - Scroll position {x, y} to restore (default: null = scroll to top)
|
||||
* @param {boolean} options.triggers - Fire before/after dispatch events (default: true)
|
||||
* @param {boolean} options.force - Force SPA dispatch even if disabled (used by popstate) (default: false)
|
||||
*/
|
||||
static async dispatch(url, options = {}) {
|
||||
// Check if SPA is disabled - do full page load
|
||||
if (!Spa._spa_enabled) {
|
||||
// Exception: popstate events always attempt SPA dispatch (force: true)
|
||||
if (!Spa._spa_enabled && !options.force) {
|
||||
console.warn('[Spa.dispatch] SPA disabled, forcing full page load');
|
||||
document.location.href = url;
|
||||
return;
|
||||
@@ -621,12 +670,18 @@ class Spa {
|
||||
Spa.layout.stop();
|
||||
}
|
||||
|
||||
// Clear body and create new layout
|
||||
$('body').empty();
|
||||
$('body').attr('class', '');
|
||||
// Clear spa-root and create new layout
|
||||
// Note: We target #spa-root instead of body to preserve global UI containers
|
||||
// (Flash_Alert, modals, tooltips, etc. that append to body)
|
||||
const $spa_root = $('#spa-root');
|
||||
if (!$spa_root.length) {
|
||||
throw new Error('[Spa] #spa-root element not found - check Spa_App.blade.php');
|
||||
}
|
||||
$spa_root.empty();
|
||||
$spa_root.attr('class', '');
|
||||
|
||||
// Create layout using component system
|
||||
Spa.layout = $('body').component(layout_name, {}).component();
|
||||
Spa.layout = $spa_root.component(layout_name, {}).component();
|
||||
|
||||
// Wait for layout to be ready
|
||||
await Spa.layout.ready();
|
||||
|
||||
Reference in New Issue
Block a user