Files
rspade_system/docs/CLAUDE.dist.md
2025-12-26 02:17:31 +00:00

43 KiB

=============================================================================== FRAMEWORK DOCUMENTATION - READ ONLY

This file: /var/www/html/system/docs/CLAUDE.dist.md (symlinked to ~/.claude/CLAUDE.md) Your file: /var/www/html/CLAUDE.md

This file is replaced during php artisan rsx:framework:pull. Do not edit it. For application-specific notes, edit /var/www/html/CLAUDE.md instead.

DOCUMENTATION SIZE CONSTRAINTS

Claude Code recommends combined CLAUDE.md files under 40kb - already tight for framework + application documentation. Every line must justify its existence.

When editing /var/www/html/CLAUDE.md:

  • Terse, not verbose - no filler words or redundant explanations
  • Complete, not partial - include all critical information
  • Patterns over prose - code examples beat paragraphs
  • Reference this file's tone and density as the standard

===============================================================================

RSpade Framework - AI/LLM Development Guide

PURPOSE: Essential directives for AI/LLM assistants developing RSX applications with RSpade.

CRITICAL: Questions vs Commands

  • Questions get answers, NOT actions - "Is that fire?" gets "Yes" not "Let me run through it". User has a plan, don't take destructive action when asked a question.
  • Commands get implementation - Clear directives result in code changes

What is RSpade?

Visual Basic-like development for PHP/Laravel. Think: VB6 apps → VB6 runtime → Windows = RSX apps → RSpade runtime → Laravel.

Philosophy: RSpade is rebellion against JavaScript fatigue. Like VB6 made Windows programming accessible, RSpade makes web development simple again. No build steps, no config hell, just code and reload.

Important: RSpade is built on Laravel but diverges significantly. Do not assume Laravel patterns work in RSX without verification.

Terminology: RSpade = Complete framework | RSX = Your application code in /rsx/


CRITICAL RULES

CRITICAL: RSpade Builds Automatically - NEVER RUN BUILD COMMANDS

RSpade is INTERPRETED - changes compile on-the-fly. NO manual build steps.

FORBIDDEN (unless explicitly instructed):

  • npm run compile/build - Don't exist
  • bin/publish - For releases, not testing
  • rsx:bundle:compile / rsx:manifest:build / rsx:clean - Automatic
  • ANY "build", "compile", or "publish" command

Edit → Save → Refresh browser → Changes live (< 1 second)

Framework Updates

php artisan rsx:framework:pull  # User-initiated only

Fail Loud - No Silent Fallbacks

ALWAYS fail visibly. No redundant fallbacks, silent failures, or alternative code paths.

// ❌ WRONG
try { $clean = Sanitizer::sanitize($input); }
catch (Exception $e) { $clean = $input; }  // DISASTER

// ✅ CORRECT
$clean = Sanitizer::sanitize($input);  // Let it throw

SECURITY-CRITICAL: Never continue after security failures. Only catch expected failures (file uploads, APIs, user input). Let exceptions bubble to global handler.

No Defensive Coding

Core classes ALWAYS exist. Never check.

// ❌ BAD: if (typeof Rsx !== 'undefined') { Rsx.Route('Controller::method') }
// ✅ GOOD: Rsx.Route('Controller::method')

Static-First Philosophy

Classes are namespacing tools. Use static unless instances needed (models, resources). Avoid dependency injection.

No Field Aliasing

Field names must be identical across all layers. Database → PHP → JSON → JavaScript: same names, always.

// ❌ WRONG - renaming during serialization
return ['type_label' => $contact->type_id_label];

// ✅ CORRECT - names match source
return ['type_id_label' => $contact->type_id_label];

One string everywhere. Grep finds all usages. No mental mapping between layers.

Git Workflow - Framework is READ-ONLY

