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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ jobs:
- name: Frontend install and build
working-directory: frontend
run: npm ci && npm run build

- name: Viewer (Vite) install and build
working-directory: viewer
run: npm ci && npm run build
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ dist/
build/
*.egg

# Local SQLite (modulr-core default database_path)
data/
# Local SQLite (modulr-core default database_path) — repo root only
/data/

# Keymaster — local vault (encrypted blobs still belong only on disk; keeps clones clean)
keymaster_data/
Expand All @@ -29,6 +29,9 @@ keymaster/keymaster_data/
.coverage
htmlcov/

# Node (frontend / viewer)
node_modules/

# Editors / OS
.idea/
.vscode/
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,20 +312,32 @@ A **config file is required**: use **`--config dev.toml`** or set **`MODULR_CORE

### Customer web UI (stage 1)

A **Next.js** app in **`frontend/`** is the customer-facing shell (theme blend, glass layout, firefly/gradient backgrounds, settings for Modulr.Core URLs). It is kept beside Core for convenience and is expected to move to its own repository later.
A **Next.js** app in **`frontend/`** is the original customer-facing shell (theme blend, glass layout, firefly/gradient backgrounds, settings for Modulr.Core URLs). It is kept beside Core for convenience and is expected to move to its own repository later.

A **Vite + React** copy lives in **`viewer/`** — same routes and components, **no Next.js** — intended for simpler local dev, parity with other Vite-based Modulr apps, and **static hosting** (e.g. AWS Amplify). The Next app remains a known-good reference until you standardize on one.

```bash
cd frontend
npm install
npm run dev
```

Details: **`frontend/README.md`**. Phased product plan: **`plan/customer_web_interface.md`**.
```bash
cd viewer
npm install
npm run dev
```

Details: **`frontend/README.md`**, **`viewer/README.md`**. Phased product plan: **`plan/customer_web_interface.md`**.

### Planned: module branding (tracking)

Module publishers should be able to register a **logo** (or icon) for their module so explorers and the customer shell can show it next to the module name. **Format policy is not fixed yet** — **SVG** is a strong default (scales cleanly at any size); allowing PNG/WebP with size limits is an alternative. This will need a wire contract, validation, and storage or URL policy on Core or the module registry when implemented.

### Planned: Modulr ID — `get_modulr_id` (tracking)

Optional **identity / trust** layer: a protocol-category wire read (**`get_modulr_id`**) to resolve an opaque **Modulr ID** and related trust signals (e.g. centralized onboarding, tier) for use alongside org/name identity. **Issuance / mutation stays private** — only trusted operators (e.g. module owners, bootstrap/service keys), not arbitrary callers setting their own IDs on the open wire. Details: payload shape, storage, and whether attestations are minimal hashes vs richer records TBD when implemented.

---

## License
Expand Down
61 changes: 61 additions & 0 deletions frontend/components/methods/MethodsMock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ const LIVE_SIGNED_METHOD_IDS = new Set<string>([
"get_protocol_methods",
"get_core_genesis_branding",
"get_organization_logo",
"get_user_description",
"get_user_profile_image",
"set_organization_logo",
"set_user_description",
"set_user_profile_image",
"lookup_module",
"get_module_methods",
Expand Down Expand Up @@ -110,12 +112,18 @@ function liveExecuteHint(methodId: string): string {
if (methodId === "get_user_profile_image") {
return "Same signing path. Payload: exactly one of user_handle or user_signing_public_key_hex. Response includes profile_image_base64 + MIME (image preview in results).";
}
if (methodId === "get_user_description") {
return "Same signing path. Payload: exactly one of user_handle or user_signing_public_key_hex. Response includes description (public bio) from Core.";
}
if (methodId === "set_organization_logo") {
return "Sender must match organization_signing_public_key_hex or be bootstrap. logo_svg null clears; optional organization_key scopes the row.";
}
if (methodId === "set_user_profile_image") {
return "Sender must match user_signing_public_key_hex or be bootstrap. Base64 + MIME together or both empty to clear; optional user_handle.";
}
if (methodId === "set_user_description") {
return "Sender must match user_signing_public_key_hex or be bootstrap. Optional description text or empty to clear; optional user_handle to mirror under handle + pubkey.";
}
if (methodId === "get_module_route") {
return "Same signing path. Returns route_detail (full JSON) and, when the doc has route_type + route strings, those flattened for convenience.";
}
Expand Down Expand Up @@ -395,6 +403,33 @@ export function MethodsMock() {
return;
}

if (selected.id === "get_user_description") {
const base = primaryCoreBaseUrl(settings.coreEndpoints);
if (!base) {
setError("Set a Core base URL in settings.");
return;
}
const handle = values.user_handle?.trim() ?? "";
const pk = values.user_signing_public_key_hex?.trim() ?? "";
if ((handle && pk) || (!handle && !pk)) {
setError("Fill in exactly one of user handle or user public key (hex).");
return;
}
setLoading(true);
try {
const payload: Record<string, unknown> = handle
? { user_handle: handle }
: { user_signing_public_key_hex: pk };
const data = await executeSignedCoreOperation(base, "get_user_description", payload);
setResult(data);
} catch (e: unknown) {
setError(formatClientError(e));
} finally {
setLoading(false);
}
return;
}

if (selected.id === "set_organization_logo") {
const base = primaryCoreBaseUrl(settings.coreEndpoints);
if (!base) {
Expand Down Expand Up @@ -453,6 +488,32 @@ export function MethodsMock() {
return;
}

if (selected.id === "set_user_description") {
const base = primaryCoreBaseUrl(settings.coreEndpoints);
if (!base) {
setError("Set a Core base URL in settings.");
return;
}
const userPk = values.user_signing_public_key_hex?.trim() ?? "";
const descRaw = values.description?.trim() ?? "";
const uh = values.user_handle?.trim() ?? "";
setLoading(true);
try {
const payload: Record<string, unknown> = {
user_signing_public_key_hex: userPk,
description: descRaw.length > 0 ? descRaw : null,
};
if (uh) payload.user_handle = uh;
const data = await executeSignedCoreOperation(base, "set_user_description", payload);
setResult(data);
} catch (e: unknown) {
setError(formatClientError(e));
} finally {
setLoading(false);
}
return;
}

if (selected.id === "get_module_route") {
const base = primaryCoreBaseUrl(settings.coreEndpoints);
if (!base) {
Expand Down
114 changes: 81 additions & 33 deletions frontend/components/methods/mockMethodCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,28 @@ export const METHOD_CATALOG: MethodDef[] = [
},
],
},
{
id: "get_user_description",
title: "get_user_description",
summary:
"Return the public user description (bio). Pass exactly one of user_handle or user_signing_public_key_hex.",
category: "protocol",
coreSurface: true,
params: [
{
name: "user_handle",
label: "User handle",
placeholder: "@alice or alice — leave empty if using pubkey",
required: false,
},
{
name: "user_signing_public_key_hex",
label: "User Ed25519 public key (hex)",
placeholder: "64 hex chars",
required: false,
},
],
},
{
id: "set_organization_logo",
title: "set_organization_logo",
Expand Down Expand Up @@ -202,6 +224,35 @@ export const METHOD_CATALOG: MethodDef[] = [
},
],
},
{
id: "set_user_description",
title: "set_user_description",
summary:
"Create or replace the public user description (bio). Sender must match user_signing_public_key_hex or be bootstrap.",
category: "protocol",
coreSurface: true,
params: [
{
name: "user_signing_public_key_hex",
label: "User Ed25519 public key (hex)",
placeholder: "64 hex chars",
required: true,
},
{
name: "user_handle",
label: "User handle (optional)",
placeholder: "Mirrors the row under h:<handle> as well as p:<pubkey>",
required: false,
},
{
name: "description",
label: "Description (bio)",
placeholder: "Short public bio — empty to clear",
required: false,
multiline: true,
},
],
},
{
id: "lookup_module",
title: "lookup_module",
Expand Down Expand Up @@ -522,38 +573,18 @@ export const METHOD_CATALOG: MethodDef[] = [
},
],
},
{
id: "register_module",
title: "register_module",
summary: "Publish a module declaration (bootstrap / authorized senders in production).",
category: "protocol",
coreSurface: true,
params: [
{
name: "module_name",
label: "Module name",
placeholder: "e.g. modulr.assets",
required: true,
},
{
name: "version",
label: "Version",
placeholder: "e.g. 1.0.0",
required: false,
},
],
},
{
id: "register_org",
title: "register_org",
summary: "Claim an organization key under Core policy.",
summary:
"Register an apex org (name + resolved_id) and optionally the module row (signing_public_key + route).",
category: "protocol",
coreSurface: true,
params: [
{
name: "organization_key",
label: "Organization key",
placeholder: "e.g. labs.acme",
label: "Organization apex",
placeholder: "e.g. modulr.assets or acme.network",
required: true,
},
],
Expand Down Expand Up @@ -643,17 +674,18 @@ const CORE_OPERATION_NAMES = [
"get_organization_logo",
"get_protocol_methods",
"get_protocol_version",
"get_user_description",
"get_user_profile_image",
"heartbeat_update",
"lookup_module",
"register_module",
"register_name",
"register_org",
"remove_module_route",
"report_module_state",
"resolve_name",
"reverse_resolve_name",
"set_organization_logo",
"set_user_description",
"set_user_profile_image",
"submit_module_route",
] as const;
Expand All @@ -665,10 +697,12 @@ const PROTOCOL_METHOD_NAMES = [
"get_organization_logo",
"get_protocol_methods",
"get_protocol_version",
"get_user_description",
"get_user_profile_image",
"heartbeat_update",
"report_module_state",
"set_organization_logo",
"set_user_description",
"set_user_profile_image",
] as const;

Expand All @@ -691,8 +725,10 @@ function mockCatalogRow(method: string): MockWireMethodRow {
else if (
method === "get_core_genesis_branding" ||
method === "get_organization_logo" ||
method === "get_user_description" ||
method === "get_user_profile_image" ||
method === "set_organization_logo" ||
method === "set_user_description" ||
method === "set_user_profile_image"
)
group = "branding";
Expand Down Expand Up @@ -873,20 +909,15 @@ export function buildMockMethodResponse(
"Core would verify the proof binds this module identity to the publisher’s claimed key or upstream identity — not store module source here.",
};
}
case "register_module":
return {
status: "accepted_mock",
module_name: payload.module_name?.trim(),
version: payload.version?.trim() || "0.0.0",
registration_id: `reg_${(seed >>> 0).toString(16)}`,
message: "Would enqueue signed envelope validation in production.",
};
case "register_org":
return {
status: "accepted_mock",
organization_key: payload.organization_key?.trim(),
anchor_usd_floor_next: 100 * Math.pow(2, seed % 4),
registration_id: `org_${(seed >>> 0).toString(16)}`,
module_registered: Boolean(
payload.signing_public_key && String(payload.signing_public_key).trim(),
),
};
case "register_name":
return {
Expand Down Expand Up @@ -931,6 +962,15 @@ export function buildMockMethodResponse(
source: "mock",
server_time: now,
};
case "get_user_description":
return {
status: "ok",
user_handle: "mock",
user_signing_public_key_hex: "b".repeat(64),
description: "Mock public bio — set once on Core, reuse in every app.",
source: "mock",
server_time: now,
};
case "get_user_profile_image":
return {
status: "ok",
Expand Down Expand Up @@ -959,6 +999,14 @@ export function buildMockMethodResponse(
profile_image_stored: Boolean(payload.profile_image_base64?.trim()),
server_time: now,
};
case "set_user_description":
return {
status: "ok",
user_handle: payload.user_handle?.trim() || null,
user_signing_public_key_hex: payload.user_signing_public_key_hex?.trim() || "",
description: payload.description?.trim() || null,
server_time: now,
};
default:
return { status: "unknown_operation", operation };
}
Expand Down
18 changes: 9 additions & 9 deletions frontend/components/shell/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ export function AppShell({ children }: { children: React.ReactNode }) {
className="flex min-w-0 flex-1 flex-wrap items-center justify-start gap-x-10 gap-y-2 border-t border-[var(--modulr-glass-border)] pt-3 text-sm font-semibold tracking-tight text-[var(--modulr-text)] sm:border-t-0 sm:pt-0"
aria-label="Core tools"
>
<Link
href="/profile"
aria-current={pathname === "/profile" ? "page" : undefined}
className={`transition-colors duration-200 hover:text-[var(--modulr-accent)] ${
pathname === "/profile" ? "text-[var(--modulr-accent)]" : ""
}`}
>
Profile
</Link>
<Link
href="/registration"
aria-current={pathname === "/registration" ? "page" : undefined}
Expand Down Expand Up @@ -146,15 +155,6 @@ export function AppShell({ children }: { children: React.ReactNode }) {
>
Methods
</Link>
<Link
href="/profile"
aria-current={pathname === "/profile" ? "page" : undefined}
className={`transition-colors duration-200 hover:text-[var(--modulr-accent)] ${
pathname === "/profile" ? "text-[var(--modulr-accent)]" : ""
}`}
>
Profile
</Link>
</nav>
</div>

Expand Down
Loading
Loading