repos

Symbol page: fix unary-minus parse on past next-earnings date

70a628b2 by Isaac Bythewood · 1 month ago

Symbol page: fix unary-minus parse on past next-earnings date

In minijinja the unary `-` binds tighter than attribute access, so
`-earnings.next_days` parsed as `(-earnings).next_days` and tripped
`invalid operation` whenever Yahoo had stamped a stale (past) next-
earnings timestamp on a stock (e.g. /s/A). Swap to `|abs`.

Also replace the bare "render error" / "template error" 500 bodies
with a themed pages/error.html that surfaces the underlying minijinja
message (single-operator app, the message is the point), and add the
matching .empty__detail style.
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
modified src/render.rs
@@ -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' %} &middot; estimated from cadence{% endif %}