NEVER modify /var/www/html/system/ - It's like node_modules or the Linux kernel.

  • App repo: /var/www/html/.git (you control)
  • Framework: /var/www/html/system/ (submodule, don't touch)
  • Your code: /var/www/html/rsx/ (all changes here)

Commit discipline: ONLY commit when explicitly asked. Commits are milestones, not individual changes.

Class Overrides

To customize framework classes without modifying /system/, copy them to rsx/ with the same class name. The manifest automatically uses your version and renames the framework file to .upstream.

Common override targets (copy from system/app/RSpade/Core/Models/):

  • User_Model - Add custom fields, relationships, methods
  • User_Profile_Model - Extend profile data
  • Site_Model - Add site-specific settings
cp system/app/RSpade/Core/Models/User_Model.php rsx/models/user_model.php
# Edit namespace to Rsx\Models, customize as needed

Details: php artisan rsx:man class_override

Trust Code Quality Rules

Each rsx:check rule has remediation text that tells AI assistants exactly what to do:

  • Some rules say "fix immediately"
  • Some rules say "present options and wait for decision"

AI should follow the rule's guidance precisely. Rules are deliberately written and well-reasoned.


NAMING CONVENTIONS

Enforced by rsx:check:

Context Convention Example
PHP Methods/Variables underscore_case user_name
PHP Classes Like_This User_Controller
JavaScript Classes Like_This User_Card
Files lowercase_underscore user_controller.php
Database Tables lowercase_plural users
Constants UPPERCASE MAX_SIZE

File Prefix Grouping

Files sharing a common prefix are a related set. When renaming, maintain the grouping across ALL files with that prefix.

Example - rsx/app/frontend/calendar/:

frontend_calendar_event.scss
frontend_calendar_event_controller.php
frontend_calendar_event.jqhtml
frontend_calendar_event.js

Critical: Never create same-name different-case files (e.g., user.php and User.php).

Component Naming Pattern

Input components follow: {Supertype}_{Variant}_{Supertype} → e.g., Select_Country_Input, Select_State_Input, Select_Ajax_Input


DIRECTORY STRUCTURE

/var/www/html/
├── rsx/                        # YOUR CODE
│   ├── app/                    # Modules
│   ├── models/                 # Database models
│   ├── services/               # Background tasks, external integrations
│   ├── public/                 # Static files (web-accessible)
│   ├── resource/               # Framework-ignored
│   └── theme/                  # Global assets
└── system/                     # FRAMEWORK (read-only)

Services: Extend Rsx_Service_Abstract for non-HTTP functionality like scheduled tasks or external system integrations.

Special Directories (Path-Agnostic)

resource/ - ANY directory named this is framework-ignored. Store helpers, docs, third-party code. Exception: /rsx/resource/config/ IS processed.

public/ - ANY directory named this is web-accessible, framework-ignored. 5min cache, 30d with ?v=.

Path-Agnostic Loading

Classes found by name, not path. No imports needed.

$user = User_Model::find(1);  // Framework finds it
// NOT: use Rsx\Models\User_Model;  // Auto-generated

CONFIGURATION

Two-tier system:

  • Framework: /system/config/rsx.php (never modify)
  • User: /rsx/resource/config/rsx.php (your overrides)

Merged via array_merge_deep(). Common overrides: development.auto_rename_files, bundle_aliases, console_debug.


ROUTING & CONTROLLERS

class Frontend_Controller extends Rsx_Controller_Abstract
{
    public static function pre_dispatch(Request $request, array $params = [])
    {
        if (!Session::is_logged_in()) return response_unauthorized();
        return null;
    }

    #[Route('/', methods: ['GET'])]
    public static function index(Request $request, array $params = [])
    {
        return rsx_view('Frontend_Index', [
            'bundle' => Frontend_Bundle::render()
        ]);
    }
}

Rules: Only GET/POST. Use :param syntax. Manual auth checks in pre_dispatch or method body.

Authentication Pattern

// Controller-wide auth (recommended)
public static function pre_dispatch(Request $request, array $params = []) {
    if (!Session::is_logged_in()) return response_unauthorized();
    return null;
}

Type-Safe URLs

MANDATORY: All URLs must be generated using Rsx::Route() - hardcoded URLs are forbidden.

// PHP - Controller (defaults to 'index' method)
Rsx::Route('User_Controller')

// PHP - Controller with explicit method.  Passing an integer for param two implies the param two is 'id' (id=123)
Rsx::Route('User_Controller::show', 123);

// PHP - With query parameters.  Extra params not defined in route itself become query string - automatically URL-encoded
Rsx::Route('Login_Controller::logout', ['redirect' => '/dashboard']);
// Generates: /logout?redirect=%2Fdashboard

// JavaScript (identical syntax)
Rsx.Route('User_Controller')
Rsx.Route('User_Controller::show', 123);
Rsx.Route('Login_Controller::logout', {redirect: '/dashboard'});
  • Unimplemented routes: Prefix method with #Rsx::Route('Feature::#index') generates href="#" and bypasses validation

Enforcement: rsx:check will flag hardcoded URLs like /login or /logout?redirect=... and require you to use Rsx::Route(). Do it right the first time to avoid rework.


SPA (SINGLE PAGE APPLICATION) ROUTING

Client-side routing for authenticated application areas. One PHP bootstrap controller, multiple JavaScript actions that navigate without page reloads.

SPA Components

1. PHP Bootstrap Controller - ONE per module with auth in pre_dispatch

public static function pre_dispatch(Request $request, array $params = []) {
    if (!Session::is_logged_in()) return response_unauthorized();
    return null;
}

#[SPA]
public static function index(Request $request, array $params = []) {
    return rsx_view(SPA);
}

One #[SPA] per module at rsx/app/(module)/(module)_spa_controller::index. Segregates code by permission level.

2. JavaScript Actions (MANY)

@route('/contacts')
@layout('Frontend_Layout')
@spa('Frontend_Spa_Controller::index')
@title('Contacts')  // Optional browser title
class Contacts_Index_Action extends Spa_Action {
    async on_load() {
        this.data.contacts = await Frontend_Contacts_Controller.datagrid_fetch();
    }
}

3. Layout

class Frontend_Layout extends Spa_Layout {
    on_action(url, action_name, args) {
        // Called after action created, before on_ready
        // Access this.action immediately
        this.update_navigation(url);
    }
}

Layout template must have $sid="content" element where actions render.

URL Generation & Navigation

// PHP/JavaScript - same syntax
Rsx::Route('Contacts_Index_Action')  // /contacts
Rsx::Route('Contacts_View_Action', 123)  // /contacts/123
Spa.dispatch('/contacts/123');  // Programmatic navigation
Spa.layout   // Current layout instance
Spa.action   // Current action instance

URL Parameters

// URL: /contacts/123?tab=history
@route('/contacts/:id')
class Contacts_View_Action extends Spa_Action {
    on_create() {
        console.log(this.args.id);   // "123" (route param)
        console.log(this.args.tab);  // "history" (query param)
    }
}

File Organization

Pattern: /rsx/app/(module)/(feature)/

  • Module: Major functionality (login, frontend, root)
  • Feature: Screen within module (contacts, reports, invoices)
  • Submodule: Feature grouping (settings), often with sublayouts
/rsx/app/frontend/                      # Module
├── Frontend_Spa_Controller.php         # Single SPA bootstrap
├── Frontend_Layout.js
├── Frontend_Layout.jqhtml
└── contacts/                           # Feature
    ├── frontend_contacts_controller.php    # Ajax endpoints only
    ├── Contacts_Index_Action.js            # /contacts
    ├── Contacts_Index_Action.jqhtml
    ├── Contacts_View_Action.js             # /contacts/:id
    └── Contacts_View_Action.jqhtml

Use SPA for: Authenticated areas, dashboards, admin panels Avoid for: Public pages (SEO needed), simple static pages

Sublayouts

Sublayouts are Spa_Layout classes for nested persistent UI (e.g., settings sidebar). Use multiple @layout decorators - first is outermost: @layout('Frontend_Spa_Layout') then @layout('Settings_Layout'). Each must have $sid="content". Layouts persist when unchanged; only differing parts recreated. All receive on_action(url, action_name, args) with final action info.

Details: php artisan rsx:man spa

View Action Pattern

Three-state pattern for data-loading actions:

on_create() {
    this.data.record = { name: '' };  // Stub
    this.data.error_data = null;
    this.data.loading = true;
}
async on_load() {
    try { this.data.record = await Controller.get({id: this.args.id}); }
    catch (e) { this.data.error_data = e; }
    this.data.loading = false;
}

Template: <Loading_Spinner><Universal_Error_Page_Component> → content. Details: rsx:man view_action_patterns


CONVERTING BLADE PAGES TO SPA ACTIONS

For converting server-side Blade pages to client-side SPA actions, see php artisan rsx:man blade_to_spa. The process involves creating Action classes with @route decorators and converting templates from Blade to jqhtml syntax.


BLADE & VIEWS

Note: SPA pages are the preferred standard. Use Blade only for SEO-critical public pages or authentication flows.

@rsx_id('Frontend_Index')  {{-- Every view starts with this --}}
<body class="{{ rsx_body_class() }}">  {{-- Adds view class --}}

NO inline styles, scripts, or event handlers - Use companion .scss and .js files. jqhtml components work fully in Blade (no slots).

SCSS Component-First Architecture

SCSS in rsx/app/ and rsx/theme/components/ must wrap in a single component class matching the jqhtml/blade file. Components auto-render with class="Component_Name" on root. rsx/lib/ is non-visual. rsx/theme/ (outside components/) holds primitives, variables, Bootstrap overrides.

BEM: Child classes use component's exact name as prefix. .Component_Name { &__element } → HTML: <div class="Component_Name__element"> (no kebab-case).

Variables: rsx/theme/variables.scss - must be included before directory includes in bundles. Multiple SCSS files can target same component if primary file exists.

Details: php artisan rsx:man scss

Responsive Breakpoints

RSX replaces Bootstrap breakpoints with semantic names. Bootstrap defaults (col-md-6, d-lg-none) do NOT work.

Tier 1 Range Tier 2 Range
mobile 0-1023px phone 0-799px
desktop 1024px+ tablet 800-1023px
desktop-sm/md/lg 1024+

SCSS: @include mobile { }, @include desktop { }, @include phone { }, etc. Classes: .col-mobile-6, .d-desktop-none, .mobile-only, .hide-tablet JS: Responsive.is_mobile(), Responsive.is_phone(), Responsive.is_desktop_sm(), etc.

Details: php artisan rsx:man responsive

JavaScript for Blade Pages

Unlike SPA actions (which use component lifecycle), Blade pages use static on_app_ready() with a page guard:

class My_Page {  // Matches @rsx_id('My_Page')
    static on_app_ready() {
        if (!$('.My_Page').exists()) return;  // Guard required - fires for ALL pages in bundle
        // Page code here
    }
}

Passing Data to JavaScript

Use @rsx_page_data for page-specific data needed by JavaScript (IDs, config, etc.):

@rsx_page_data(['user_id' => $user->id, 'mode' => 'edit'])

@section('content')
  <!-- Your HTML -->
@endsection

Access in JavaScript:

const user_id = window.rsxapp.page_data.user_id;

Use when data doesn't belong in DOM attributes. Multiple calls merge together.


BUNDLE SYSTEM

One bundle per module (rsx/app/(module)). Compiles JS/CSS automatically on request - no manual build steps.

class Frontend_Bundle extends Rsx_Bundle_Abstract
{
    public static function define(): array
    {
        return [
            'include' => [
                'jquery',                    // Required
                'lodash',                    // Required
                'rsx/theme/variables.scss',  // Order matters
                'rsx/theme',                 // Everything else from theme - but variables.scss will be first
                'rsx/app/frontend',          // Directory - 
                'rsx/models',                // For JS stubs
            ],
        ];
    }
}
<head>
    {!! Frontend_Bundle::render() !!}
</head>

JQHTML COMPONENTS

Philosophy

For mechanical thinkers who see structure, not visuals. Write <User_Card> not <div class="card">. Name what things ARE.

Template Syntax

CRITICAL: <Define> IS the element, not a wrapper

<!-- ✅ CORRECT - Define becomes button -->
<Define:Save_Button tag="button" class="btn btn-primary">
    Save
</Define:Save_Button>

<!-- Renders as: -->
<button class="Save_Button Component btn btn-primary">Save</button>

Interpolation: <%= escaped %> | <%!= unescaped %> | <% javascript %> Conditional Attributes <input <% if (this.args.required) { %>required="required"<% } %> /> Inline Logic: <% this.handler = () => action(); %> then @click=this.handler - No JS file needed for simple components Event Handlers: @click=this.method (unquoted) - Methods defined inline or in companion .js Validation: <% if (!this.args.required) throw new Error('Missing arg'); %> - Fail loud in template

Simple Components (No JS File)

<Define:CSV_Renderer>
  <%
    if (!this.args.csv_data) throw new Error('csv_data required');
    const rows = this.args.csv_data.split('\n').map(r => r.split(','));
    this.toggle = () => { this.args.expanded = !this.args.expanded; this.render(); };
  %>
  <table>
    <% for (let row of rows) { %>
      <tr><% for (let cell of row) { %><td><%= cell %></td><% } %></tr>
    <% } %>
  </table>
  <button @click=this.toggle>Toggle View</button>
</Define:CSV_Renderer>

Use inline JS for simple transformations/handlers. Create .js file when JS overwhelms template or needs external data.

State Management Rules (ENFORCED)

Quick Guide:

  • Loading from API? → Use this.data in on_load()
  • Need reload with different params? → Modify this.args, call reload()
  • UI state (toggles, selections)? → Use this.state

this.args - Component arguments (read-only in on_load(), modifiable elsewhere) this.data - Ajax-loaded data (writable ONLY in on_create() and on_load()) this.state - Arbitrary component state (modifiable anytime)

// WITH Ajax data
class Users_List extends Component {
    on_create() {
        this.data.users = [];  // Defaults
    }
    async on_load() {
        this.data.users = await User_Controller.fetch({filter: this.args.filter});
    }
    on_ready() {
        // Change filter → reload
        this.args.filter = 'new';
        this.reload();
    }
}

// WITHOUT Ajax data
class Toggle_Button extends Component {
    on_create() {
        this.state = {is_open: false};
    }
    on_ready() {
        this.$.on('click', () => {
            this.state.is_open = !this.state.is_open;
        });
    }
}

on_load() restrictions (enforced):

  • Read this.args, write this.data
  • NO DOM access, NO this.state, NO modifying this.args

Lifecycle

  1. on_create() → Setup defaults (sync) - this.data.rows = []; this.data.loading = true;
  2. render → Template executes
  3. on_render() → Hide uninitialized UI (sync)
  4. on_load() → Fetch data into this.data (async)
  5. on_ready() → DOM manipulation safe (async)

If on_load() modifies this.data, component renders twice (defaults → populated).

Component API

DOM Access:

Method Returns Purpose
this.$ jQuery Root element (NOT this.$el)
this.$sid('name') jQuery Child with $sid="name"
this.sid('name') Component/null Child component instance

reload() vs render():

reload() = on_load() → render() → on_ready()  ← ALWAYS USE THIS
render() = template only (no on_ready)        ← NEVER USE

After mutations, call this.reload() - the server round-trip is intentional:

async add_item() {
    await Controller.add({name: 'Test'});
    this.reload();  // Refreshes this.data via on_load(), reattaches handlers via on_ready()
}

Event handlers go in on_ready() - they auto-reattach after reload. WRONG: Event delegation like this.$.on('click', '[data-sid="btn"]', handler) to "survive" render calls - use reload() instead.

this.data rules (enforced): Writable only in on_create() (defaults) and on_load() (fetched data). Read-only elsewhere.

on_render(): Ignore - use on_ready() for post-render work.

Loading Pattern

async on_load() {
    const result = await Product_Controller.list({page: 1});
    this.data.products = result.products;
    this.data.loaded = true;  // Simple flag at END
}
<% if (!this.data.loaded) { %>
    Loading...
<% } else { %>
    <!-- Show data -->
<% } %>

NEVER call this.render() in on_load() - automatic re-render happens.

Attributes

  • $quoted="string" → String literal
  • $unquoted=expression → JavaScript expression
  • $sid="name" → Scoped element ID
  • attr="<%= expr %>" → HTML attribute with interpolation

Key restrictions:

  • <Define> attributes are static - No <%= %> on the <Define> tag. For dynamic attributes on the root element, use inline JS: <% this.$.attr('data-id', this.args.id); %>
  • $prefix = component args, NOT HTML attributes - <My_Component $data-id=123 /> creates this.args['data-id'], not a data-id DOM attribute
  • Conditional attributes use if-statements - <% if (cond) { %>checked<% } %> not ternaries

Component Access

$sid attribute = "scoped ID" - unique within component instance

From within component methods:

  • this.$ → jQuery selector for the component element itself
  • this.$sid(name) → jQuery selector for child element with $sid="name"
  • this.sid(name) → Component instance of child (or null if not a component)
  • $(selector).component() → Get component instance from jQuery element
  • await $(selector).component().ready() → Await component initialization. Rarely needed - on_ready() auto-waits for children created during render. Use for dynamically created components or Blade page JS interaction.

Custom Component Events

Fire: this.trigger('event_name', data) | Listen: this.sid('child').on('event_name', (component, data) => {})

Key difference from jQuery: Events fired BEFORE handler registration still trigger the callback when registered. This solves component lifecycle timing issues where child events fire before parent registers handlers. Never use this.$.trigger() for custom events (enforced by JQHTML-EVENT-01).

Dynamic Component Creation

To dynamically create/replace a component in JavaScript:

// Destroys existing component (if any) and creates new one in its place
$(selector).component('Component_Name', { arg1: value1, arg2: value2 });

// Example: render a component into a container
this.$sid('result_container').component('My_Component', {
    data: myData,
    some_option: true
});

Class preservation: Only PascalCase component names (capital start, no __) are replaced. Utility classes (text-muted), BEM child classes (Parent__child), and all attributes are preserved.

Incremental Scaffolding

Undefined components work immediately - they render as div with the component name as a class.

<Dashboard>
  <Stats_Panel />
  <Recent_Activity />
</Dashboard>

Key Pitfalls

  • <Define> IS the element - use tag="" attribute
  • this.data starts {} - set defaults in on_create()
  • this.data writable only in on_create() and on_load()
  • on_load(): only this.args and this.data (no DOM, no this.state)
  • this.state for UI state, this.args + reload() for refetch
  • Controller.method() not $.ajax() - #[Ajax_Endpoint] auto-callable
  • on_create/render/stop sync; this.sid() → component, $(el).component() → component

FORM COMPONENTS

Form fields (<Rsx_Form> with $data, $controller, $method):

<Rsx_Form $data="{!! json_encode($form_data) !!}" $controller="Controller" $method="save">
  <Form_Field $name="email" $label="Email" $required=true>
    <Text_Input $type="email" />
  </Form_Field>

  <Form_Field_Hidden $name="id" />
</Rsx_Form>
  • Form_Field - Standard formatted field with label, errors, help text
  • Form_Field_Hidden - Single-tag hidden input (extends Form_Field_Abstract)
  • Form_Field_Abstract - Base class for custom formatting (advanced)

Disabled fields: Use $disabled=true attribute on input components to disable fields. Unlike standard HTML, disabled fields still return values via vals() (useful for read-only data that should be submitted).

<Text_Input $type="email" $disabled=true />
<Select_Input $options="{!! json_encode($options) !!}" $disabled=true />
<Checkbox_Input $label="Subscribe" $disabled=true />

Form component classes use the vals() dual-mode pattern:

class My_Form extends Component {
    vals(values) {
        if (values) {
            // Setter - populate form
            this.$sid('name').val(values.name || '');
            return null;
        } else {
            // Getter - extract values
            return {name: this.$sid('name').val()};
        }
    }
}

Validation: Form_Utils.apply_form_errors(form.$, errors) - Matches by name attribute.

Form Conventions (Action/Controller Pattern)

Forms follow a load/save pattern mirroring traditional Laravel: Action loads data, Controller saves it.

// Action: on_create() sets defaults, on_load() fetches for edit mode
on_create() {
    this.data.form_data = { title: '', status_id: Model.STATUS_ACTIVE };
    this.data.is_edit = !!this.args.id;
}
async on_load() {
    if (!this.data.is_edit) return;
    const record = await My_Model.fetch(this.args.id);
    this.data.form_data = { id: record.id, title: record.title };
}
// Controller: save() receives all form values, validates, persists
#[Ajax_Endpoint]
public static function save(Request $request, array $params = []) {
    if (empty($params['title'])) return response_form_error('Validation failed', ['title' => 'Required']);
    $record = $params['id'] ? My_Model::find($params['id']) : new My_Model();
    $record->title = $params['title'];
    $record->save();
    return ['redirect' => Rsx::Route('View_Action', $record->id)];
}

Key principles: form_data must be serializable (plain objects, no models) | Keep load/save in same controller for field alignment | on_load() loads data, on_ready() is UI-only

Details: php artisan rsx:man form_conventions

Polymorphic Form Fields

For fields that can reference multiple model types (e.g., an Activity linked to either a Contact or Project), use JSON-encoded polymorphic values.

use App\RSpade\Core\Polymorphic_Field_Helper;

$eventable = Polymorphic_Field_Helper::parse($params['eventable'], [
    Contact_Model::class,
    Project_Model::class,
]);

if ($error = $eventable->validate('Please select an entity')) {
    $errors['eventable'] = $error;
}

$model->eventable_type = $eventable->model;
$model->eventable_id = $eventable->id;

Client submits: {"model":"Contact_Model","id":123}. Always use Model::class for the whitelist.

Details: php artisan rsx:man polymorphic


MODALS

Built-in Dialog Types

Method Returns Description
Modal.alert(body) void Simple notification
Modal.alert(title, body, buttonLabel?) void Alert with title
Modal.confirm(body) boolean Yes/no confirmation
Modal.confirm(title, body, confirmLabel?, cancelLabel?) boolean Confirmation with labels
Modal.prompt(body) string|false Text input
Modal.prompt(title, body, default?, multiline?) string|false Prompt with options
Modal.select(body, options) string|false Dropdown selection
Modal.select(title, body, options, default?, placeholder?) string|false Select with options
Modal.error(error, title?) void Error with red styling
Modal.unclosable(title, body) void Modal user cannot close
await Modal.alert("File saved");
if (await Modal.confirm("Delete?")) { /* confirmed */ }
const name = await Modal.prompt("Enter name:");
const choice = await Modal.select("Choose:", [{value: 'a', label: 'A'}, {value: 'b', label: 'B'}]);
await Modal.error("Something went wrong");

Form Modals

const result = await Modal.form({
    title: "Edit User",
    component: "User_Form",
    component_args: {data: user},
    on_submit: async (form) => {
        const response = await User_Controller.save(form.vals());
        if (response.errors) {
            Form_Utils.apply_form_errors(form.$, response.errors);
            return false; // Keep open
        }
        return response.data; // Close and return
    }
});

Form component must implement vals() and include <div $sid="error_container"></div>.

Modal Classes

For complex/reusable modals, create dedicated classes:

class Add_User_Modal extends Modal_Abstract {
    static async show() {
        return await Modal.form({...}) || false;
    }
}

// Usage
const user = await Add_User_Modal.show();
if (user) {
    grid.reload();
    await Next_Modal.show(user.id);
}

Pattern: Extend Modal_Abstract, implement static show(), return data or false.

Details: php artisan rsx:man modals


JQUERY EXTENSIONS

Method Purpose
.exists() Check element exists (instead of .length > 0)
.shallowFind(selector) Find children without nested component interference
.closest_sibling(selector) Search within ancestor hierarchy
.checkValidity() Form validation helper
.click() Auto-prevents default
.click_allow_default() Native click behavior

Details: php artisan rsx:man jquery


MODELS & DATABASE

No Mass Assignment

// ✅ CORRECT
$user = new User_Model();
$user->email = $email;
$user->save();

// ❌ WRONG
User_Model::create(['email' => $email]);

Enums

CRITICAL: Read php artisan rsx:man enum for complete documentation before implementing.

Integer-backed enums with model-level mapping to constants, labels, and custom properties. Uses BEM-style double underscore naming for magic properties.

class Project_Model extends Rsx_Model_Abstract {
    public static $enums = [
        'status_id' => [
            1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active', 'badge' => 'bg-success', 'order' => 1],
            2 => ['constant' => 'STATUS_ARCHIVED', 'label' => 'Archived', 'selectable' => false, 'order' => 99],
        ],
    ];
}

// Usage - BEM-style: field__property (double underscore)
$project->status_id = Project_Model::STATUS_ACTIVE;
echo $project->status_id__label;  // "Active"
echo $project->status_id__badge;  // "bg-success" (custom property)

Special properties:

  • order - Sort position in dropdowns (default: 0, lower first)
  • selectable - Include in dropdown options (default: true). Non-selectable items excluded from field__enum_select() but still shown if current value.

JavaScript static methods (BEM-style double underscore):

// Get all enum data
Project_Model.status_id__enum()

// Get specific enum's metadata by ID
Project_Model.status_id__enum(Project_Model.STATUS_ACTIVE).badge  // "bg-success"
Project_Model.status_id__enum(2).selectable  // false

// Other methods
Project_Model.status_id__enum_select()  // For dropdowns
Project_Model.status_id__enum_labels()  // id => label map
Project_Model.status_id__enum_ids()     // Array of valid IDs

Migration: Use BIGINT for enum columns, TINYINT(1) for booleans. Run rsx:migrate:document_models after adding enums.

Model Fetch

#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
    if (!Session::is_logged_in()) return false;
    return static::find($id);
}
const project = await Project_Model.fetch(1);  // Throws if not found
const maybe = await Project_Model.fetch_or_null(999);  // Returns null if not found
console.log(project.status_id__label);  // Enum properties (BEM-style)
console.log(Project_Model.STATUS_ACTIVE);  // Static enum constants

