@cloudgrid-io/mcp 0.12.0 → 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 +1 -1
- package/src/corpus/capability-map.md +21 -2
- 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/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/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/api-docs.md +36 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "MCP server for CloudGrid. Three editions: a local stdio server (full toolset), a hosted web server (light, CLI-free toolset), and a public docs server (search_cloudgrid_documentation) over MCP Streamable HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,16 +13,27 @@ cloudgrid.yaml schema (every field, the `needs:` injection table, the
|
|
|
13
13
|
requires-vs-needs caveat, validation rules), fetch the companion reference:
|
|
14
14
|
`gridctl_fetch("doc", "cloudgrid-yaml")`.
|
|
15
15
|
|
|
16
|
-
## The
|
|
16
|
+
## The 19 templates
|
|
17
17
|
|
|
18
18
|
| Intent (match on `when:`) | Template | `needs:` | Deploy | Edition |
|
|
19
19
|
|---|---|---|---|---|
|
|
20
|
-
| landing page, marketing/product/hero page, coming-soon,
|
|
20
|
+
| landing page, marketing/product/hero page, coming-soon, link-in-bio, event page — a single-section page | `landing-page` | none | inspiration (instant) | all |
|
|
21
|
+
| SaaS marketing site, product marketing page, features + pricing page — multi-section with pricing tiers, testimonials, FAQ | `saas-marketing` | none | inspiration (instant) | all |
|
|
22
|
+
| documentation site, docs, developer docs, guide, manual, knowledge base — prose docs with sidebar + search | `docs-site` | none | inspiration (instant) | all |
|
|
23
|
+
| API documentation, API reference, endpoint docs, REST API docs — method badges, params tables, request/response examples | `api-docs` | none | inspiration (instant) | all |
|
|
24
|
+
| status page, service status, uptime page, incident history, is-it-down page — display-only, status baked in | `status-page` | none | inspiration (instant) | all |
|
|
25
|
+
| changelog, release notes, what's new, product updates, version history | `changelog` | none | inspiration (instant) | all |
|
|
26
|
+
| portfolio, personal site, freelancer site, my work, showcase — projects grid + about + skills + contact | `portfolio` | none | inspiration (instant) | all |
|
|
27
|
+
| waitlist, coming soon, early access, join the list, launch page — email capture form (form is static; storing signups needs a runtime crud-app) | `waitlist` | none | inspiration (instant) | all |
|
|
21
28
|
| calculator, converter, generator, timer, quiz, interactive tool, mini-app, widget — computed client-side, no saved data | `web-app` | none | inspiration (instant) | all |
|
|
22
29
|
| dashboard, metrics, KPIs, stats page, status board, charts, analytics view — display-only, static data baked in | `dashboard` | none | inspiration (instant) | all |
|
|
23
30
|
| report, one-pager, summary, brief, whitepaper, case study, formatted document | `report` | none | inspiration (instant) | all |
|
|
24
31
|
| slides, deck, pitch, presentation, slideshow | `presentation` (dir: `deck`) | none | inspiration (instant) | all |
|
|
25
32
|
| to-do, task list, notes app, guestbook, CRUD app, a form that SAVES/STORES submissions, anything that PERSISTS data or shares state across users/sessions, sign-in/accounts | `app-with-data` | `database: true` | runtime (async, poll) | local |
|
|
33
|
+
| CRM, contacts, leads, sales pipeline, customer manager, deal tracker | `crm` | `database: true` | runtime (async, poll) | local |
|
|
34
|
+
| kanban board, task board, trello-style board, workflow columns | `kanban` | `database: true` | runtime (async, poll) | local |
|
|
35
|
+
| task manager, to-do app (richer than the app-with-data starter), task list with due dates/priorities | `task-manager` | `database: true` | runtime (async, poll) | local |
|
|
36
|
+
| admin dashboard, admin panel, back-office, CRUD admin, manage records | `admin-dashboard` | `database: true` | runtime (async, poll) | local |
|
|
26
37
|
| REST API, backend for X, API endpoint(s), CRUD API, JSON API, webhook receiver — a backend/service that stores data and isn't a full web UI | `api-service` | `database: true` | runtime (async, poll) | local |
|
|
27
38
|
| chatbot, AI assistant, Q&A bot, conversational app, support bot, ask-me-anything, an app that talks to an LLM / generates text with AI | `ai-app` | `ai: true, database: true` | runtime (async, poll) | local |
|
|
28
39
|
|
|
@@ -32,6 +43,14 @@ edition. Pick by shape: a full web UI → `app-with-data`; a plain backend/JSON
|
|
|
32
43
|
(no UI) → `api-service`; an app that talks to an LLM → `ai-app` (adds `ai:`).
|
|
33
44
|
Otherwise a static template deploys instantly anywhere.
|
|
34
45
|
|
|
46
|
+
**The DB-CRUD family.** `crm`, `kanban`, `task-manager`, and `admin-dashboard`
|
|
47
|
+
all share the exact `app-with-data` shape (nextjs + `needs: { database: true }`,
|
|
48
|
+
app under `services/web/`, lazy Mongo getter, force-dynamic App-Router routes) —
|
|
49
|
+
they differ only by domain schema + UI. Match the request to the closest one by
|
|
50
|
+
its `when:`; if it is a bare to-do or generic "save this data" with no specific
|
|
51
|
+
domain, use `app-with-data` (the minimal reference). Deploy path is identical for
|
|
52
|
+
all five.
|
|
53
|
+
|
|
35
54
|
## Held / pending platform (not yet buildable)
|
|
36
55
|
|
|
37
56
|
Two more archetypes are designed but blocked on platform work — the LLM should
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Example: crm — "Sales Pipeline CRM"
|
|
2
|
+
|
|
3
|
+
A filled reference imitating the `crm` template: a persistent sales-pipeline CRM.
|
|
4
|
+
Same stack (Next.js App Router + the `mongodb` driver, grid-shared Mongo via
|
|
5
|
+
`process.env.DATABASE_MONGODB_URL`). Contacts have a `name`, `email`, `company`,
|
|
6
|
+
a `stage` (lead / qualified / customer) and a `note`; the table lets you add a
|
|
7
|
+
contact, change its stage inline, and delete it.
|
|
8
|
+
|
|
9
|
+
Data persists in the grid-shared Mongo, so the whole team sees the same pipeline
|
|
10
|
+
across sessions and refresh.
|
|
11
|
+
|
|
12
|
+
**Same three proven rules as the template:** (1) app code lives under
|
|
13
|
+
**`services/web/`** (the service name, NOT the repo root — `path:` is the URL
|
|
14
|
+
mount); (2) the DB connection string is read **lazily inside the `getDb`
|
|
15
|
+
getter**, never at module top level (a top-level read fails `next build`); (3)
|
|
16
|
+
the datastore is declared with the canonical **`needs: { database: true }`** —
|
|
17
|
+
the deployer provisions Mongo and injects `DATABASE_MONGODB_URL` (plus the legacy
|
|
18
|
+
`MONGODB_URL` alias). Don't use the deprecated `requires:` alias.
|
|
19
|
+
|
|
20
|
+
## cloudgrid.yaml
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
# `needs: { database: true }` is canonical: the deployer provisions Mongo and
|
|
24
|
+
# injects DATABASE_MONGODB_URL (plus the legacy MONGODB_URL alias). `requires:`
|
|
25
|
+
# is the deprecated v1 alias — don't mix the two (the validator rejects it).
|
|
26
|
+
name: sales-pipeline-crm
|
|
27
|
+
services:
|
|
28
|
+
web:
|
|
29
|
+
type: nextjs
|
|
30
|
+
path: /
|
|
31
|
+
needs:
|
|
32
|
+
database: true
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## services/web/package.json
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"name": "sales-pipeline-crm",
|
|
40
|
+
"version": "0.1.0",
|
|
41
|
+
"private": true,
|
|
42
|
+
"scripts": { "dev": "next dev", "build": "next build", "start": "next start" },
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"next": "^15.1.0",
|
|
45
|
+
"react": "^19.0.0",
|
|
46
|
+
"react-dom": "^19.0.0",
|
|
47
|
+
"mongodb": "^6.12.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## services/web/lib/db.js
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
// Cached Mongo client. The grid injects DATABASE_MONGODB_URL (plus the legacy
|
|
56
|
+
// MONGODB_URL alias) at dev + runtime. Read the canonical var first, fall back to
|
|
57
|
+
// the legacy alias. Resolve it LAZILY inside the getter — a top-level read fails
|
|
58
|
+
// `next build`.
|
|
59
|
+
import { MongoClient } from "mongodb";
|
|
60
|
+
|
|
61
|
+
function clientPromise() {
|
|
62
|
+
const uri = process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL;
|
|
63
|
+
if (!uri) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"DATABASE_MONGODB_URL is not set. The grid injects it — run `grid dev` " +
|
|
66
|
+
"locally or deploy with `grid plug`. Do not set it by hand.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (!globalThis.__mongoClientPromise) {
|
|
70
|
+
globalThis.__mongoClientPromise = new MongoClient(uri).connect();
|
|
71
|
+
}
|
|
72
|
+
return globalThis.__mongoClientPromise;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function getDb() {
|
|
76
|
+
return (await clientPromise()).db();
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## services/web/app/api/contacts/route.js
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
import { NextResponse } from "next/server";
|
|
84
|
+
import { ObjectId } from "mongodb";
|
|
85
|
+
import { getDb } from "../../../lib/db.js";
|
|
86
|
+
|
|
87
|
+
export const dynamic = "force-dynamic";
|
|
88
|
+
|
|
89
|
+
const STAGES = ["lead", "qualified", "customer"];
|
|
90
|
+
|
|
91
|
+
async function contacts() {
|
|
92
|
+
return (await getDb()).collection("contacts");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function shape(c) {
|
|
96
|
+
return {
|
|
97
|
+
id: c._id.toString(),
|
|
98
|
+
name: c.name || "",
|
|
99
|
+
email: c.email || "",
|
|
100
|
+
company: c.company || "",
|
|
101
|
+
stage: STAGES.includes(c.stage) ? c.stage : "lead",
|
|
102
|
+
note: c.note || "",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// GET /api/contacts — list the pipeline.
|
|
107
|
+
export async function GET() {
|
|
108
|
+
const col = await contacts();
|
|
109
|
+
const items = await col.find({}).sort({ createdAt: -1 }).toArray();
|
|
110
|
+
return NextResponse.json(items.map(shape));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// POST /api/contacts — add a contact. Body: { name, email, company, note }.
|
|
114
|
+
export async function POST(request) {
|
|
115
|
+
const body = await request.json().catch(() => ({}));
|
|
116
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
117
|
+
if (!name) return NextResponse.json({ error: "name is required" }, { status: 400 });
|
|
118
|
+
const doc = {
|
|
119
|
+
name,
|
|
120
|
+
email: typeof body.email === "string" ? body.email.trim() : "",
|
|
121
|
+
company: typeof body.company === "string" ? body.company.trim() : "",
|
|
122
|
+
stage: "lead",
|
|
123
|
+
note: typeof body.note === "string" ? body.note.trim() : "",
|
|
124
|
+
createdAt: new Date(),
|
|
125
|
+
};
|
|
126
|
+
const col = await contacts();
|
|
127
|
+
const res = await col.insertOne(doc);
|
|
128
|
+
return NextResponse.json({ id: res.insertedId.toString(), ...doc }, { status: 201 });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// PATCH /api/contacts — move a contact along the pipeline. Body: { id, stage }.
|
|
132
|
+
export async function PATCH(request) {
|
|
133
|
+
const body = await request.json().catch(() => ({}));
|
|
134
|
+
const { id, stage } = body;
|
|
135
|
+
if (!id || !ObjectId.isValid(id) || !STAGES.includes(stage)) {
|
|
136
|
+
return NextResponse.json({ error: "valid id and stage required" }, { status: 400 });
|
|
137
|
+
}
|
|
138
|
+
const col = await contacts();
|
|
139
|
+
await col.updateOne({ _id: new ObjectId(id) }, { $set: { stage } });
|
|
140
|
+
return NextResponse.json({ ok: true });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// DELETE /api/contacts?id=<id>
|
|
144
|
+
export async function DELETE(request) {
|
|
145
|
+
const id = new URL(request.url).searchParams.get("id");
|
|
146
|
+
if (!id || !ObjectId.isValid(id)) {
|
|
147
|
+
return NextResponse.json({ error: "valid id is required" }, { status: 400 });
|
|
148
|
+
}
|
|
149
|
+
const col = await contacts();
|
|
150
|
+
await col.deleteOne({ _id: new ObjectId(id) });
|
|
151
|
+
return NextResponse.json({ ok: true });
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## services/web/app/page.js
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
import { getDb } from "../lib/db.js";
|
|
159
|
+
import ContactManager from "./contact-manager.js";
|
|
160
|
+
|
|
161
|
+
export const dynamic = "force-dynamic";
|
|
162
|
+
|
|
163
|
+
const STAGES = ["lead", "qualified", "customer"];
|
|
164
|
+
|
|
165
|
+
async function listContacts() {
|
|
166
|
+
const items = await (await getDb())
|
|
167
|
+
.collection("contacts")
|
|
168
|
+
.find({})
|
|
169
|
+
.sort({ createdAt: -1 })
|
|
170
|
+
.toArray();
|
|
171
|
+
return items.map((c) => ({
|
|
172
|
+
id: c._id.toString(),
|
|
173
|
+
name: c.name || "",
|
|
174
|
+
email: c.email || "",
|
|
175
|
+
company: c.company || "",
|
|
176
|
+
stage: STAGES.includes(c.stage) ? c.stage : "lead",
|
|
177
|
+
note: c.note || "",
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export default async function Page() {
|
|
182
|
+
const contacts = await listContacts();
|
|
183
|
+
return (
|
|
184
|
+
<main>
|
|
185
|
+
<h1>Sales Pipeline</h1>
|
|
186
|
+
<p className="hint">Shared across the team — persisted in grid Mongo.</p>
|
|
187
|
+
<ContactManager initialContacts={contacts} />
|
|
188
|
+
</main>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## services/web/app/contact-manager.js
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
"use client";
|
|
197
|
+
import { useState } from "react";
|
|
198
|
+
|
|
199
|
+
const STAGES = ["lead", "qualified", "customer"];
|
|
200
|
+
const LABELS = { lead: "Lead", qualified: "Qualified", customer: "Customer" };
|
|
201
|
+
|
|
202
|
+
export default function ContactManager({ initialContacts }) {
|
|
203
|
+
const [contacts, setContacts] = useState(initialContacts);
|
|
204
|
+
const [name, setName] = useState("");
|
|
205
|
+
const [email, setEmail] = useState("");
|
|
206
|
+
const [company, setCompany] = useState("");
|
|
207
|
+
|
|
208
|
+
async function add(e) {
|
|
209
|
+
e.preventDefault();
|
|
210
|
+
if (!name.trim()) return;
|
|
211
|
+
const res = await fetch("/api/contacts", {
|
|
212
|
+
method: "POST",
|
|
213
|
+
headers: { "content-type": "application/json" },
|
|
214
|
+
body: JSON.stringify({ name, email, company }),
|
|
215
|
+
});
|
|
216
|
+
if (res.ok) { setContacts((p) => [await res.json(), ...p]); setName(""); setEmail(""); setCompany(""); }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function setStage(id, stage) {
|
|
220
|
+
const res = await fetch("/api/contacts", {
|
|
221
|
+
method: "PATCH",
|
|
222
|
+
headers: { "content-type": "application/json" },
|
|
223
|
+
body: JSON.stringify({ id, stage }),
|
|
224
|
+
});
|
|
225
|
+
if (res.ok) setContacts((p) => p.map((c) => (c.id === id ? { ...c, stage } : c)));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function remove(id) {
|
|
229
|
+
const res = await fetch(`/api/contacts?id=${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
230
|
+
if (res.ok) setContacts((p) => p.filter((c) => c.id !== id));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<div>
|
|
235
|
+
<form onSubmit={add} className="row">
|
|
236
|
+
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
|
|
237
|
+
<input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
|
|
238
|
+
<input value={company} onChange={(e) => setCompany(e.target.value)} placeholder="Company" />
|
|
239
|
+
<button type="submit">Add</button>
|
|
240
|
+
</form>
|
|
241
|
+
<table>
|
|
242
|
+
<thead>
|
|
243
|
+
<tr><th>Name</th><th>Email</th><th>Company</th><th>Stage</th><th></th></tr>
|
|
244
|
+
</thead>
|
|
245
|
+
<tbody>
|
|
246
|
+
{contacts.map((c) => (
|
|
247
|
+
<tr key={c.id}>
|
|
248
|
+
<td>{c.name}</td>
|
|
249
|
+
<td>{c.email}</td>
|
|
250
|
+
<td>{c.company}</td>
|
|
251
|
+
<td>
|
|
252
|
+
<select value={c.stage} onChange={(e) => setStage(c.id, e.target.value)}>
|
|
253
|
+
{STAGES.map((s) => <option key={s} value={s}>{LABELS[s]}</option>)}
|
|
254
|
+
</select>
|
|
255
|
+
</td>
|
|
256
|
+
<td><button type="button" onClick={() => remove(c.id)}>Delete</button></td>
|
|
257
|
+
</tr>
|
|
258
|
+
))}
|
|
259
|
+
</tbody>
|
|
260
|
+
</table>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Notes
|
|
267
|
+
|
|
268
|
+
- Deploy the same way as the template: `grid dev` locally, `grid plug` to deploy
|
|
269
|
+
(async — poll status to a live URL). Re-plug the same entity to keep one URL.
|
|
270
|
+
- The only store this needs is Mongo (`needs: { database: true }`). Add
|
|
271
|
+
`cache: true` to `needs:` only for caching/sessions you actually use.
|
|
272
|
+
- All app code lives under `services/web/`; the connection is read lazily inside
|
|
273
|
+
`getDb`, never at module top level.
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Example: kanban — "Workflow Board"
|
|
2
|
+
|
|
3
|
+
A filled reference imitating the `kanban` template: a persistent trello-style
|
|
4
|
+
board. Same stack (Next.js App Router + the `mongodb` driver, grid-shared Mongo
|
|
5
|
+
via `process.env.DATABASE_MONGODB_URL`). Cards have a `title`, a `column` (todo /
|
|
6
|
+
doing / done) and an `order`; you add a card to the first column, move it between
|
|
7
|
+
columns, and delete it.
|
|
8
|
+
|
|
9
|
+
Data persists in the grid-shared Mongo, so the whole team sees the same board
|
|
10
|
+
across sessions and refresh.
|
|
11
|
+
|
|
12
|
+
**Same three proven rules as the template:** (1) app code lives under
|
|
13
|
+
**`services/web/`** (the service name, NOT the repo root — `path:` is the URL
|
|
14
|
+
mount); (2) the DB connection string is read **lazily inside the `getDb`
|
|
15
|
+
getter**, never at module top level (a top-level read fails `next build`); (3)
|
|
16
|
+
the datastore is declared with the canonical **`needs: { database: true }`** —
|
|
17
|
+
the deployer provisions Mongo and injects `DATABASE_MONGODB_URL` (plus the legacy
|
|
18
|
+
`MONGODB_URL` alias). Don't use the deprecated `requires:` alias.
|
|
19
|
+
|
|
20
|
+
## cloudgrid.yaml
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
# `needs: { database: true }` is canonical: the deployer provisions Mongo and
|
|
24
|
+
# injects DATABASE_MONGODB_URL (plus the legacy MONGODB_URL alias). `requires:`
|
|
25
|
+
# is the deprecated v1 alias — don't mix the two (the validator rejects it).
|
|
26
|
+
name: workflow-board
|
|
27
|
+
services:
|
|
28
|
+
web:
|
|
29
|
+
type: nextjs
|
|
30
|
+
path: /
|
|
31
|
+
needs:
|
|
32
|
+
database: true
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## services/web/package.json
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"name": "workflow-board",
|
|
40
|
+
"version": "0.1.0",
|
|
41
|
+
"private": true,
|
|
42
|
+
"scripts": { "dev": "next dev", "build": "next build", "start": "next start" },
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"next": "^15.1.0",
|
|
45
|
+
"react": "^19.0.0",
|
|
46
|
+
"react-dom": "^19.0.0",
|
|
47
|
+
"mongodb": "^6.12.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## services/web/lib/db.js
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
// Cached Mongo client. The grid injects DATABASE_MONGODB_URL (plus the legacy
|
|
56
|
+
// MONGODB_URL alias) at dev + runtime. Read the canonical var first, fall back to
|
|
57
|
+
// the legacy alias. Resolve it LAZILY inside the getter — a top-level read fails
|
|
58
|
+
// `next build`.
|
|
59
|
+
import { MongoClient } from "mongodb";
|
|
60
|
+
|
|
61
|
+
function clientPromise() {
|
|
62
|
+
const uri = process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL;
|
|
63
|
+
if (!uri) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"DATABASE_MONGODB_URL is not set. The grid injects it — run `grid dev` " +
|
|
66
|
+
"locally or deploy with `grid plug`. Do not set it by hand.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (!globalThis.__mongoClientPromise) {
|
|
70
|
+
globalThis.__mongoClientPromise = new MongoClient(uri).connect();
|
|
71
|
+
}
|
|
72
|
+
return globalThis.__mongoClientPromise;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function getDb() {
|
|
76
|
+
return (await clientPromise()).db();
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## services/web/app/api/cards/route.js
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
import { NextResponse } from "next/server";
|
|
84
|
+
import { ObjectId } from "mongodb";
|
|
85
|
+
import { getDb } from "../../../lib/db.js";
|
|
86
|
+
|
|
87
|
+
export const dynamic = "force-dynamic";
|
|
88
|
+
|
|
89
|
+
const COLUMNS = ["todo", "doing", "done"];
|
|
90
|
+
|
|
91
|
+
async function cards() {
|
|
92
|
+
return (await getDb()).collection("cards");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function shape(c) {
|
|
96
|
+
return {
|
|
97
|
+
id: c._id.toString(),
|
|
98
|
+
title: c.title || "",
|
|
99
|
+
column: COLUMNS.includes(c.column) ? c.column : "todo",
|
|
100
|
+
order: typeof c.order === "number" ? c.order : 0,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// GET /api/cards — list the board, ordered.
|
|
105
|
+
export async function GET() {
|
|
106
|
+
const col = await cards();
|
|
107
|
+
const items = await col.find({}).sort({ order: 1, createdAt: 1 }).toArray();
|
|
108
|
+
return NextResponse.json(items.map(shape));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// POST /api/cards — add a card to the first column. Body: { title }.
|
|
112
|
+
export async function POST(request) {
|
|
113
|
+
const body = await request.json().catch(() => ({}));
|
|
114
|
+
const title = typeof body.title === "string" ? body.title.trim() : "";
|
|
115
|
+
if (!title) return NextResponse.json({ error: "title is required" }, { status: 400 });
|
|
116
|
+
const doc = { title, column: "todo", order: Date.now(), createdAt: new Date() };
|
|
117
|
+
const col = await cards();
|
|
118
|
+
const res = await col.insertOne(doc);
|
|
119
|
+
return NextResponse.json({ id: res.insertedId.toString(), ...doc }, { status: 201 });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// PATCH /api/cards — move a card to another column. Body: { id, column }.
|
|
123
|
+
export async function PATCH(request) {
|
|
124
|
+
const body = await request.json().catch(() => ({}));
|
|
125
|
+
const { id, column } = body;
|
|
126
|
+
if (!id || !ObjectId.isValid(id) || !COLUMNS.includes(column)) {
|
|
127
|
+
return NextResponse.json({ error: "valid id and column required" }, { status: 400 });
|
|
128
|
+
}
|
|
129
|
+
const col = await cards();
|
|
130
|
+
await col.updateOne({ _id: new ObjectId(id) }, { $set: { column } });
|
|
131
|
+
return NextResponse.json({ ok: true });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// DELETE /api/cards?id=<id>
|
|
135
|
+
export async function DELETE(request) {
|
|
136
|
+
const id = new URL(request.url).searchParams.get("id");
|
|
137
|
+
if (!id || !ObjectId.isValid(id)) {
|
|
138
|
+
return NextResponse.json({ error: "valid id is required" }, { status: 400 });
|
|
139
|
+
}
|
|
140
|
+
const col = await cards();
|
|
141
|
+
await col.deleteOne({ _id: new ObjectId(id) });
|
|
142
|
+
return NextResponse.json({ ok: true });
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## services/web/app/page.js
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
import { getDb } from "../lib/db.js";
|
|
150
|
+
import Board from "./board.js";
|
|
151
|
+
|
|
152
|
+
export const dynamic = "force-dynamic";
|
|
153
|
+
|
|
154
|
+
const COLUMNS = ["todo", "doing", "done"];
|
|
155
|
+
|
|
156
|
+
async function listCards() {
|
|
157
|
+
const items = await (await getDb())
|
|
158
|
+
.collection("cards")
|
|
159
|
+
.find({})
|
|
160
|
+
.sort({ order: 1, createdAt: 1 })
|
|
161
|
+
.toArray();
|
|
162
|
+
return items.map((c) => ({
|
|
163
|
+
id: c._id.toString(),
|
|
164
|
+
title: c.title || "",
|
|
165
|
+
column: COLUMNS.includes(c.column) ? c.column : "todo",
|
|
166
|
+
order: typeof c.order === "number" ? c.order : 0,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export default async function Page() {
|
|
171
|
+
const cards = await listCards();
|
|
172
|
+
return (
|
|
173
|
+
<main>
|
|
174
|
+
<h1>Workflow Board</h1>
|
|
175
|
+
<p className="hint">Shared across the team — persisted in grid Mongo.</p>
|
|
176
|
+
<Board initialCards={cards} />
|
|
177
|
+
</main>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## services/web/app/board.js
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
"use client";
|
|
186
|
+
import { useState } from "react";
|
|
187
|
+
|
|
188
|
+
const COLUMNS = ["todo", "doing", "done"];
|
|
189
|
+
const LABELS = { todo: "To do", doing: "Doing", done: "Done" };
|
|
190
|
+
|
|
191
|
+
export default function Board({ initialCards }) {
|
|
192
|
+
const [cards, setCards] = useState(initialCards);
|
|
193
|
+
const [title, setTitle] = useState("");
|
|
194
|
+
|
|
195
|
+
async function add(e) {
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
if (!title.trim()) return;
|
|
198
|
+
const res = await fetch("/api/cards", {
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: { "content-type": "application/json" },
|
|
201
|
+
body: JSON.stringify({ title }),
|
|
202
|
+
});
|
|
203
|
+
if (res.ok) { setCards((p) => [...p, await res.json()]); setTitle(""); }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function move(id, column) {
|
|
207
|
+
const res = await fetch("/api/cards", {
|
|
208
|
+
method: "PATCH",
|
|
209
|
+
headers: { "content-type": "application/json" },
|
|
210
|
+
body: JSON.stringify({ id, column }),
|
|
211
|
+
});
|
|
212
|
+
if (res.ok) setCards((p) => p.map((c) => (c.id === id ? { ...c, column } : c)));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function remove(id) {
|
|
216
|
+
const res = await fetch(`/api/cards?id=${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
217
|
+
if (res.ok) setCards((p) => p.filter((c) => c.id !== id));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<div>
|
|
222
|
+
<form onSubmit={add} className="row">
|
|
223
|
+
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Add a card…" />
|
|
224
|
+
<button type="submit">Add</button>
|
|
225
|
+
</form>
|
|
226
|
+
<div className="board">
|
|
227
|
+
{COLUMNS.map((column) => (
|
|
228
|
+
<section key={column}>
|
|
229
|
+
<h2>{LABELS[column]}</h2>
|
|
230
|
+
{cards.filter((c) => c.column === column).map((c) => (
|
|
231
|
+
<article key={c.id} className="card">
|
|
232
|
+
<span className="title">{c.title}</span>
|
|
233
|
+
<div className="moves">
|
|
234
|
+
{COLUMNS.filter((o) => o !== column).map((o) => (
|
|
235
|
+
<button key={o} type="button" onClick={() => move(c.id, o)}>→ {LABELS[o]}</button>
|
|
236
|
+
))}
|
|
237
|
+
<button type="button" onClick={() => remove(c.id)}>Delete</button>
|
|
238
|
+
</div>
|
|
239
|
+
</article>
|
|
240
|
+
))}
|
|
241
|
+
</section>
|
|
242
|
+
))}
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Notes
|
|
250
|
+
|
|
251
|
+
- Deploy the same way as the template: `grid dev` locally, `grid plug` to deploy
|
|
252
|
+
(async — poll status to a live URL). Re-plug the same entity to keep one URL.
|
|
253
|
+
- The only store this needs is Mongo (`needs: { database: true }`). Add
|
|
254
|
+
`cache: true` to `needs:` only for caching/sessions you actually use.
|
|
255
|
+
- All app code lives under `services/web/`; the connection is read lazily inside
|
|
256
|
+
`getDb`, never at module top level.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# admin-dashboard template — persistent Next.js + Mongo admin panel
|
|
2
|
+
|
|
3
|
+
A real, deployable back-office. It manages two generic resources (`users` and
|
|
4
|
+
`orders`) in the grid-shared MongoDB, so records survive refresh and are shared
|
|
5
|
+
across users/sessions. Same proven shape as `app-with-data`, adapted to a
|
|
6
|
+
metrics header (counts + revenue) over a couple of collections plus a management
|
|
7
|
+
table with add/delete.
|
|
8
|
+
|
|
9
|
+
## How the grid gives you a database
|
|
10
|
+
|
|
11
|
+
You do **not** provision a database or set a connection string. In
|
|
12
|
+
`cloudgrid.yaml` you declare `needs: { database: true }`, and the grid:
|
|
13
|
+
|
|
14
|
+
- provisions shared Mongo for the app, and
|
|
15
|
+
- injects the connection string as the **`DATABASE_MONGODB_URL`** environment
|
|
16
|
+
variable (plus the legacy `MONGODB_URL` alias) — at dev-time (`grid dev`) and
|
|
17
|
+
at runtime (after `grid plug`).
|
|
18
|
+
|
|
19
|
+
The app reads it via `process.env.DATABASE_MONGODB_URL` (with a legacy
|
|
20
|
+
`process.env.MONGODB_URL` fallback) in `services/web/lib/db.js` — **lazily,
|
|
21
|
+
inside the `getDb` getter, never at module top level** (a top-level read fails
|
|
22
|
+
`next build`, which imports the module for route analysis before the grid injects
|
|
23
|
+
the var). Never hardcode a connection string; never commit a secret.
|
|
24
|
+
|
|
25
|
+
> **Declare `needs: { database: true }` (the canonical shape).** The deployer
|
|
26
|
+
> provisions Mongo and injects `DATABASE_MONGODB_URL` (plus the legacy
|
|
27
|
+
> `MONGODB_URL` alias). `requires:` is the deprecated v1 alias — don't author new
|
|
28
|
+
> yaml with it, and never set `needs:` and `requires:` together (the validator
|
|
29
|
+
> rejects the combination).
|
|
30
|
+
|
|
31
|
+
## Service layout
|
|
32
|
+
|
|
33
|
+
App code lives under **`services/web/`**, not the template root. `path:` in
|
|
34
|
+
`cloudgrid.yaml` is the URL mount, not the filesystem path — the service named
|
|
35
|
+
`web` means the CLI looks for `services/web/`. Files at the root fail with
|
|
36
|
+
`Error: Service directory not found: …/services/web`.
|
|
37
|
+
|
|
38
|
+
## Run locally
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install
|
|
42
|
+
grid dev # runs Next.js with DATABASE_MONGODB_URL injected against dev Mongo
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Deploy
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
grid plug # builds + deploys the folder (async — poll status until live)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A runtime deploy is asynchronous: `plug` returns `status: building`; poll status
|
|
52
|
+
until the app is live, then use the returned live URL. Re-plug the same entity
|
|
53
|
+
to update the same URL.
|
|
54
|
+
|
|
55
|
+
## File tree
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
cloudgrid.yaml # name + services.web (nextjs) + needs: { database: true }
|
|
59
|
+
services/web/package.json # next, react, react-dom, mongodb driver only
|
|
60
|
+
services/web/lib/db.js # lazy Mongo client from DATABASE_MONGODB_URL (legacy MONGODB_URL fallback)
|
|
61
|
+
services/web/app/layout.js # root layout + inline CSS
|
|
62
|
+
services/web/app/page.js # server component: reads users + orders, computes metrics
|
|
63
|
+
services/web/app/resource-table.js # generic client table (POST add / DELETE remove), reused per resource
|
|
64
|
+
services/web/app/api/users/route.js # GET / POST / DELETE on the users collection
|
|
65
|
+
services/web/app/api/orders/route.js # GET / POST / DELETE on the orders collection
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Adapt it
|
|
69
|
+
|
|
70
|
+
- Rename or replace the `users` / `orders` collections and their routes with
|
|
71
|
+
your own resources.
|
|
72
|
+
- Add or change fields by editing the `columns` config passed to `ResourceTable`
|
|
73
|
+
in `services/web/app/page.js` and the matching route's document shape.
|
|
74
|
+
- Add PATCH (edit) or extra metrics as the panel grows.
|
|
75
|
+
- Add `cache: true` to `needs:` only if you actually need Redis.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Rename this app. The grid injects the DB connection string as an environment
|
|
2
|
+
# variable at runtime — do NOT set it yourself, and never commit a connection
|
|
3
|
+
# string or secret.
|
|
4
|
+
#
|
|
5
|
+
# `needs: { database: true }` is the canonical, recommended shape. The deployer
|
|
6
|
+
# provisions Mongo and injects DATABASE_MONGODB_URL (plus the legacy MONGODB_URL
|
|
7
|
+
# alias). `requires:` is the deprecated v1 alias — don't author new yaml with it,
|
|
8
|
+
# and never set `needs:` and `requires:` together (the validator rejects it).
|
|
9
|
+
name: my-admin
|
|
10
|
+
services:
|
|
11
|
+
web:
|
|
12
|
+
type: nextjs
|
|
13
|
+
path: /
|
|
14
|
+
needs:
|
|
15
|
+
database: true
|