=============================================================================== 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 ```bash 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. ```php // ❌ 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. ```javascript // ❌ 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. ```php // ❌ 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 ```bash 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. ```php $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`. --- ## APPLICATION MODES Three modes control build behavior: `development`, `debug`, `production`. **Switch modes**: `php artisan rsx:mode:set dev|debug|prod` | Behavior | Development | Debug | Production | |----------|-------------|-------|------------| | Auto-rebuild on file change | Yes | No | No | | Minification (JS/CSS) | No | Yes | Yes | | Inline sourcemaps | Yes | Yes | No | | console_debug() works | Yes | Yes | Stripped | | Bundle merging | No | No | Yes (single app.js/css) | | CDN assets | External URLs | External URLs | Cached & bundled | **Development**: Active coding - files auto-compile, full debugging. **Debug**: Test production-like build locally - minified but with sourcemaps and working console_debug. **Production**: Optimized deployment - everything merged, minified, debug code stripped. **Build commands**: - `rsx:mode:set` - Switch mode and rebuild - `rsx:prod:build` - Rebuild for debug/production - `rsx:prod:export` - Export deployable package **Environment**: `RSX_MODE` in `.env` (default: `development`) --- ## ROUTING & CONTROLLERS ```php 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 ```php // 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 // 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 ```php 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)** ```javascript @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** ```javascript 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 // PHP/JavaScript - same syntax Rsx::Route('Contacts_Index_Action') // /contacts Rsx::Route('Contacts_View_Action', 123) // /contacts/123 ``` ```javascript Spa.dispatch('/contacts/123'); // Programmatic navigation Spa.layout // Current layout instance Spa.action() // Current action instance ``` ### URL Parameters ```javascript // 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: ```javascript 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: `` → `` → 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. Pattern recognition: - `@rsx_id('View_Name')` - required first line - `rsx_body_class()` - adds view class for CSS scoping - `@rsx_page_data()` - pass data to JS - `on_app_ready()` with page guard for JS (fires for ALL pages in bundle) **Detailed guidance in `blade-views` skill** - auto-activates when building Blade pages. --- ## SCSS ARCHITECTURE **Component-first**: Every styled element is a component with scoped SCSS. No generic classes scattered across files. **Scoping rules**: - Files in `rsx/app/` and `rsx/theme/components/` **must** wrap in single component class - Wrapper class matches JS class or Blade `@rsx_id` - BEM children use PascalCase: `.Component_Name__element` (NOT kebab-case) **Responsive breakpoints** (Bootstrap defaults do NOT work): - Tier 1: `mobile` (0-1023px), `desktop` (1024px+) - Tier 2: `phone`, `phone-sm`, `phone-lg`, `tablet`, `desktop-sm`, `desktop-md`, `desktop-lg`, `desktop-xl` - SCSS: `@include mobile { }`, `@include phone-sm { }`, `@include phone-lg { }`, `@include desktop-xl { }` - Classes: `.col-mobile-12`, `.d-tablet-none`, `.mobile-only`, `.col-phone-sm-12`, `.col-phone-lg-6` - Fifth-width columns: `.col-5ths`, `.col-mobile-5ths`, `.col-desktop-5ths`, etc. - JS: `Responsive.is_mobile()`, `Responsive.is_phone()` **Detailed guidance in `scss` skill** - auto-activates when styling components. --- ## BUNDLE SYSTEM **One bundle per module (rsx/app/(module)).** Compiles JS/CSS automatically on request - no manual build steps. ```php 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 ], ]; } } ``` ```blade {!! Frontend_Bundle::render() !!} ``` --- ## JQHTML COMPONENTS ### Philosophy For mechanical thinkers who see structure, not visuals. Write `` not `
`. Name what things ARE. ### Template Syntax **CRITICAL: `` IS the element, not a wrapper** ```jqhtml Save ``` **Interpolation**: `<%= escaped %>` | `<%!= unescaped %>` | `<% javascript %>` **Conditional Attributes** `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) ```jqhtml <% 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(); }; %> <% for (let row of rows) { %> <% for (let cell of row) { %><% } %> <% } %>
<%= cell %>
``` 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) ```javascript // 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: ```javascript 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 ```javascript async on_load() { const result = await Product_Controller.list({page: 1}); this.data.products = result.products; this.data.loaded = true; // Simple flag at END } ``` ```jqhtml <% if (!this.data.loaded) { %> Loading... <% } else { %> <% } %> ``` **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:** - **`` attributes are static** - No `<%= %>` on the `` tag. For dynamic attributes on the root element, use inline JS: `<% this.$.attr('data-id', this.args.id); %>` - **`$prefix` = component args, NOT HTML attributes** - `` 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: ```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. ```blade ``` ### Key Pitfalls - `` 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 Forms use `` with `$data`, `$controller`, `$method` for automatic data binding. Key components: `Form_Field`, `Form_Field_Hidden`, input types (`Text_Input`, `Select_Input`, `Checkbox_Input`). Pattern recognition: - `vals()` dual-mode method for get/set - `Form_Utils.apply_form_errors()` for validation - Action loads data, Controller saves it - `$disabled=true` still returns values (unlike HTML) - **Text_Input requires `$max_length`**: Use `Model.field_length('column')` for database-driven limits, numeric value for custom, or `-1` for unlimited **Detailed guidance in `forms` skill** - auto-activates when building forms. --- ## MODALS Built-in dialogs: `Modal.alert()`, `Modal.confirm()`, `Modal.prompt()`, `Modal.select()`, `Modal.error()`. Form modals: `Modal.form({title, component, on_submit})` - component must implement `vals()`. Reusable modals: Extend `Modal_Abstract`, implement static `show()`. **Detailed guidance in `modals` skill** - auto-activates when building 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 ```php // ✅ CORRECT $user = new User_Model(); $user->email = $email; $user->save(); // ❌ WRONG User_Model::create(['email' => $email]); ``` ### Enums Integer-backed enums with model-level mapping. Define in `$enums` array with `constant`, `label`, and custom properties. Pattern recognition: - BEM-style access: `$model->status_id__label`, `$model->status_id__badge` - JS methods: `Model.status_id__enum()`, `Model.status_id__enum_select()` - Static constants: `Model::STATUS_ACTIVE` - Use BIGINT columns, run `rsx:migrate:document_models` after changes **Detailed guidance in `model-enums` skill** - auto-activates when implementing enums. ### Model Fetch ```php #[Ajax_Endpoint_Model_Fetch] public static function fetch($id) { if (!Session::is_logged_in()) return false; return static::find($id); } ``` ```javascript 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.** ```bash php artisan make:migration:safe create_users_table php artisan migrate:begin php artisan migrate php artisan migrate:commit ``` **NO defensive coding in migrations:** ```php // ❌ 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. Pattern recognition: - `File_Attachment_Model::find_by_key()` + `can_user_assign_this_file()` - `attach_to()` (single/replaces) vs `add_to()` (multiple/adds) - `get_attachment()` / `get_attachments()` for retrieval - `get_url()`, `get_download_url()`, `get_thumbnail_url()` **Detailed guidance in `file-attachments` skill** - auto-activates when handling uploads. --- ## POLYMORPHIC TYPE REFERENCES Polymorphic `*_type` columns store BIGINT integers. Framework maps to class names transparently. ```php protected static $type_ref_columns = ['fileable_type']; $attachment->fileable_type = 'Contact_Model'; // Stored as integer echo $attachment->fileable_type; // Reads as 'Contact_Model' ``` **NEVER USE morphTo()**: Laravel's `morphTo()` bypasses accessors, receives integers, crashes. Use `get_polymorphic_parent()`: ```php // ❌ WRONG public function parent() { return $this->morphTo(...); } // ✅ CORRECT public function getParentAttribute() { return $this->get_polymorphic_parent('parent_type', 'parent_id'); } ``` **Simple Names Only**: Use `class_basename($model)`, never `get_class($model)` Details: `php artisan rsx:man polymorphic` --- ## AJAX ENDPOINTS ```php #[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 // 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)`: ```php // 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:** ```javascript 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. ```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. ```php 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**: ISO strings, not Carbon. Never use `$casts` with `'date'`/`'datetime'` - blocked by `rsx:check`. **MODEL ATTRIBUTES ARE ALREADY STRINGS**: `$model->created_at` returns `"2025-12-24T15:30:00.000Z"`, NOT Carbon. Never call `->format()` on datetime attributes. Use `Rsx_Time::format($model->created_at)`. Pattern recognition: - `Rsx_Time::now()`, `Rsx_Time::format()`, `Rsx_Time::relative()` - `Rsx_Date::today()`, `Rsx_Date::format()`, `Rsx_Date::relative()` - uses user → site → default timezone - `Rsx_Time::to_database()` for UTC storage - Functions throw if wrong type passed **Detailed guidance in `date-time` skill** - auto-activates when working with dates/times. --- ## JAVASCRIPT DECORATORS ```javascript /** @decorator */ function logCalls(target, key, descriptor) { /* ... */ } class Service { @logCalls @mutex async save() { /* ... */ } } ``` --- ## COMMANDS ### Module Creation ```bash rsx:app:module:create # /name rsx:app:module:feature:create # /m/f rsx:app:component:create --name=x # Component ``` ### Development ```bash rsx:check # Code quality rsx:debug /page # Test routes (see below) rsx:man # Documentation db:query "SQL" --json ``` ### Testing Routes **`rsx:debug /path`** - Uses Playwright to render pages with full JS execution. ```bash 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 ```php 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) --- ## SKILLS (Auto-Activated) Detailed guidance for specific tasks is available via Claude Code skills. These activate automatically when relevant - no action needed. | Skill | Activates When | |-------|---------------| | `forms` | Building forms, validation, vals() pattern | | `modals` | Creating dialogs, Modal.form(), Modal_Abstract | | `model-enums` | Implementing status_id, type_id, enum properties | | `file-attachments` | Upload flow, attach_to(), thumbnails | | `date-time` | Rsx_Time, Rsx_Date, timezone handling | | `blade-views` | Server-rendered pages, @rsx_id, @rsx_page_data | | `scss` | Component styling, BEM, responsive breakpoints | | `ajax-error-handling` | response_error(), Form_Utils, error codes | | `model-fetch` | Model.fetch(), lazy relationships, #[Ajax_Endpoint_Model_Fetch] | | `jquery-extensions` | .click() override, .exists(), .shallowFind() | | `background-tasks` | #[Task], #[Schedule], Task::dispatch() | | `crud-patterns` | List/view/edit structure, DataGrid, dual-route actions | | `js-decorators` | @route, @spa, @layout, @mutex, custom decorators | | `event-hooks` | #[OnEvent], filters, gates, actions | | `migrations` | Raw SQL, make:migration:safe, forward-only | | `polymorphic` | Type refs, morphTo, Polymorphic_Field_Helper | **Framework skills**: `~/.claude/skills/` (symlinked from `system/docs/skills/`) - READ-ONLY, do not modify **Project skills**: `/var/www/html/.claude/skills/` - Custom skills for this specific project go here --- ## GETTING HELP ```bash php artisan rsx:man # 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.