// Lazy relationships (requires #[Ajax_Endpoint_Model_Fetch] on relationship method)
const client = await project.client();  // belongsTo → Model or null
const tasks = await project.tasks();    // hasMany → Model[]

Security: Both fetch() and relationships require #[Ajax_Endpoint_Model_Fetch] attribute. Related models must also implement fetch() with this attribute.

fetch() is for SECURITY, not aliasing: The fetch() method exists to remove private data users shouldn't see. NEVER alias enum properties (e.g., type_label instead of type_id__label) or format dates server-side. Use the full BEM-style names and format dates on client with Rsx_Date/Rsx_Time.

Details: php artisan rsx:man model_fetch

Migrations

Forward-only, no rollbacks. Deterministic transformations against known state.

php artisan make:migration:safe create_users_table
php artisan migrate:begin
php artisan migrate
php artisan migrate:commit

NO defensive coding in migrations:

// ❌ WRONG - conditional logic
$fk_exists = DB::select("SELECT ... FROM information_schema...");
if (!empty($fk_exists)) { DB::statement("ALTER TABLE foo DROP FOREIGN KEY bar"); }

// ✅ CORRECT - direct statements
DB::statement("ALTER TABLE foo DROP FOREIGN KEY bar");
DB::statement("ALTER TABLE foo DROP COLUMN baz");

