Files
rspade_system/docs/CLAUDE.dist.md
root ff77724e2b Fix contact add link to use Contacts_Edit_Action SPA route
Add multi-route support for controllers and SPA actions
Add screenshot feature to rsx:debug and convert contacts edit to SPA

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 19:55:49 +00:00

33 KiB

RSpade Framework - AI/LLM Development Guide

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

What is RSpade?

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

Philosophy: Modern anti-modernization. While JavaScript fragments into complexity and React demands quarterly paradigm shifts, RSpade asks: "What if coding was easy again?" Business apps need quick builds and easy maintenance, not bleeding-edge architecture.

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

🔴 RSpade Builds Automatically - NEVER RUN BUILD COMMANDS

RSpade is an INTERPRETED framework - like Python or PHP, changes are automatically detected and compiled on-the-fly. There is NO manual build step.

ABSOLUTELY FORBIDDEN (unless explicitly instructed):

  • npm run compile / npm run build - DO NOT EXIST
  • bin/publish - Creates releases for OTHER developers (not for testing YOUR changes)
  • rsx:bundle:compile - Bundles compile automatically in dev mode
  • rsx:manifest:build - Manifest rebuilds automatically in dev mode
  • ANY command with "build", "compile", or "publish"

How it works:

  1. Edit JS/SCSS/PHP files
  2. Refresh browser
  3. Changes are live (< 1 second)

If you find yourself wanting to run build commands: STOP. You're doing something wrong. Changes are already live.

🔴 Framework Updates

php artisan rsx:framework:pull  # 5-minute timeout required

Updates take 2-5 minutes. Includes code pull, manifest rebuild, bundle recompilation. For AI: Always use 5-minute timeout.

🔴 Fail Loud - No Silent Fallbacks

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

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

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

SECURITY-CRITICAL: If sanitization/validation/auth fails, NEVER continue. Always throw immediately.

NO BLANKET TRY/CATCH: Use try/catch only for expected failures (file uploads, external APIs, user input parsing). NEVER wrap database operations or entire functions "just in case".

// ❌ WRONG - Defensive "on error resume"
try {
    $user->save();
    $result = process_data($user);
    return $result;
} catch (Exception $e) {
    throw new Exception("Failed: " . $e->getMessage(), 0, $e);
}

// ✅ CORRECT - Let exceptions bubble
$user->save();
$result = process_data($user);
return $result;

Exception handlers format errors for different contexts (Ajax JSON, CLI, HTML). Don't wrap exceptions with generic messages - let them bubble to the 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.

🔴 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.

🔴 DO NOT RUN rsx:clean

RSpade's cache auto-invalidates on file changes. Running rsx:clean causes 30-60 second rebuilds with zero benefit.

When to use: Only on catastrophic corruption, after framework updates (automatic), or when explicitly instructed.

Correct workflow: Edit → Save → Reload browser → See changes (< 1 second)

🔴 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.blade.php
frontend_calendar_event.js

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


DIRECTORY STRUCTURE

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

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
{
    #[Auth('Permission::anybody()')]
    #[Route('/', methods: ['GET'])]
    public static function index(Request $request, array $params = [])
    {
        return rsx_view('Frontend_Index', [
            'bundle' => Frontend_Bundle::render()
        ]);
    }
}

Rules: Only GET/POST allowed. Use :param syntax. All routes MUST have #[Auth].

#[Auth] Attribute

#[Auth('Permission::anybody()')]           // Public
#[Auth('Permission::authenticated()')]     // Require login
#[Auth('Permission::has_role("admin")')]   // Custom

Controller-wide: Add to pre_dispatch(). Multiple attributes = all must pass.

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
Rsx::Route('User_Controller::show', 123);

// PHP - With query parameters
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'});

Signature: Rsx::Route($action, $params = null) / Rsx.Route(action, params = null)

  • $action - Controller class, SPA action, or "Class::method" (defaults to 'index' if no :: present)
  • $params - Integer sets 'id', array/object provides named params
  • Unimplemented routes: Prefix method with #Rsx::Route('Feature::#index') generates href="#" and bypasses validation

Query Parameters: Extra params become query string - automatically URL-encoded

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 feature/bundle)

class Frontend_Spa_Controller extends Rsx_Controller_Abstract {
    #[SPA]
    #[Auth('Permission::authenticated()')]
    public static function index(Request $request, array $params = []) {
        return rsx_view(SPA);
    }
}

