Skip to content

AMP Manager for Developers ​

This guide is for developers who want to understand, extend, or contribute to AMP Manager.

Quick Start ​

bash
# Clone and setup
git clone https://github.com/Amp-Manager/amp-manager.git
cd amp-manager
npm install

# Development UI (Vite dev server on port 3000)
npm run dev

# Run full Neutralino desktop app
npm run dev:app

# Build React UI (vite build)
npm run build

# Build Windows executable
npm run build:app

# Clean non-Windows files + bin/
npm run build:clean

# Build app + clean 
npm run build:win
POST-BUILD

IMPORTANT: After build the executable, you MUST run post-build.bat to apply the UAC manifest. The app must run with admin privileges.

TypeScript Types ​

AMP Manager uses @neutralinojs/lib npm package but also provides custom type definitions:

FilePurpose
node_modules/@neutralinojs/lib/Used by neu build (no import needed)
src/types/neutralino.d.tsCustom types for window.Neutralino
src/types/amp.d.tsCustom types for window.AMP API

No need to import @neutralinojs/lib in your code - it's automatically embedded by the CLI during neu build.

The custom types provide IDE autocomplete for:

  • window.AMP (the backend bridge)
  • window.Neutralino (Neutralino APIs)

Stability Patterns ​

AMP Manager uses these patterns to prevent NeutralinoJS backend freeze:

PatternPurposeUsage
execWithTimeout()Wraps batch calls with 30s timeoutexecWithTimeout('amp-tasks.bat docker.up')
execWithRetry()Retry on transient IPC failuresexecWithRetry(() => ampBridge.status())
startKeepalive()Heartbeat (30s) to prevent Windows suspensionAuto-started in main.tsx
WatchdogZombie recovery - monitors PID/port, restarts appSpawned by ampBridge.spawnWatchdog() on startup

Why these exist:

  • NeutralinoJS runs on a single-threaded event loop - a hanging os.execCommand() blocks everything
  • Windows may suspend "inactive" desktop apps after ~2 minutes
  • Batch commands (Docker, SSL, etc.) can hang indefinitely without timeout
  • The Neutralino event loop can become unresponsive (zombie state) without external monitoring

Watchdog Details ​

The watchdog runs as a detached background process:

StepAction
1App starts → saves PID, port, instanceId to config.json
2ampBridge.spawnWatchdog() spawns amp-tasks.bat watch as background process
3Every 20s, checks: Is stored PID still running? Is exitFlag set?
4If 3 failures in a row → kill app, restart automatically
5On clean exit: set exitFlag → delete lock → exit

Lock File Mechanism (prevents duplicate watchdogs):

bat
:: Check if lock file exists (another watchdog is running)
if exist "%temp%\amp_watchdog.lock" (
    exit /b 0
)

:: Acquire lock by creating empty file
type nul > "%temp%\amp_watchdog.lock"

ExitFlag Pattern (clean user exit vs crash detection):

bat
:: Check exitFlag FIRST - if user clicked X, this is true
findstr /i "\"exitFlag\":true" "%config.json%"
if not errorlevel 1 goto :CLEANUP

:: Otherwise check if app is still running...
tasklist /fi "PID eq %APP_PID%"

Clean Close Flow:

  1. handleClose() sets exitFlag=true in config.json
  2. Deletes %temp%\amp_watchdog.lock
  3. Exits app → watchdog sees exitFlag, cleans up lock, exits

See:

  • src/services/AMPBridge.ts:342-353 - spawnWatchdog() implementation
  • src/services/AMPBridge.ts:359-389 - killStaleWatchdogs() (cleanup on startup)
  • src/components/layout/Layout.tsx:245-277 - handleClose() 3-step flow
  • amp-tasks.bat:1512-1585 - Full watchdog implementation

Architecture Overview ​

bash
AMP MANAGER STACK
================

  React          NeutralinoJS        Windows
  Frontend  -->  Native Bridge  -->  Batch
  (Vite)        (window.AMP)        (amp-tasks)

  JSON Files         (OS APIs)        (Hosts, SSL)
  (data/)

Key Files ​