No IF EXISTS, no information_schema queries, no fallbacks. Know current state, write exact transformation. Failures fail loud - snapshot rollback exists for recovery.


FILE ATTACHMENTS

Files upload UNATTACHED → validate → assign via API. Session-based validation prevents cross-user file assignment.

// Controller: Assign uploaded file
$attachment = File_Attachment_Model::find_by_key($params['photo_key']);
if ($attachment && $attachment->can_user_assign_this_file()) {
    $attachment->attach_to($user, 'profile_photo');  // Single (replaces)
    $attachment->add_to($project, 'documents');      // Multiple (adds)
}

// Model: Retrieve attachments
$photo = $user->get_attachment('profile_photo');
$documents = $project->get_attachments('documents');

// Display
$photo->get_thumbnail_url('cover', 128, 128);
$photo->get_url();
$photo->get_download_url();

Endpoints: POST /_upload, GET /_download/:key, GET /_thumbnail/:key/:type/:width/:height

Details: php artisan rsx:man file_upload


AJAX ENDPOINTS

#[Ajax_Endpoint]
public static function method(Request $request, array $params = []) {
    return $data;  // Success - framework wraps as {_success: true, _ajax_return_value: ...}
}

PHP→JS Auto-mapping:

// PHP: My_Controller class
#[Ajax_Endpoint]
public static function save(Request $request, array $params = []) {
    return ['id' => 123];
}

