Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 118e925580 | |||
| 11acb3a5b1 | |||
| efc9c8edb9 | |||
| f7b76d5370 | |||
| 4bb9763633 | |||
| a456d51f17 | |||
| ac3c4ffe36 |
@@ -0,0 +1,42 @@
|
||||
# deployment.toml — generated by define-deployment (launch-app SPEC §5.1, §6.6).
|
||||
# Derived from the One Name 'ecomm' (§3.3); edit the toml, re-import to reconcile (§3.2.1).
|
||||
|
||||
[app]
|
||||
name = "ecomm"
|
||||
repo = "wiggleverse/wiggleverse-ecomm"
|
||||
gitea_host = "https://git.wiggleverse.org"
|
||||
version_source = { kind = "file", path = "VERSION" }
|
||||
gitea_read_secret_ref = "wiggleverse-ecomm/ecomm-gitea-read-token"
|
||||
|
||||
[vm]
|
||||
name = "ecomm-ppe"
|
||||
zone = "us-central1-a"
|
||||
project = "wiggleverse-ecomm"
|
||||
machine_type = "e2-micro"
|
||||
disk_gb = 10
|
||||
service_user = "ecomm"
|
||||
install_dir = "/opt/ecomm"
|
||||
systemd_unit = "ecomm.service"
|
||||
tunnel_through_iap = true
|
||||
gcloud_config = "wiggleverse-ecomm"
|
||||
|
||||
[edge]
|
||||
domain = "ecomm-ppe.wiggleverse.org"
|
||||
# ecomm's health endpoint is /healthz (SD-0001 §6.4); body carries {status, version}.
|
||||
health_url = "https://ecomm-ppe.wiggleverse.org/healthz"
|
||||
|
||||
# No DATABASE_PATH: ecomm runs Cloud SQL PostgreSQL (SD-0001 D-7/D-8) — the DSN is the
|
||||
# ECOMM_DATABASE_URL secret reference below, minted by launch-app's provision-datastore.
|
||||
[overlay] # non-secret env, plaintext (guide §8)
|
||||
APP_URL = "https://ecomm-ppe.wiggleverse.org"
|
||||
ECOMM_MAILER = "smtp"
|
||||
ECOMM_COOKIE_SECURE = "1"
|
||||
ECOMM_SMTP_HOST = "smtp.gmail.com"
|
||||
ECOMM_SMTP_PORT = "587"
|
||||
ECOMM_SMTP_USER = "ben.stull@wiggleverse.org"
|
||||
ECOMM_SMTP_FROM = "ecomm <ben.stull@wiggleverse.org>"
|
||||
|
||||
[secrets] # REFERENCES only — never bytes (§8.3)
|
||||
ECOMM_SESSION_SECRET = "wiggleverse-ecomm/ecomm-ppe-session-secret"
|
||||
ECOMM_DATABASE_URL = "wiggleverse-ecomm/ecomm-ppe-database-url"
|
||||
ECOMM_SMTP_PASSWORD = "wiggleverse-ohm/ohm-rfc-app-smtp-password"
|
||||
@@ -122,6 +122,18 @@ From empty persistence: the deploy migrates the schema at startup (INV-7); then
|
||||
real sign-up → one-time code arriving by **real email** → create storefront → admin,
|
||||
through the public flows alone. Record each rehearsal here when it happens.
|
||||
|
||||
**Rehearsals:**
|
||||
|
||||
- **2026-06-11 — PPE first bootstrap (v0.4.0, session 0024): ✅** First-ever ecomm
|
||||
deploy (`flotilla-core deploy ecomm`, 9/9 phases green, deploys.id=40) onto a
|
||||
freshly provisioned environment — project `wiggleverse-ecomm`, Cloud SQL
|
||||
`ecomm-ppe-pg` (provisioned by launch-app `provision-datastore`, first run), VM
|
||||
`ecomm-ppe`. The app self-migrated the empty database at startup; the operator
|
||||
then walked sign-up → real emailed code (Gmail relay) → create storefront →
|
||||
honestly-empty admin in a browser, through the public flows alone. Findings
|
||||
captured: OTC email branding (#16); identity should outgrow ecomm
|
||||
(engineering#49/#50).
|
||||
|
||||
## Production
|
||||
|
||||
Lands with the prod stand-up — the **identical gesture** on a prod deployment record
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# ui/designs Content-Repo Collection Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Establish `ui/designs/` as a standard content-repo collection (alongside `specs/` and `plans/`) — concretely in `wiggleverse-ecomm-content`, and centrally in the engineering repo's schema docs so it binds all `*-content` repos.
|
||||
|
||||
**Architecture:** Docs/convention change only, two repos, one PR each. The collection convention's canonical home is the engineering repo's `schemas/` docs (the `content` descriptor description strings in `app.schema.json` + `schemas/README.md` changelog), so the central change is a docs-only minor schema bump (1.2 → 1.3). No tooling changes: the spec-linkage gate, backfill verb, and GUIDE/TEMPLATE Design field are tracked separately as `wiggleverse-dev-claude-plugin#93`.
|
||||
|
||||
**Tech Stack:** Markdown, JSON Schema (description strings only), git + Gitea PRs over SSH.
|
||||
|
||||
**Anchor:** `wiggleverse/wiggleverse-ecomm#8` (type/task, ELIGIBLE R2b). Related: `wiggleverse/wiggleverse-dev-claude-plugin#93`.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: `ui/designs/` collection in wiggleverse-ecomm-content
|
||||
|
||||
**Files:**
|
||||
- Create: `/Users/benstull/git/wiggleverse.org/wiggleverse/wiggleverse-ecomm-content/ui/designs/README.md`
|
||||
- Modify: `/Users/benstull/git/wiggleverse.org/wiggleverse/wiggleverse-ecomm-content/README.md` (layout table, lines 10–14)
|
||||
|
||||
- [ ] **Step 1: Branch**
|
||||
|
||||
```bash
|
||||
git -C /Users/benstull/git/wiggleverse.org/wiggleverse/wiggleverse-ecomm-content checkout -b ui-designs-collection
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create the collection README**
|
||||
|
||||
`ui/designs/README.md`:
|
||||
|
||||
```markdown
|
||||
# ui/designs — UI-design artifacts
|
||||
|
||||
Standard content-repo collection (alongside `specs/` and `plans/`) holding this
|
||||
app's UI-design artifacts — primarily Claude Design outputs generated from a
|
||||
Solution Design (rubric: `engineering/solution-design/claude-design-vs-code.md`).
|
||||
|
||||
A Solution Design with a UX-involving slice references its design artifact here
|
||||
by path. The spec-linkage gate and the backfill gesture for adding that
|
||||
reference once a design exists are tracked in
|
||||
`wiggleverse/wiggleverse-dev-claude-plugin#93`.
|
||||
|
||||
Suggested layout: one subfolder per design, named for the spec/slice it serves,
|
||||
e.g. `ui/designs/SD-0001-slice-3-storefront/`.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the layout-table row**
|
||||
|
||||
In the top-level `README.md`, extend the table:
|
||||
|
||||
```markdown
|
||||
| Path | Holds |
|
||||
| --- | --- |
|
||||
| `specs/` | reviewed Solution-Design specs (submitted at session finalize) |
|
||||
| `plans/` | archived implementation plans |
|
||||
| `ui/designs/` | UI-design artifacts (Claude Design outputs), referenced from specs |
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit, push, PR, merge**
|
||||
|
||||
```bash
|
||||
git -C …/wiggleverse-ecomm-content add ui/designs/README.md README.md
|
||||
git -C …/wiggleverse-ecomm-content commit -m "content: add ui/designs/ collection (ecomm#8)"
|
||||
git -C …/wiggleverse-ecomm-content push -u origin ui-designs-collection
|
||||
```
|
||||
|
||||
PR via Gitea API (default per-host token, NOT the issue-scoped one — TOKENS.md), then merge; body cites `wiggleverse/wiggleverse-ecomm#8` + plugin `#93`.
|
||||
|
||||
### Task 2: Standardize centrally in engineering schemas docs
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/benstull/git/wiggleverse.org/wiggleverse/engineering/schemas/app.schema.json` (lines 13, 94, 181, 184)
|
||||
- Modify: `/Users/benstull/git/wiggleverse.org/wiggleverse/engineering/schemas/README.md` (repos[] bullets + changelog)
|
||||
|
||||
- [ ] **Step 1: Branch**
|
||||
|
||||
```bash
|
||||
git -C /Users/benstull/git/wiggleverse.org/wiggleverse/engineering checkout -b ui-designs-collection
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Schema description strings + enum**
|
||||
|
||||
1. `schemaVersion.enum`: `["1.0", "1.1", "1.2"]` → `["1.0", "1.1", "1.2", "1.3"]`
|
||||
2. `content` property description (line 94): "…where this app's reviewed specs/ and archived plans/ collections live…" → "…where this app's reviewed specs/, archived plans/, and ui/designs/ collections live…"
|
||||
3. `$defs.content` description (line 181): "(reviewed specs/, archived plans/)" → "(reviewed specs/, archived plans/, ui/designs/ UI-design artifacts)"; and "The specs/ and plans/ collection subdirs are appended by the submit tooling" → "The specs/, plans/, and ui/designs/ collection subdirs are conventions (specs/ and plans/ are appended by the submit tooling; ui/designs/ holds Claude Design outputs referenced from specs)"
|
||||
4. `$defs.content.subdir` description (line 184): "under which the specs/ and plans/ collections live" → "under which the specs/, plans/, and ui/designs/ collections live"
|
||||
|
||||
- [ ] **Step 3: schemas/README.md**
|
||||
|
||||
Changelog entry above 1.2:
|
||||
|
||||
```markdown
|
||||
- **1.3** — docs-only: the content-repo collection convention gains a third
|
||||
standard collection, `ui/designs/` — UI-design artifacts (Claude Design
|
||||
outputs generated from a Solution Design), referenced from specs. Like
|
||||
`specs/`/`plans/`, the subdir is a convention, not a schema field; no
|
||||
validation change (the spec-linkage gate/backfill tooling is
|
||||
wiggleverse-dev-claude-plugin#93). Existing files stay valid.
|
||||
```
|
||||
|
||||
If the repos[] bullet list documents the `content` descriptor, name the three collections there too; if 1.2 never added a `content` bullet, add one.
|
||||
|
||||
- [ ] **Step 4: Validate JSON, commit, push, PR, merge**
|
||||
|
||||
```bash
|
||||
python3 -m json.tool /Users/benstull/git/wiggleverse.org/wiggleverse/engineering/schemas/app.schema.json > /dev/null && echo OK
|
||||
git -C …/engineering add schemas/app.schema.json schemas/README.md
|
||||
git -C …/engineering commit -m "schemas: 1.3 — ui/designs/ standard content collection (ecomm#8, plugin#93)"
|
||||
git -C …/engineering push -u origin ui-designs-collection
|
||||
```
|
||||
|
||||
PR + merge (default token for `/pulls`).
|
||||
|
||||
### Task 3: Cross-link and close out
|
||||
|
||||
- [ ] **Step 1: Comment on plugin #93** noting the location is now standard (link both merged PRs) — its gate/backfill work can assume `ui/designs/` exists.
|
||||
- [ ] **Step 2: Close ecomm#8** with a comment naming both merged PRs.
|
||||
- [ ] **Step 3: Checkpoint the transcript** (`publish-transcript.sh` on the `--INPROGRESS` file).
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wiggleverse-ecomm-frontend",
|
||||
"version": "0.2.0",
|
||||
"version": "0.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wiggleverse-ecomm-frontend",
|
||||
"version": "0.2.0",
|
||||
"version": "0.4.0",
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { cooldownAppliesTo } from "./cooldown";
|
||||
|
||||
describe("resend cooldown keying (SD-0001 §5.2, INV-3; bug #20)", () => {
|
||||
it("no cooldown running -> send not blocked", () => {
|
||||
expect(cooldownAppliesTo("a@example.com", null, 0)).toBe(false);
|
||||
});
|
||||
|
||||
it("cooldown running for the same address -> blocked", () => {
|
||||
expect(cooldownAppliesTo("a@example.com", "a@example.com", 31)).toBe(true);
|
||||
});
|
||||
|
||||
it("same address modulo case/whitespace -> still blocked", () => {
|
||||
expect(cooldownAppliesTo(" A@Example.COM ", "a@example.com", 31)).toBe(true);
|
||||
});
|
||||
|
||||
it("cooldown running but the input is a different address -> not blocked", () => {
|
||||
expect(cooldownAppliesTo("right@example.com", "wrong@example.com", 31)).toBe(false);
|
||||
});
|
||||
|
||||
it("cooldown expired -> not blocked even for the same address", () => {
|
||||
expect(cooldownAppliesTo("a@example.com", "a@example.com", 0)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
// Resend-cooldown keying (SD-0001 §5.2, INV-3; bug #20). The server's cooldown is
|
||||
// per-address, so the client countdown must be too: it blocks sending only while it
|
||||
// is running AND the current input is the address it was set for. Normalization
|
||||
// mirrors the backend's normalize_email (lowercase + strip, INV-2).
|
||||
|
||||
function normalize(email: string): string {
|
||||
return email.trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function cooldownAppliesTo(
|
||||
input: string,
|
||||
cooldownEmail: string | null,
|
||||
secondsLeft: number,
|
||||
): boolean {
|
||||
if (secondsLeft <= 0 || cooldownEmail === null) return false;
|
||||
return normalize(input) === normalize(cooldownEmail);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
// is handled by App on success. Visuals per the ui/designs export (hf-signin).
|
||||
import { useEffect, useState } from "react";
|
||||
import { requestCode, verifyCode, type ApiError, type VerifyResult } from "../api";
|
||||
import { cooldownAppliesTo } from "../cooldown";
|
||||
import CodeInput from "../ui/CodeInput";
|
||||
import { AuthCard, Banner, Field, Footer, PrimaryButton, Screen, TopBar, Wordmark } from "../ui/kit";
|
||||
|
||||
@@ -34,6 +35,9 @@ export default function SignIn({ door, onAuthed, onBack }: Props) {
|
||||
const [codeError, setCodeError] = useState<string | null>(null);
|
||||
const [bannerError, setBannerError] = useState<ApiError | null>(null);
|
||||
const [cooldown, setCooldown] = useCooldown();
|
||||
// The address the running cooldown belongs to — the server's cooldown is per-address
|
||||
// (INV-3), so a different address must not be blocked by it (bug #20).
|
||||
const [cooldownFor, setCooldownFor] = useState<string | null>(null);
|
||||
|
||||
function applyError(err: ApiError) {
|
||||
setFieldError(null);
|
||||
@@ -53,9 +57,11 @@ export default function SignIn({ door, onAuthed, onBack }: Props) {
|
||||
setBusy(false);
|
||||
if (err) {
|
||||
applyError(err);
|
||||
if (err.code === "resend_cooldown") setCooldownFor(email);
|
||||
return err.code === "resend_cooldown"; // a cooldown still means a code is out there
|
||||
}
|
||||
setCooldown(60);
|
||||
setCooldownFor(email);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -131,8 +137,12 @@ export default function SignIn({ door, onAuthed, onBack }: Props) {
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<PrimaryButton busy={busy} disabled={cooldown > 0}>
|
||||
{busy ? "Sending…" : cooldown > 0 ? `Resend in ${cooldown}s` : "Send code"}
|
||||
<PrimaryButton busy={busy} disabled={cooldownAppliesTo(email, cooldownFor, cooldown)}>
|
||||
{busy
|
||||
? "Sending…"
|
||||
: cooldownAppliesTo(email, cooldownFor, cooldown)
|
||||
? `Resend in ${cooldown}s`
|
||||
: "Send code"}
|
||||
</PrimaryButton>
|
||||
<p className="note">
|
||||
We'll email you a one-time code. That's all we need — no password, ever.
|
||||
|
||||
Reference in New Issue
Block a user