Files
rspade_system/docs/CLAUDE.dist.md
root d523f0f600 Fix code quality violations and exclude Manifest from checks
Document application modes (development/debug/production)
Add global file drop handler, order column normalization, SPA hash fix
Serve CDN assets via /_vendor/ URLs instead of merging into bundles
Add production minification with license preservation
Improve JSON formatting for debugging and production optimization
Add CDN asset caching with CSS URL inlining for production builds
Add three-mode system (development, debug, production)
Update Manifest CLAUDE.md to reflect helper class architecture
Refactor Manifest.php into helper classes for better organization
Pre-manifest-refactor checkpoint: Add app_mode documentation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-14 10:38:22 +00:00

1143 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 %>` | `<% 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: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> # /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.