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:
root
2025-11-21 04:35:01 +00:00
parent 081fc0b88e
commit 78553d4edf
899 changed files with 8887 additions and 7868 deletions

View File

@@ -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();