feat(otm): shared OTM login look, ported verbatim (0.4.2)
Build / Build and release (push) Failing after 1s

Port OTM_LOGIN_CSS + the otm-login markup structure from
@otm/account-panel otm-login-core.ts 0.44.0 verbatim into
login_page() (brand "13ft" / "Read the article without the cruft.",
accent --otm-accent:#b5562a copper, otherwise the module's neutral
ink defaults). Still OTM-only (no "Other ways to sign in"), fail-closed
503 copy + reasons + tides beacon unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014LCQmFvKshyAJsE8C3B5ET
This commit is contained in:
2026-10-19 00:21:58 -05:00
co-authored by Claude Fable 5.1
parent 9b37dd5f2b
commit 59ed6a5951
2 changed files with 48 additions and 19 deletions
+4
View File
@@ -1,5 +1,9 @@
# Changelog — 13ft (OTM fork) # Changelog — 13ft (OTM fork)
## 0.4.2 — 2026-10-19
- Sign-in page now wears the shared otm-login module's actual CSS/markup (ported verbatim from `@otm/account-panel` otm-login-core.ts 0.44.0), not a one-off lookalike — same brand/card/button structure every other OTM app uses, accent set to copper (`--otm-accent:#b5562a`) like the platform. Same OTM-only behavior: no "Other ways to sign in", fail-closed "isn't wired to OTM yet" copy unchanged.
## 0.4.1 — 2026-10-18 ## 0.4.1 — 2026-10-18
- Sign-in page restyled onto the platform's shared OTM sign-in standard: brand (name + tagline) above one centered card, the "Sign in with OTM" button full-width and primary, a muted Face ID/Touch ID/passkey line underneath. 13ft has no local accounts, so there's no "Other ways to sign in" step — the fail-closed "isn't wired to OTM yet" copy still shows when unconfigured. - Sign-in page restyled onto the platform's shared OTM sign-in standard: brand (name + tagline) above one centered card, the "Sign in with OTM" button full-width and primary, a muted Face ID/Touch ID/passkey line underneath. 13ft has no local accounts, so there's no "Other ways to sign in" step — the fail-closed "isn't wired to OTM yet" copy still shows when unconfigured.
+44 -19
View File
@@ -32,7 +32,7 @@ from typing import Any, Optional
import requests import requests
APP_VERSION = "0.4.1" APP_VERSION = "0.4.2"
OTM_AUDIENCE = "app:13ft" OTM_AUDIENCE = "app:13ft"
OTM_BASE = os.environ.get("OTM_SSO_AUTHORIZE_BASE", "https://admin.poweredbyotm.com") OTM_BASE = os.environ.get("OTM_SSO_AUTHORIZE_BASE", "https://admin.poweredbyotm.com")
SESSION_COOKIE = "thirteen_session" SESSION_COOKIE = "thirteen_session"
@@ -243,34 +243,59 @@ SSO_REASONS = {
} }
# Ported from @otm/account-panel otm-login-core.ts 0.44.0 (OTM_LOGIN_CSS) —
# keep in sync, verbatim. 13ft is OTM-only (no local accounts), so only the
# .otm-login rules are needed — no .otm-menu/.otm-badge here.
OTM_LOGIN_CSS = """
.otm-login{--otm-accent:#b5562a;--otm-accent-ink:#fff;--otm-bg:#faf8f5;--otm-card:#fff;--otm-fg:#1f1b18;--otm-muted:#6b6560;--otm-line:rgba(31,27,24,.12);--otm-radius:20px;--otm-font-display:inherit;
min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px 16px;background:var(--otm-bg);color:var(--otm-fg);box-sizing:border-box}
@media (prefers-color-scheme:dark){.otm-login{--otm-bg:#161412;--otm-card:#211e1b;--otm-fg:#f3efe9;--otm-muted:#a39b92;--otm-line:rgba(243,239,233,.12)}}
.otm-login *{box-sizing:border-box}
.otm-login__col{width:100%;max-width:26rem}
.otm-login__brand{text-align:center;margin-bottom:24px}
.otm-login__mark{width:56px;height:56px;border-radius:999px;background:var(--otm-accent);color:var(--otm-accent-ink);display:inline-flex;align-items:center;justify-content:center;font-weight:700;font-size:20px;letter-spacing:.02em;margin-bottom:12px}
.otm-login__mark svg,.otm-login__mark img{width:28px;height:28px}
.otm-login__name{font-family:var(--otm-font-display);font-size:30px;line-height:1.1;letter-spacing:-.01em;font-weight:600;margin:0}
.otm-login__tagline{color:var(--otm-muted);font-size:14px;margin:6px 0 0}
.otm-login__card{background:var(--otm-card);border:1px solid var(--otm-line);border-radius:var(--otm-radius);padding:24px;box-shadow:0 4px 24px rgba(0,0,0,.06)}
.otm-login__btn{display:flex;align-items:center;justify-content:center;width:100%;height:44px;border-radius:12px;border:0;background:var(--otm-accent);color:var(--otm-accent-ink);font:inherit;font-weight:600;font-size:15px;text-decoration:none;cursor:pointer}
.otm-login__btn:hover{filter:brightness(1.06)}
.otm-login__btn--outline{background:transparent;color:var(--otm-fg);border:1px solid var(--otm-line)}
.otm-login__btn--outline:hover{background:rgba(127,127,127,.06);filter:none}
.otm-login__btn[disabled]{opacity:.55;cursor:default}
.otm-login__helper{color:var(--otm-muted);font-size:14px;text-align:center;margin:12px 0 0;line-height:1.45}
.otm-login__error{color:#b3556b;font-size:14px;margin:0 0 12px;text-align:center}
.otm-login__link{display:inline-block;background:none;border:0;padding:0;font:inherit;font-size:14px;color:var(--otm-muted);text-decoration:underline;text-underline-offset:3px;cursor:pointer}
.otm-login__link:hover{color:var(--otm-fg)}
.otm-login__foot{text-align:center;margin-top:16px}
.otm-login__stack>*+*{margin-top:10px}
"""
def login_page(next_path: str, reason: Optional[str], configured: bool) -> str: def login_page(next_path: str, reason: Optional[str], configured: bool) -> str:
msg = "" msg = ""
if reason: if reason:
msg = f'<p style="color:#c0566e;margin:0 0 1rem;font-size:.9rem">{html_mod.escape(SSO_REASONS.get(reason, "Sign-in failed."))}</p>' msg = f'<p class="otm-login__error" role="alert">{html_mod.escape(SSO_REASONS.get(reason, "Sign-in failed."))}</p>'
if not configured: if not configured:
msg = '<p style="color:#c0566e;margin:0;font-size:.9rem">This 13ft isn\'t wired to OTM yet, so nobody can sign in. (Fails closed on purpose — it\'s a fetch proxy.)</p>' msg = '<p class="otm-login__error" role="alert">This 13ft isn\'t wired to OTM yet, so nobody can sign in. (Fails closed on purpose — it\'s a fetch proxy.)</p>'
# OTM-only app (no local accounts) — step 2's primary button is the whole # OTM-only app (no local accounts) — the "Sign in with OTM" button is the
# card, with the passkey helper line under it. There is no step 3 ("Other # whole card, with the passkey helper line under it. There is no "Other
# ways to sign in"): there is no other way. # ways to sign in": there is no other way.
card = msg + ( card = msg + (
f'<a href="{html_mod.escape(otm_sign_in_url(next_path))}" ' f'<a class="otm-login__btn" href="{html_mod.escape(otm_sign_in_url(next_path))}">Sign in with OTM</a>'
'style="display:block;box-sizing:border-box;width:100%;text-align:center;padding:.85em 1.2em;' '<p class="otm-login__helper">Your OTM account — Face ID, Touch ID or a passkey works there.</p>'
'margin-top:.75rem;border-radius:9px;border:1px solid var(--accent);background:var(--accent);'
'color:var(--accent-ink);text-decoration:none;font-weight:600">Sign in with OTM</a>'
'<p style="text-align:center;font-size:.8rem;opacity:.7;margin:.9rem 0 0">'
'Your OTM account — Face ID, Touch ID or a passkey works there.</p>'
if configured else "" if configured else ""
) )
return ( return (
'<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">' '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">'
'<title>13ft — sign in</title><style>:root{color-scheme:light dark;--accent:#3a6df0;--accent-ink:#fff}' f'<title>13ft — sign in</title><style>{OTM_LOGIN_CSS}</style></head>'
'body{font-family:system-ui,sans-serif;max-width:24rem;margin:10vh auto;padding:0 1rem;line-height:1.5}'
'.brand{text-align:center;margin:0 0 1.75rem}.brand h1{margin:0 0 .3rem;font-size:1.9rem}'
'.brand p{margin:0;opacity:.7;font-size:.95rem}'
'.card{border:1px solid #8884;border-radius:14px;padding:1.5rem}</style></head>'
'<body>' '<body>'
'<div class="brand"><h1>13ft</h1><p>Read the article without the cruft.</p></div>' '<div class="otm-login" style="--otm-accent:#b5562a;--otm-accent-ink:#fff">'
f'<div class="card">{card}</div>' '<div class="otm-login__col">'
'<div class="otm-login__brand"><div class="otm-login__mark">13</div>'
'<h1 class="otm-login__name">13ft</h1><p class="otm-login__tagline">Read the article without the cruft.</p></div>'
f'<div class="otm-login__card">{card}</div>'
'</div></div>'
f'{tides_beacon()}</body></html>' f'{tides_beacon()}</body></html>'
) )