CRITICAL: One #[SPA] per feature/bundle (e.g., /app/frontend, /app/root, /app/login). Bundles separate features to save bandwidth, reduce processing time, and segregate confidential code (e.g., root admin from unauthorized users). The #[SPA] bootstrap performs server-side auth checks with failure/redirect before loading client-side actions. Typically one #[SPA] per feature at rsx/app/(feature)/(feature)_spa_controller::index.

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 $id="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

/rsx/app/frontend/
├── Frontend_Spa_Controller.php         # Single SPA bootstrap
├── Frontend_Layout.js
├── Frontend_Layout.jqhtml
└── contacts/
    ├── 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

Details: php artisan rsx:man spa


CONVERTING BLADE PAGES TO SPA ACTIONS

7-step procedure to convert server-side pages to client-side SPA:

1. Create Action Files

# In feature directory
touch Feature_Index_Action.js Feature_Index_Action.jqhtml

2. Create Action Class (.js)

@route('/path')
@layout('Frontend_Spa_Layout')
@spa('Frontend_Spa_Controller::index')
@title('Page Title')
class Feature_Index_Action extends Spa_Action {
    full_width = true;  // For DataGrid pages
    async on_load() {
        this.data.items = await Feature_Controller.fetch_items();
    }
}

3. Convert Template (.jqhtml)

Blade → jqhtml syntax:

  • {{ $var }}<%= this.data.var %>
  • {!! $html !!}<%!= this.data.html %>
  • @if($cond)<% if (this.data.cond) { %>
  • @foreach($items as $item)<% for (let item of this.data.items) { %>
  • @endforeach<% } %>
  • {{-- comment --}}<%-- comment --%>
  • {{ Rsx::Route('Class') }}<%= Rsx.Route('Class') %>
<Define:Feature_Index_Action>
    <Page>
        <Page_Header><Page_Title>Title</Page_Title></Page_Header>
        <% for (let item of this.data.items) { %>
            <div><%= item.name %></div>
        <% } %>
    </Page>
</Define:Feature_Index_Action>

4. Update Controller - Remove server-side route entirely:

// Remove #[Route] method completely. Add Ajax endpoints:
#[Ajax_Endpoint]
public static function fetch_items(Request $request, array $params = []): array {
    return ['items' => Feature_Model::all()];
}

CRITICAL: Do NOT add #[SPA] to feature controllers. The #[SPA] attribute only exists in the bootstrap controller (e.g., Frontend_Spa_Controller::index). Feature controllers should only contain #[Ajax_Endpoint] methods for data fetching.

5. Update Route References

Search entire codebase for old route references:

grep -r "Feature_Controller::method" rsx/app/

Find/replace in all files:

  • Rsx::Route('Feature_Controller::method')Rsx::Route('Feature_Action')
  • Rsx.Route('Feature_Controller::method')Rsx.Route('Feature_Action')
  • Hardcoded /feature/method/123Rsx.Route('Feature_Action', 123)

Check: DataGrids, dashboards, save endpoints, navigation, breadcrumbs

6. Archive Old Files

mkdir -p rsx/resource/archive/frontend/feature/
mv feature_index.blade.php rsx/resource/archive/frontend/feature/

7. Test

php artisan rsx:debug /path

Verify: No JS errors, page renders, data loads.

Common Patterns:

DataGrid (no data loading):

class Items_Index_Action extends Spa_Action {
    full_width = true;
    async on_load() {} // DataGrid loads own data
}

Detail page (load data):

@route('/items/:id')
class Items_View_Action extends Spa_Action {
    async on_load() {
        this.data.item = await Items_Controller.get({id: this.args.id});
    }
}

BLADE & VIEWS

@rsx_id('Frontend_Index')  {{-- Every view starts with this --}}

<body class="{{ rsx_body_class() }}">  {{-- Adds view class --}}

@rsx_include('Component_Name')  {{-- Include by name, not path --}}

NO inline styles, scripts, or event handlers in Blade views:

  • No <style> tags
  • No <script> tags
  • No inline event handlers: onclick="", onchange="", etc. are forbidden
  • Use companion .scss and .js files instead
  • Exception: jqhtml templates may use @click directive syntax (jqhtml-specific feature)

Why no inline handlers:

  • Violates separation of concerns (HTML structure vs behavior)
  • Makes code harder to maintain and test
  • rsx:check will flag inline event handlers and require refactoring

Correct pattern:

// Blade view - NO event handlers
<button id="submit-btn" class="btn btn-primary">Submit</button>

// Companion .js file
$('#submit-btn').click(() => {
    // Handle click
});

SCSS Pairing

/* frontend_index.scss - Same directory as view */
.Frontend_Index {  /* Matches @rsx_id */
    .content { padding: 20px; }
}

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.


JAVASCRIPT

Auto-Initialization

class Frontend_Index {
    static async on_app_ready() {
        // DOM ready
    }

    static async on_jqhtml_ready() {
        // Components ready
    }
}

CRITICAL: JavaScript only executes when bundle rendered.


BUNDLE SYSTEM

One bundle per page required.

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/app/frontend',          // Directory
                'rsx/models',                // For JS stubs
                '/public/vendor/css/core.css', // Public directory asset (filemtime cache-busting)
            ],
        ];
    }
}