FilePurpose
src/services/AMPBridge.tsCentral hub for all backend calls
src/lib/db.tsJSON storage functions
resources/js/main.jsFrontend bridge, validates tasks
src/types/neutralino.d.tsTypeScript types for Neutralino API
src/types/amp.d.tsTypeScript types for AMP API
amp-tasks.batWindows batch backend commands
neutralino.config.jsonNeutralino settings + API allowlist

Key Concepts ​

1. Sync Flow (Every Login) ​

AMP runs a sync on every login to ensure consistency:

Why sync runs every login: See Security

2. JSON Storage + Encryption ​

All user data is stored in JSON files in users/user_{username}/ with selective encryption:

typescript
// Sensitive files (encrypted with user's AES key)
- notes (users/user_{username}/notes.json)
- credentials (users/user_{username}/credentials.json) 
- settings (users/user_{username}/settings.json)
- workflows (users/user_{username}/workflows.json)
- site_configs (users/user_{username}/site_configs.json)

// Plain files (no encryption)
- sites (users/user_{username}/sites.json)
- tags (users/user_{username}/tags.json)
- tunnels (users/user_{username}/tunnels.json)
- activity_logs (users/user_{username}/activity_logs.json)
- domain_status (users/user_{username}/domain_status.json)
- databases (users/user_{username}/databases.json)
- databases_cache (users/user_{username}/databases_cache.json)
- user (users/user_{username}/user.json) - salt + validation token only

Encryption flow:

Password -> PBKDF2 -> AES Key -> Encrypt -> JSON Files

3. App Initialization & User State ​

On app startup, AMP Manager restores user state to enable JSON operations:

Flow:

  1. main.tsx loads config.json → gets lastUser
  2. Calls setCurrentUser(lastUser) to set global _currentUser
  3. Global _currentUser enables ensureUser() in db.ts

Why this matters:

  • Users don't need to re-login after app restart
  • JSON storage functions work immediately without auth errors
  • Dashboard loads correct user data on startup

Key code:

  • src/lib/db.ts:12-14 - setCurrentUser() function
  • src/lib/db.ts:28-31 - ensureUser() throws if not set
  • src/main.tsx:16-18 - Restores user on startup

4. User Parameter Pattern for JSON Functions ​

When saving data, use the explicit user parameter for reliability:

typescript
// Encrypted files - user at args[0], data at args[1], key at args[2]
await saveNotesJSON(user, allNotes, formData.is_encrypted ? encryptionKey : undefined);
await saveCredentialsJSON(user, creds, encryptionKey);
await saveSettingsJSON(user, settings, encryptionKey);

// Plain files - data at args[0] (user comes from global _currentUser)
await saveSitesJSON(allSites);
await saveDomainStatusJSON(statuses);

// BUT: Pass user explicitly in sync to ensure correct folder
await saveSitesJSON(user, filteredSites);
await saveDomainStatusJSON(user, allStatuses);

Key rule: In async operations (like sync), always pass user explicitly to ensure data saves to the correct user's folder.

3. Service Layer Pattern ​

AMP uses a hybrid architecture:

LayerPatternExample
ServicesOOP (class + singleton)DatabaseService
HooksReact hooksuseProjectSync
StateZustanduseDockerSettings

See Contributing for full patterns.

Common Development Tasks ​

Adding a New Backend Command ​

1. Add to amp-tasks.bat:

batch
:myCommand
echo %~1
exit /b 0

2. Whitelist in public/js/main.js:

javascript
const AMP_TASKS = {
  // ...
  'myCommand': true,
};

3. Add to AMPBridge.ts:

typescript
public myCommand(param: string) {
  return this.call<AmpResponse>('myCommand', param);
}

4. Add to nativeAllowList (if needed): In neutralino.config.json, add required APIs.

Adding a New Page ​

1. Create the component:

typescript
// src/pages/MyNewPage.tsx
export function MyNewPage() {
  return <div>My New Page</div>;
}

2. Add the route:

typescript
// src/App.tsx
import { MyNewPage } from './pages/MyNewPage';

<Route path="/my-new-page" element={<MyNewPage />} />

