modified
frontend/static_src/base/styles/base.scss
@@ -485,6 +485,23 @@ main { text-underline-offset: 2px;}/* Render-error detail block: the raw error from the template engine, shown to the single operator so a broken page is debuggable in place. */.empty__detail { margin: 18px auto 0; max-width: 60ch; padding: 10px 14px; background: var(--paper-edge); border: 1px solid var(--rule); border-radius: 4px; font-family: var(--font-mono); font-size: 0.85rem; color: var(--ink-dim); text-align: left; white-space: pre-wrap; word-break: break-word;}/* ---------- page heading ---------- *//* The page's h1. Sits above the first section, with a strong rule below. The new `.section-title` is the second-tier heading; the page-head's
@@ -19,7 +19,7 @@ pub fn render_to_string() -> Result<String, Response> { let tmpl = state.env.get_template(template).map_err(|e| { tracing::error!("template '{}': {}", template, e); (StatusCode::INTERNAL_SERVER_ERROR, "template error").into_response() server_error(state, path, &format!("template '{template}': {e}")) })?; tmpl.render(minijinja::context! { request => RequestCtx { path: path.to_string() },
@@ -33,7 +33,15 @@ pub fn render_to_string( }) .map_err(|e| { tracing::error!("render '{}': {}", template, e); (StatusCode::INTERNAL_SERVER_ERROR, "render error").into_response() // minijinja's `Display` carries the bare error; the source chain carries // the location and line span, which is what the operator needs to look at. let mut detail = format!("render '{template}': {e}"); let mut source = std::error::Error::source(&e); while let Some(s) = source { detail.push_str(&format!("\n caused by: {s}")); source = s.source(); } server_error(state, path, &detail) })}
@@ -56,3 +64,34 @@ pub fn not_found(state: &AppState) -> Response { ); (StatusCode::NOT_FOUND, body).into_response()}/// The themed 500 page with the underlying error detail. Single-operator app/// (no public sign-up, see `PLAN.md`), so leaking the message back is fine./// It is the whole point of the page. Falls back to plain text if the error/// page itself fails to render, so we never recurse.fn server_error(state: &AppState, path: &str, detail: &str) -> Response { let ctx = minijinja::context! { title => "Page failed to render", path => path, detail => detail, }; let body = state .env .get_template("pages/error.html") .and_then(|t| { t.render(minijinja::context! { request => RequestCtx { path: path.to_string() }, now => minijinja::context! { year => chrono::Local::now().year() }, site => minijinja::context! { title => &state.config.site_title, base_url => &state.config.base_url, }, base_url => &state.config.base_url, ..ctx }) }); match body { Ok(html) => (StatusCode::INTERNAL_SERVER_ERROR, Html(html)).into_response(), Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, detail.to_string()).into_response(), }}
added
templates/pages/error.html
@@ -0,0 +1,19 @@{% extends "base.html" %}{% block title %}Something broke{% endblock %}{% block main %}<div class="wrap"> <section class="empty"> <svg viewBox="0 0 64 64" fill="none" aria-hidden="true"> <polyline points="4,20 16,30 24,24 32,40" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/> <line x1="36" y1="44" x2="56" y2="24" stroke="currentColor" stroke-width="3.5" stroke-linecap="round"/> <line x1="36" y1="24" x2="56" y2="44" stroke="currentColor" stroke-width="3.5" stroke-linecap="round"/> </svg> <h1>Page failed to render</h1> <p>Something on this page is wrong. Head back to <a href="/">the markets dashboard</a>{% if path %}, or try the <a href="{{ path }}">previous URL</a>{% endif %}.</p> {% if detail %} <pre class="empty__detail">{{ detail }}</pre> {% endif %} </section></div>{% endblock %}
modified
templates/pages/symbol.html
@@ -280,7 +280,7 @@ {%- if earnings.next_days >= 0 -%} in {{ earnings.next_days }} day{% if earnings.next_days != 1 %}s{% endif %} {%- else -%} {{ -earnings.next_days }} day{% if -earnings.next_days != 1 %}s{% endif %} ago {{ earnings.next_days|abs }} day{% if earnings.next_days|abs != 1 %}s{% endif %} ago {%- endif -%} {%- endif -%} {% if earnings.next_source == 'estimate' %} · estimated from cadence{% endif %}