// JS: Automatically callable
const result = await My_Controller.save({name: 'Test'});
console.log(result.id); // 123

Error Responses

Use response_error(Ajax::ERROR_CODE, $metadata):

// Not found
return response_error(Ajax::ERROR_NOT_FOUND, 'Project not found');

// Validation
return response_error(Ajax::ERROR_VALIDATION, [
    'email' => 'Invalid',
    'name' => 'Required'
]);

// Auto-message
return response_error(Ajax::ERROR_UNAUTHORIZED);

Codes: ERROR_VALIDATION, ERROR_NOT_FOUND, ERROR_UNAUTHORIZED, ERROR_AUTH_REQUIRED, ERROR_FATAL, ERROR_GENERIC

Client:

try {
    const data = await Controller.get(id);
} catch (e) {
    if (e.code === Ajax.ERROR_NOT_FOUND) {
        // Handle
    } else {
        alert(e.message);  // Generic
    }
}

Unhandled errors auto-show flash alert.


DATA FETCHING (CRITICAL)

DEFAULT: Use Model.fetch(id) for all single-record retrieval from JavaScript.

const user = await User_Model.fetch(1);         // Throws if not found
const user = await User_Model.fetch_or_null(1); // Returns null if not found

Requires #[Ajax_Endpoint_Model_Fetch] on the model's fetch() method.

