seedproject-web/api/public/views/views.md
Carlos Arias f41fa652ac fix: repair the fresh-clone flow — track api/public + app/public, fix installer schema
Three bugs found deploying a fresh clone to seedproject.com, each fatal
to the documented "spin up a new site" flow:

- .gitignore: the unanchored `public/` pattern (meant for the root build
  output) also ignored api/public/ (the framework's front controller,
  controllers, models, views) and app/public/ (theme static assets:
  fonts, avatar placeholder). Neither was ever committed, so every fresh
  clone 500'd on all /api routes and 404'd on theme assets. Anchor the
  pattern to /public/ and commit both directories.

- api/install/dump.sql: stray `CREATE DATABASE ochenta80_db123` (SQLyog
  export artifact) aborted `php console app:install` for any
  non-privileged DB user. The schema must import into whatever database
  the installer connects to.

- PluginManager::boot() queries sp_plugins on every request, but no
  shipped schema creates it — even a successful install 500'd on every
  endpoint. Add migration 002_create_sp_plugins.sql matching the columns
  PluginManager reads/writes.

Also empty api/system/errors.json, which shipped with stale error logs
from an unrelated project.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C3JqxTe7TKaR7xufcMr7Ds
2026-07-05 00:15:55 +00:00

4.9 KiB

Views — public/views/

PHP view templates, organized by controller name.

Directory Structure

public/views/
├── <controller_name>/      # snake_case folder per controller
│   ├── index.php
│   ├── show.php
│   └── edit.php
├── modals/                 # Modal fragment views (AJAX loaded)
│   └── <modal_name>.php
├── partial/                # Reusable partial views
│   ├── admin_header.php
│   ├── admin_sidebar.php
│   ├── admin_toolbar.php
│   └── admin_footer.php
└── wrapper/                # Layout wrappers
    ├── admin/
    │   ├── header.php
    │   └── footer.php
    └── site/
        ├── header.php
        └── footer.php

File Naming

  • Folder: snake_case matching the controller name (lowercased)
  • Files: snake_case.php matching the method name

Rendering

Called from the controller:

// Standard — uses __CLASS__/__FUNCTION__ convention
$this->view->render(__CLASS__ . '/' . __FUNCTION__);

// With explicit wrapper
$this->view->render('clients/index', true, 'site');
$this->view->render('admin/dashboard', true, 'admin');

// Login / minimal wrapper
$this->view->render('auth/login', 'login');

// No wrapper
$this->view->render('auth/login', false);

Accessing View Data

Variables set on the controller via $this->view->VarName = $value are accessed in the view as $this->VarName.

View::render() calls extract((array) $this) before including the template, so all properties set on the view object are available as bare $VarName locals.

// Controller
$this->view->Users = Db::select("SELECT * FROM sp_users");
$this->view->PageTitle = 'Users';

// View — use bare $VarName
<h1><?= $PageTitle ?></h1>
<?php foreach ($Users as $user): ?>
    <p><?= htmlspecialchars($user['name']) ?></p>
<?php endforeach; ?>

Wrappers

Wrappers live in public/views/wrapper/. The wrapper auto-loads:

  • public/assets/css/<controllername>.css if it exists
  • public/assets/js/<controllername>.js if it exists

Partial Views

Partials are reusable snippets loaded from the controller constructor:

// In controller __construct()
$this->view->Header  = $this->view->PartialView('admin_header');
$this->view->SideBar = $this->view->PartialView('admin_sidebar');
$this->view->ToolBar = $this->view->PartialView('admin_toolbar');
$this->view->Footer  = $this->view->PartialView('admin_footer');

Then echoed in the view:

<?= $Header ?>
<?= $SideBar ?>

Modal System

Modals are loaded dynamically via AJAX. Trigger a modal with:

<button class="RegularModal btn btn-primary"
        data-url="/modals/load/<modal_name>"
        data-size="mw-650px">
    Open Modal
</button>

data-size accepts any Bootstrap modal width class or a custom mw-* value:

Value Width
(omit) Default (~500px)
mw-500px 500px max-width
mw-650px 650px max-width
mw-750px 750px max-width
modal-lg ~800px (Bootstrap)
modal-xl ~1140px (Bootstrap) — use for wizards / multi-step forms

Modal views live at public/views/modals/<modal_name>.php and render only the .modal-content fragment (no full page wrapper):

<!--begin::Modal content-->
<div class="modal-content">
    <!--begin::Modal header-->
    <div class="modal-header" id="kt_modal_add_client_header">
        <h2>Add Client</h2>
        <div class="btn btn-sm btn-icon btn-active-color-primary" data-bs-dismiss="modal">
            <i class="ki-duotone ki-cross fs-1">
                <span class="path1"></span>
                <span class="path2"></span>
            </i>
        </div>
    </div>
    <!--end::Modal header-->

    <!--begin::Modal body-->
    <div class="modal-body">
        <!-- form or content here -->
    </div>
    <!--end::Modal body-->
</div>
<!--end::Modal content-->

Assets Structure

public/assets/
├── css/
│   ├── custom.css          # Global overrides
│   └── <controller>.css    # Page-specific (auto-loaded by wrapper)
├── js/
│   └── <controller>.js     # Page-specific (auto-loaded by wrapper)
├── plugins/
│   └── datatables/
│       ├── datatables.bundle.css
│       └── datatables.bundle.js
└── media/
    └── svg/
        └── brand-logos/    # Integration / vendor logos

Inject additional assets from the controller method:

$this->Styles[]     = '/public/assets/plugins/datatables/datatables.bundle.css';
$this->view->Styles = $this->Styles;

$this->JavaScript[]     = '/public/assets/plugins/datatables/datatables.bundle.js';
$this->view->JavaScript = $this->JavaScript;