From caa18c671cfe380aa33fe3eae9907ff0a3fd9e84 Mon Sep 17 00:00:00 2001 From: Igor Serebryany Date: Fri, 7 Nov 2025 19:08:27 -0800 Subject: [PATCH 01/12] feat: prod sql commands --- mise.toml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/mise.toml b/mise.toml index da32e11..9fc3dec 100644 --- a/mise.toml +++ b/mise.toml @@ -79,6 +79,33 @@ description = "Remove the Cloud Run service and migration job" run = "gcloud beta run services logs tail prod-app --project=csheet-475917 --region=us-central1" description = "Tail production Cloud Run logs" +[tasks."db:prod:psql"] +run = "gcloud sql connect app-db-7205418 --project=csheet-475917 --database=csheet --user=app" +description = "Connect to production database (interactive psql)" + +[tasks."db:prod:proxy"] +run = """ + echo "Starting Cloud SQL Proxy for production database..." + echo "Connection will be available at: localhost:5433" + echo "Database: csheet" + echo "User: app" + echo "" + echo "Connect with DBeaver using:" + echo " Host: localhost" + echo " Port: 5433" + echo " Database: csheet" + echo " Username: app" + echo " Password: (retrieve from Secret Manager)" + echo "" + echo "To get the password, run:" + echo " gcloud secrets versions access latest --secret=prod-postgres-password --project=csheet-475917" + echo "" + echo "Press Ctrl+C to stop the proxy" + echo "" + gcloud sql instances describe app-db-7205418 --project=csheet-475917 --format="value(connectionName)" | xargs -I {} cloud-sql-proxy {} --port 5433 +""" +description = "Start Cloud SQL Proxy tunnel to production database (localhost:5433)" + [tasks.install] run = "bun install" description = "Install dependencies" From 3117a1d0c86da4feefeb05b63f2084f9d835f5b1 Mon Sep 17 00:00:00 2001 From: Igor Serebryany Date: Fri, 7 Nov 2025 19:13:30 -0800 Subject: [PATCH 02/12] infra: give the db a public ip allows the proxy commands to work so i can look at the db from my laptop --- pulumi/infra/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pulumi/infra/index.ts b/pulumi/infra/index.ts index 503cc98..56e45db 100644 --- a/pulumi/infra/index.ts +++ b/pulumi/infra/index.ts @@ -230,13 +230,15 @@ const sqlInstance = new gcp.sql.DatabaseInstance( databaseVersion: "POSTGRES_16", project, region, + deletionProtection: stack === "prod" ? true : undefined, settings: { tier: dbTier, availabilityType: stack === "prod" ? "REGIONAL" : undefined, edition: stack === "prod" ? "ENTERPRISE" : undefined, ipConfiguration: { privateNetwork: network.id, - ipv4Enabled: false, + ipv4Enabled: stack === "prod", + authorizedNetworks: stack === "prod" ? [] : undefined, }, backupConfiguration: { enabled: true, @@ -245,7 +247,8 @@ const sqlInstance = new gcp.sql.DatabaseInstance( location: "us", transactionLogRetentionDays: 7, backupRetentionSettings: { - retainedBackups: stack === "prod" ? 90 : 7, + retainedBackups: stack === "prod" ? 30 : 7, + retentionUnit: stack === "prod" ? "COUNT" : undefined, }, }, diskAutoresize: true, From c3cfc5f5cbe59f30f21ba5bd6889569b290617f6 Mon Sep 17 00:00:00 2001 From: Igor Serebryany Date: Sun, 9 Nov 2025 09:16:02 -0800 Subject: [PATCH 03/12] refactor: correct form change delay we want a delay, so the submit request can cancel a validation request. otherwise, here's not enough time. we kill the delay in forms we haven't configured correctly yet. we figure out a way to handle from error hiding -- it should be the UIs job, not the service layers --- src/components/AbilitiesEditForm.tsx | 2 +- src/components/CastSpellForm.tsx | 2 +- src/components/ChargeManagementForm.tsx | 2 +- src/components/ClassEditForm.tsx | 2 +- src/components/CoinsEditForm.tsx | 2 +- src/components/CreateItemForm.tsx | 2 ++ src/components/HitDiceEditForm.tsx | 19 +++++++++++----- src/components/HitPointsEditForm.tsx | 17 +++++++++----- src/components/LearnSpellForm.tsx | 2 +- src/components/LongRestForm.tsx | 2 +- src/components/PrepareSpellForm.tsx | 2 +- src/components/ShortRestForm.tsx | 2 +- src/components/SkillsEditForm.tsx | 2 +- src/components/SpellSlotsEditForm.tsx | 2 +- src/components/TraitEditForm.tsx | 2 +- src/lib/formErrors.ts | 20 +++++++++++++++++ src/services/updateHitDice.ts | 18 +++++++-------- src/services/updateHitPoints.ts | 30 +++++++++++-------------- 18 files changed, 79 insertions(+), 51 deletions(-) diff --git a/src/components/AbilitiesEditForm.tsx b/src/components/AbilitiesEditForm.tsx index 04026bd..2891eae 100644 --- a/src/components/AbilitiesEditForm.tsx +++ b/src/components/AbilitiesEditForm.tsx @@ -133,7 +133,7 @@ export const AbilitiesEditForm = ({ character, values, errors = {} }: AbilitiesE id="abilities-edit-form" hx-post={`/characters/${character.id}/edit/abilities`} hx-vals='{"is_check": "true"}' - hx-trigger="change delay:300ms" + hx-trigger="change" hx-target="#editModalContent" hx-swap="innerHTML" class="needs-validation" diff --git a/src/components/CastSpellForm.tsx b/src/components/CastSpellForm.tsx index 8cad22c..119a00d 100644 --- a/src/components/CastSpellForm.tsx +++ b/src/components/CastSpellForm.tsx @@ -88,7 +88,7 @@ export const CastSpellForm = ({ character, values = {}, errors = {} }: CastSpell id="cast-spell-form" hx-post={`/characters/${character.id}/castspell`} hx-vals='{"is_check": "true"}' - hx-trigger="change delay:300ms" + hx-trigger="change" hx-target="#editModalContent" hx-swap="innerHTML" class="needs-validation" diff --git a/src/components/ChargeManagementForm.tsx b/src/components/ChargeManagementForm.tsx index 3f7f794..bdacaa4 100644 --- a/src/components/ChargeManagementForm.tsx +++ b/src/components/ChargeManagementForm.tsx @@ -47,7 +47,7 @@ export const ChargeManagementForm = ({ id="charge-edit-form" hx-post={`/characters/${characterId}/items/${item.id}/charges`} hx-vals='{"is_check": "true"}' - hx-trigger="change delay:300ms" + hx-trigger="change" hx-target="#editModalContent" hx-swap="innerHTML" class="needs-validation" diff --git a/src/components/ClassEditForm.tsx b/src/components/ClassEditForm.tsx index d328cfd..4a11466 100644 --- a/src/components/ClassEditForm.tsx +++ b/src/components/ClassEditForm.tsx @@ -76,7 +76,7 @@ export const ClassEditForm = ({ character, values, errors }: ClassEditFormProps) id="class-edit-form" hx-post={`/characters/${character.id}/edit/class`} hx-vals='{"is_check": "true"}' - hx-trigger="change delay:300ms" + hx-trigger="change" hx-target="#editModalContent" hx-swap="innerHTML" class="needs-validation" diff --git a/src/components/CoinsEditForm.tsx b/src/components/CoinsEditForm.tsx index cd53209..c42dec1 100644 --- a/src/components/CoinsEditForm.tsx +++ b/src/components/CoinsEditForm.tsx @@ -121,7 +121,7 @@ export const CoinsEditForm = ({ character, values = {}, errors = {} }: CoinsEdit id="coins-edit-form" hx-post={`/characters/${character.id}/edit/coins`} hx-vals='{"is_check": "true"}' - hx-trigger="change delay:300ms" + hx-trigger="change" hx-target="#editModalContent" hx-swap="innerHTML" class="needs-validation" diff --git a/src/components/CreateItemForm.tsx b/src/components/CreateItemForm.tsx index 1d1b093..6155371 100644 --- a/src/components/CreateItemForm.tsx +++ b/src/components/CreateItemForm.tsx @@ -211,6 +211,7 @@ export const CreateItemForm = ({ character, values, errors }: CreateItemFormProp - - - ))} - + + {/* Search input */} + + + {/* Results count */} + {showSearchResults && ( + + Showing {filteredSpells.length} of {availableSpells.length} spells + + )} + + {/* Spell list */} + {filteredSpells.length === 0 ? ( +
No spells match your search.
+ ) : ( +
+ {filteredSpells.map((spell) => ( +
+ + +
+ ))} +
+ )} + {error &&
{error}
} ) From 874d14dd655ccd05f722aae62519ed937695854d Mon Sep 17 00:00:00 2001 From: Igor Serebryany Date: Tue, 11 Nov 2025 15:02:18 -0800 Subject: [PATCH 12/12] feat: use ideomorph in most of my forms it definitely helps in learnspell and preparespell form with input fields. not sure if it helps in any other case. we added a bunch of ids for ideomorph to hook into anyway. --- .gitignore | 1 + Dockerfile | 2 +- bun.lock | 3 +++ package.json | 3 ++- src/components/AbilitiesEditForm.tsx | 4 ++-- src/components/CastSpellForm.tsx | 4 ++-- src/components/CharacterImport.tsx | 17 +++++++++-------- src/components/CharacterNew.tsx | 3 ++- src/components/ChargeManagementForm.tsx | 4 ++-- src/components/ClassEditForm.tsx | 7 ++++--- src/components/CoinsEditForm.tsx | 7 ++++--- src/components/CreateItemForm.tsx | 16 +++++++++------- src/components/EditItemForm.tsx | 14 +++++++++----- src/components/HitDiceEditForm.tsx | 13 +++++++------ src/components/HitPointsEditForm.tsx | 4 ++-- src/components/Layout.tsx | 1 + src/components/LearnSpellForm.tsx | 7 ++++--- src/components/LongRestForm.tsx | 4 ++-- src/components/PrepareSpellForm.tsx | 4 ++-- src/components/SessionNotes.tsx | 2 +- src/components/ShortRestForm.tsx | 4 ++-- src/components/SkillsEditForm.tsx | 4 ++-- src/components/SpellSlotsEditForm.tsx | 4 ++-- src/components/TraitEditForm.tsx | 3 ++- src/components/ui/SpellPicker.tsx | 1 + 25 files changed, 78 insertions(+), 58 deletions(-) diff --git a/.gitignore b/.gitignore index aee8305..65ee0c1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules/ # statically served FE dependencies static/htmx.min.js static/htmx-ext-sse.js +static/idiomorph-ext.min.js # output out diff --git a/Dockerfile b/Dockerfile index c5ac7e1..873e2ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,7 @@ COPY --from=deps /app/node_modules ./node_modules COPY . . # Then overlay the generated htmx files from deps stage (after postinstall) -COPY --from=deps /app/static/htmx.min.js /app/static/htmx-ext-sse.js ./static/ +COPY --from=deps /app/static/htmx.min.js /app/static/htmx-ext-sse.js /app/static/idiomorph-ext.min.js ./static/ ENV NODE_ENV=production \ PORT=3000 diff --git a/bun.lock b/bun.lock index b1320de..f3df0ab 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "hono": "^4.9.6", "htmx-ext-sse": "^2.2.4", "htmx.org": "^2.0.8", + "idiomorph": "^0.7.4", "marked": "^16.4.1", "nodemailer": "^7.0.10", "ulid": "^3.0.1", @@ -599,6 +600,8 @@ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "idiomorph": ["idiomorph@0.7.4", "", {}, "sha512-uCdSpLo3uMfqOmrwXTpR1k/sq4sSmKC7l4o/LdJOEU+MMMq+wkevRqOQYn3lP7vfz9Mv+USBEqPvi0XhdL9ENw=="], + "ignore-walk": ["ignore-walk@6.0.5", "", { "dependencies": { "minimatch": "^9.0.0" } }, "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A=="], "import-in-the-middle": ["import-in-the-middle@1.15.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA=="], diff --git a/package.json b/package.json index 2bd1f4e..563b1df 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "type": "module", "private": true, "scripts": { - "postinstall": "mkdir -p static && cp node_modules/htmx.org/dist/htmx.min.js static/ && cp node_modules/htmx-ext-sse/sse.js static/htmx-ext-sse.js" + "postinstall": "mkdir -p static && cp node_modules/htmx.org/dist/htmx.min.js static/ && cp node_modules/htmx-ext-sse/sse.js static/htmx-ext-sse.js && cp node_modules/idiomorph/dist/idiomorph-ext.min.js static/idiomorph-ext.min.js" }, "devDependencies": { "@biomejs/biome": "^2.2.6", @@ -40,6 +40,7 @@ "hono": "^4.9.6", "htmx-ext-sse": "^2.2.4", "htmx.org": "^2.0.8", + "idiomorph": "^0.7.4", "marked": "^16.4.1", "nodemailer": "^7.0.10", "ulid": "^3.0.1", diff --git a/src/components/AbilitiesEditForm.tsx b/src/components/AbilitiesEditForm.tsx index 2891eae..d8a597a 100644 --- a/src/components/AbilitiesEditForm.tsx +++ b/src/components/AbilitiesEditForm.tsx @@ -135,7 +135,7 @@ export const AbilitiesEditForm = ({ character, values, errors = {} }: AbilitiesE hx-vals='{"is_check": "true"}' hx-trigger="change" hx-target="#editModalContent" - hx-swap="innerHTML" + hx-swap="morph:innerHTML" class="needs-validation" novalidate > @@ -183,7 +183,7 @@ export const AbilitiesEditForm = ({ character, values, errors = {} }: AbilitiesE hx-post={`/characters/${character.id}/edit/abilities`} hx-vals='{"is_check": "false"}' hx-target="#editModalContent" - hx-swap="innerHTML" + hx-swap="morph:innerHTML" > Update Abilities diff --git a/src/components/CastSpellForm.tsx b/src/components/CastSpellForm.tsx index 119a00d..f2e8da8 100644 --- a/src/components/CastSpellForm.tsx +++ b/src/components/CastSpellForm.tsx @@ -90,7 +90,7 @@ export const CastSpellForm = ({ character, values = {}, errors = {} }: CastSpell hx-vals='{"is_check": "true"}' hx-trigger="change" hx-target="#editModalContent" - hx-swap="innerHTML" + hx-swap="morph:innerHTML" class="needs-validation" novalidate > @@ -175,7 +175,7 @@ export const CastSpellForm = ({ character, values = {}, errors = {} }: CastSpell hx-post={`/characters/${character.id}/castspell`} hx-vals='{"is_check": "false"}' hx-target="#editModalContent" - hx-swap="innerHTML" + hx-swap="morph:innerHTML" > Cast {spell.name} diff --git a/src/components/CharacterImport.tsx b/src/components/CharacterImport.tsx index 7affb31..ef4787f 100644 --- a/src/components/CharacterImport.tsx +++ b/src/components/CharacterImport.tsx @@ -74,7 +74,7 @@ const MultiClassSelector = ({ values = {}, errors = {} }: MultiClassSelectorProp @@ -85,7 +85,7 @@ const MultiClassSelector = ({ values = {}, errors = {} }: MultiClassSelectorProp {isSelected && ( @@ -197,7 +197,7 @@ const MaxHPInput = ({ values = {}, errors = {} }: MaxHPInputProps) => { @@ -514,6 +514,7 @@ export const CharacterImport = ({ values = {}, errors = {} }: CharacterImportPro
diff --git a/src/components/ClassEditForm.tsx b/src/components/ClassEditForm.tsx index 4a11466..42607c4 100644 --- a/src/components/ClassEditForm.tsx +++ b/src/components/ClassEditForm.tsx @@ -78,7 +78,7 @@ export const ClassEditForm = ({ character, values, errors }: ClassEditFormProps) hx-vals='{"is_check": "true"}' hx-trigger="change" hx-target="#editModalContent" - hx-swap="innerHTML" + hx-swap="morph:innerHTML" class="needs-validation" novalidate > @@ -146,7 +146,7 @@ export const ClassEditForm = ({ character, values, errors }: ClassEditFormProps) "bg-secondary-subtle text-muted": hitDieReadonly, "is-invalid": errors?.hit_die_roll, })} - id="hit_die_roll" + id="classedit-hit-die-roll" name="hit_die_roll" value={hitDieValue} min="1" @@ -180,11 +180,12 @@ export const ClassEditForm = ({ character, values, errors }: ClassEditFormProps) diff --git a/src/components/CoinsEditForm.tsx b/src/components/CoinsEditForm.tsx index c42dec1..246f10b 100644 --- a/src/components/CoinsEditForm.tsx +++ b/src/components/CoinsEditForm.tsx @@ -123,7 +123,7 @@ export const CoinsEditForm = ({ character, values = {}, errors = {} }: CoinsEdit hx-vals='{"is_check": "true"}' hx-trigger="change" hx-target="#editModalContent" - hx-swap="innerHTML" + hx-swap="morph:innerHTML" class="needs-validation" novalidate > @@ -142,7 +142,7 @@ export const CoinsEditForm = ({ character, values = {}, errors = {} }: CoinsEdit diff --git a/src/components/CreateItemForm.tsx b/src/components/CreateItemForm.tsx index e555086..0e245fd 100644 --- a/src/components/CreateItemForm.tsx +++ b/src/components/CreateItemForm.tsx @@ -161,7 +161,7 @@ export const CreateItemForm = ({ character, values, errors }: CreateItemFormProp hx-vals='{"is_check": "true"}' hx-trigger="change" hx-target="#editModalContent" - hx-swap="innerHTML" + hx-swap="morph:innerHTML" class="modal-body needs-validation" > {/* General error message */} @@ -209,8 +209,7 @@ export const CreateItemForm = ({ character, values, errors }: CreateItemFormProp Category * + > + {values.note || ""} + {errors?.note &&
{errors.note}
}
@@ -719,11 +720,12 @@ export const CreateItemForm = ({ character, values, errors }: CreateItemFormProp diff --git a/src/components/HitDiceEditForm.tsx b/src/components/HitDiceEditForm.tsx index 0a382fe..815d120 100644 --- a/src/components/HitDiceEditForm.tsx +++ b/src/components/HitDiceEditForm.tsx @@ -65,9 +65,9 @@ export const HitDiceEditForm = ({ id="hitdice-edit-form" hx-post={`/characters/${characterId}/edit/hitdice`} hx-vals='{"is_check": "true"}' - hx-trigger="change delay:100ms" + hx-trigger="change" hx-target="#editModalContent" - hx-swap="innerHTML" + hx-swap="morph:innerHTML" class="needs-validation" novalidate > @@ -121,7 +121,7 @@ export const HitDiceEditForm = ({ @@ -169,7 +169,7 @@ export const HitDiceEditForm = ({ diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index d7516e8..88e969e 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -83,6 +83,7 @@ export const Layout = ({ + diff --git a/src/components/LearnSpellForm.tsx b/src/components/LearnSpellForm.tsx index 2dcb715..4339dc4 100644 --- a/src/components/LearnSpellForm.tsx +++ b/src/components/LearnSpellForm.tsx @@ -39,7 +39,7 @@ function LearnSpellFormBody({ character, values = {}, errors = {} }: LearnSpellF hx-vals='{"is_check": "true"}' hx-trigger="input from:[name='spell_search'] changed delay:300ms, change" hx-target="#editModalContent" - hx-swap="innerHTML" + hx-swap="morph:innerHTML" class="needs-validation" novalidate > @@ -83,7 +83,7 @@ function LearnSpellFormBody({ character, values = {}, errors = {} }: LearnSpellF