Auto-populates enum properties and enables lazy relationship loading.

If model not available in JS bundle: STOP and ask the developer. Bundles should include all models they need (rsx/models in include paths). Do not create workaround endpoints without approval.

Custom Ajax endpoints require developer approval and are only for:

  • Aggregations, batch operations, or complex result sets
  • System/root-only models intentionally excluded from bundle
  • Queries beyond simple ID lookup

Details: php artisan rsx:man model_fetch


AUTHENTICATION

Always use Session - Static methods only. Never Laravel Auth or $_SESSION.

Session::is_logged_in();     // Returns true if user logged in
Session::get_user();         // Returns user model or null
Session::get_user_id();      // Returns user ID or null
Session::get_site();         // Returns site model
Session::get_site_id();      // Returns current site ID
Session::get_session_id();   // Returns session ID

Sessions persist 365 days. Never implement "Remember Me".


DATE & TIME HANDLING

Two Classes - Strict Separation: Rsx_Time (datetimes with timezone) | Rsx_Date (calendar dates, no timezone)

String-Based Philosophy: RSpade uses ISO strings, not Carbon objects. Dates are "2025-12-24", datetimes are "2025-12-24T15:30:00-06:00". Same format in PHP, JavaScript, JSON, database queries. No object serialization issues.

Model Casts: Rsx_Model_Abstract auto-applies Rsx_Date_Cast (DATE columns) and Rsx_DateTime_Cast (DATETIME columns). Never define $casts with 'date', 'datetime', or 'timestamp' - these use Carbon and are blocked by rsx:check.

