112 tested
All checks were successful
Deploy / build-deploy (push) Successful in 3m21s

This commit is contained in:
Jorijn van der Graaf 2026-08-18 14:52:49 +02:00
commit 593bdb17d4
6 changed files with 28 additions and 16 deletions

View file

@ -73,10 +73,13 @@ export const std::vector<Product>& Products() {
p.image = "/fp6-pmos.jpg"; p.image = "/fp6-pmos.jpg";
p.summary = "The Fairphone (Gen. 6), reflashed by Catcrafts to run postmarketOS with the patches and IMS/VoLTE implementation Catcrafts maintains. All of that software is open source. You can download it and flash a Fairphone yourself, and you are welcome to. What you pay for here is the thing open source doesn't come with: real support. A phone that arrives working, and one email address that answers for it. Not a forum, not a git issue, but support like any other manufacturer offers. And the margin funds the development itself."; p.summary = "The Fairphone (Gen. 6), reflashed by Catcrafts to run postmarketOS with the patches and IMS/VoLTE implementation Catcrafts maintains. All of that software is open source. You can download it and flash a Fairphone yourself, and you are welcome to. What you pay for here is the thing open source doesn't come with: real support. A phone that arrives working, and one email address that answers for it. Not a forum, not a git issue, but support like any other manufacturer offers. And the margin funds the development itself.";
p.warranty = "Two years from Catcrafts, worldwide, one counter: every claim goes to Catcrafts, whatever turns out to be broken. The software (postmarketOS, the patches, imsd) is Catcrafts' own work and is fixed by Catcrafts with updates, delivered over the air. Even a phone that no longer boots is normally recovered in place: as long as fastboot still comes up, Catcrafts walks you through reflashing it over a USB cable in minutes. Only a phone that shows nothing at all, not even fastboot, travels for a software fault. For a hardware fault Catcrafts takes the phone back and handles the manufacturer's process, including the temporary reflash to stock Android it requires, and returns it running postmarketOS. When a phone does have to travel, warranty shipping is paid by Catcrafts, both directions, worldwide. User inflicted damage, including software faults caused by relocking the bootloader do not fall under this policy. The full terms are on the terms page."; p.warranty = "Two years from Catcrafts, worldwide, one counter: every claim goes to Catcrafts, whatever turns out to be broken. The software (postmarketOS, the patches, imsd) is Catcrafts' own work and is fixed by Catcrafts with updates, delivered over the air. Even a phone that no longer boots is normally recovered in place: as long as fastboot still comes up, Catcrafts walks you through reflashing it over a USB cable in minutes. Only a phone that shows nothing at all, not even fastboot, travels for a software fault. For a hardware fault Catcrafts takes the phone back and handles the manufacturer's process, including the temporary reflash to stock Android it requires, and returns it running postmarketOS. When a phone does have to travel, warranty shipping is paid by Catcrafts, both directions, worldwide. User inflicted damage, including software faults caused by relocking the bootloader do not fall under this policy. The full terms are on the terms page.";
// The one claim on this page that is about safety rather than // No safety note. There was one — the emergency-calling caveat — and
// features. It stays until emergency calling has been verified on a // it came off on 2026-08-18, when imsd placed calls to 112 on a live
// real network — removing it is a decision, not a cleanup. // network in a test session approved by TB112 (the Dutch 1-1-2
p.safetyNote = "Emergency calling (112/911) is implemented in imsd, including carrier-broadcast emergency numbers, but not yet verified against a live network."; // operator). That verification is exactly what the caveat was waiting
// for, so the claim it hedged is now a plain fact and the warning box
// would only mislead. The field stays in the model for the next real
// safety claim; leaving it empty is the decision, not an oversight.
p.variants = { p.variants = {
// supplier €513.30 incl VAT // supplier €513.30 incl VAT
{ "green", "Forest Green", 51330 + kMarkupMinor }, { "green", "Forest Green", 51330 + kMarkupMinor },

View file

@ -226,10 +226,11 @@ export struct Product {
std::string image; std::string image;
std::string summary; std::string summary;
std::string warranty; std::string warranty;
// Rendered as a prominent warning box when non-empty. Exists for exactly // Rendered as a prominent warning box when non-empty. No product sets it
// one thing today: the emergency-calling caveat — the single claim on the // today: it existed for the emergency-calling caveat, which came off once
// page that is about safety rather than features, which is why it gets a // 112 was verified on a live network. It stays because the next safety
// warning box instead of a table row. // claim — something about safety rather than features — belongs in a
// warning box above the fold, not in a spec-table row.
std::string safetyNote; std::string safetyNote;
std::vector<Spec> specs; std::vector<Spec> specs;

View file

@ -1279,8 +1279,7 @@ export RenderedPage RenderProduct(const Product& product,
R"(<h2 class="section__title">Specifications</h2>)" R"(<h2 class="section__title">Specifications</h2>)"
R"(<p class="section__lede">The hardware is a stock Fairphone )" R"(<p class="section__lede">The hardware is a stock Fairphone )"
R"((Gen. 6), unmodified. Fairphone's spec sheet is this product's spec sheet, )" R"((Gen. 6), unmodified. Fairphone's spec sheet is this product's spec sheet, )"
R"(and all of it works under postmarketOS. The one caveat is the )" R"(and all of it works under postmarketOS.</p>)"
R"(emergency-calling warning above.</p>)"
R"(<table class="spec-table"><tbody>{}</tbody></table>)" R"(<table class="spec-table"><tbody>{}</tbody></table>)"
R"(</section>)", R"(</section>)",
Join(specRows)); Join(specRows));

View file

@ -964,8 +964,9 @@ treatment it replaces, which read as the wrong category for the work.
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
/* The emergency-calling note on the product page. Warn colours rather than /* A product's safety note. Warn colours rather than danger: the box flags a
danger: it flags an unverified path, not a known-broken one. */ caveat, not a known-broken feature. Unused since the emergency-calling
caveat came off kept with the renderer that would use it again. */
.notice--warn { .notice--warn {
color: var(--warn); color: var(--warn);
border-color: color-mix(in oklab, var(--warn) 40%, transparent); border-color: color-mix(in oklab, var(--warn) 40%, transparent);

View file

@ -66,7 +66,11 @@ int main(int argc, char** argv) {
// The image file itself is Caddy's to serve (static asset), so its // The image file itself is Caddy's to serve (static asset), so its
// presence is asserted against the repo, not this server. // presence is asserted against the repo, not this server.
Check(std::filesystem::exists("images/fp6-pmos.jpg"), "product photo exists in the repo"); Check(std::filesystem::exists("images/fp6-pmos.jpg"), "product photo exists in the repo");
srv.BodyHas("/shop/fp6-pmos", "not yet verified", "emergency-calling caveat is on the page"); // 112 was verified against a live network on 2026-08-18, so the caveat
// that used to sit above the spec sheet is gone from the served page and
// must not creep back in.
srv.BodyLacks("/shop/fp6-pmos", "not yet verified",
"no unverified-emergency-calling caveat on the page");
srv.BodyLacks("/shop", "reservation", "no reservation copy survives on /shop"); srv.BodyLacks("/shop", "reservation", "no reservation copy survives on /shop");
srv.BodyLacks("/shop/fp6-pmos", "Reserve one", "no reservation form survives"); srv.BodyLacks("/shop/fp6-pmos", "Reserve one", "no reservation form survives");

View file

@ -89,9 +89,13 @@ void CatalogueContract() {
Check(MaxUnitsFor(ladder, pr.shipWeightGrams) == 14, Check(MaxUnitsFor(ladder, pr.shipWeightGrams) == 14,
"content: and caps one parcel at fourteen units"); "content: and caps one parcel at fourteen units");
} }
Check(pr.safetyNote.find("112") != std::string::npos // This used to assert the emergency-calling caveat was present. It
&& pr.safetyNote.find("not yet verified") != std::string::npos, // came off on 2026-08-18, when imsd called 112 over a live network in
"content: emergency-calling safety warning present and honest"); // an approved test session, so the assertion inverts: the page must
// not go back to telling buyers that path is unverified. A future
// safety claim may fill this field again — it just cannot be that one.
Check(pr.safetyNote.find("not yet verified") == std::string::npos,
"content: no stale unverified-emergency-calling caveat");
Check(pr.warranty.find("TODO") == std::string::npos && pr.warranty.size() > 100, Check(pr.warranty.find("TODO") == std::string::npos && pr.warranty.size() > 100,
"content: warranty is written, not a placeholder"); "content: warranty is written, not a placeholder");
// The product page's schema.org record must parse with our own // The product page's schema.org record must parse with our own