Bundles support /public/ prefix for including static assets from public directories with automatic cache-busting.

Auto-compiles on page reload in development.

<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 (v2.2.162+): Apply attributes conditionally using <% if (condition) { %>attr="value"<% } %> directly in attribute context. Works with static values, interpolations, and multiple conditions per element. Example: <input <% if (this.args.required) { %>required="required"<% } %> />

🔴 State Management Rules (ENFORCED)

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)

Quick Guide:

  • Loading from API? Use this.data in on_load()
  • Need to reload with new params? Modify this.args, call reload()
  • UI state (toggles, counters, etc)? Use this.state
// 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 default state BEFORE template (sync)
  2. render → Template executes with initialized state
  3. on_render() → Hide uninitialized UI (sync)
  4. on_load() → Fetch data into this.data (async)
  5. on_ready() → DOM manipulation safe (async)

on_create() now runs first - Initialize this.data properties here so templates can safely reference them:

on_create() {
    this.data.rows = [];      // Prevents "not iterable" errors
    this.data.loading = true; // Template can check loading state
}

Double-render: If on_load() modifies this.data, component renders twice (defaults → populated).

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
  • $id="name" → Scoped element ID

Component Access

this.$id(name) → jQuery object (for DOM):

this.$id('button').on('click', ...);

this.id(name) → Component instance (for methods):

const comp = this.id('child');  // ✅ Returns component
await comp.reload();

const comp = this.id('child').component();  // ❌ WRONG

Incremental Scaffolding

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

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

Common Pitfalls

  1. <Define> IS the element - use tag="" attribute
  2. this.data starts empty {}, set defaults in on_create()
  3. ONLY modify this.data in on_create() and on_load() (enforced)
  4. on_load() can ONLY access this.args and this.data (enforced)
  5. Use this.state = {} in on_create() for component state (not loaded from Ajax)
  6. Use this.args for reload parameters, reload() to re-fetch
  7. Use Controller.method() not $.ajax()
  8. Blade components self-closing only
  9. on_create/render/stop must be sync
  10. Use this.id() for components, NOT this.id().component()

Bundle Integration Required

{!! Frontend_Bundle::render() !!}  {{-- Required for JS --}}
<User_Card user_id="123" />       {{-- Now JS executes --}}

For advanced topics: php artisan rsx:man jqhtml


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.$id('name').val(values.name || '');
            return null;
        } else {
            // Getter - extract values
            return {name: this.$id('name').val()};
        }
    }
}

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


MODALS

Basic dialogs:

await Modal.alert("File saved");
if (await Modal.confirm("Delete?")) { /* confirmed */ }
let name = await Modal.prompt("Enter name:");

Form modals:

const result = await Modal.form({
    title: "Edit User",
    component: "User_Form",
    component_args: {data: user},
    on_submit: async (form) => {
        const values = form.vals();
        const response = await User_Controller.save(values);

        if (response.errors) {
            Form_Utils.apply_form_errors(form.$, response.errors);
            return false; // Keep open
        }

        return response.data; // Close and return
    }
});

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

Modal Classes (for complex/reusable modals):

// Define modal class
class Add_User_Modal extends Modal_Abstract {
    static async show() {
        const result = await Modal.form({...});
        return result || false;
    }
}

// Use from page JS
const user = await Add_User_Modal.show();
if (user) {
    // Orchestrate post-modal actions
    grid.reload();
    await Next_Modal.show(user.id);
}

Pattern: Extend Modal_Abstract, implement static show(), return data or false. Page JS orchestrates flow, modal classes encapsulate UI.