Using Services in Components ​

typescript
import { ampBridge } from '@/services/AMPBridge';
import { databaseService } from '@/services/DatabaseService';

function MyComponent() {
  const handleClick = async () => {
    const result = await ampBridge.listDomains();
    // or
    const databases = await databaseService.listDatabases();
  };
}

Debugging Tips ​

Browser Console ​

  • Open DevTools (F12)
  • Check Console for errors
  • Filter by "AMP" or your feature

Backend Logs ​

bash
# Check amp-tasks output
# In App Terminal, run commands directly

Docker Logs ​

bash
# View container logs
docker logs angie_amp
docker logs db_amp

Testing Checklist ​

Before submitting a PR:

text
[ ] `npm run lint` passes  
[ ] `npm run build` succeeds  
[ ] New feature works in browser (`npm run dev`)  
[ ] New feature works in desktop app  
[ ] Error states handled gracefully  
[ ] No console.log statements

File Organization ​

text
src/
|-- components/      # Reusable UI components
|   |-- layout/      # Layout (Sidebar, Layout)
|   |-- domains/     # Domain-related components
|   |-- databases/   # Database components
|   |-- settings/    # Settings panels
|   |-- workflow/    # Workflow editor
|   |-- notes/       # Notes components
|-- pages/           # Route pages
|-- services/        # Business logic (AMPBridge, DatabaseService)
|-- context/         # React contexts (Auth, Sync)
|-- hooks/           # Custom hooks (useProjectSync)
|-- stores/          # Zustand stores
|-- lib/             # Utilities (db, crypto)
|-- types/           # TypeScript definitions

JSON Storage ​

Function Signatures IMPORTANT ​

Understanding the argument patterns is critical to avoid bugs.
There are three distinct patterns:


Pattern 1: Encrypted Files user data key ​

Used for files that store sensitive data. Key is at args[2], data at args[1]:

typescript
// Functions using this pattern:
saveNotesJSON(user, data, key?)
saveCredentialsJSON(user, data, key?)
saveSettingsJSON(user, data, key?)
saveWorkflowsJSON(user, data, key?)
saveSiteConfigsJSON(user, data, key?)

// Example:
await saveNotesJSON(user, notes, encryptionKey);
await saveSettingsJSON(user, settings, encryptionKey);  // key is optional

In db.ts:

typescript
export async function saveNotesJSON(..._args: any[]): Promise<void> { 
  const data = _args[1] || [];  // data at index 1
  const key = _args[2] instanceof CryptoKey ? _args[2] : null;  // key at index 2
  await dataStorage.saveUser(ensureUser(), 'notes.json', data, key ? { encrypt: true, key } : undefined); 
}

Pattern 2: Plain User Files data ​

Used for non-sensitive data. Data at args[0]:

typescript
// Functions using this pattern:
saveTagsJSON(data)
saveTunnelsJSON(data)
saveActivityLogsJSON(data)
saveDatabasesJSON(data)
saveDatabasesCacheJSON(data)

// Example:
await saveTagsJSON(tags);
await saveTunnelsJSON(tunnels);

In db.ts:

typescript
export async function saveTagsJSON(..._args: any[]): Promise<void> { 
  const data = _args[0] || [];  // data at index 0
  await dataStorage.saveUser(ensureUser(), 'tags.json', data); 
}

Pattern 3: Plain Files WITH user data ​

Used for files that need explicit user (for sync reliability). User at args[0], data at args[1]:

typescript
// Functions using this pattern:
saveSitesJSON(user, data)
saveDomainStatusJSON(user, data)

// Example:
await saveSitesJSON(user, filteredSites);
await saveDomainStatusJSON(user, allStatuses);

In db.ts:

typescript
export async function saveSitesJSON(..._args: any[]): Promise<void> { 
  const data = _args[1] || [];  // data at index 1 (NOT index 0!)
  await dataStorage.saveUser(ensureUser(), 'sites.json', data); 
}

Common Bug ​

Using Wrong Index

