@cloudgrid-io/mcp 0.11.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/corpus/capability-map.md +40 -6
- package/src/corpus/examples/ai-app/index.md +237 -0
- package/src/corpus/examples/api-service/index.md +190 -0
- package/src/corpus/examples/crm/index.md +273 -0
- package/src/corpus/examples/kanban/index.md +256 -0
- package/src/corpus/templates/admin-dashboard/README.md +75 -0
- package/src/corpus/templates/admin-dashboard/cloudgrid.yaml +15 -0
- package/src/corpus/templates/admin-dashboard/index.md +78 -0
- package/src/corpus/templates/admin-dashboard/services/web/app/api/orders/route.js +64 -0
- package/src/corpus/templates/admin-dashboard/services/web/app/api/users/route.js +63 -0
- package/src/corpus/templates/admin-dashboard/services/web/app/layout.js +39 -0
- package/src/corpus/templates/admin-dashboard/services/web/app/page.js +73 -0
- package/src/corpus/templates/admin-dashboard/services/web/app/resource-table.js +94 -0
- package/src/corpus/templates/admin-dashboard/services/web/lib/db.js +32 -0
- package/src/corpus/templates/admin-dashboard/services/web/package.json +16 -0
- package/src/corpus/templates/ai-app/README.md +95 -0
- package/src/corpus/templates/ai-app/cloudgrid.yaml +20 -0
- package/src/corpus/templates/ai-app/index.md +269 -0
- package/src/corpus/templates/ai-app/services/web/app/api/chat/route.js +51 -0
- package/src/corpus/templates/ai-app/services/web/app/chat.js +60 -0
- package/src/corpus/templates/ai-app/services/web/app/layout.js +36 -0
- package/src/corpus/templates/ai-app/services/web/app/page.js +25 -0
- package/src/corpus/templates/ai-app/services/web/lib/db.js +32 -0
- package/src/corpus/templates/ai-app/services/web/package.json +17 -0
- package/src/corpus/templates/api-docs/README.md +28 -0
- package/src/corpus/templates/api-docs/cloudgrid.yaml +10 -0
- package/src/corpus/templates/api-docs/index.html +136 -0
- package/src/corpus/templates/api-docs/index.md +30 -0
- package/src/corpus/templates/api-service/README.md +87 -0
- package/src/corpus/templates/api-service/cloudgrid.yaml +18 -0
- package/src/corpus/templates/api-service/index.md +225 -0
- package/src/corpus/templates/api-service/services/api/package.json +13 -0
- package/src/corpus/templates/api-service/services/api/src/index.js +132 -0
- package/src/corpus/templates/changelog/README.md +27 -0
- package/src/corpus/templates/changelog/cloudgrid.yaml +10 -0
- package/src/corpus/templates/changelog/index.html +80 -0
- package/src/corpus/templates/changelog/index.md +30 -0
- package/src/corpus/templates/crm/README.md +72 -0
- package/src/corpus/templates/crm/cloudgrid.yaml +15 -0
- package/src/corpus/templates/crm/index.md +68 -0
- package/src/corpus/templates/crm/services/web/app/api/contacts/route.js +80 -0
- package/src/corpus/templates/crm/services/web/app/contact-manager.js +102 -0
- package/src/corpus/templates/crm/services/web/app/layout.js +35 -0
- package/src/corpus/templates/crm/services/web/app/page.js +34 -0
- package/src/corpus/templates/crm/services/web/lib/db.js +32 -0
- package/src/corpus/templates/crm/services/web/package.json +16 -0
- package/src/corpus/templates/docs-site/README.md +28 -0
- package/src/corpus/templates/docs-site/cloudgrid.yaml +10 -0
- package/src/corpus/templates/docs-site/index.html +122 -0
- package/src/corpus/templates/docs-site/index.md +30 -0
- package/src/corpus/templates/kanban/README.md +72 -0
- package/src/corpus/templates/kanban/cloudgrid.yaml +15 -0
- package/src/corpus/templates/kanban/index.md +68 -0
- package/src/corpus/templates/kanban/services/web/app/api/cards/route.js +70 -0
- package/src/corpus/templates/kanban/services/web/app/board.js +93 -0
- package/src/corpus/templates/kanban/services/web/app/layout.js +37 -0
- package/src/corpus/templates/kanban/services/web/app/page.js +32 -0
- package/src/corpus/templates/kanban/services/web/lib/db.js +32 -0
- package/src/corpus/templates/kanban/services/web/package.json +16 -0
- package/src/corpus/templates/portfolio/README.md +29 -0
- package/src/corpus/templates/portfolio/cloudgrid.yaml +10 -0
- package/src/corpus/templates/portfolio/index.html +126 -0
- package/src/corpus/templates/portfolio/index.md +30 -0
- package/src/corpus/templates/saas-marketing/README.md +30 -0
- package/src/corpus/templates/saas-marketing/cloudgrid.yaml +10 -0
- package/src/corpus/templates/saas-marketing/index.html +212 -0
- package/src/corpus/templates/saas-marketing/index.md +30 -0
- package/src/corpus/templates/status-page/README.md +28 -0
- package/src/corpus/templates/status-page/cloudgrid.yaml +10 -0
- package/src/corpus/templates/status-page/index.html +117 -0
- package/src/corpus/templates/status-page/index.md +30 -0
- package/src/corpus/templates/task-manager/README.md +72 -0
- package/src/corpus/templates/task-manager/cloudgrid.yaml +15 -0
- package/src/corpus/templates/task-manager/index.md +68 -0
- package/src/corpus/templates/task-manager/services/web/app/api/tasks/route.js +77 -0
- package/src/corpus/templates/task-manager/services/web/app/layout.js +39 -0
- package/src/corpus/templates/task-manager/services/web/app/page.js +33 -0
- package/src/corpus/templates/task-manager/services/web/app/task-list.js +110 -0
- package/src/corpus/templates/task-manager/services/web/lib/db.js +32 -0
- package/src/corpus/templates/task-manager/services/web/package.json +16 -0
- package/src/corpus/templates/waitlist/README.md +38 -0
- package/src/corpus/templates/waitlist/cloudgrid.yaml +10 -0
- package/src/corpus/templates/waitlist/index.html +69 -0
- package/src/corpus/templates/waitlist/index.md +41 -0
- package/src/corpus/workflows/admin-dashboard.md +102 -0
- package/src/corpus/workflows/ai-app.md +136 -0
- package/src/corpus/workflows/api-docs.md +36 -0
- package/src/corpus/workflows/api-service.md +118 -0
- package/src/corpus/workflows/changelog.md +33 -0
- package/src/corpus/workflows/crm.md +101 -0
- package/src/corpus/workflows/docs-site.md +35 -0
- package/src/corpus/workflows/kanban.md +102 -0
- package/src/corpus/workflows/portfolio.md +35 -0
- package/src/corpus/workflows/saas-marketing.md +37 -0
- package/src/corpus/workflows/status-page.md +35 -0
- package/src/corpus/workflows/task-manager.md +101 -0
- package/src/corpus/workflows/waitlist.md +42 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// App Router route handler for the `tasks` collection.
|
|
2
|
+
// GET -> list tasks, POST -> add a task, PATCH -> toggle done, DELETE -> remove
|
|
3
|
+
// a task by id.
|
|
4
|
+
//
|
|
5
|
+
// To adapt this app, rename the collection or change the fields.
|
|
6
|
+
import { NextResponse } from "next/server";
|
|
7
|
+
import { ObjectId } from "mongodb";
|
|
8
|
+
import { getDb } from "../../../lib/db.js";
|
|
9
|
+
|
|
10
|
+
// Never cache — always read/write live data.
|
|
11
|
+
export const dynamic = "force-dynamic";
|
|
12
|
+
|
|
13
|
+
const PRIORITIES = ["low", "medium", "high"];
|
|
14
|
+
|
|
15
|
+
async function tasks() {
|
|
16
|
+
const db = await getDb();
|
|
17
|
+
return db.collection("tasks");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function shape(t) {
|
|
21
|
+
return {
|
|
22
|
+
id: t._id.toString(),
|
|
23
|
+
title: t.title || "",
|
|
24
|
+
done: !!t.done,
|
|
25
|
+
due: t.due || "",
|
|
26
|
+
priority: PRIORITIES.includes(t.priority) ? t.priority : "medium",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// GET /api/tasks — list all tasks, newest first.
|
|
31
|
+
export async function GET() {
|
|
32
|
+
const col = await tasks();
|
|
33
|
+
const items = await col.find({}).sort({ createdAt: -1 }).toArray();
|
|
34
|
+
return NextResponse.json(items.map(shape));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// POST /api/tasks — add a task. Body: { title, due, priority }.
|
|
38
|
+
export async function POST(request) {
|
|
39
|
+
const body = await request.json().catch(() => ({}));
|
|
40
|
+
const title = typeof body.title === "string" ? body.title.trim() : "";
|
|
41
|
+
if (!title) {
|
|
42
|
+
return NextResponse.json({ error: "title is required" }, { status: 400 });
|
|
43
|
+
}
|
|
44
|
+
const doc = {
|
|
45
|
+
title,
|
|
46
|
+
done: false,
|
|
47
|
+
due: typeof body.due === "string" ? body.due.trim() : "",
|
|
48
|
+
priority: PRIORITIES.includes(body.priority) ? body.priority : "medium",
|
|
49
|
+
createdAt: new Date(),
|
|
50
|
+
};
|
|
51
|
+
const col = await tasks();
|
|
52
|
+
const res = await col.insertOne(doc);
|
|
53
|
+
return NextResponse.json({ id: res.insertedId.toString(), ...doc }, { status: 201 });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// PATCH /api/tasks — toggle a task's done state. Body: { id, done }.
|
|
57
|
+
export async function PATCH(request) {
|
|
58
|
+
const body = await request.json().catch(() => ({}));
|
|
59
|
+
const { id, done } = body;
|
|
60
|
+
if (!id || !ObjectId.isValid(id) || typeof done !== "boolean") {
|
|
61
|
+
return NextResponse.json({ error: "valid id and done (boolean) required" }, { status: 400 });
|
|
62
|
+
}
|
|
63
|
+
const col = await tasks();
|
|
64
|
+
await col.updateOne({ _id: new ObjectId(id) }, { $set: { done } });
|
|
65
|
+
return NextResponse.json({ ok: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// DELETE /api/tasks?id=<id> — remove a task.
|
|
69
|
+
export async function DELETE(request) {
|
|
70
|
+
const id = new URL(request.url).searchParams.get("id");
|
|
71
|
+
if (!id || !ObjectId.isValid(id)) {
|
|
72
|
+
return NextResponse.json({ error: "valid id is required" }, { status: 400 });
|
|
73
|
+
}
|
|
74
|
+
const col = await tasks();
|
|
75
|
+
await col.deleteOne({ _id: new ObjectId(id) });
|
|
76
|
+
return NextResponse.json({ ok: true });
|
|
77
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Root layout + plain inline CSS (no external CDNs, fonts, or stylesheets).
|
|
2
|
+
export const metadata = {
|
|
3
|
+
title: "Task manager",
|
|
4
|
+
description: "A persistent task manager on CloudGrid, backed by grid-shared Mongo.",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const css = `
|
|
8
|
+
:root { color-scheme: light dark; }
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
body { margin: 0; font-family: system-ui, -apple-system, sans-serif; line-height: 1.5; }
|
|
11
|
+
main { max-width: 44rem; margin: 3rem auto; padding: 0 1.25rem; }
|
|
12
|
+
h1 { margin: 0 0 .25rem; }
|
|
13
|
+
.hint { margin: 0 0 1.5rem; opacity: .7; font-size: .9rem; }
|
|
14
|
+
.row { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1rem; }
|
|
15
|
+
input, select { padding: .5rem .75rem; border: 1px solid #8886; border-radius: .5rem; font: inherit; }
|
|
16
|
+
input[type="text"] { flex: 1; min-width: 10rem; }
|
|
17
|
+
button { padding: .5rem .9rem; border: 1px solid #8886; border-radius: .5rem; background: #6663; font: inherit; cursor: pointer; }
|
|
18
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
19
|
+
.filters { display: flex; gap: .4rem; margin-bottom: 1rem; }
|
|
20
|
+
.filters button.active { background: #6669; }
|
|
21
|
+
ul { list-style: none; padding: 0; margin: 0; }
|
|
22
|
+
li { display: flex; align-items: center; gap: .6rem; padding: .6rem 0; border-bottom: 1px solid #8883; }
|
|
23
|
+
li.done .title { text-decoration: line-through; opacity: .6; }
|
|
24
|
+
li .title { flex: 1; }
|
|
25
|
+
li .meta { font-size: .8rem; opacity: .7; }
|
|
26
|
+
.pri { font-size: .75rem; padding: .1rem .45rem; border: 1px solid #8886; border-radius: 1rem; }
|
|
27
|
+
.empty { opacity: .6; }
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
export default function RootLayout({ children }) {
|
|
31
|
+
return (
|
|
32
|
+
<html lang="en">
|
|
33
|
+
<head>
|
|
34
|
+
<style dangerouslySetInnerHTML={{ __html: css }} />
|
|
35
|
+
</head>
|
|
36
|
+
<body>{children}</body>
|
|
37
|
+
</html>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Home page: a server component reads the tasks straight from Mongo, and a small
|
|
2
|
+
// client list toggles / adds / removes them via the API route. Data persists
|
|
3
|
+
// across refresh and across users because it lives in the grid-shared Mongo, not
|
|
4
|
+
// in memory.
|
|
5
|
+
import { getDb } from "../lib/db.js";
|
|
6
|
+
import TaskList from "./task-list.js";
|
|
7
|
+
|
|
8
|
+
export const dynamic = "force-dynamic";
|
|
9
|
+
|
|
10
|
+
const PRIORITIES = ["low", "medium", "high"];
|
|
11
|
+
|
|
12
|
+
async function listTasks() {
|
|
13
|
+
const db = await getDb();
|
|
14
|
+
const items = await db.collection("tasks").find({}).sort({ createdAt: -1 }).toArray();
|
|
15
|
+
return items.map((t) => ({
|
|
16
|
+
id: t._id.toString(),
|
|
17
|
+
title: t.title || "",
|
|
18
|
+
done: !!t.done,
|
|
19
|
+
due: t.due || "",
|
|
20
|
+
priority: PRIORITIES.includes(t.priority) ? t.priority : "medium",
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default async function Page() {
|
|
25
|
+
const tasks = await listTasks();
|
|
26
|
+
return (
|
|
27
|
+
<main>
|
|
28
|
+
<h1>Tasks</h1>
|
|
29
|
+
<p className="hint">Persisted in the grid-shared Mongo — survives refresh.</p>
|
|
30
|
+
<TaskList initialTasks={tasks} />
|
|
31
|
+
</main>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Small client component: renders the task list with a complete-toggle + filter
|
|
4
|
+
// and posts to /api/tasks.
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
|
|
7
|
+
const PRIORITIES = ["low", "medium", "high"];
|
|
8
|
+
const FILTERS = ["all", "active", "done"];
|
|
9
|
+
|
|
10
|
+
export default function TaskList({ initialTasks }) {
|
|
11
|
+
const [tasks, setTasks] = useState(initialTasks);
|
|
12
|
+
const [title, setTitle] = useState("");
|
|
13
|
+
const [due, setDue] = useState("");
|
|
14
|
+
const [priority, setPriority] = useState("medium");
|
|
15
|
+
const [filter, setFilter] = useState("all");
|
|
16
|
+
const [busy, setBusy] = useState(false);
|
|
17
|
+
|
|
18
|
+
async function add(e) {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
const value = title.trim();
|
|
21
|
+
if (!value) return;
|
|
22
|
+
setBusy(true);
|
|
23
|
+
const res = await fetch("/api/tasks", {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "content-type": "application/json" },
|
|
26
|
+
body: JSON.stringify({ title: value, due, priority }),
|
|
27
|
+
});
|
|
28
|
+
if (res.ok) {
|
|
29
|
+
const task = await res.json();
|
|
30
|
+
setTasks((prev) => [task, ...prev]);
|
|
31
|
+
setTitle("");
|
|
32
|
+
setDue("");
|
|
33
|
+
setPriority("medium");
|
|
34
|
+
}
|
|
35
|
+
setBusy(false);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function toggle(id, done) {
|
|
39
|
+
setBusy(true);
|
|
40
|
+
const res = await fetch("/api/tasks", {
|
|
41
|
+
method: "PATCH",
|
|
42
|
+
headers: { "content-type": "application/json" },
|
|
43
|
+
body: JSON.stringify({ id, done }),
|
|
44
|
+
});
|
|
45
|
+
if (res.ok) setTasks((prev) => prev.map((t) => (t.id === id ? { ...t, done } : t)));
|
|
46
|
+
setBusy(false);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function remove(id) {
|
|
50
|
+
setBusy(true);
|
|
51
|
+
const res = await fetch(`/api/tasks?id=${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
52
|
+
if (res.ok) setTasks((prev) => prev.filter((t) => t.id !== id));
|
|
53
|
+
setBusy(false);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const shown = tasks.filter((t) =>
|
|
57
|
+
filter === "all" ? true : filter === "done" ? t.done : !t.done,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div>
|
|
62
|
+
<form onSubmit={add} className="row">
|
|
63
|
+
<input
|
|
64
|
+
type="text"
|
|
65
|
+
value={title}
|
|
66
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
67
|
+
placeholder="Add a task…"
|
|
68
|
+
aria-label="New task"
|
|
69
|
+
/>
|
|
70
|
+
<input type="date" value={due} onChange={(e) => setDue(e.target.value)} aria-label="Due date" />
|
|
71
|
+
<select value={priority} onChange={(e) => setPriority(e.target.value)} aria-label="Priority">
|
|
72
|
+
{PRIORITIES.map((p) => (
|
|
73
|
+
<option key={p} value={p}>{p}</option>
|
|
74
|
+
))}
|
|
75
|
+
</select>
|
|
76
|
+
<button type="submit" disabled={busy}>Add</button>
|
|
77
|
+
</form>
|
|
78
|
+
<div className="filters">
|
|
79
|
+
{FILTERS.map((f) => (
|
|
80
|
+
<button
|
|
81
|
+
key={f}
|
|
82
|
+
type="button"
|
|
83
|
+
className={filter === f ? "active" : ""}
|
|
84
|
+
onClick={() => setFilter(f)}
|
|
85
|
+
>
|
|
86
|
+
{f}
|
|
87
|
+
</button>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
<ul>
|
|
91
|
+
{shown.length === 0 && <li className="empty">No tasks.</li>}
|
|
92
|
+
{shown.map((t) => (
|
|
93
|
+
<li key={t.id} className={t.done ? "done" : ""}>
|
|
94
|
+
<input
|
|
95
|
+
type="checkbox"
|
|
96
|
+
checked={t.done}
|
|
97
|
+
onChange={(e) => toggle(t.id, e.target.checked)}
|
|
98
|
+
disabled={busy}
|
|
99
|
+
aria-label="Done"
|
|
100
|
+
/>
|
|
101
|
+
<span className="title">{t.title}</span>
|
|
102
|
+
<span className="pri">{t.priority}</span>
|
|
103
|
+
{t.due && <span className="meta">{t.due}</span>}
|
|
104
|
+
<button type="button" onClick={() => remove(t.id)} disabled={busy}>Delete</button>
|
|
105
|
+
</li>
|
|
106
|
+
))}
|
|
107
|
+
</ul>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Cached MongoDB client for the App Router.
|
|
2
|
+
//
|
|
3
|
+
// The grid injects the connection string as the DATABASE_MONGODB_URL environment
|
|
4
|
+
// variable (plus the legacy MONGODB_URL alias) at dev-time (`grid dev`) and
|
|
5
|
+
// runtime (after `grid plug`). Read the canonical var first, fall back to the
|
|
6
|
+
// legacy alias. Never hardcode a connection string here and never commit a secret.
|
|
7
|
+
//
|
|
8
|
+
// The env var and client are resolved LAZILY (inside getDb), never at module top
|
|
9
|
+
// level — otherwise `next build` throws when it imports this module for route
|
|
10
|
+
// analysis, before the grid injects the var.
|
|
11
|
+
import { MongoClient } from "mongodb";
|
|
12
|
+
|
|
13
|
+
function clientPromise() {
|
|
14
|
+
const uri = process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL;
|
|
15
|
+
if (!uri) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"DATABASE_MONGODB_URL is not set. The grid injects it automatically — run " +
|
|
18
|
+
"this app with `grid dev` locally, or deploy it with `grid plug` (the grid " +
|
|
19
|
+
"injects the DB connection string at runtime). Do not set it by hand.",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
if (!globalThis.__mongoClientPromise) {
|
|
23
|
+
globalThis.__mongoClientPromise = new MongoClient(uri).connect();
|
|
24
|
+
}
|
|
25
|
+
return globalThis.__mongoClientPromise;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getDb() {
|
|
29
|
+
const client = await clientPromise();
|
|
30
|
+
// The default DB comes from the connection-string path segment the grid injects.
|
|
31
|
+
return client.db();
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-tasks",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"next": "^15.1.0",
|
|
12
|
+
"react": "^19.0.0",
|
|
13
|
+
"react-dom": "^19.0.0",
|
|
14
|
+
"mongodb": "^6.12.0"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# waitlist
|
|
2
|
+
|
|
3
|
+
A single self-contained HTML launch waitlist / coming-soon page: a hero, an email capture form, and a social-proof counter.
|
|
4
|
+
|
|
5
|
+
Static inspiration — a single self-contained `index.html` (inline CSS and JS,
|
|
6
|
+
no external CDNs). It deploys instantly via `gridctl_drop` and works on every
|
|
7
|
+
CloudGrid edition, including hosted.
|
|
8
|
+
|
|
9
|
+
## Fill it in
|
|
10
|
+
|
|
11
|
+
Fetch the template with `gridctl_fetch("template", "waitlist")`, then replace the
|
|
12
|
+
placeholders:
|
|
13
|
+
|
|
14
|
+
- The product name, coming-soon headline, and subheadline.
|
|
15
|
+
- The signup counter and any social-proof line.
|
|
16
|
+
- The form action: set the `<form>` `action` to a `mailto:` address or an external form endpoint (for example a hosted form provider). The default is a `mailto:` that opens the visitor's mail client.
|
|
17
|
+
|
|
18
|
+
Keep it a single self-contained HTML file. Do not add external scripts,
|
|
19
|
+
stylesheets, fonts, or large embedded media.
|
|
20
|
+
|
|
21
|
+
## Deploy
|
|
22
|
+
|
|
23
|
+
- Hosted MCP edition: call the drop tool with the HTML.
|
|
24
|
+
- Local MCP / CLI edition: write the HTML to a file and run `gridctl plug`.
|
|
25
|
+
|
|
26
|
+
It deploys as an inspiration and returns a live share URL right away. Re-deploy
|
|
27
|
+
with the same entity id to update the same URL.
|
|
28
|
+
|
|
29
|
+
## Note on storing signups
|
|
30
|
+
|
|
31
|
+
The signup form here is **static**. It posts to a configurable action — a
|
|
32
|
+
`mailto:` (the default) or an external form endpoint you set on the `<form>`.
|
|
33
|
+
It does NOT store submissions anywhere on CloudGrid.
|
|
34
|
+
|
|
35
|
+
To actually **store** waitlist signups (persist them, count them, export them),
|
|
36
|
+
you need a runtime app with a database — `needs: { database: true }`. That is a
|
|
37
|
+
CRUD app, coming in the crud-app archetype (Wave 1b). Do not pretend this static
|
|
38
|
+
page persists data.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# static — deploys as an inspiration (instant, works on hosted) via gridctl_drop;
|
|
2
|
+
# this yaml is only for plugging it as an OWNED static runtime (async, local
|
|
3
|
+
# edition only). To publish the page, prefer gridctl_drop, not grid plug.
|
|
4
|
+
#
|
|
5
|
+
# needs: {} # none — static, no infrastructure
|
|
6
|
+
name: waitlist
|
|
7
|
+
services:
|
|
8
|
+
web:
|
|
9
|
+
type: static
|
|
10
|
+
path: /
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>{{PRODUCT_NAME}} — {{TAGLINE}}</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0b0f17; --panel: #121826; --fg: #e8edf5; --muted: #9aa7bd;
|
|
10
|
+
--accent: #5b8cff; --accent-2: #7c6bff; --border: #223049; --ok: #34d399;
|
|
11
|
+
}
|
|
12
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
13
|
+
body {
|
|
14
|
+
background:
|
|
15
|
+
radial-gradient(1200px 600px at 50% -10%, rgba(91,140,255,0.16), transparent 60%),
|
|
16
|
+
var(--bg);
|
|
17
|
+
color: var(--fg); min-height: 100vh;
|
|
18
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
19
|
+
-webkit-font-smoothing: antialiased; line-height: 1.55;
|
|
20
|
+
display: grid; place-items: center; padding: 1.5rem;
|
|
21
|
+
}
|
|
22
|
+
.card { width: 100%; max-width: 560px; text-align: center; display: grid; gap: 1.5rem; }
|
|
23
|
+
.eyebrow { color: var(--accent); font-weight: 600; font-size: 0.9rem; letter-spacing: 0.02em; }
|
|
24
|
+
h1 { font-size: clamp(2.4rem, 8vw, 3.6rem); letter-spacing: -0.02em; line-height: 1.05; }
|
|
25
|
+
.sub { color: var(--muted); font-size: clamp(1.05rem, 3vw, 1.3rem); max-width: 44ch; margin: 0 auto; }
|
|
26
|
+
|
|
27
|
+
/* Static form: posts to the action below (mailto by default, or an external
|
|
28
|
+
form endpoint). It does NOT store signups on CloudGrid. See README. */
|
|
29
|
+
form { display: flex; gap: 0.6rem; max-width: 440px; margin: 0.5rem auto 0; flex-wrap: wrap; }
|
|
30
|
+
input[type="email"] {
|
|
31
|
+
flex: 1 1 220px; padding: 0.85rem 1rem; border-radius: 10px; border: 1px solid var(--border);
|
|
32
|
+
background: var(--panel); color: var(--fg); font-size: 1rem;
|
|
33
|
+
}
|
|
34
|
+
input[type="email"]::placeholder { color: var(--muted); }
|
|
35
|
+
button {
|
|
36
|
+
padding: 0.85rem 1.5rem; border-radius: 10px; border: none; cursor: pointer;
|
|
37
|
+
background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: #fff;
|
|
38
|
+
font-weight: 600; font-size: 1rem;
|
|
39
|
+
}
|
|
40
|
+
button:hover { filter: brightness(1.08); }
|
|
41
|
+
|
|
42
|
+
.proof { display: flex; align-items: center; justify-content: center; gap: 0.5rem; color: var(--muted); font-size: 0.92rem; }
|
|
43
|
+
.proof .count { color: var(--fg); font-weight: 700; }
|
|
44
|
+
.proof .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); }
|
|
45
|
+
footer { color: var(--muted); font-size: 0.85rem; margin-top: 1rem; }
|
|
46
|
+
</style>
|
|
47
|
+
</head>
|
|
48
|
+
<body>
|
|
49
|
+
<main class="card">
|
|
50
|
+
<span class="eyebrow">{{HERO_EYEBROW}}</span>
|
|
51
|
+
<h1>{{HEADLINE}}</h1>
|
|
52
|
+
<p class="sub">{{SUBHEAD}}</p>
|
|
53
|
+
|
|
54
|
+
<!-- Static form. Set `action` to a mailto: address or an external form
|
|
55
|
+
endpoint (for example a hosted form provider). Default = mailto. -->
|
|
56
|
+
<form action="mailto:{{SIGNUP_EMAIL}}" method="post" enctype="text/plain">
|
|
57
|
+
<input type="email" name="email" placeholder="you@example.com" required aria-label="Email address" />
|
|
58
|
+
<button type="submit">{{CTA_LABEL}}</button>
|
|
59
|
+
</form>
|
|
60
|
+
|
|
61
|
+
<div class="proof">
|
|
62
|
+
<span class="dot"></span>
|
|
63
|
+
<span><span class="count">{{SIGNUP_COUNT}}</span> {{SOCIAL_PROOF_TEXT}}</span>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<footer>{{FOOTER_TEXT}}</footer>
|
|
67
|
+
</main>
|
|
68
|
+
</body>
|
|
69
|
+
</html>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Template: waitlist (static)
|
|
2
|
+
|
|
3
|
+
A single self-contained HTML launch waitlist / coming-soon page: a hero, an email capture form, and a social-proof counter. **No infrastructure** — no database, no backend, no `needs:`.
|
|
4
|
+
State (if any) lives only in memory and is lost on refresh.
|
|
5
|
+
|
|
6
|
+
## How it deploys
|
|
7
|
+
|
|
8
|
+
To publish this page, use **`gridctl_drop`** (or `gridctl_plug` with the inline
|
|
9
|
+
HTML): it deploys as an **inspiration** — instant, and works on the **hosted**
|
|
10
|
+
edition (Claude Web) as well as local. The reference `cloudgrid.yaml` in this
|
|
11
|
+
directory (`type: static`) is ONLY for plugging the page as an OWNED static
|
|
12
|
+
runtime (async, local edition only); the fast path is `gridctl_drop`.
|
|
13
|
+
|
|
14
|
+
The fillable HTML is the real template — fetch it with
|
|
15
|
+
`gridctl_fetch("template", "waitlist")` and fill in the user's content.
|
|
16
|
+
|
|
17
|
+
## Note on storing signups
|
|
18
|
+
|
|
19
|
+
The signup form here is **static**. It posts to a configurable action — a
|
|
20
|
+
`mailto:` (the default) or an external form endpoint you set on the `<form>`.
|
|
21
|
+
It does NOT store submissions anywhere on CloudGrid.
|
|
22
|
+
|
|
23
|
+
To actually **store** waitlist signups (persist them, count them, export them),
|
|
24
|
+
you need a runtime app with a database — `needs: { database: true }`. That is a
|
|
25
|
+
CRUD app, coming in the crud-app archetype (Wave 1b). Do not pretend this static
|
|
26
|
+
page persists data.
|
|
27
|
+
|
|
28
|
+
## cloudgrid.yaml (reference only)
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
# static — deploys as an inspiration (instant, works on hosted) via gridctl_drop;
|
|
32
|
+
# this yaml is only for plugging it as an OWNED static runtime (async, local
|
|
33
|
+
# edition only). To publish the page, prefer gridctl_drop, not grid plug.
|
|
34
|
+
#
|
|
35
|
+
# needs: {} # none — static, no infrastructure
|
|
36
|
+
name: waitlist
|
|
37
|
+
services:
|
|
38
|
+
web:
|
|
39
|
+
type: static
|
|
40
|
+
path: /
|
|
41
|
+
```
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: admin-dashboard
|
|
3
|
+
when: admin dashboard, admin panel, back-office, CRUD admin, manage records — a persistent admin over one or two Mongo collections with a metrics header + management table. Needs a database → runtime → local edition.
|
|
4
|
+
needs: database
|
|
5
|
+
deploy: runtime
|
|
6
|
+
editions: local
|
|
7
|
+
capabilities_note: persistent — needs a database (Mongo). Runtime app, async build, local edition only. Declare the canonical `needs: { database: true }`; the deployer injects DATABASE_MONGODB_URL (+legacy MONGODB_URL).
|
|
8
|
+
summary: Build a persistent Next.js + Mongo admin panel on the grid — a couple of resource collections (e.g. users, orders) with a metrics header (counts + revenue) and a management table (add/delete). Edition-gate first, scaffold, put the app under services/web/, wire process.env.DATABASE_MONGODB_URL (legacy MONGODB_URL fallback) lazily, declare needs:{database:true} (not requires:), deploy async, poll to a live URL.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Workflow: admin-dashboard
|
|
12
|
+
|
|
13
|
+
The user wants an admin panel / back-office to **manage records** — a couple of
|
|
14
|
+
resources (e.g. users and orders) with a metrics header (counts, revenue) and a
|
|
15
|
+
management table (add / delete), all persisted across refresh and shared across
|
|
16
|
+
sessions. That is a **runtime app** backed by the grid's shared Mongo, not a
|
|
17
|
+
static dashboard page. It is the same proven shape as `app-with-data`, generalised
|
|
18
|
+
over one or two Mongo collections. (For a display-only metrics page with baked-in
|
|
19
|
+
data, use the static `dashboard` template instead.)
|
|
20
|
+
|
|
21
|
+
Follow this recipe. Be honest that a runtime deploy is async (not instant like a
|
|
22
|
+
static drop) and that it needs the local edition.
|
|
23
|
+
|
|
24
|
+
## 1. Edition check FIRST (hard gate)
|
|
25
|
+
|
|
26
|
+
A persistent app is a built + deployed container. It requires the **local
|
|
27
|
+
edition** (Claude Desktop / Claude Code) or the CLI.
|
|
28
|
+
|
|
29
|
+
- **Hosted edition (Claude Web / hosted MCP):** you CANNOT build a runtime app —
|
|
30
|
+
hosted is inline-only and can only publish static pages. Tell the user
|
|
31
|
+
plainly, offer a **static version** instead, and STOP the runtime path here.
|
|
32
|
+
- **Local edition:** continue.
|
|
33
|
+
|
|
34
|
+
## 2. Auth + grid
|
|
35
|
+
|
|
36
|
+
1. Ensure signed in: `gridctl_login_status`; if not, `gridctl_login`.
|
|
37
|
+
2. A grid is required. Respect the grid picker: if the user has more than one
|
|
38
|
+
grid, ask which to use; do not assume a target.
|
|
39
|
+
|
|
40
|
+
## 3. Scaffold
|
|
41
|
+
|
|
42
|
+
`gridctl_init` an app `<name>`. `init` creates the entity + `.cloudgrid/link.json`
|
|
43
|
+
and writes a `cloudgrid.yaml` with an EMPTY `services: {}`. `plug` needs a linked
|
|
44
|
+
directory, so run `init` FIRST. Then (a) write the app under **`services/web/`**,
|
|
45
|
+
and (b) fill `cloudgrid.yaml` to the shape below (`services.web` + `needs:
|
|
46
|
+
{ database: true }`).
|
|
47
|
+
|
|
48
|
+
## 4. Add the datastore + wire Mongo
|
|
49
|
+
|
|
50
|
+
1. Set `cloudgrid.yaml`. **App code MUST live under `services/<name>/`** —
|
|
51
|
+
`path:` is the URL mount, NOT the filesystem path.
|
|
52
|
+
```yaml
|
|
53
|
+
name: my-admin
|
|
54
|
+
services:
|
|
55
|
+
web:
|
|
56
|
+
type: nextjs
|
|
57
|
+
path: /
|
|
58
|
+
needs:
|
|
59
|
+
database: true
|
|
60
|
+
```
|
|
61
|
+
**Declare the datastore with `needs: { database: true }`** — the canonical
|
|
62
|
+
shape. The deployer provisions Mongo and injects `DATABASE_MONGODB_URL` (plus
|
|
63
|
+
the legacy `MONGODB_URL` alias). `requires:` is the deprecated v1 alias; don't
|
|
64
|
+
author new yaml with it, and never set `needs:` and `requires:` together (the
|
|
65
|
+
validator rejects it).
|
|
66
|
+
2. Fetch the template for the Mongo wiring + CRUD shape:
|
|
67
|
+
`gridctl_fetch("template", "admin-dashboard")`. It is a minimal, real Next.js
|
|
68
|
+
+ `mongodb`-driver admin panel under `services/web/`: a lazy client in
|
|
69
|
+
`services/web/lib/db.js`, two App-Router GET/POST/DELETE routes (`users` and
|
|
70
|
+
`orders`), a server-component page that computes a metrics header, and a
|
|
71
|
+
reusable client management table.
|
|
72
|
+
3. Adapt it (rename or replace the resource collections, change fields, adjust
|
|
73
|
+
the metrics). **Read the DB from `process.env.DATABASE_MONGODB_URL`** (legacy
|
|
74
|
+
`process.env.MONGODB_URL` fallback) — the grid injects it at dev-time and
|
|
75
|
+
runtime.
|
|
76
|
+
- **Put the DB connection behind a lazy getter — never read the connection
|
|
77
|
+
string at module top level, or `next build` fails.**
|
|
78
|
+
|
|
79
|
+
## 5. (Optional) Run locally
|
|
80
|
+
|
|
81
|
+
Mention that the user can `grid dev` to run locally against the injected Mongo
|
|
82
|
+
before deploying. Don't require it.
|
|
83
|
+
|
|
84
|
+
## 6. Config
|
|
85
|
+
|
|
86
|
+
- API keys / secrets → `gridctl_secrets`. Non-secret config → `gridctl_env`.
|
|
87
|
+
- Do **NOT** set the DB connection vars yourself (`DATABASE_MONGODB_URL` or the
|
|
88
|
+
legacy `MONGODB_URL`) — the grid injects them.
|
|
89
|
+
|
|
90
|
+
## 7. Deploy (async)
|
|
91
|
+
|
|
92
|
+
Deploy the folder with `gridctl_plug`. A **runtime deploy is ASYNC**: the first
|
|
93
|
+
response is `status: "building"`, NOT a live URL yet.
|
|
94
|
+
- Poll `gridctl_status` (or the returned poll_url) until the entity is live.
|
|
95
|
+
- Surface a liveness signal while it builds — never a bare silent wait.
|
|
96
|
+
- Only once it is live, return the deployed app URL (NOT the build/log link).
|
|
97
|
+
|
|
98
|
+
## 8. Return the live URL + iterate
|
|
99
|
+
|
|
100
|
+
Give the user the live app URL — that is the deliverable. To iterate, re-plug the
|
|
101
|
+
SAME entity so it updates the same URL. Keep it honest: async build, local-edition
|
|
102
|
+
only, credentials injected by the grid.
|