Files
2026-06-08 10:31:31 -03:00

23 KiB
Raw Permalink Blame History

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.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
  <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 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.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.

<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 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 <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:

<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:

<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:

<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>.

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:

[{ "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:

[{ "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.jsupdate 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

Step 1 — request reset email
POST  https://n8n.derez.ai/webhook/c2ce0eba-eb26-405d-8a90-8d982ec30698
Body: { email }

Step 2 — save new password
POST  https://n8n.derez.ai/webhook/c2ce0eba-eb26-405d-8a90-8d982ec30698/update
Body: { token, password, email }

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.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<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 / 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<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:

["copywriting.json", "cold-email.json"]

skills/<slug>.json — fetched in parallel when the skills step opens, cached in wizardSkillsCache:

{ "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:

["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:

{ "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

# 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).