Rsx_Time - Moments in Time

use App\RSpade\Core\Time\Rsx_Time;

Rsx_Time::now();                    // Current time in user's timezone
Rsx_Time::now_iso();                // ISO 8601 format: 2025-12-24T15:30:00-06:00
Rsx_Time::format($datetime);        // "Dec 24, 2025 3:30 PM"
Rsx_Time::format_short($datetime);  // "Dec 24, 3:30 PM"
Rsx_Time::to_database($datetime);   // UTC for storage

Rsx_Time::get_user_timezone();      // User's timezone or default
Rsx_Time.now();                     // Current moment (timezone-aware)
Rsx_Time.format(datetime);          // Formatted for display
Rsx_Time.relative(datetime);        // "2 hours ago", "in 3 days"

Rsx_Date - Calendar Dates

use App\RSpade\Core\Time\Rsx_Date;

Rsx_Date::today();                  // "2025-12-24" (user's timezone)
Rsx_Date::format($date);            // "Dec 24, 2025"
Rsx_Date::is_today($date);          // Boolean
Rsx_Date::is_past($date);           // Boolean

Key Principle: Functions throw if wrong type passed (datetime to date function or vice versa).

Server Time Sync

Client time syncs automatically via rsxapp data on page load and AJAX responses. No manual sync required.

