Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
node_modules
dist/
module/
logs/
.DS_Store
35 changes: 21 additions & 14 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,30 @@ This template provides:
- Consistent layout components (header, sidebar, main content area)
- Help modal system
- Local development server with WebSocket support
- **Composer-ready bundle**: `npm run build:module` outputs `module/` for hosts that load `/simulations/{id}/content.html`, `simulation.css`, and `simulation.js`
- Standardized file structure and naming conventions

## Quick Start

1. **Customize the HTML template** (`client/index.html`):
- Replace `<!-- APP_TITLE -->` with your page title
- Replace `<!-- APP_NAME -->` with your app name
- Add your main content at `<!-- APP_SPECIFIC_MAIN_CONTENT -->`
- Replace the default `<main id="standalone-sim-mount" …>` block with your layout (keep that `id` and `data-bespoke-sim-root` if you use `standalone.js` and composer hosts)
- Keep `client/content.html` in sync with the same inner markup for `build:module`
- Add app-specific CSS links at `<!-- APP_SPECIFIC_CSS -->`
- Add app-specific JavaScript at `<!-- APP_SPECIFIC_SCRIPTS -->`

2. **Create your application files**:
- App-specific CSS (e.g., `my-app.css`)
- App-specific JavaScript (e.g., `my-app.js`)
- App-specific JavaScript (e.g., `my-app.js`); export `init(context)` from `simulation-app.js` for composer hosts
- Help content (based on `help-content-template.html`)

3. **Start the development server**:
3. **Composer / multi-app host** (optional):
- Run `npm run build` for the full static app (`dist/`) and `npm run build:module` for the host bundle (`module/`)
- Serve the module tree with `IS_PRODUCTION=true SERVE_DIR=module PORT=<port> node server.js` (each app on its own port when the host proxies `/simulations/{id}/…`)
- Match `SIM_ID` in `client/standalone.js` to the `id` in the host’s `simulations.json`

4. **Start the development server**:
```bash
npm start
```
Expand Down Expand Up @@ -93,17 +100,17 @@ See [BESPOKE-TEMPLATE.md](./BESPOKE-TEMPLATE.md).

```
client/
├── index.html # Main HTML template
├── app.js # Application logic
├── bespoke-template.css # Template-specific styles
├── help-modal.js # Help modal system
├── help-content-template.html # Help content template
── design-system/ # CodeSignal Design System
├── colors/
├── spacing/
├── typography/
└── components/
server.js # Development server
├── index.html # Main HTML template
├── app.js # Shell: help modal + WebSocket
├── standalone.js # Standalone init(context) + /api/log
├── simulation-app.js # Composer entry: export init(context)
├── content.html # Fragment for composer host
── bespoke-simulation.css # Styles for fragment → module/simulation.css
├── bespoke-template.css # Template layout
├── help-content.html # Help body HTML
└── design-system/ # CodeSignal Design System (git submodule)
server.js # API + static (dist/ or module/ via SERVE_DIR)
vite.config.module.js # build:module → module/
```

## Notes for AI Agents
Expand Down
27 changes: 22 additions & 5 deletions BESPOKE-TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
This document provides precise implementation instructions for creating
embedded applications using the Bespoke Simulation template. Follow these
instructions exactly to ensure consistency across all applications.
NOTE: Never edit this `BESPOKE-TEMPLATE.md` file. Codebase changes should be reflected in the `AGENTS.md` file.
Keep this document aligned with template behavior; `AGENTS.md` summarizes the same conventions for contributors.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update the required file list to match the new module workflow.

The new composer section introduces client/standalone.js, client/simulation-app.js, client/content.html, and client/bespoke-simulation.css, but "Required Files Structure" still only lists app.js and server.js. That leaves the document internally inconsistent for new template adopters.

Based on learnings, "Maintain consistency with the template's file structure and patterns when adding new files".