WRONG DON'T do this:
typescript
// WRONG - saves "nuno" (username) instead of data!
await saveSitesJSON(user, filteredSites);  // if function expects data at args[0]
CORRECT DO this:
typescript
// CORRECT - know which pattern your function uses
await saveSitesJSON(user, filteredSites);   // Pattern 3: user at args[0], data at args[1]
await saveTagsJSON(tags);                   // Pattern 2: data at args[0]
await saveNotesJSON(user, notes, key);      // Pattern 1: user at args[0], data at args[1], key at args[2]

The is_encrypted Toggle Pattern ​

For notes, the UI toggle determines whether to encrypt:

typescript
// In useNotes.ts handleSaveNote:
await saveNotesJSON(user, allNotes, formData.is_encrypted ? encryptionKey : undefined);
//                   ^user    ^data         ^key only when toggle is ON

This ensures normal notes are saved as plain text (when toggle is OFF), while encrypted notes use the encryption key.

Sync Process Flow IMPORTANT ​

The sync in useProjectSync.ts follows this flow:

typescript
// 1. Load existing data
const existingSitesArray = Array.isArray(existingSites) ? existingSites : [];
const existingStatusesArray = Array.isArray(existingStatuses) ? existingStatuses : [];

// 2. Scan for new domains from backend
const domainStatuses = ampDomains.map(d => ({ ... }));
const newDomains = new Set(domainStatuses.map(s => s.domain));

// 3. Build combined statuses (remove old for scanned domains, add fresh data)
// IMPORTANT: Filter OUT domains that will be re-scanned (!newDomains.has)
const existingToKeep = existingStatusesArray.filter(s => !newDomains.has(s.domain));
const allStatuses = [...existingToKeep, ...domainStatuses];
await saveDomainStatusJSON(user, allStatuses);

// 4. Build combined sites (update existing or add new)
// Uses findIndex to update if exists, push if new
const allSites = [...existingSitesArray];
for (const status of domainStatuses) {
  const siteIndex = allSites.findIndex(s => s.id === status.domain);
  if (siteIndex >= 0) {
    allSites[siteIndex] = site;  // Update existing
  } else {
    allSites.push(site);  // Add new
  }
}
await saveSitesJSON(user, allSites);

// 5. Save timestamp
await saveSettingsJSON(user, settings);

Key points:

  • Step 3 uses !newDomains.has() to REMOVE old entries before adding fresh scan data - prevents duplication
  • Step 4 uses findIndex to update in-place rather than filter + concatenate (which would cause duplication)
  • This ensures exactly 1 entry per domain in both domain_status.json and sites.json

Config.json Merge Pattern IMPORTANT ​

When updating config.json, you must merge with existing data to preserve watchdog instance info:

The Problem ​

The config.json file stores both:

  • User data: lastUser
  • Instance data: instanceId, pid, port, launchedAt, processName

If you overwrite with only { lastUser }, you lose instance info and the watchdog stops working.

CORRECT Use this pattern:
typescript
// CORRECT - always merge
const existing = await dataStorage.load('config.json') || {};
await dataStorage.save('config.json', { ...existing, lastUser: username });
WRONG DON'T do this:
typescript
// WRONG - overwrites instance info
await dataStorage.save('config.json', { lastUser: username });

Where This Applies ​

LocationContextStatus
AuthContext.tsxLogin/Register/Logout✅ Fixed
db.tsdeleteUserData()✅ Fixed
main.tsxupdateInstanceInfo()✅ Already correct

Why It Matters ​

  • Watchdog reads PID/port from config.json to monitor app health
  • If overwritten, watchdog can't detect app failure
  • Auto-recovery fails, users see frozen app

Instance Info Fields ​

FieldSourcePurpose
instanceIdGenerated at startupUnique identifier
processNameHardcodedProcess name for taskkill
pidwindow.NL_PIDProcess ID to monitor
portwindow.NL_PORTPort to check responsiveness
launchedAtDate.now()Timestamp for debugging

What's Next? ​

GoalLink
Full architectureArchitecture Overview
State managementState Management
TroubleshootingTroubleshooting
WorkflowsWorkflows
API ReferenceAPI Reference

AMP Manager
Released under the MIT License.