# AGENTS.md — Agent Loft User Portal Read this file fully before taking any action. --- ## Project Context Single-page user portal for **derez.ai** — lets customers sign up, log in, and manage their AI agents. No framework, no build step, no external dependencies. **Active visual theme:** Digital Grid — deep navy/black with electric-cyan `#00f5ff` accent, monospace `Courier New` font, 3-D perspective grid floor, star-field sky. Full design spec in `style_guide.md`. | 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.derez.ai` | ### 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 password-reset.html ← standalone page — receives ?token=XXX, sets new password, redirects to app start ← dev server launcher (live-server) AGENTS.md ← this file style_guide.md ← visual design spec (tokens, typography, components) skills/ index.json ← summary: [{ name, file, description }] — loaded on wizard step 1 .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 .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 shown (demo shell hidden) Sign In └── POST AUTH_URL {email, password} 200 + sessionid → store session → showApp() (animates auth card out) → loadAgents() !200 → show inline error + reveal #reset-pw-wrap (forgot password section) Password Reset — step 1 (auth screen) └── User clicks “Send reset email” in #reset-pw-wrap POST PW_RESET_URL {email} always → hide signin-error → show #reset-pw-sent confirmation (response not checked — never reveal whether address exists) Password Reset — step 2 (password-reset.html?token=XXX) └── token missing or < 8 chars → show invalid-state token valid → show password form POST PW_RESET_URL {token, password} 200 → show success state → progress bar → redirect to https://app.derez.ai !200 → show error banner, re-enable Save button Sign Up └── “Hire Agent” button links to https://derez.ai/#pricing (no inline form) 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`. See `style_guide.md` for full usage rules and visual examples. | Variable | Value | Usage | |---|---|---| | `--bg` | `#000810` | page background | | `--bg-card` | `#010f20` | app shell / auth card surface | | `--bg-inner` | `#000d1a` | inner cards, tabs bar | | `--bg-hover` | `#001a30` | hover states, ghost button bg | | `--bg-input` | `#000813` | input / textarea background | | `--border` | `#0a3060` | default borders | | `--border-hi` | `rgba(0,245,255,0.45)` | shell outer border glow | | `--accent` | `#00f5ff` | primary electric-cyan accent | | `--accent-dim` | `#002535` | active tab bg, logo bg | | `--danger` | `#ff3b3b` | destructive actions, errors | | `--danger-dim` | `#3b0000` | error message background | | `--success` | `#00ff88` | positive indicators, active dot | | `--warning` | `#ffaa00` | mid-range credit bar, contract warning | | `--text` | `#c8f0ff` | primary text | | `--text-muted` | `#2e6a80` | secondary text, labels | | `--radius` | `8px` | default border-radius | | `--shadow` | neon glow + depth (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
Title
``` Card backgrounds are `--bg-inner` (one level darker than the shell's `--bg-card`). ### Buttons | Class | Appearance | Use for | |---|---|---| | `btn btn-primary btn-sm` | transparent bg · white text · `--accent` border + neon glow | primary navigation actions (Open Agent, Sign In, Next →) | | `btn btn-ghost btn-sm` | `--bg-hover` dark bg · `--text` · `--border` | all card-header and inline actions (Restart Agent, Buy Credits, Restore, Extend Contract, Cancel, Make Backup) | | `btn btn-danger` | `--danger` red fill · white text | confirm dialog destructive confirm only | | `btn btn-block` | — width modifier, full width | auth form submit buttons | **Card-header action buttons must:** - Use `btn btn-ghost btn-sm` (dark) by default, or `btn btn-primary btn-sm` (cyan outline) 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 `
` rather than placing them as siblings. **`` 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
``` 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
``` ### Loading states Use `.loading-row` with a `.spinner` while async calls are in flight: ```html
``` 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`. ```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=&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= ``` 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) - `50–79 %` 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 `` 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= ``` 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= ← list all backups POST …/30eaa32f-… Body: { uuid, email, action: "make" } ← create backup POST …/30eaa32f-… Body: { uuid, email, action: "restore", backup_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= ``` 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.derez.ai/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. ### 10 · Password Reset ``` POST https://n8n.derez.ai/webhook/c2ce0eba-eb26-405d-8a90-8d982ec30698 Body (step 1 — request email): { email } Body (step 2 — save new password): { token, password } ``` The same endpoint handles both steps — the presence of `token` distinguishes them. **Step 1** is triggered from `index.html` when the user clicks **Send reset email** inside `#reset-pw-wrap` (visible only after a failed sign-in). The response is intentionally ignored and the sent-confirmation is always shown — this avoids revealing whether the email address exists. **Step 2** is handled entirely by `password-reset.html`, which: - Reads `?token=XXX` from the URL on load; shows an invalid-state if absent. - Requires both password fields to match and be ≥ 8 characters before enabling Save. - On 200 response shows a success state with a 2 s animated progress bar then redirects to `https://app.derez.ai`. - On !200 shows an inline error banner and re-enables the Save button. --- ## 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` | indices of selected skills | | `wizardSelectedIntegrations` | `Set` | 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 / Hire Agent; clears error + hides reset section | | `doSignIn()` | validate → POST auth webhook → enter app; on failure shows error + calls `showResetOption()` | | `showResetOption()` | reveals `#reset-pw-wrap` in idle state below the sign-in error | | `sendPasswordReset()` | POST `PW_RESET_URL` with email → hides error panel → shows sent confirmation | | `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` | | `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/.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/.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 `