Files
livetrail/AGENTS.md
T
franzz b0d8a79cdc
Deploy Livetrail / deploy (push) Successful in 23s
Dev guidelines for agents
2026-10-23 14:17:21 +02:00

10 KiB

AGENTS.md

Guidelines for AI coding agents working on Livetrail. See readme.md for installation and dependencies.

Project overview

Self-hosted web app that shares journeys on a map: SPOT GPS positions, GPX routes, posts, photos/videos and a chronological feed.

  • Backend: PHP 8.5+, MariaDB/MySQL, namespace Franzz\Livetrail (PSR-4 → lib/).
  • Frontend: Vue 3 (Options API), MapLibre GL, SCSS, built with Vite.
  • Shared library: franzz/objects (Franzz\Objects\*: Main, PhpObject, Db, Translator, ToolBox, Mask…). It lives in a separate repo (../objects). Change generic behaviour there, not by duplicating it here.

Architecture

Directory layout

Path Role
public/ Web root only. index.php is the single front controller. assets/ is Vite output (generated, never edit). files and geo are symlinks created by the build.
lib/ PHP classes, one class per file, file name = class name.
config/ settings.php (git-ignored, copied from settings-sample.php) defines the global Settings class of constants. Web server samples.
resources/lang/*.json Translations (en, es, fr).
resources/masks/*.html Server-side HTML templates (main page, emails).
resources/geo/ <codename>.gpx source routes; .geojson files are generated from them.
src/ Frontend sources: app.js (entry), App.vue (hash router), components/, scripts/ (plain JS classes), styles/, images/.
cli/cron.sh Hourly a=update_project call that polls the SPOT feeds.

Web root

  • The web server serves public/ as the document root.
  • PHP sources, configuration, Composer dependencies, uploaded files and GPX data stay outside public/.
  • Runtime data is exposed through symlinks only: public/files -> ../files and public/geo -> ../resources/geo. The build must never copy uploaded media or GPX data into public/.

Request flow

  1. public/index.php → Controller::handle().
  2. Controller reads the action parameter a and validates request values with setReqVal(), manages the session and checks the CSRF token. Then it dispatches through a match:
    • dispatch() handles public actions.
    • dispatchAdmin() handles actions that require User::CLEARANCE_ADMIN.
  3. Livetrail (extends Franzz\Objects\Main) is the application facade. It owns Project, Media, User and Map, and returns JSON through self::getJsonResult($bSuccess, $sLangId, $asData, $asLangParams).
  4. With no action, getAppMainPage() renders resources/masks/index.html. It injects the app config (projects, user, constants, CSRF token) and the Vite assets read from public/.vite/manifest.json.

Adding a backend action

  • Add the public method to Livetrail (or to the domain class it delegates to) and return getJsonResult(...).
  • Add the action to the right match in Controller.
  • If the action changes state, add it to Controller::MUTATING_ACTIONS so it requires POST + CSRF. If it writes the session, also add it to SESSION_WRITING_ACTIONS.
  • Read new request parameters through setReqVal(). Use a validation such as 'positiveInt' where it applies.
  • Results and errors carry a translation key (desc_lang_id), not literal text. Add that key to all files in resources/lang/.

Database

  • The schema is declared in code in Livetrail::getSqlOptions() (tables, types, constraints, cascading_delete). Update it there when adding columns or tables.
  • Table names are class constants, e.g. Project::PROJ_TABLE, Feed::MSG_TABLE, Media::MEDIA_TABLE. Never hard-code table names outside raw SQL strings.
  • Primary/foreign keys follow id_<singular> and are obtained with Db::getId(TABLE).
  • Use the Db helpers (selectRows, selectColumn, insertRow, updateRow, deleteRow) rather than raw SQL wherever possible.
  • Timezones: follow the "Site Time" / "Local Time" convention documented at the top of lib/Livetrail.php. unix_time is UTC, *_time/*_on timestamps are Site Time, and the timezone column holds Local Time.

Frontend

  • src/app.js creates the shared singletons and provides them: appConfig, api, lang, projects, user. Components get them through inject, never through globals or imports.
  • Routing is hash-based (#page-item-item, separator consts.hash_sep), handled in App.vue. Routes are declared in aoRoutes. Lazy-load non-default pages with defineAsyncComponent.
  • Talk to the server only through api.get(sAction, params) / api.post(sAction, params) (src/scripts/api.js). This adds the timezone and CSRF token and throws an Error carrying desc_lang_* on failure.
  • Use translations through lang.get('section.key', params). Placeholders are $0, $1…
  • Use the import aliases @components, @scripts, @styles, @images.
  • No v-html (lint error).

Naming conventions

Hungarian-style prefixes (PHP and JS)

Variables, parameters and properties carry a type prefix:

Prefix Type Example
s string $sCodeName, sAction
i integer / number $iProjectId, iDistance
f / d float / decimal $fValue, $dValue
b boolean $bSuccess, bSecure
o object / instance $oDb, oFeed
a generic array $aValues
as array of strings $asProject, asParams
ai, ao array of ints / objects $aiCenter, aoCoords
j JSON string jNewHash

Exceptions: loop counters (i), short comparator args ($a, $b), Vue data/props/computed names (plain camelCase: postMessage, anchorVisible), and keys of JSON exchanged with the backend (snake_case).

PHP

  • Classes: PascalCase, one per file, extending Franzz\Objects\PhpObject (call parent::__construct(__CLASS__)). Report errors with $this->addError().
  • Methods: camelCase with verb prefixes: get*, set*, is*/has*, add*, create*, delete*, check*.
  • Constants: UPPER_SNAKE_CASE, grouped under short comments (//DB Tables, //Spot Mode). Table constants end in _TABLE.
  • Controller actions, DB columns, array keys sent to the frontend and request parameters: snake_case (id_project, ref_type, last_update).
  • New code: add parameter/return types (string $sCodeName, : int) as in Controller. Don't mass-retype old code.

JavaScript / Vue

  • Component files: PascalCase, named after their parent area, for example:
    • App* for generic UI (AppIcon, AppButton)
    • Project* for the project page (ProjectFeed, ProjectPost)
    • Admin* for the admin page
  • Templates use camelCase component tags (<appIcon>, <projectMapLink>). Import names are mixed between camelCase and PascalCase, so follow the file you are editing.
  • Script modules in src/scripts/: lowerCamelCase file names, each exporting a class (export default class Api) or named functions (common.js).
  • Module-level constants: UPPER_SNAKE_CASE.
  • Components use the Options API (data, inject, computed, methods…), not <script setup>.

Translations

  • Keys are nested section.key in snake_case (account.invalid_email, error.commit_db, unit.days).
  • Every key must exist in en.json, es.json and fr.json.

SCSS

  • Partials are prefixed with _ and named by scope with dots: _page.project.panel.feed.scss, _color.scss, _var.scss.
  • Register them in livetrail.scss with @use.
  • Use the shared variables (var.$elem-spacing, var.$block-radius, color.$post…) instead of literal values.
  • Use kebab-case for CSS ids/classes and SCSS variables (#feed-panel, .post-item, $trans-quick).

Code style

Both linters codify existing style. Their configs explain each rule.

  • Tabs for indentation, LF line endings.
  • Single quotes in PHP and JS.
  • No space before control-structure parentheses: if(...), foreach(...), switch(...), function(x).
  • PHP:
    • Opening brace on the same line for classes and functions.
    • Short array syntax [].
    • No spaces around the . concatenation operator.
    • else/elseif on a new line after }.
  • JS:
    • Semicolons, except after the export default {...} of a Vue SFC.
    • {a, b} without padding for objects; import { x } from 'y' with padding.
  • Short one-statement if without braces is common: if($x) return;.
  • Comments: //Capitalized comment with no space after //. Keep them sparse and explain the why.

Development workflow

Commands

Task Command
Dev build (watch) npm run dev: development assets into public/assets/, also runs ESLint (reports issues, never blocks the watcher)
Production build npm run prod: hashed assets into public/assets/, aborts on any ESLint error
JS/Vue lint npm run lint
PHP lint / fix composer lint (php-cs-fixer dry run) / composer lint-fix (applies it). Needs the dev dependencies below.
Local dev with sibling objects repo COMPOSER=composer.dev.json composer update

Working with the objects library

  • composer.dev.json symlinks vendor/franzz/objects to ../objects and autoloads that namespace from the local source path. It also installs the dev dependencies (php-cs-fixer).
  • Production uses composer.json, which installs franzz/objects from its Git repository.
  • Commit and publish objects changes before updating or deploying a Livetrail version that relies on them.

Before committing

  • Run composer lint and npm run lint manually and fix any issues.
  • There is no automated test suite. Verify changes by running the app locally.

Rules for agents

  • Never edit generated or runtime content:
    • public/assets/, public/.vite/, vendor/, node_modules/
    • files/, resources/geo/*.geojson
  • Never read, open, search or print config/settings.php: it contains credentials and API tokens. To learn which settings exist, read config/settings-sample.php instead. It declares the same Settings constants with empty values.
  • Don't copy media or GPX data into public/. They are exposed through symlinks only.
  • Don't commit secrets. New settings go into config/settings-sample.php as empty constants.
  • Pushing to master deploys to production (.gitea/workflows/deploy.yml). Never push without explicit approval.
  • Keep changes in the style of the surrounding code. Don't reformat unrelated lines.