Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The format follows the spirit of [Keep a Changelog](https://keepachangelog.com/e
- Added project provenance, contribution authorship guidance, full-tree formatting, and type-aware ESLint parsing.
- Removed fundraising, commercial, design-partner, community-operations, and enterprise-roadmap material from the public tree and npm package.
- Added a release workflow for signed annotated tags, exact-tarball smoke testing, checksums, SBOM generation, build attestations, npm provenance, and post-publish `gitHead` verification.
- Centered the README hero and dashboard product identity, and added responsive navigation and request/detail layouts for mobile screens.

## 0.1.8 - 2026-08-22

Expand Down
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
# GhostAPI

**Local API simulation and test evidence for AI-assisted development.**

GhostAPI gives applications and coding agents a local target for selected Stripe, Resend, OpenAI, Twilio, GitHub, Discord, and generic REST workflows. It records heuristically sanitized traffic, injects deterministic failures, exposes MCP controls, and turns captured behavior into repeatable tests.

On supported Linux hosts, `ghostapi run` can execute a test command inside a loopback-only network namespace. It is not a hostile-code or filesystem sandbox.

<p align="center">
<div align="center">
<img src="docs/assets/ghostapi-avatar.png" alt="GhostAPI logo" width="88" height="88">
<h1>GhostAPI</h1>
<p><strong>Local API simulation and test evidence for AI-assisted development.</strong></p>
<p>GhostAPI gives applications and coding agents a local target for selected Stripe, Resend, OpenAI, Twilio, GitHub, Discord, and generic REST workflows. It records heuristically sanitized traffic, injects deterministic failures, exposes MCP controls, and turns captured behavior into repeatable tests.</p>
<p>On supported Linux hosts, <code>ghostapi run</code> can execute a test command inside a loopback-only network namespace. It is not a hostile-code or filesystem sandbox.</p>
<p>
<a href="https://www.npmjs.com/package/@yiaany/ghostapi"><img alt="npm" src="https://img.shields.io/npm/v/@yiaany/ghostapi?color=0f172a&label=npm"></a>
<a href="https://github.com/yiaany/ghostapi/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-0f172a"></a>
<a href="https://github.com/yiaany/ghostapi/actions/workflows/ci.yml"><img alt="ci" src="https://github.com/yiaany/ghostapi/actions/workflows/ci.yml/badge.svg"></a>
<img alt="node" src="https://img.shields.io/badge/node-%3E%3D20-0f172a">
<img alt="MCP" src="https://img.shields.io/badge/MCP-enabled-0f172a">
</p>

[Quickstart](#quickstart) · [Security model](SECURITY.md) · [Provider coverage](docs/providers/) · [Release evidence](docs/releases/)

```bash
npx @yiaany/ghostapi start --open
```
</p>
<p>
<a href="#quickstart">Quickstart</a> ·
<a href="SECURITY.md">Security model</a> ·
<a href="docs/providers/">Provider coverage</a> ·
<a href="docs/releases/">Release evidence</a>
</p>
<p><code>npx @yiaany/ghostapi start --open</code></p>
</div>

<p align="center">
<img src="docs/assets/dashboard.png" alt="GhostAPI dashboard with live local Stripe, OpenAI, and REST traffic">
Expand Down
Binary file modified docs/assets/dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 26 additions & 1 deletion src/dashboard/app.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// DOM References
const els = {
sidebar: document.getElementById("sidebar"),
sidebarBackdrop: document.getElementById("sidebar-backdrop"),
toggleBtn: document.getElementById("sidebar-toggle"),
statusIndicator: document.getElementById("status-indicator"),
statusText: document.getElementById("status-text"),
Expand Down Expand Up @@ -82,7 +83,25 @@ async function fetchProviderManifests() {

// Side-effects & Listeners
function setupInteractions() {
els.toggleBtn.onclick = () => els.sidebar.classList.toggle("closed");
els.toggleBtn.onclick = () => {
if (window.matchMedia("(max-width: 760px)").matches) {
const open = els.sidebar.classList.toggle("mobile-open");
els.sidebarBackdrop.classList.toggle("open", open);
return;
}
els.sidebar.classList.toggle("closed");
};
els.sidebarBackdrop.onclick = closeMobileSidebar;
els.sidebar.addEventListener("click", (event) => {
if (event.target.closest(".nav-item")) closeMobileSidebar();
});
window.addEventListener("resize", () => {
if (window.matchMedia("(max-width: 760px)").matches) {
els.sidebar.classList.remove("closed");
return;
}
closeMobileSidebar();
});

// Command + K
document.addEventListener("keydown", (e) => {
Expand Down Expand Up @@ -183,8 +202,14 @@ window.setFilter = (filter) => {
}

renderList();
closeMobileSidebar();
};

function closeMobileSidebar() {
els.sidebar.classList.remove("mobile-open");
els.sidebarBackdrop.classList.remove("open");
}

window.clearStorage = async (target, event) => {
try {
const btnText = event.currentTarget.querySelector(".nav-item-title");
Expand Down
32 changes: 26 additions & 6 deletions src/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -400,12 +400,25 @@
</div>
</aside>

<button
class="sidebar-backdrop"
id="sidebar-backdrop"
type="button"
aria-label="Close navigation"
></button>

<!-- Main Content Structure -->
<main class="main">
<!-- Topbar -->
<header class="topbar">
<div class="topbar-left">
<button class="toggle-btn" id="sidebar-toggle">
<button
class="toggle-btn"
id="sidebar-toggle"
type="button"
aria-label="Toggle navigation"
title="Toggle navigation"
>
<svg
viewBox="0 0 24 24"
width="18"
Expand All @@ -420,11 +433,18 @@
<line x1="9" y1="3" x2="9" y2="21"></line>
</svg>
</button>
<div class="breadcrumb">
<span>GhostAPI</span>
<span>/</span>
<span class="breadcrumb-active">Live Traffic</span>
</div>
</div>
<div class="breadcrumb topbar-center" aria-label="Current view">
<span class="brand-lockup">
<img
class="topbar-brand-icon"
src="/docs/assets/ghostapi-avatar.png"
alt=""
/>
<strong>GhostAPI</strong>
</span>
<span class="breadcrumb-divider">/</span>
<span class="breadcrumb-active">Live Traffic</span>
</div>
<div class="topbar-right">
<div class="status-indicator connected" id="status-indicator">
Expand Down
169 changes: 167 additions & 2 deletions src/dashboard/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ body {
opacity: 0;
border-right: none;
}
.sidebar-backdrop {
display: none;
position: fixed;
inset: 0;
z-index: 30;
border: 0;
background: rgba(0, 0, 0, 0.58);
backdrop-filter: blur(2px);
}

/* Workspace Switcher */
.ws-switcher {
Expand Down Expand Up @@ -292,9 +301,9 @@ body {
.topbar {
height: 56px;
border-bottom: 1px solid var(--border);
display: flex;
display: grid;
grid-template-columns: minmax(44px, 1fr) auto minmax(44px, 1fr);
align-items: center;
justify-content: space-between;
padding: 0 16px;
background: var(--card);
flex-shrink: 0;
Expand All @@ -303,6 +312,8 @@ body {
display: flex;
align-items: center;
gap: 12px;
grid-column: 1;
justify-self: start;
}
.toggle-btn {
padding: 6px;
Expand All @@ -326,6 +337,35 @@ body {
font-size: 14px;
color: var(--muted-foreground);
}
.topbar-center {
position: fixed;
top: 0;
left: 50%;
z-index: 5;
height: 56px;
transform: translateX(-50%);
min-width: 0;
}
.brand-lockup {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--foreground);
}
.brand-lockup strong {
font-size: 14px;
font-weight: 650;
letter-spacing: -0.01em;
}
.topbar-brand-icon {
width: 22px;
height: 22px;
border-radius: 6px;
object-fit: cover;
}
.breadcrumb-divider {
color: rgba(161, 161, 170, 0.42);
}
.breadcrumb-active {
font-weight: 500;
color: var(--foreground);
Expand All @@ -335,6 +375,8 @@ body {
display: flex;
align-items: center;
gap: 12px;
grid-column: 3;
justify-self: end;
}
.status-indicator {
display: flex;
Expand Down Expand Up @@ -820,6 +862,129 @@ body {
}
}

@media (max-width: 760px) {
.sidebar {
position: fixed;
inset: 0 auto 0 0;
z-index: 40;
width: min(84vw, 300px);
transform: translateX(-100%);
opacity: 0;
pointer-events: none;
border-right: 1px solid rgba(255, 255, 255, 0.14);
background: #0d0d10;
box-shadow: 24px 0 64px rgba(0, 0, 0, 0.42);
transition:
transform 0.22s ease,
opacity 0.22s ease;
}
.sidebar.closed {
width: min(84vw, 300px);
padding: 12px;
opacity: 0;
border-right: 1px solid rgba(255, 255, 255, 0.14);
}
.sidebar.mobile-open {
transform: translateX(0);
opacity: 1;
pointer-events: auto;
}
.sidebar-backdrop.open {
display: block;
}
.topbar {
height: 52px;
padding: 0 12px;
grid-template-columns: 36px minmax(0, 1fr) 36px;
}
.topbar-center {
height: 52px;
max-width: 100%;
gap: 6px;
}
.breadcrumb-active {
max-width: 32vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-indicator {
padding: 5px;
border-radius: 50%;
}
#status-text {
display: none;
}
.content-wrapper {
flex-direction: column;
}
.event-list-pane {
width: 100%;
height: 42%;
min-height: 210px;
border-right: none;
border-bottom: 1px solid var(--border);
}
.details-pane {
min-height: 0;
}
.dep-header {
align-items: flex-start;
flex-direction: column;
gap: 12px;
padding: 12px 16px;
}
.dep-title {
width: 100%;
overflow: hidden;
}
.dep-title > span:last-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dep-badges {
width: 100%;
flex-wrap: wrap;
}
.json-split {
flex-direction: column;
overflow: auto;
}
.json-pane {
min-height: 230px;
border-right: none;
border-bottom: 1px solid var(--border);
}
.json-pane:last-child {
border-bottom: none;
}
.rules-modal-header {
flex-wrap: wrap;
}
}

@media (max-width: 420px) {
.brand-lockup strong {
font-size: 13px;
}
.topbar-brand-icon {
width: 19px;
height: 19px;
}
.breadcrumb-divider,
.breadcrumb-active {
font-size: 12px;
}
.event-list-pane {
height: 46%;
min-height: 190px;
}
.event-card {
padding: 10px 12px;
}
}

@media (max-height: 720px) {
.setup-modal-content {
height: calc(100vh - 48px);
Expand Down
20 changes: 20 additions & 0 deletions test/dashboardAssets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe("dashboard assets", () => {
expect(app).toContain("els.eventCount.textContent = list.length");
expect(app).toContain('scenarioButton("Arm"');
expect(app).not.toContain('scenarioButton("Replay"');
expect(app).toContain("closeMobileSidebar");
});

it("does not load dashboard assets from external origins", async () => {
Expand All @@ -34,4 +35,23 @@ describe("dashboard assets", () => {
expect(content).not.toContain("@import url(");
}
});

it("keeps the product identity centered and provides a mobile layout", async () => {
const [html, styles, readme] = await Promise.all([
readFile("src/dashboard/index.html", "utf8"),
readFile("src/dashboard/styles.css", "utf8"),
readFile("README.md", "utf8"),
]);

expect(html).toContain('class="breadcrumb topbar-center"');
expect(html).toContain('id="sidebar-backdrop"');
expect(styles).toContain("grid-template-columns: minmax(44px, 1fr) auto");
expect(styles).toContain("grid-column: 3");
expect(styles).toContain("transform: translateX(-50%)");
expect(styles).toContain("@media (max-width: 760px)");
expect(styles).toContain(".sidebar.mobile-open");
expect(styles).toContain("flex-direction: column");
expect(readme).toContain('<div align="center">');
expect(readme).toContain('<a href="#quickstart">Quickstart</a>');
});
});
Loading