Files
app.derez.ai/AGENTS.md
T
oliver 794c3dd22e INIT
2026-06-06 06:05:17 -03:00

476 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AGENTS.md — Agent Loft User Portal
Read this file fully before taking any action.
---
## Project Context
Single-page user portal for **agent-loft.com** — lets customers sign up, log in, and manage their AI agents. No framework, no build step, no external dependencies.
| Item | Detail |
|---|---|
| Entry point | `index.html` — only HTML file |
| Styling | Vanilla CSS · `styles.css` (edit directly, no compilation) |
| Logic | Vanilla JS · `app.js` (edit directly, no bundler) |
| Dev server | `./start` (runs `live-server` on port 8080) |
| Auth | Email stored in `localStorage` key `al_email` |
| Backend | n8n webhooks on `n8n.agent-loft.com` |
### File map
```
index.html ← all markup — auth card + app shell + confirm dialog
styles.css ← all styles — design tokens, layout, components
app.js ← all logic — auth, agents, keys, password, restart, backups, contract
start ← dev server launcher (live-server)
AGENTS.md ← this file
skills/
index.json ← summary: [{ name, file, description }] — loaded on wizard step 1
<slug>.json ← one file per skill: { name, description, prompt }
integrations/
index.json ← summary: [{ name, file, description, fields[], signup_url, signup_label }] — loaded on wizard step 2 + fields
<slug>.json ← one file per integration: adds prompt to the index entry
```
---
## Visual Architecture
The UI has **two mutually exclusive top-level views**. `body` is a flex container that centres whichever is visible.
```
body (display:flex; align-items:center; justify-content:center; overflow:hidden)
├── #auth-card ← shown when NOT logged in (440 px wide, auto height)
│ ├── .auth-logo
│ ├── .auth-switch ← Sign In | Sign Up pill toggle
│ ├── #signin-form
│ └── #signup-form
└── #app-shell ← shown when logged in (max 1060×780 px, rounded panel)
├── .app-header ← logo · email pill · Sign Out
├── .agent-tabs-bar ← one pill tab per agent
└── .app-body
├── #no-agents ← shown when agent list is empty
└── #agent-panel ← shown for the active agent
└── .content-grid (2 columns: left / right)
├── .content-col [left]
│ ├── card: Server Actions (#server-info-body, #restart-btn)
│ ├── card: Instance Password (#pw-new-input)
│ └── card: API Keys (#keys-body)
└── .content-col [right]
├── card: Backups (#backups-body)
└── card: Contract (#contract-body)
```
**Non-scrolling rule:** `body` and `#app-shell` never scroll. Only `.content-grid` (inside the shell) scrolls when content overflows, and `.backups-list` scrolls internally at `max-height: 320px`.
---
## Auth Flow
```
Page load
└── localStorage has 'al_email'?
Yes → showApp() + loadAgents() ← shell made visible immediately, no auth animation
No → showAuth() ← auth card + demo shell shown side-by-side
Sign In
└── GET AGENTS_URL?email=…&password=…
200 → store email → showApp() (animates auth card out) → processAgents()
!200 → show inline error
Sign Up
└── POST SIGNUP_URL {email, password, agent, location}
200 → open Stripe checkout tab + auto-login immediately
!200 → show inline error
Sign Out
└── remove 'al_email' from localStorage → showAuth()
```
The email is the only session token. There is no password verification on the frontend — authentication is enforced by the n8n webhooks.
### `showApp()` behaviour
- If called while `body.auth-layout` is present (user just signed in): animates the auth card upward and fades it out, then reveals the shell.
- If called on direct page load (user already logged in): immediately hides the auth card and shows the shell — no blink animation.
- Always sets `shell.style.display = "flex"` explicitly; the shell's default CSS is `display: none`.
---
## Design Tokens
All tokens are CSS custom properties declared in `:root` inside `styles.css`.
| Variable | Value | Usage |
|---|---|---|
| `--bg` | `#0f1117` | page background |
| `--bg-card` | `#1a1d27` | app shell / auth card surface |
| `--bg-inner` | `#141720` | inner cards, tabs bar |
| `--bg-hover` | `#22263a` | hover states, ghost button bg |
| `--bg-input` | `#12151f` | input / textarea background |
| `--border` | `#2a2f45` | default borders |
| `--border-hi` | `rgba(79,142,247,0.28)` | shell outer border glow |
| `--accent` | `#4f8ef7` | primary brand blue |
| `--accent-dim` | `#1a2d5e` | active tab bg, logo bg |
| `--danger` | `#e05252` | destructive actions, errors |
| `--danger-dim` | `#5a1f1f` | error message background |
| `--success` | `#3fc97e` | positive indicators, active dot |
| `--warning` | `#f0a04b` | mid-range credit bar, contract warning |
| `--text` | `#e4e8f5` | primary text |
| `--text-muted` | `#7a82a0` | secondary text, labels |
| `--radius` | `8px` | default border-radius |
| `--shadow` | (see CSS) | shell / auth card drop shadow |
**Never use raw hex values in HTML or JS** — always reference a token via `var(--token-name)` in CSS or as a string literal only when building inline styles in JS (acceptable only for dynamic status colours in `renderKeys` and `renderContract`).
---
## Component Patterns
### Cards
Every content block is a `.card` with `.card-header` + body content.
```html
<div class="card">
<div class="card-header">
<span class="card-title">
<!-- 14×14 SVG icon --> Title
</span>
<!-- optional action button -->
</div>
<!-- body -->
</div>
```
Card backgrounds are `--bg-inner` (one level darker than the shell's `--bg-card`).
### Buttons
| Class | Colour | Use for |
|---|---|---|
| `btn btn-primary btn-sm` | `--accent` blue | navigation / open actions (Open Agent) |
| `btn btn-ghost btn-sm` | `--bg-hover` grey + border | all card-header and inline actions (Restart Agent, Buy Credits, Restore, Extend Contract, Cancel, Make Backup) |
| `btn btn-danger` | `--danger` red | confirm dialog destructive confirm only |
| `btn btn-block` | — modifier, full width | auth form submit buttons |
**Card-header action buttons must:**
- Use `btn btn-ghost btn-sm` (grey) by default, or `btn btn-primary btn-sm` (blue) for a primary navigation action.
- Have **no SVG icons** — text label only.
- When two buttons appear side-by-side in a header, wrap them in `<div style="display:flex;gap:8px;">` rather than placing them as siblings.
**`<a>` tags used as buttons** (e.g. contract links that open Stripe): apply the same `btn btn-ghost btn-sm` classes. The `.btn` base class sets `text-decoration: none` so no underline appears.
#### Inline icon-only buttons
Use the `.pw-save-btn` pattern for buttons embedded inside input fields: `position: absolute`, no text, icon only, hidden by default, shown/hidden via JS.
### Forms
Inputs and selects use `.form-group` inside `.form-row`:
```html
<div class="form-row">
<div class="form-group">
<label>Label</label>
<input type="text" ... />
</div>
</div>
```
Focus state: `border-color` switches to `--accent`.
#### Inline-button input (`.pw-input-wrap`)
Wrap an input + absolutely-positioned button together when the button should appear inside the field:
```html
<div class="pw-input-wrap">
<input type="password" id="pw-new-input" oninput="togglePwSave(this.value)" />
<button id="pw-save-btn" class="pw-save-btn" style="display:none" onclick="savePassword()">
<!-- SVG checkmark icon -->
</button>
</div>
```
### Loading states
Use `.loading-row` with a `.spinner` while async calls are in flight:
```html
<div class="loading-row"><div class="spinner"></div></div>
```
Use `.spinner-sm` (13 px) inside buttons when a call is pending (see `btnLoad()`).
### Toast notifications
Call `toast(message, type)` from anywhere. `type` is `'info'` | `'success'` | `'error'` | `'warning'`. Toasts self-remove after 4 s with a slide-out animation.
### Confirm dialog
Always use `confirmDialog(title, message)` before destructive actions. It returns a `Promise<boolean>`.
```js
const ok = await confirmDialog('Delete?', 'This cannot be undone.');
if (!ok) return;
```
---
## Webhook Contracts
### 1 · Sign Up
```
POST https://n8n.agent-loft.com/webhook/4de196b7-2ad2-4f9a-9b4d-dc8d3b30865b
Body: { email, password, agent, location }
```
Response: `200` on success. User receives a Stripe billing email separately.
### 2 · List Agents
```
GET https://n8n.agent-loft.com/webhook/73b31740-d2c7-46d7-ab71-7a3fef5f77ff
?email=<email>&password=<password>
```
Response shape:
```json
[{ "email": "…", "UUID": "apple", "server": "fr", "id": 1,
"createdAt": "…", "updatedAt": "…" }]
```
An empty array `[]` means no agents — show `#no-agents` state.
### 3 · List API Keys
```
GET https://n8n.agent-loft.com/webhook/1f1a6a11-727b-4965-a59a-fde77806d27f
?uuid=<UUID>
```
Response shape: `[{ "data": [ { "name", "label", "limit", "limit_remaining",
"expires_at", "disabled", "usage_monthly", … } ] }]`
`renderKeys()` reads `json[0].data`. The credit progress bar colour follows:
- `< 50 %` used → `prog-low` (green)
- `5079 %` used → `prog-mid` (amber)
- `≥ 80 %` used → `prog-high` (red)
### 4 · Update Instance Password
```
POST https://n8n.agent-loft.com/webhook/51098cf4-ecfd-4db4-8977-db04f01ce2b1
Body: { uuid, email, password }
```
Requires confirm dialog before calling. Uses a single `<input type="password">` embedded in `.pw-input-wrap`; the save button (checkmark icon, no text) only appears when the field has content. No GET endpoint — the field is always blank on load. Minimum password length: 8 characters.
### 5 · Restart Server
```
POST https://n8n.agent-loft.com/webhook/dac205df-66e0-4728-90e5-d784cde167af
Body: { uuid, email, action: "restart" }
```
Requires confirm dialog.
### 6 · Agent Info
```
GET https://n8n.agent-loft.com/webhook/e01d06a3-14c3-4e4e-830f-7d4be9a5f529
?uuid=<UUID>
```
Response shape: `{ agent, domain, ssh_port, created, comment, … }` (n8n may wrap values in extra quotes — use `stripQuotes()` before display).
`renderAgentInfo()` builds the Server Actions card body with rows: Agent Type, Dashboard, SSH Access (clickable — copies `ssh root@UUID.agent-loft.com -p PORT` to clipboard), Created.
This same URL accepts **POST** `{ uuid, key, value }` for all config writes:
- `key: "comment"` — saves the header comment
- `key: "WIZZARD", value: "false"` — dismisses the Integrations wizard
### 7 · Backups
```
GET https://n8n.agent-loft.com/webhook/30eaa32f-378a-4963-9d80-533229d25766
?uuid=<UUID> ← list all backups
POST …/30eaa32f-…
Body: { uuid, email, action: "make" } ← create backup
POST …/30eaa32f-…
Body: { uuid, email, action: "restore",
backup_id: <id> } ← restore; requires confirm dialog
```
The backup object shape from the API is flexible. The renderer reads `b.name || b.id` for display and `b.created_at || b.createdAt || b.date || b.timestamp` for the date.
### 8 · Contract
```
GET https://n8n.agent-loft.com/webhook/18591766-147e-4bcb-b9ac-b0f9a92e74bf
?uuid=<UUID>
```
Response shape:
```json
[{ "type": "Auto - Monthly", "expires": "" }]
```
`renderContract()` shows a status dot + contract type + expiration date. Status logic (evaluated in order):
1. `expires` is a past date → 🔴 red
2. `type` contains `"none"` (case-insensitive) → 🔴 red
3. `type` contains `"auto"` (case-insensitive) → 🟢 green
4. Otherwise → 🟡 yellow
When status is red, an **Extend Contract →** link is shown pointing to `CONTRACT_EXTEND_URL` (defined at the top of `app.js`**update this to the Stripe payment link**).
### 9 · Wizard Complete
```
POST https://n8n.agent-loft.com/webhook/e01d06a3-14c3-4e4e-830f-7d4be9a5f529 (same as Agent Info)
Body: { uuid, key: "WIZZARD", value: "false" }
```
Called when the user clicks **Copy & Finish** in the wizard. Reuses `AGENT_INFO_URL`. The same endpoint accepts all agent config writes via `{ uuid, key, value }` — e.g. comments use `key: "comment"`. No separate constant needed.
The agent info GET response (webhook 6) drives wizard visibility: if the response contains `WIZZARD` with any value other than `false` (or the key is absent), the wizard is shown above the Backups card.
---
## State Variables (`app.js`)
| Variable | Type | Description |
|---|---|---|
| `currentEmail` | `string \| null` | logged-in user's email |
| `agents` | `Array` | full agent objects from AGENTS_URL |
| `activeUUID` | `string \| null` | UUID of the currently selected agent tab |
| `activeAgentInfo` | `object \| null` | last loaded agent info response (used by `copySSHAccess`) |
| `wizardPhase` | `string \| null` | current wizard phase: `'skills'` \| `'integrations'` \| `'fields'` \| `'review'` \| `null` |
| `wizardSkillsData` | `Array \| null` | cached contents of `skills.json` (loaded once, reused) |
| `wizardIntegrationsData` | `Array \| null` | cached contents of `integrations.json` (loaded once, reused) |
| `wizardSelectedSkills` | `Set<number>` | indices of selected skills |
| `wizardSelectedIntegrations` | `Set<number>` | indices of selected integrations |
| `wizardFieldValues` | `object` | `{ stepIndex: { fieldKey: value } }` — values entered per integration |
| `wizardIntegrationStep` | `number` | current integration index during the `'fields'` phase |
| `wizardSelectedIntegrationList` | `Array` | ordered integration objects chosen in step 2 |
There is no global keys, backups, contract, or wizard-JSON state that is re-fetched per tab switch — agent selections and field values reset per-agent, but `skills.json` / `integrations.json` are cached for the session.
---
## Key Functions
| Function | What it does |
|---|---|
| `showAuth()` / `showApp()` | toggle between auth card and app shell |
| `switchAuthTab(tab)` | toggle Sign In / Sign Up forms |
| `doSignIn()` | validate email → fetch agents → enter app |
| `doSignUp()` | validate form → POST signup webhook → show Stripe notice |
| `doSignOut()` | clear localStorage → reset state → showAuth |
| `loadAgents()` | fetch agent list, call `processAgents()` |
| `selectAgent(uuid)` | switch active tab, load keys + backups + agent info + contract |
| `loadKeys(uuid)` | fetch + render API keys card |
| `renderKeys(keys)` | build key rows with credit bar + Buy Credits button |
| `togglePwSave(value)` | show/hide the inline password save button based on field content |
| `savePassword()` | validate → confirm → POST password webhook |
| `loadAgentInfo(uuid)` | fetch agent info, call `renderAgentInfo()` + `renderComment()` |
| `renderAgentInfo(info)` | build Server Actions info rows including clickable SSH Access row |
| `copySSHAccess()` | copy `ssh root@UUID.agent-loft.com -p PORT` to clipboard + info toast |
| `doRestart()` | confirm → POST restart webhook |
| `loadBackups(uuid)` | fetch + render backup list |
| `makeBackup()` | POST backup make → reload list |
| `restoreBackup(backup)` | confirm → POST backup restore |
| `loadContract(uuid)` | fetch + render contract card |
| `renderContract(data)` | determine status dot colour + show type, expiry, extend link if red |
| `openAgent()` | open the agent's dashboard URL (from `activeAgentInfo.domain`) in a new tab |
| `loadWizard(info)` | check `info.WIZZARD`; if not `false`, reset wizard state and show the card |
| `renderWizardStep()` | async — fetch JSON if needed, render the current phase into `#wizard-body`, call `updateWizardNav()` |
| `renderWizardSkills()` | render skills checklist (step 1) with search |
| `renderWizardIntegrations()` | render integrations checklist (step 2) with search |
| `renderWizardFields()` | render form fields for `wizardSelectedIntegrationList[wizardIntegrationStep]` |
| `renderWizardReview()` | build combined init-prompt textarea from skills + integrations |
| `buildWizardPrompt()` | assemble skills prompts + integration prompts (with field substitution) into one string |
| `updateWizardNav()` | set step-label text + show/hide Back button + set Next/Copy label |
| `wizardNext()` | advance phase or trigger `wizardCopyAndFinish()` |
| `wizardBack()` | retreat phase |
| `wizardFilter(input, listId)` | real-time search filter on `.wizard-list-item` elements |
| `wizardCopyAndFinish()` | copy prompt to clipboard → POST wizard webhook → hide card |
| `confirmDialog(title, msg)` | shows modal, returns `Promise<boolean>` |
| `toast(msg, type)` | bottom-right notification, auto-removes after 4 s |
| `btnLoad(btn, label)` | disable button, show spinner + label, save original HTML |
| `btnReset(btn)` | re-enable button, restore original HTML |
| `escHtml(s)` | HTML-escape a string for safe innerHTML insertion |
| `escAttr(s)` | HTML + single-quote escape for safe attribute values |
| `stripQuotes(v)` | strip extra surrounding quotes that n8n injects into string values |
---
## Wizard Data Files
### Skills
`skills/index.json` — an array of filenames; fetched once on wizard step 1, cached in `wizardSkillsData`:
```json
["copywriting.json", "cold-email.json"]
```
`skills/<slug>.json` — fetched in parallel when the skills step opens, cached in `wizardSkillsCache`:
```json
{ "name": "Copywriting", "description": "Short blurb shown in the checklist.", "prompt": "Full skill prompt text." }
```
### Integrations
`integrations/index.json` — an array of filenames; fetched once on wizard step 2, cached in `wizardIntegrationsData`:
```json
["odoo-community.json", "imap-smtp-email.json"]
```
`integrations/<slug>.json` — fetched in parallel when the integrations step opens, cached in `wizardIntegrationsCache`. Contains everything needed for the checklist, fields UI, and prompt:
```json
{ "name": "ODOO Community", "description": "...",
"fields": [{ "label", "key", "type", "placeholder" }],
"signup_url": "...", "signup_label": "...",
"prompt": "Prompt with {placeholder} substitutions." }
```
`prompt` uses `{key}` placeholders that are replaced with user input from the fields form.
All JSON files are plain static files — no build step required.
---
## How to Add a New Feature
1. **New card** — add a `.card` inside the appropriate `.content-col` in `index.html`. Give the dynamic content container a unique `id`.
2. **New webhook call** — declare the URL as a `const` at the top of `app.js`. Use `btnLoad` / `btnReset` for the trigger button and wrap the call in `try/catch` with `toast(err.message, 'error')` in the catch.
3. **Destructive action** — always gate with `await confirmDialog(...)` before the fetch.
4. **New CSS component** — add it to `styles.css` in the appropriate section (marked with `/* ─── Section ─ */` comments). Use only existing token variables.
5. **New agent-tab action** — call it from `selectAgent()` so it reloads when the user switches agents. Note: `loadWizard(info)` is called from inside `loadAgentInfo()` (which is called by `selectAgent()`), not directly from `selectAgent()` — this is acceptable when the loader depends on async data fetched by another loader.
6. **Demo shell** — update `populateDemoShell()` to populate the new card's `id` with representative static data so the auth screen preview stays consistent.
---
## Commands
```bash
# Start dev server (live-reload on file save)
./start
# → http://localhost:8080
# No build step required — CSS and JS are plain files.
```
---
## Rules
- **NEVER** add external scripts, CDN links, or npm packages.
- **NEVER** introduce a framework (React, Vue, Alpine, etc.).
- **NEVER** use raw hex colour values — reference CSS token variables.
- **NEVER** scroll the outer page — keep `overflow: hidden` on `body`.
- **NEVER** call a destructive webhook (restart, restore, password update) without `confirmDialog`.
- On successful sign-up, open the Stripe checkout tab **and** log the user in immediately via `showApp()` + `loadAgents()`.
- **ALWAYS** use `escHtml()` when inserting user-supplied or API-returned strings into `innerHTML`.
- **ALWAYS** use `btnLoad` / `btnReset` around async operations on buttons.
- **ALWAYS** show an error toast (`toast(err.message, 'error')`) when a webhook call fails.
- **ALWAYS** call new per-agent loaders from `selectAgent()` and populate them in `populateDemoShell()`.
---
## Failure Conditions
These actions constitute a failure:
- Removing `escHtml()` from any innerHTML insertion of external data.
- Skipping `confirmDialog` before restart, password update, or backup restore.
- Adding a `<script src="…">` or `<link>` to an external CDN.
- Editing the page so it scrolls at the `body` level.
- Breaking the centered floating-panel layout on desktop (1280 px+).
- Using hardcoded hex values instead of CSS token variables.
- Adding a new per-agent data loader without calling it from `selectAgent()`.
- Setting `app-shell` visible without `shell.style.display = "flex"` (CSS default is `none`).