Also applies to: 55-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@BESPOKE-TEMPLATE.md` at line 6, Update the "Required Files Structure" section
in BESPOKE-TEMPLATE.md to include the new composer module files
(client/standalone.js, client/simulation-app.js, client/content.html,
client/bespoke-simulation.css) so the list matches the template behavior; locate
the "Required Files Structure" block (references to the existing app.js and
server.js entries) and add those four filenames, and also review and correct the
corresponding lines/paragraphs mentioned around 55-69 to ensure the document and
AGENTS.md conventions remain consistent.


## Required Files Structure

Expand Down Expand Up @@ -37,9 +37,10 @@ Every application should include these files in the following order:
Replace with your application's display name (appears in header)
Example: "Database Designer" or "Task Manager"

c) `<!-- APP_SPECIFIC_MAIN_CONTENT -->`
Add your application's main content area
Example: `<div id="canvas"></div>` or `<div id="editor"></div>`
c) Main content area
Replace the default `<main id="standalone-sim-mount" data-bespoke-sim-root>…</main>` block with your layout.
If you use a composer host, keep the same inner markup in `client/content.html` (fragment the host injects).
Preserve `id="standalone-sim-mount"` when using `standalone.js` unless you update `simulation-app.js` accordingly.

d) `<!-- APP_SPECIFIC_CSS -->`
Add links to your application-specific CSS files
Expand All @@ -49,7 +50,23 @@ Every application should include these files in the following order:
Add links to your application-specific JavaScript files
Example: `<script src="./my-app-logic.js"></script>`

2. DO NOT modify the core structure (header, script loading order, etc.)
2. DO NOT modify the core structure (header, script loading order, etc.) without reviewing `app.js`, `standalone.js`, and host loaders.

## Composer hosts (multi-app workspace)

Composer-style hosts load each app from the same origin under `/simulations/{id}/`:

1. `content.html` — HTML fragment injected into a `.sim-slot[data-sim-id="{id}"]`
2. `simulation.css` — styles for that fragment (from `bespoke-simulation.css` in this template)
3. `simulation.js` — ES module exporting `init(context)` (and optionally `onAction`, `onMessage`)

**Build:** `npm run build:module` writes these files under `module/`. The Rollup build marks `design-system/*` as external so the host page supplies those assets once.

**Serve for the host:** `IS_PRODUCTION=true SERVE_DIR=module PORT=<port> node server.js` serves `module/` as static files. Use a distinct `PORT` per app when the host reverse-proxies to each repo.

**Runtime contract:** `context` includes `config` (spread from the host registry, plus `basePath` like `/simulations/your-id`) and `emit(eventType, payload)` for telemetry. The template resolves DOM via `.sim-slot[data-sim-id]` + `[data-bespoke-sim-root]`; standalone mode uses `#standalone-sim-mount`.

**Logging:** `POST /api/log` accepts `{ "entries": [...] }` and appends JSON lines to `logs/events.jsonl` (used by `standalone.js` and typical hosts).

## CSS Implementation

Expand Down
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

This directory contains a template for creating embedded applications that share a consistent design system and user experience.

Each app can run as a **normal static site** (`npm run build` → `dist/`) and as a **bundle for composer hosts** (`npm run build:module` → `module/`). Hosts fetch `/simulations/{id}/content.html`, `simulation.css`, and `simulation.js`; serve the module tree with `IS_PRODUCTION=true SERVE_DIR=module PORT=<port> node server.js`. See `AGENTS.md` and `BESPOKE-TEMPLATE.md`.

Comment on lines +5 to +6
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Document PORT consistently for production mode.

These lines correctly show PORT=<port> for module serving, but the environment-variable section later still says production ignores PORT. Please update that section too so the deployment instructions do not contradict each other.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 5 - 6, The README has inconsistent docs about PORT:
the module serving example uses IS_PRODUCTION=true SERVE_DIR=module PORT=<port>
node server.js but the later environment-variable section says production
ignores PORT; update that section to state that when running the module bundle
in production mode (IS_PRODUCTION=true) the server will honor the PORT env var
for server.js serving from SERVE_DIR, and remove or correct the sentence that
says production ignores PORT so both examples consistently document PORT usage
for module deployment.

## Components

### 1. Design System Integration
Expand All @@ -24,12 +26,8 @@ A base HTML template that includes:
- Help modal integration
- Proper CSS and JavaScript loading

### 4. `client/help-modal.js`
A dependency-free JavaScript module for the help modal system:
- Consistent modal behavior across all apps
- Keyboard navigation (ESC to close)
- Focus management
- Custom event system
### 4. `client/app.js` and `client/standalone.js`
Shell behavior (help modal via design system `Modal`, WebSocket) plus standalone `init(context)` wiring for the same contract composer hosts use.

### 5. `client/help-content-template.html`
A template for creating consistent help content:
Expand All @@ -52,7 +50,7 @@ A template for creating consistent help content:
- `<!-- APP_TITLE -->` - Your application title
- `<!-- APP_NAME -->` - Your application name (appears in header)
- `<!-- APP_SPECIFIC_HEADER_CONTENT -->` - Any additional header elements
- `<!-- APP_SPECIFIC_MAIN_CONTENT -->` - Your main content area
- Default `<main id="standalone-sim-mount" …>` / `client/content.html` — your main UI (keep in sync for composer)
- `<!-- APP_SPECIFIC_CSS -->` - Links to your app-specific CSS files
- `<!-- APP_SPECIFIC_SCRIPTS -->` - Links to your app-specific JavaScript files

Expand Down
12 changes: 12 additions & 0 deletions client/bespoke-simulation.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* Styles for simulation fragment (bundled as module/simulation.css) */
.bespoke-sim-intro {
max-width: 36rem;
}

