Files
rspade_system/docs/CLAUDE.dist.md
root a5e1c604ab Framework updates
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-19 04:33:43 +00:00

1145 lines
38 KiB
Markdown

===============================================================================
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: `<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.
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
<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**
```jqhtml
<!-- ✅ 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 %>` | `<%br= newlines %>` | `<% 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)
```jqhtml
<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)
```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 { %>
<!-- 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:
```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
<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
Forms use `<Rsx_Form>` 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
```
In development mode, `migrate` automatically creates a snapshot, runs migrations, and commits on success. Failed migrations auto-rollback to pre-migration state.
**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 - automatic snapshot rollback handles recovery in dev mode.
---
## 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.
**If model lacks fetch()**: Add a `fetch()` method with `#[Ajax_Endpoint_Model_Fetch]` to the model. For framework models (in `system/`), create an override in `rsx/models/`. Do NOT create separate controller endpoints to fetch single records - this duplicates ORM functionality and is an anti-pattern.
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> # /name
rsx:app:module:feature:create <m> <f> # /m/f
rsx:app:component:create --name=x # Component
```
### Development
```bash
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.
```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 <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.