User Timezone

Stored in login_users.timezone (IANA format). Falls back to config('rsx.datetime.default_timezone').

Details: php artisan rsx:man time


JAVASCRIPT DECORATORS

/** @decorator */
function logCalls(target, key, descriptor) { /* ... */ }

class Service {
    @logCalls
    @mutex
    async save() { /* ... */ }
}

COMMANDS

Module Creation

rsx:app:module:create <name>              # /name
rsx:app:module:feature:create <m> <f>     # /m/f
rsx:app:component:create --name=x         # Component

Development

rsx:check          # Code quality
rsx:debug /page    # Test routes (see below)
rsx:man <topic>    # Documentation
db:query "SQL" --json

Testing Routes

rsx:debug /path - Uses Playwright to render pages with full JS execution.

rsx:debug /dashboard --user=1                    # Authenticated user
rsx:debug /page --screenshot-path=/tmp/page.png  # Capture screenshot
rsx:debug /contacts --eval="$('.btn').click(); await sleep(1000)"  # Simulate interaction
rsx:debug / --eval="return Rsx_Time.now_iso()"   # Get eval result (use return)
rsx:debug / --console --eval="console.log(Rsx_Date.today())"  # Or console.log with --console

Options: --user=ID, --console, --screenshot-path, --screenshot-width=mobile|tablet|desktop-*, --dump-dimensions=".selector", --eval="js", --help

SPA routes ARE server routes. If you get 404, the route doesn't exist - check route definitions. Never dismiss as "SPA can't be tested server-side".

rsx:debug captures the fully-rendered final DOM state after all async operations, component lifecycles, and data loading complete. If the DOM doesn't match expectations, it's not a timing issue - what you see is what the user sees. Investigate the actual code, not the capture timing.

Debugging

  • rsx_dump_die() - Debug output
  • console_debug("CHANNEL", ...) - Channel logging
  • CONSOLE_DEBUG_FILTER=CHANNEL - Filter output

ERROR HANDLING

if (!$expected) {
    shouldnt_happen("Class {$expected} missing");
}

Use for "impossible" conditions that indicate broken assumptions.


CODE QUALITY

Professional UI: Hover effects ONLY on buttons, links, form fields. Static elements remain static.

z-index: Bootstrap defaults + 1100 (modal children), 1200 (flash alerts), 9000+ (system). Details: rsx:man zindex

Run rsx:check before commits. Enforces naming, prohibits animations on non-actionable elements.


KEY REMINDERS

  1. Fail loud - No silent failures
  2. Static by default - Unless instances needed
  3. Path-agnostic - Reference by name
  4. Bundles required - For JavaScript
  5. Use Session - Never Laravel Auth
  6. No mass assignment - Explicit only
  7. Forward migrations - No rollbacks
  8. Don't run rsx:clean - Cache auto-invalidates
  9. All routes need auth checks - In pre_dispatch() or method body (@auth-exempt for public)

GETTING HELP

php artisan rsx:man <topic>  # Framework documentation
php artisan list rsx          # All commands

Topics: bundle_api, jqhtml, routing, migrations, console_debug, model_fetch, vs_code_extension, deployment, framework_divergences


PROJECT DOCUMENTATION

Project-specific man pages in /rsx/resource/man/*.txt. Create when features have non-obvious details or component interactions. See /rsx/resource/man/CLAUDE.md for format.