.bespoke-sim-hint code {
font-size: 0.9em;
}

.bespoke-sim-standalone-mount {
padding: var(--UI-Spacing-spacing-ml, 1rem);
}
10 changes: 10 additions & 0 deletions client/content.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!-- Fragment mounted by composer hosts under /simulations/{id}/content.html -->
<main data-bespoke-sim-root>
<section class="box card bespoke-sim-intro">
<h2 class="heading-small">Your app</h2>
<p class="body-default bespoke-sim-hint">
Replace this markup and extend <code>simulation-app.js</code>. Keep this file in sync with the standalone block in <code>index.html</code> when you use both modes.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep the fragment text identical to client/index.html to avoid drift.

Line 6 does not match the corresponding standalone block copy, so the two files are already out of sync.

Suggested sync fix
-      Replace this markup and extend <code>simulation-app.js</code>. Keep this file in sync with the standalone block in <code>index.html</code> when you use both modes.
+      Replace this block when customizing; keep in sync with <code>client/content.html</code> for composer builds.

As per coding guidelines, "Keep client/content.html in sync with the same inner markup as client/index.html for build:module."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Replace this markup and extend <code>simulation-app.js</code>. Keep this file in sync with the standalone block in <code>index.html</code> when you use both modes.
Replace this block when customizing; keep in sync with <code>client/content.html</code> for composer builds.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/content.html` at line 6, The fragment text in client/content.html is
out of sync with the standalone block in client/index.html; open both files and
replace the markup in client/content.html (the placeholder line referencing
simulation-app.js) with the exact inner markup from the standalone block in
client/index.html so they are identical; ensure you preserve the reference to
simulation-app.js and keep whitespace/inline elements identical to avoid drift,
then run a quick diff between the two files to confirm they match.

</p>
<button type="button" class="button button-primary" id="btn-sim-demo">Send sample event</button>
</section>
</main>
16 changes: 13 additions & 3 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,32 @@

<!-- Template-specific components (layout, utilities, temporary components) -->
<link rel="stylesheet" href="./bespoke-template.css" />
<link rel="stylesheet" href="./bespoke-simulation.css" />
<!-- APP_SPECIFIC_CSS -->
</head>
<body class="bespoke">
<!-- Navigation Header -->
<header class="header">
<h1 class="heading-small">APP NAME</h1>
<h1 class="heading-small"><!-- APP_NAME --></h1>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace the APP_NAME placeholder with the actual app name.

Line 33 still contains a template placeholder instead of user-facing title text.

As per coding guidelines, "Replace <!-- APP_NAME --> placeholder in client/index.html with your app name."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/index.html` at line 33, The h1 still contains the template placeholder
<!-- APP_NAME -->; replace that HTML comment placeholder with your actual
application name so the user-facing title is readable (update the <h1
class="heading-small"> element to contain the app name text instead of the <!--
APP_NAME --> comment).

<!-- APP_SPECIFIC_HEADER_CONTENT -->
<div class="spacer"></div>
<button id="btn-help" class="button button-text">Help</button>
</header>

<!-- Main Application Content -->
<!-- APP_SPECIFIC_MAIN_CONTENT -->
<!-- Main Application Content: default starter below — replace when customizing; keep id & data-bespoke-sim-root if you use standalone.js + composer -->
<main id="standalone-sim-mount" class="bespoke-sim-standalone-mount" data-bespoke-sim-root>
<section class="box card bespoke-sim-intro">
<h2 class="heading-small">Your app</h2>
<p class="body-default bespoke-sim-hint">
Replace this block when customizing; keep in sync with <code>client/content.html</code> for composer builds.
</p>
<button type="button" class="button button-primary" id="btn-sim-demo">Send sample event</button>
</section>
</main>

<!-- Core Scripts -->
<script type="module" src="./app.js"></script>
<script type="module" src="./standalone.js"></script>
<!-- APP_SPECIFIC_SCRIPTS -->
</body>
</html>
47 changes: 47 additions & 0 deletions client/simulation-app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Simulation entry for composer hosts: exports init(context).
* Hosts load content.html, simulation.css, then import ./simulation.js
* (see composer template simulation-loader.js).
*/

function escapeSelector(id) {
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
return CSS.escape(id);
}
return id.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}

function resolveRoot(context) {
const simId = context.config?.id;
if (simId) {
const slot = document.querySelector(
`.sim-slot[data-sim-id="${escapeSelector(simId)}"]`
);
const inner = slot?.querySelector('[data-bespoke-sim-root]');
if (inner) return inner;
if (slot) return slot;
}
return document.getElementById('standalone-sim-mount');
}

