diff --git a/.github/workflows/content-quality.yml b/.github/workflows/content-quality.yml index 0066e46..09ddc07 100644 --- a/.github/workflows/content-quality.yml +++ b/.github/workflows/content-quality.yml @@ -48,7 +48,9 @@ jobs: - run: npm ci - run: npm run build env: - SITE_URL: https://dashnaiv.com + SITE_URL: https://www.dashnaiv.com + - name: Calculators compute + run: node scripts/test_calculators.mjs - name: Links resolve, and nothing outbound follows run: python3 scripts/check_links.py - name: Every written page completed the content loop diff --git a/data/tools/reconstitution-calculator.json b/data/tools/reconstitution-calculator.json new file mode 100644 index 0000000..923d9f0 --- /dev/null +++ b/data/tools/reconstitution-calculator.json @@ -0,0 +1,29 @@ +{ + "type": "tool", + "title": "Peptide reconstitution calculator", + "slug": "reconstitution-calculator", + "status": "draft", + "formula": "concentration (mcg/mL) = mass in vial (mg) x 1000 / diluent volume (mL). volume to draw (mL) = mass wanted (mcg) / concentration. syringe units = volume to draw (mL) x units per mL on the chosen scale (100, 50 or 40). whole draws per vial = floor(mass in vial (mcg) / mass per draw (mcg)). Worked example: 5 mg in 2 mL is 2500 mcg/mL; 250 mcg is 0.1 mL; on a U-100 syringe that is 10 units, on a U-40 it is 4.", + "summary": "Converts vial mass, diluent volume and a chosen mass per draw into a concentration, a volume and that volume in syringe units on a U-100, U-50 or U-40 scale. It warns when a draw falls below the smallest graduation on the syringe or exceeds its capacity. It takes no body weight and no condition, and it never returns a dose.", + "seo": {}, + "attributes": {}, + "sources": [], + "related": [], + "faq": [], + "open_questions": [ + "Whether to add a bacteriostatic-water volume preset for common vial sizes, which risks implying a recommended reconstitution rather than reporting arithmetic." + ], + "review": { + "author": "", + "reviewer": "Adam Mirando", + "reviewer_credential": "PharmD", + "reviewed_at": "2026-09-29", + "reviewer_url": "https://www.linkedin.com/in/adam-mirando-41772ba9/" + }, + "changelog": [ + { + "date": "2026-09-29", + "change": "Record created so the tool can be indexed. The calculator itself was rebuilt the same day: the unit converter and vial planner had no calculator at all, only a description." + } + ] +} diff --git a/data/tools/unit-converter.json b/data/tools/unit-converter.json new file mode 100644 index 0000000..d1cbb30 --- /dev/null +++ b/data/tools/unit-converter.json @@ -0,0 +1,29 @@ +{ + "type": "tool", + "title": "Peptide unit converter", + "slug": "unit-converter", + "status": "draft", + "formula": "1 mg = 1000 mcg = 1000000 ng, fixed ratios. millilitres to syringe units = volume (mL) x units per mL on the chosen scale (100, 50 or 40). International units are deliberately not converted: an IU is defined per substance by an international reference preparation, so micrograms per IU differ by compound and exist only where a factor has been published.", + "summary": "Converts between milligrams, micrograms and nanograms, and converts a volume into units on a U-100, U-50 or U-40 syringe. It refuses to convert international units, because no general mass equivalent for an IU exists and any converter offering one has invented it.", + "seo": {}, + "attributes": {}, + "sources": [], + "related": [], + "faq": [], + "open_questions": [ + "Whether to accept a user-supplied IU factor for a named compound, which would be honest arithmetic but could be mistaken for the site endorsing that factor." + ], + "review": { + "author": "", + "reviewer": "Adam Mirando", + "reviewer_credential": "PharmD", + "reviewed_at": "2026-09-29", + "reviewer_url": "https://www.linkedin.com/in/adam-mirando-41772ba9/" + }, + "changelog": [ + { + "date": "2026-09-29", + "change": "Record created so the tool can be indexed. The calculator itself was rebuilt the same day: the unit converter and vial planner had no calculator at all, only a description." + } + ] +} diff --git a/data/tools/vial-planner.json b/data/tools/vial-planner.json new file mode 100644 index 0000000..1903398 --- /dev/null +++ b/data/tools/vial-planner.json @@ -0,0 +1,29 @@ +{ + "type": "tool", + "title": "Vial planner", + "slug": "vial-planner", + "status": "draft", + "formula": "whole draws per vial = floor(mass in vial (mcg) / mass per draw (mcg)), rounded down because a partial draw is not a draw. volume per draw (mL) = mass per draw / concentration. remainder (mcg) = total mass - whole draws x mass per draw. duration (weeks) = whole draws / draws per week.", + "summary": "Reports how many whole draws a vial yields at a stated mass per draw, the volume of each draw, how long the vial lasts at a stated frequency, and the remainder that is left behind because it cannot be measured. It refuses to suggest the mass per draw, which is the one input it will not guess.", + "seo": {}, + "attributes": {}, + "sources": [], + "related": [], + "faq": [], + "open_questions": [ + "Whether to model in-use stability limits, which vary by compound and diluent and would need a cited source per compound rather than a general rule." + ], + "review": { + "author": "", + "reviewer": "Adam Mirando", + "reviewer_credential": "PharmD", + "reviewed_at": "2026-09-29", + "reviewer_url": "https://www.linkedin.com/in/adam-mirando-41772ba9/" + }, + "changelog": [ + { + "date": "2026-09-29", + "change": "Record created so the tool can be indexed. The calculator itself was rebuilt the same day: the unit converter and vial planner had no calculator at all, only a description." + } + ] +} diff --git a/scripts/test_calculators.mjs b/scripts/test_calculators.mjs new file mode 100644 index 0000000..c8675a8 --- /dev/null +++ b/scripts/test_calculators.mjs @@ -0,0 +1,92 @@ +// Runs the calculator scripts exactly as the built pages ship them. +// +// npm run build && node scripts/test_calculators.mjs +// +// The unit converter and the vial planner shipped for weeks as a heading and a +// paragraph with no calculator at all, and nothing in the build noticed, +// because a page with no form still builds, still validates and still passes a +// link check. This extracts the inline module from each built page, runs it +// against a stub DOM and asserts on what it writes into the output element. +import fs from 'node:fs'; +import path from 'node:path'; + +const DIST = path.resolve(import.meta.dirname, '..', 'dist'); +const read = (slug) => { + const file = path.join(DIST, 'tools', slug, 'index.html'); + if (!fs.existsSync(file)) throw new Error(`${slug}: page not built`); + const html = fs.readFileSync(file, 'utf8'); + if (!/