Compare commits

...

7 Commits

Author SHA1 Message Date
ben.stull 118e925580 fix(signin): key the resend cooldown to the address it was set for (#20)
ci / check (push) Has been cancelled
ci / check (pull_request) Has been cancelled
The email-step Send button disabled on any running cooldown, so a merchant
who mistyped their address was locked out of sending to the corrected one
for the rest of the 60s window — a guard the server never had: request_code
enforces the cooldown per address (SD-0001 INV-3 -> PUC-2c).

Track which address the running cooldown belongs to (cooldownFor) and gate
the email-step button through cooldownAppliesTo(), which blocks only while
the countdown runs AND the input normalizes (strip+lowercase, mirroring
normalize_email / INV-2) to that address. Same address still shows the
honest 'Resend in Ns'; any other address sends immediately. The code-step
resend is unchanged. Verified in the live UI (wrong address -> Wrong
address? -> corrected address sends at once).

package-lock.json: sync recorded version with package.json (0.4.0).

Closes #20

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 01:21:23 -07:00
ben.stull 11acb3a5b1 Merge pull request 'docs: land session 0023 plan doc (stray untracked file)' (#18) from docs-archive-0023-plan into main
ci / check (push) Has been cancelled
2026-06-11 07:56:34 +00:00
ben.stull efc9c8edb9 docs: land session 0023's ui-designs-collection plan in-repo (was untracked; content archive already had it)
ci / check (pull_request) Has been cancelled
ci / check (push) Has been cancelled
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 00:56:31 -07:00
ben.stull f7b76d5370 Merge pull request 'SLICE-4: record the first PPE bootstrap rehearsal (PUC-11) — green' (#17) from slice-4-rehearsal-record into main
ci / check (push) Has been cancelled
2026-06-11 07:54:50 +00:00
ben.stull 4bb9763633 docs(slice-4): record the first PPE bootstrap rehearsal (PUC-11, 2026-06-11) — green
ci / check (pull_request) Has been cancelled
ci / check (push) Has been cancelled
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 00:54:47 -07:00
ben.stull a456d51f17 Merge pull request 'SLICE-4: deployment.toml — the ecomm PPE record' (#11) from slice-4-deployment-record into main
ci / check (push) Has been cancelled
2026-06-11 06:45:08 +00:00
ben.stull ac3c4ffe36 feat(slice-4): deployment.toml — the ecomm PPE record (launch-app §5.1; Cloud SQL via secret ref)
ci / check (push) Has been cancelled
ci / check (pull_request) Has been cancelled
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:45:05 -07:00
7 changed files with 227 additions and 4 deletions
+42
View File
@@ -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"
+12
View File
@@ -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 1014)
- [ ] **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).
+2 -2
View File
@@ -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"
+24
View File
@@ -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);
});
});
+17
View File
@@ -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);
}
+12 -2
View File
@@ -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.