function bindDemo(root, emit) {
const btn = root.querySelector('#btn-sim-demo');
if (!btn) return;
btn.addEventListener('click', () => {
emit('demo:click', { source: 'template' });
});
Comment on lines +27 to +32
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make event binding idempotent to prevent duplicate emits on re-init.

If init() runs multiple times for the same root, Line 30 adds multiple listeners and one click can emit demo:click repeatedly.

Suggested fix
 function bindDemo(root, emit) {
   const btn = root.querySelector('#btn-sim-demo');
-  if (!btn) return;
+  if (!btn || btn.dataset.simDemoBound === 'true') return;
+  btn.dataset.simDemoBound = 'true';
   btn.addEventListener('click', () => {
     emit('demo:click', { source: 'template' });
   });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function bindDemo(root, emit) {
const btn = root.querySelector('#btn-sim-demo');
if (!btn) return;
btn.addEventListener('click', () => {
emit('demo:click', { source: 'template' });
});
function bindDemo(root, emit) {
const btn = root.querySelector('#btn-sim-demo');
if (!btn || btn.dataset.simDemoBound === 'true') return;
btn.dataset.simDemoBound = 'true';
btn.addEventListener('click', () => {
emit('demo:click', { source: 'template' });
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/simulation-app.js` around lines 27 - 32, The handler in bindDemo
currently adds a new click listener every time init() runs, causing duplicate
'demo:click' emits; make the binding idempotent by ensuring the previous
listener is removed or not re-added: define a stable handler function (e.g.,
inside bindDemo) and call btn.removeEventListener('click', handler) before
btn.addEventListener('click', handler), or set a marker on the element (e.g.,
btn.dataset.simBound) and skip adding if already set; keep references to the
'demo:click' emit usage and the btn element so the same handler can be
reused/removed.

}

export function init(context = {}) {
const emit =
typeof context.emit === 'function'
? context.emit
: () => {};

const root = resolveRoot(context);
if (!root) {
console.warn('simulation-app: no mount root found');
return;
}
bindDemo(root, emit);
}
43 changes: 43 additions & 0 deletions client/standalone.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Standalone page: same init(context) contract as composer hosts.
* Set SIM_ID to the id used in the host's simulations.json.
*/
import { init } from './simulation-app.js';

const SIM_ID = 'bespoke-app';

const logBuffer = [];
let flushTimer = null;

function flushLogs() {
const entries = logBuffer.splice(0);
flushTimer = null;
if (entries.length === 0) return;
fetch('/api/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ entries })
}).catch(err => console.error('Failed to flush logs:', err));
Comment on lines +12 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Requeue failed log batches instead of dropping them.

logBuffer.splice(0) removes the batch before the request succeeds, and this only logs transport failures. Any transient network issue or non-2xx response will permanently lose telemetry. Keep the batch until the server acknowledges it, then add bounded retry/backoff for failed flushes.

As per coding guidelines, "Implement retry logic for network operations".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/standalone.js` around lines 12 - 20, flushLogs currently removes the
batch immediately with logBuffer.splice(0) and simply logs transport errors,
which loses telemetry on non-2xx or transient failures; change flushLogs to keep
a copy of the entries (e.g. const batch = logBuffer.slice(0, N)) and only remove
them from logBuffer after a successful response (check response.ok), and on
failure requeue the batch back into logBuffer and schedule a retry with
exponential backoff and a bounded retry count; reference functions/variables
flushLogs, logBuffer, flushTimer and ensure you guard against unbounded growth
by enforcing a max buffer size and maxRetries per batch.

}

function pushLog(entry) {
logBuffer.push({ ...entry, ts: new Date().toISOString() });
if (!flushTimer) flushTimer = setTimeout(flushLogs, 1000);
}

const context = {
config: { id: SIM_ID, basePath: '' },
emit: (eventType, payload = {}) => {
pushLog({ simId: SIM_ID, dir: 'event', type: eventType, payload });
}
};

function boot() {
init(context);
Comment on lines +35 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Catch init(context) failures and render a fallback.

A thrown or rejected init() leaves the standalone page blank with no user-facing signal. Wrap this call so you console.error(...) and show a short failure message in the mount root.

As per coding guidelines, "Provide meaningful error messages to users" and "Log errors to console for debugging".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/standalone.js` around lines 35 - 36, Wrap the call to init(context)
inside boot() with a try/catch (or handle the returned promise) so any thrown or
rejected errors are caught; in the catch log the error via console.error and set
a short user-visible failure message into the mount root element (e.g.,
document.getElementById(...) or the same mount root used elsewhere) so the
standalone page doesn't remain blank; update the boot function around
init(context) to perform this error handling and rendering of the fallback
message.

}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"start:dev": "concurrently \"npm run dev:vite\" \"npm run dev:api\"",
"dev:vite": "vite",
"dev:api": "PORT=3001 node server.js",
"build": "vite build"
"build": "vite build",
"build:module": "vite build --config vite.config.module.js"
},
"keywords": [
"bespoke",
Expand Down
Loading