Details: php artisan rsx:man modals


JQUERY EXTENSIONS

RSpade extends jQuery with utility methods:

Element existence: $('.element').exists() instead of .length > 0

Component traversal: this.$.shallowFind('.Widget') - Finds child elements matching selector that don't have another element of the same class as a parent between them and the component. Prevents selecting widgets from nested child components.

// Use case: Finding form widgets without selecting nested widgets
this.$.shallowFind('.Form_Field').each(function() {
    // Only processes fields directly in this form,
    // not fields in nested sub-forms
});

Sibling component lookup: $('.element').closest_sibling('.Widget') - Searches for elements within progressively higher ancestors. Like .closest() but searches within ancestors instead of matching them. Stops at body tag. Useful for component-to-component communication.

Form validation: $('form').checkValidity() instead of $('form')[0].checkValidity()

Click override: .click() automatically calls e.preventDefault(). Use .click_allow_default() for native behavior.

For complete 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

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

Integer-backed enums with model-level mapping to constants, labels, and custom properties.

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

// Usage
$project->status_id = Project_Model::STATUS_ACTIVE;
echo $project->status_label;  // "Active"
echo $project->status_badge;  // "bg-success" (custom property)

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

Migrations

Forward-only, no rollbacks.

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

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

Return data directly - framework auto-wraps as {success: true, data: ...}

#[Ajax_Endpoint]
public static function fetch_client(Request $request, array $params = []) {
    $client = Client_Model::find($params['id']);
    if (!$client) throw new \Exception('Not found');
    return $client;  // ✅ Return data directly
}

NEVER: return ['success' => true, 'data' => $client] - framework adds this

Special returns (bypass auto-wrap):

  • ['errors' => [...]] - Form validation errors
  • ['redirect' => '/path'] - Client-side navigation
  • ['error' => 'msg'] - Operation failure
// Form validation
if (empty($params['name'])) {
    return ['errors' => ['name' => 'Required']];
}

// Success with redirect
Flash_Alert::success('Saved');
return ['redirect' => Rsx::Route('View_Action', $id)];

// Permission error
if (!can_delete()) {
    return ['error' => 'Permission denied'];
}

Flash Alerts in Ajax Endpoints

Use server-side success alerts ONLY with redirects (no on-screen element to show message).

Client-side: Use Flash_Alert.error(), Flash_Alert.warning() on case-by-case basis.


BROWSER STORAGE

Rsx_Storage - Scoped sessionStorage/localStorage with automatic fallback and quota management. All keys automatically scoped by session, user, site, and build. Gracefully handles unavailable storage and quota exceeded errors. Storage is volatile - use only for non-critical data (caching, UI state, transient messages).

Rsx_Storage.session_set(key, value) / Rsx_Storage.session_get(key) / Rsx_Storage.local_set(key, value) / Rsx_Storage.local_get(key)

Details: php artisan rsx:man storage


Model Fetch

#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
    if (!RsxAuth::check()) return false;
    return static::find($id);
}
const user = await User_Model.fetch(1);

AUTHENTICATION

Always use RsxAuth, never Laravel Auth or $_SESSION.

RsxAuth::check();     // Is authenticated
RsxAuth::user();      // User model
RsxAuth::id();        // User ID

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


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 - Preferred method for testing routes

Uses Playwright to render the page and show rendered output, JavaScript errors, and console messages.

rsx:debug /clients              # Test route
rsx:debug /dashboard --user=1   # Simulate authenticated user
rsx:debug /contacts --console   # Show console.log output
rsx:debug /page --screenshot-path=/tmp/page.png --screenshot-width=mobile  # Capture screenshot
rsx:debug /path --help          # Show all options

Screenshot presets: mobile, iphone-mobile, tablet, desktop-small, desktop-medium, desktop-large

Use this instead of manually browsing, especially for SPA pages and Ajax-heavy features.

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.

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


MAIN_ABSTRACT MIDDLEWARE

Optional /rsx/main.php:

class Main extends Main_Abstract
{
    public function init() { }                   // Bootstrap once
    public function pre_dispatch($request, $params) { return null; } // Before routes
    public function unhandled_route($request, $params) { }          // 404s
}

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 RsxAuth - 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] - No exceptions

GETTING HELP

php artisan rsx:man <topic>  # Detailed docs
php artisan list rsx          # All commands

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

Remember: RSpade prioritizes simplicity and rapid development. When in doubt, choose the straightforward approach.