20, "wing" => "Building", "title" => "Errors you can read", "promise" => "An app's error log is visible to its creator without a shell.", "block" => "PHP warnings and job output land in RC_FACTS/log.txt within a minute, readable by the app and shown on the creator's dashboard.", "show" => function (?array $me): string { $l = error_lines(); return '

It appears below within a minute.

' . '
' . h($l ? implode("\n", $l) : "no deliberate errors in the log yet") . "
"; }, "api" => function (string $do, ?array $me, array $in, bool $post): ?array { if ($do !== "error" || !$post) return null; $mark = bin2hex(random_bytes(4)); trigger_error(ERROR_MARK . ", " . env("RC_ENV") . ", $mark", E_USER_WARNING); return ["made" => $mark]; }, // Each run makes an error, and looks for the one the previous run made (root copies the log // every minute, and the check runs every five). "check" => function (): array { $prev = meta("error_mark"); [, , $j] = (new Browser())->post("https://" . env("RC_HOST") . "/api/20/error"); if (empty($j["made"])) return [false, "the \"make an error\" button did not answer"]; meta_set("error_mark", $j["made"] . "@" . time()); if (!$prev) return [false, "made the first error; the next check looks for it in the log"]; [$mark, $at] = explode("@", $prev); $found = (bool)preg_grep("/" . preg_quote($mark, "/") . "/", error_lines()); if (!$found && time() - (int)$at < 70) return [true, "made an error; the previous one is too recent to look for"]; if (!$found) return [false, "the error made " . intdiv(time() - (int)$at, 60) . " minutes ago is not in RC_FACTS/log.txt"]; return [true, "the error made " . intdiv(time() - (int)$at, 60) . " minutes ago is in RC_FACTS/log.txt"]; }, "script" => <<<'JS' document.getElementById("err")?.addEventListener("click", async () => { const j = await zoo.call("/api/20/error", {}); document.getElementById("err-out").textContent = j.error || ("Made error " + j.made + ". Reload in a minute to see it below."); }); JS, ];