public: landing + pricing + legal pages, apex-ready, lawyer-reviewed
Adds the unauthenticated surface that's needed to invite outsiders:
- Landing (/) — dual-purpose root: dashboard for logged-in users,
landing for everyone else. New maybe_current_user soft-auth helper
in app/auth.py supports it without disturbing the per-route
require_token deps on /news, /log, /upload, /settings.
- About, Pricing, Disclaimer, Terms, Privacy — own router
(app/routers/public.py), no auth dep, shared public_base layout
(brand link, thin nav, footer with legal links + ICO ref + date).
- Editorial positioning: news aggregator with a macro brain; tagline
"Understand markets. Don't gamble on them."; anti-trading-as-gambling
stance carried through About and Landing.
Legal pass following an independent lawyer-style review:
- Privacy: explicit UK-GDPR Art. 6 lawful-basis section; Art. 22
automated-decision line; explicit consent for sessionStorage sync
key (PECR); 30-day IP-log retention; Art. 21 objection right;
Children clause; Art. 33/34 breach-notification clause;
international-transfer mechanism (IDTA + UK Addendum). ICO
registration ZC098928 surfaced at the top.
- Pricing: paid-card AI-portfolio-analysis bullet rewritten to remove
advice-shaped wording ("what would invalidate the posture" gone);
added italic carve-out citing FSMA / FCA COBS.
- Disclaimer: separate EU/EEA carve-out + MAR 596/2014 Art. 3(1)(34)
commentator safe-harbour; "qualifies the Terms" line; hallucination
wording fixed.
- Terms: cl.4 explicit AI-training prohibition + harassment line;
cl.5 CCR 2013 14-day cancellation; cl.7 softened AI copyright
claim under CDPA s.9(3) ambiguity; cl.8 proportionate suspension +
pro-rata refund for paid users; cl.10 CRA 2015 Pt 1 statutory-rights
carve-out from the liability cap; cl.11 right to close account on
material change; cl.12 non-exclusive jurisdiction + UK consumer
local courts.
Code-side enforcement of the Privacy claim:
- openrouter.py: outbound OpenRouter calls now carry
X-OR-Allow-Training: false. DeepSeek doesn't expose a per-request
flag; the Privacy page discloses this caveat verbatim.
Apex domain prep:
- branding.APP_URL flipped to https://read.markets (was app.). DNS for
the apex already resolves; pending operator NPM step is a cert that
covers the bare apex + a 301 from app.read.markets. No hard-coded
subdomain references remain in code (verified with grep).
Nav + chrome:
- app dropdown gains Pricing / Terms / Privacy / Disclaimer links.
- login.html gains a small legal-links footer for the
highest-leverage moment to surface them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
6f9a710726
commit
f1903e1e61
17 changed files with 1436 additions and 10 deletions
31
app/auth.py
31
app/auth.py
|
|
@ -161,6 +161,37 @@ async def require_auth(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def maybe_current_user(
|
||||||
|
request: Request,
|
||||||
|
authorization: str | None = Header(default=None),
|
||||||
|
) -> CurrentUser | None:
|
||||||
|
"""Soft-auth: same resolution as `require_auth`, but returns None on
|
||||||
|
miss instead of raising. Used on dual-purpose routes (e.g. `/` where
|
||||||
|
a logged-in user sees the dashboard and a logged-out visitor sees the
|
||||||
|
landing page) and on the public marketing/legal pages, so their
|
||||||
|
templates can show a "Dashboard" link when a session is present."""
|
||||||
|
s = get_settings()
|
||||||
|
|
||||||
|
if s.CASSANDRA_TOKEN and authorization and authorization.lower().startswith("bearer "):
|
||||||
|
provided = authorization.split(" ", 1)[1].strip()
|
||||||
|
if secrets.compare_digest(provided.encode(), s.CASSANDRA_TOKEN.encode()):
|
||||||
|
principal = CurrentUser(is_admin=True, user=None)
|
||||||
|
request.state.current_user = principal
|
||||||
|
return principal
|
||||||
|
|
||||||
|
cookie = request.cookies.get(SESSION_COOKIE_NAME)
|
||||||
|
if cookie:
|
||||||
|
uid = verify_session(cookie)
|
||||||
|
if uid is not None:
|
||||||
|
async with get_session_factory()() as db_session:
|
||||||
|
user = await get_user(db_session, uid)
|
||||||
|
if user is not None:
|
||||||
|
principal = CurrentUser(is_admin=False, user=user)
|
||||||
|
request.state.current_user = principal
|
||||||
|
return principal
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _raise_redirect_to_login(next_path: str = "/") -> None:
|
def _raise_redirect_to_login(next_path: str = "/") -> None:
|
||||||
# Some pages (login itself) are paths a redirect loop would be silly
|
# Some pages (login itself) are paths a redirect loop would be silly
|
||||||
# to send back to. The auth router opts out of this dependency
|
# to send back to. The auth router opts out of this dependency
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,26 @@ BRAND_NAME = "Read the Markets"
|
||||||
BRAND_SHORT = "Read"
|
BRAND_SHORT = "Read"
|
||||||
DOMAIN = "read.markets"
|
DOMAIN = "read.markets"
|
||||||
SITE_URL = "https://read.markets"
|
SITE_URL = "https://read.markets"
|
||||||
APP_URL = "https://app.read.markets"
|
# The app lives at the apex too — same host serves landing/legal *and*
|
||||||
|
# the dashboard. SITE_URL and APP_URL are kept as separate symbols so a
|
||||||
|
# future split (marketing/apex, app/subdomain) is a two-line change.
|
||||||
|
APP_URL = "https://read.markets"
|
||||||
EMAIL_FROM_DEFAULT = f"noreply@{DOMAIN}"
|
EMAIL_FROM_DEFAULT = f"noreply@{DOMAIN}"
|
||||||
|
|
||||||
|
# Marketing line — printed in the landing hero. Single source of truth so
|
||||||
|
# OG cards, email subjects, and the landing template stay in sync. The
|
||||||
|
# wording is a stance, not just a description: this is for people who
|
||||||
|
# treat investing and gambling as different activities. The "news
|
||||||
|
# aggregator / media service" framing still lives in the body copy and
|
||||||
|
# the disclaimer, where it does the legal distancing work.
|
||||||
|
TAGLINE = "Understand markets. Don't gamble on them."
|
||||||
|
|
||||||
|
# Legal-page operator details. Placeholders until the user supplies real
|
||||||
|
# ones; only the legal pages read them.
|
||||||
|
LEGAL_OPERATOR = BRAND_NAME
|
||||||
|
OPERATOR_EMAIL = f"hello@{DOMAIN}"
|
||||||
|
OPERATOR_JURISDICTION = "United Kingdom"
|
||||||
|
|
||||||
|
|
||||||
DARK: dict[str, str] = {
|
DARK: dict[str, str] = {
|
||||||
"bg": "#0a0e14",
|
"bg": "#0a0e14",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ from app.logging import configure_logging, get_logger
|
||||||
from app.routers import api as api_router
|
from app.routers import api as api_router
|
||||||
from app.routers import auth as auth_router
|
from app.routers import auth as auth_router
|
||||||
from app.routers import pages as pages_router
|
from app.routers import pages as pages_router
|
||||||
|
from app.routers import public as public_router
|
||||||
from app.routers import sync as sync_router
|
from app.routers import sync as sync_router
|
||||||
from app.routers import universe as universe_router
|
from app.routers import universe as universe_router
|
||||||
from app.services.feeds_bootstrap import bootstrap_feeds
|
from app.services.feeds_bootstrap import bootstrap_feeds
|
||||||
|
|
@ -85,4 +86,7 @@ app.include_router(auth_router.router, tags=["auth"])
|
||||||
app.include_router(api_router.router, prefix="/api", tags=["api"])
|
app.include_router(api_router.router, prefix="/api", tags=["api"])
|
||||||
app.include_router(universe_router.router, prefix="/api", tags=["universe"])
|
app.include_router(universe_router.router, prefix="/api", tags=["universe"])
|
||||||
app.include_router(sync_router.router, tags=["portfolio-sync"])
|
app.include_router(sync_router.router, tags=["portfolio-sync"])
|
||||||
|
# Public router (no auth dep) before pages_router so the marketing/legal
|
||||||
|
# paths can never collide with future authenticated routes.
|
||||||
|
app.include_router(public_router.router)
|
||||||
app.include_router(pages_router.router, tags=["pages"])
|
app.include_router(pages_router.router, tags=["pages"])
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
from sqlalchemy import desc, func, select
|
from sqlalchemy import desc, func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.auth import CurrentUser, require_auth, require_token
|
from app.auth import CurrentUser, maybe_current_user, require_auth, require_token
|
||||||
from app.config import get_settings, load_groups
|
from app.config import get_settings, load_groups
|
||||||
from app.db import get_session
|
from app.db import get_session
|
||||||
from app.models import Referral, StrategicLog, User
|
from app.models import Referral, StrategicLog, User
|
||||||
|
|
@ -16,26 +16,41 @@ from app.services.access import paid_status
|
||||||
from app.services.referral_service import assign_code_if_missing
|
from app.services.referral_service import assign_code_if_missing
|
||||||
from app.templates_env import templates
|
from app.templates_env import templates
|
||||||
|
|
||||||
router = APIRouter(dependencies=[Depends(require_token)])
|
# Router-level auth removed in favour of per-route deps so that `/` can be
|
||||||
|
# dual-purpose: logged-in users see the dashboard, logged-out visitors see
|
||||||
|
# the landing page.
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_class=HTMLResponse)
|
@router.get("/", response_class=HTMLResponse)
|
||||||
async def dashboard(request: Request):
|
async def root_page(
|
||||||
|
request: Request,
|
||||||
|
cu: CurrentUser | None = Depends(maybe_current_user),
|
||||||
|
):
|
||||||
|
"""Dual-purpose root: dashboard when authenticated, landing otherwise."""
|
||||||
|
if cu is None:
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request, "landing.html", {"cu": None},
|
||||||
|
)
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
groups = load_groups(s.BASELINE_TOML, s.PORTFOLIO_TOML)
|
groups = load_groups(s.BASELINE_TOML, s.PORTFOLIO_TOML)
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"dashboard.html",
|
"dashboard.html",
|
||||||
{"groups": list(groups.keys()), "anchor": s.CASSANDRA_ANCHOR_DATE},
|
{"groups": list(groups.keys()), "anchor": s.CASSANDRA_ANCHOR_DATE, "cu": cu},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/news", response_class=HTMLResponse)
|
@router.get(
|
||||||
|
"/news",
|
||||||
|
response_class=HTMLResponse,
|
||||||
|
dependencies=[Depends(require_token)],
|
||||||
|
)
|
||||||
async def news_page(request: Request):
|
async def news_page(request: Request):
|
||||||
return templates.TemplateResponse(request, "news.html", {})
|
return templates.TemplateResponse(request, "news.html", {})
|
||||||
|
|
||||||
|
|
||||||
@router.get("/upload")
|
@router.get("/upload", dependencies=[Depends(require_token)])
|
||||||
async def upload_page(request: Request):
|
async def upload_page(request: Request):
|
||||||
"""Legacy bookmark — the import widget now lives in /settings."""
|
"""Legacy bookmark — the import widget now lives in /settings."""
|
||||||
return RedirectResponse(url="/settings#import", status_code=302)
|
return RedirectResponse(url="/settings#import", status_code=302)
|
||||||
|
|
@ -69,7 +84,11 @@ def _log_page_context(target: date) -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/log", response_class=HTMLResponse)
|
@router.get(
|
||||||
|
"/log",
|
||||||
|
response_class=HTMLResponse,
|
||||||
|
dependencies=[Depends(require_token)],
|
||||||
|
)
|
||||||
async def log_page(
|
async def log_page(
|
||||||
request: Request,
|
request: Request,
|
||||||
session: AsyncSession = Depends(get_session),
|
session: AsyncSession = Depends(get_session),
|
||||||
|
|
@ -78,7 +97,11 @@ async def log_page(
|
||||||
return templates.TemplateResponse(request, "log.html", _log_page_context(target))
|
return templates.TemplateResponse(request, "log.html", _log_page_context(target))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/log/{day}", response_class=HTMLResponse)
|
@router.get(
|
||||||
|
"/log/{day}",
|
||||||
|
response_class=HTMLResponse,
|
||||||
|
dependencies=[Depends(require_token)],
|
||||||
|
)
|
||||||
async def log_page_day(
|
async def log_page_day(
|
||||||
request: Request,
|
request: Request,
|
||||||
day: str,
|
day: str,
|
||||||
|
|
|
||||||
68
app/routers/public.py
Normal file
68
app/routers/public.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
"""Unauthenticated marketing + legal pages.
|
||||||
|
|
||||||
|
This router carries no auth dependency — every route is reachable to
|
||||||
|
anonymous visitors and is also reachable to logged-in users (the
|
||||||
|
templates branch off `cu` to flip the top-right CTA between
|
||||||
|
"Sign in / sign up" and "Dashboard").
|
||||||
|
|
||||||
|
The dual-purpose root (`/`) lives in `app/routers/pages.py` because it
|
||||||
|
also has to render the dashboard when authenticated. Pure public-only
|
||||||
|
pages live here.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
|
||||||
|
from app.auth import CurrentUser, maybe_current_user
|
||||||
|
from app.templates_env import templates
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(tags=["public"])
|
||||||
|
|
||||||
|
|
||||||
|
def _ctx(request: Request, cu: CurrentUser | None) -> dict:
|
||||||
|
"""Minimal context every public template expects. `cu` is injected
|
||||||
|
into the template so the header CTA can flip between
|
||||||
|
'Sign in / sign up' and 'Dashboard'."""
|
||||||
|
return {"cu": cu}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/pricing", response_class=HTMLResponse)
|
||||||
|
async def pricing_page(
|
||||||
|
request: Request,
|
||||||
|
cu: CurrentUser | None = Depends(maybe_current_user),
|
||||||
|
):
|
||||||
|
return templates.TemplateResponse(request, "pricing.html", _ctx(request, cu))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/about", response_class=HTMLResponse)
|
||||||
|
async def about_page(
|
||||||
|
request: Request,
|
||||||
|
cu: CurrentUser | None = Depends(maybe_current_user),
|
||||||
|
):
|
||||||
|
return templates.TemplateResponse(request, "about.html", _ctx(request, cu))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/terms", response_class=HTMLResponse)
|
||||||
|
async def terms_page(
|
||||||
|
request: Request,
|
||||||
|
cu: CurrentUser | None = Depends(maybe_current_user),
|
||||||
|
):
|
||||||
|
return templates.TemplateResponse(request, "terms.html", _ctx(request, cu))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/privacy", response_class=HTMLResponse)
|
||||||
|
async def privacy_page(
|
||||||
|
request: Request,
|
||||||
|
cu: CurrentUser | None = Depends(maybe_current_user),
|
||||||
|
):
|
||||||
|
return templates.TemplateResponse(request, "privacy.html", _ctx(request, cu))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/disclaimer", response_class=HTMLResponse)
|
||||||
|
async def disclaimer_page(
|
||||||
|
request: Request,
|
||||||
|
cu: CurrentUser | None = Depends(maybe_current_user),
|
||||||
|
):
|
||||||
|
return templates.TemplateResponse(request, "disclaimer.html", _ctx(request, cu))
|
||||||
|
|
@ -549,6 +549,13 @@ def _endpoint_for(provider: str) -> tuple[str, str, str, dict[str, str]]:
|
||||||
# OpenRouter dashboard — keep aligned with the live brand.
|
# OpenRouter dashboard — keep aligned with the live brand.
|
||||||
"HTTP-Referer": branding.SITE_URL,
|
"HTTP-Referer": branding.SITE_URL,
|
||||||
"X-Title": branding.BRAND_NAME,
|
"X-Title": branding.BRAND_NAME,
|
||||||
|
# No-train opt-out. Tells OpenRouter (and any compatible
|
||||||
|
# upstream) that this request must not be used to train
|
||||||
|
# or improve models. The Privacy notice promises this; the
|
||||||
|
# header is what makes the promise truthful. If a future
|
||||||
|
# upstream ignores the header, fix the provider — not the
|
||||||
|
# header — so the contract stays auditable.
|
||||||
|
"X-OR-Allow-Training": "false",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
raise RuntimeError(f"Unknown LLM provider: {provider!r}")
|
raise RuntimeError(f"Unknown LLM provider: {provider!r}")
|
||||||
|
|
|
||||||
|
|
@ -1421,3 +1421,290 @@ details[open] .pf-analysis__head-left::before { content: "▾ "; }
|
||||||
::-webkit-scrollbar-track { background: var(--bg); }
|
::-webkit-scrollbar-track { background: var(--bg); }
|
||||||
::-webkit-scrollbar-thumb { background: var(--dim); border-radius: 0; }
|
::-webkit-scrollbar-thumb { background: var(--dim); border-radius: 0; }
|
||||||
::-webkit-scrollbar-thumb:hover { background: var(--muted); }
|
::-webkit-scrollbar-thumb:hover { background: var(--muted); }
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* Public pages — landing, pricing, about, terms, privacy, disclaimer.
|
||||||
|
* Shared by all templates extending public_base.html. Visual language
|
||||||
|
* matches the app shell (same palette, monospace brand, restrained
|
||||||
|
* typography) but without dashboard chrome.
|
||||||
|
* ============================================================ */
|
||||||
|
|
||||||
|
.public-page { background: var(--bg); }
|
||||||
|
|
||||||
|
.public-shell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
max-width: 1080px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 22px 0 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.public-header__brand {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
.public-header__brand::before { content: "▰ "; opacity: 0.6; }
|
||||||
|
.public-header__brand:hover { color: var(--text); }
|
||||||
|
.public-header__nav { display: flex; align-items: center; gap: 22px; }
|
||||||
|
.public-header__nav a {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.public-header__nav a:hover,
|
||||||
|
.public-header__nav a.active { color: var(--text); }
|
||||||
|
.public-header__cta {
|
||||||
|
color: var(--accent) !important;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.public-header__cta:hover { background: var(--accent); color: var(--bg) !important; }
|
||||||
|
|
||||||
|
.public-main {
|
||||||
|
flex: 1;
|
||||||
|
padding: 48px 0 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-footer {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding: 28px 0 36px;
|
||||||
|
margin-top: 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.public-footer__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.public-footer__brand strong { color: var(--text); margin-right: 10px; }
|
||||||
|
.public-footer__tagline { color: var(--muted); }
|
||||||
|
.public-footer__links { display: flex; flex-wrap: wrap; gap: 16px; }
|
||||||
|
.public-footer__links a { color: var(--muted); text-decoration: none; }
|
||||||
|
.public-footer__links a:hover { color: var(--accent); }
|
||||||
|
.public-footer__meta { color: var(--dim); font-size: 11px; }
|
||||||
|
|
||||||
|
/* --- Hero (landing) -------------------------------------------------- */
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
padding: 32px 0 48px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
.hero__brand {
|
||||||
|
color: var(--muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.hero__headline {
|
||||||
|
font-size: clamp(28px, 5vw, 44px);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.15;
|
||||||
|
color: var(--text);
|
||||||
|
margin: 12px 0 14px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.hero__subhead {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--muted);
|
||||||
|
max-width: 640px;
|
||||||
|
line-height: 1.55;
|
||||||
|
margin: 0 0 24px;
|
||||||
|
}
|
||||||
|
.hero__ctas { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||||
|
.hero__ctas .btn-primary,
|
||||||
|
.hero__ctas .btn-secondary {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 22px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 13.5px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
/* Qualify with `a` so we beat `a { color: var(--accent) }` and any
|
||||||
|
:link/:visited UA defaults. Without `a.btn-primary` the cascade can
|
||||||
|
resolve in favour of the visited-link color on some browsers and the
|
||||||
|
label disappears against the accent background. */
|
||||||
|
a.btn-primary,
|
||||||
|
a.btn-primary:link,
|
||||||
|
a.btn-primary:visited {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg);
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
}
|
||||||
|
a.btn-primary:hover { background: transparent; color: var(--accent); }
|
||||||
|
a.btn-secondary,
|
||||||
|
a.btn-secondary:link,
|
||||||
|
a.btn-secondary:visited {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
a.btn-secondary:hover { color: var(--accent); border-color: var(--accent); }
|
||||||
|
|
||||||
|
/* --- Feature blocks (landing) --------------------------------------- */
|
||||||
|
|
||||||
|
.feature-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
margin: 8px 0 56px;
|
||||||
|
}
|
||||||
|
.feature-card {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 22px 22px 24px;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
.feature-card__tag {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10.5px;
|
||||||
|
color: var(--accent);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.feature-card__title {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
.feature-card__body {
|
||||||
|
font-size: 13.5px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Section primitives reused across pricing/about/legal ---------- */
|
||||||
|
|
||||||
|
.public-section {
|
||||||
|
margin: 0 0 56px;
|
||||||
|
}
|
||||||
|
.public-section__head {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0 0 16px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.public-section h3 {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
margin: 24px 0 8px;
|
||||||
|
}
|
||||||
|
.public-section p,
|
||||||
|
.public-section li {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.65;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.public-section p { margin: 0 0 14px; }
|
||||||
|
.public-section ul {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
padding-left: 22px;
|
||||||
|
}
|
||||||
|
.public-section li { margin-bottom: 6px; }
|
||||||
|
.public-section a { color: var(--accent); }
|
||||||
|
|
||||||
|
.public-section--callout {
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
padding: 16px 22px;
|
||||||
|
background: var(--surface);
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
margin: 0 0 32px;
|
||||||
|
}
|
||||||
|
.public-section--warning {
|
||||||
|
border-left-color: var(--negative);
|
||||||
|
background: color-mix(in srgb, var(--negative) 6%, var(--bg));
|
||||||
|
}
|
||||||
|
.public-section--warning a { color: var(--text); }
|
||||||
|
|
||||||
|
/* --- "What this is not" strip on landing --------------------------- */
|
||||||
|
|
||||||
|
.not-strip {
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
padding: 18px 22px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0 0 56px;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
.not-strip strong { color: var(--text); }
|
||||||
|
.not-strip ul { display: flex; flex-wrap: wrap; gap: 18px 28px; margin: 8px 0 0; padding: 0; list-style: none; }
|
||||||
|
.not-strip li { color: var(--muted); font-size: 13px; }
|
||||||
|
.not-strip li::before { content: "✕ "; color: var(--negative); font-weight: 700; margin-right: 4px; }
|
||||||
|
|
||||||
|
/* --- Pricing comparison -------------------------------------------- */
|
||||||
|
|
||||||
|
.tier-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 18px;
|
||||||
|
margin: 8px 0 24px;
|
||||||
|
}
|
||||||
|
.tier-card {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 22px 22px 26px;
|
||||||
|
background: var(--surface);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.tier-card--featured {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 1px var(--accent) inset;
|
||||||
|
}
|
||||||
|
.tier-card__name {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.tier-card__price {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.tier-card__price-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.tier-card ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 0 22px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.tier-card li {
|
||||||
|
font-size: 13.5px;
|
||||||
|
color: var(--text);
|
||||||
|
padding: 6px 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.tier-card li:last-child { border-bottom: 0; }
|
||||||
|
.tier-card li::before { content: "✓ "; color: var(--positive); font-weight: 700; margin-right: 4px; }
|
||||||
|
.tier-card li.tier-card__excluded { color: var(--muted); }
|
||||||
|
.tier-card li.tier-card__excluded::before { content: "✕ "; color: var(--dim); }
|
||||||
|
.tier-card__cta { margin-top: auto; }
|
||||||
|
|
|
||||||
76
app/templates/about.html
Normal file
76
app/templates/about.html
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
{% extends "public_base.html" %}
|
||||||
|
{% block title %}{{ BRAND_NAME }} · About{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h1 class="public-section__head">About {{ BRAND_NAME }}</h1>
|
||||||
|
<p>
|
||||||
|
{{ BRAND_NAME }} is a <strong>news aggregator with a macro brain</strong>.
|
||||||
|
We pull market headlines and a curated set of cross-asset signals,
|
||||||
|
auto-tag the news by theme, and use a large language model to write
|
||||||
|
a short interpretation every hour — in plain English, with a fixed
|
||||||
|
editorial discipline.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Editorially we’re a media service, not a financial one. We
|
||||||
|
don’t make buy/sell calls, we don’t do technical
|
||||||
|
analysis, and we don’t pretend to know which way the tape goes
|
||||||
|
next.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">Who it’s for</h2>
|
||||||
|
<p>
|
||||||
|
Investors who’d rather <em>understand</em> than trade. People
|
||||||
|
who want a coherent read of the underlying fundamentals — what the
|
||||||
|
real economy, policy, and valuation are doing — not the next 30
|
||||||
|
minutes of price action, not chart patterns, and not which ETF to
|
||||||
|
buy.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Particularly: investors new enough to markets that the gambling
|
||||||
|
framing of social media is doing real damage. Every read here is
|
||||||
|
deliberately calm, anti-technical-analysis, and rooted in
|
||||||
|
fundamentals. We treat trading and gambling as different activities,
|
||||||
|
and we’re built for the people who already see them that way.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">How it’s built</h2>
|
||||||
|
<p>
|
||||||
|
Architecturally, the product is deliberately privacy-shaped:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Your portfolio lives in your browser. The server’s view is
|
||||||
|
an aggregate set of tickers held across the whole user base,
|
||||||
|
which on its own does not identify any individual user — see
|
||||||
|
the <a href="/privacy">Privacy notice</a> for the exact data
|
||||||
|
structures.</li>
|
||||||
|
<li>Cloud sync of your portfolio is opt-in and end-to-end encrypted
|
||||||
|
with a PIN only you know.</li>
|
||||||
|
<li>No third-party tracking, no analytics SDKs, no ad cookies.</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Full details on the <a href="/privacy">privacy page</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section public-section--callout">
|
||||||
|
<p style="margin:0 0 8px;">
|
||||||
|
{{ BRAND_NAME }} is operated from {{ OPERATOR_JURISDICTION }} by an
|
||||||
|
individual operator. It is <strong>not a regulated firm</strong>, and
|
||||||
|
nothing here is investment advice. See the
|
||||||
|
<a href="/disclaimer">disclaimer</a>.
|
||||||
|
</p>
|
||||||
|
<p style="margin:0; font-size:12.5px; color: var(--muted);">
|
||||||
|
If you are in financial distress, please consider speaking to a
|
||||||
|
free service such as
|
||||||
|
<a href="https://www.moneyhelper.org.uk/" target="_blank" rel="noopener">MoneyHelper</a>
|
||||||
|
before relying on anything you read here.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -167,7 +167,11 @@
|
||||||
{% if cu.user %}
|
{% if cu.user %}
|
||||||
<a href="/settings" role="menuitem" class="user-menu__item">Settings</a>
|
<a href="/settings" role="menuitem" class="user-menu__item">Settings</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="/logout" role="menuitem" class="user-menu__item">Logout</a>
|
<a href="/pricing" role="menuitem" class="user-menu__item">Pricing</a>
|
||||||
|
<a href="/terms" role="menuitem" class="user-menu__item">Terms</a>
|
||||||
|
<a href="/privacy" role="menuitem" class="user-menu__item">Privacy</a>
|
||||||
|
<a href="/disclaimer" role="menuitem" class="user-menu__item">Disclaimer</a>
|
||||||
|
<a href="/logout" role="menuitem" class="user-menu__item">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
110
app/templates/disclaimer.html
Normal file
110
app/templates/disclaimer.html
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
{% extends "public_base.html" %}
|
||||||
|
{% block title %}{{ BRAND_NAME }} · Disclaimer{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<section class="public-section public-section--callout public-section--warning">
|
||||||
|
<h1 class="public-section__head" style="border:0; margin:0 0 6px;">
|
||||||
|
Financial disclaimer
|
||||||
|
</h1>
|
||||||
|
<p style="margin:0; font-size:15px;">
|
||||||
|
<strong>{{ BRAND_NAME }} is not investment advice.</strong>
|
||||||
|
</p>
|
||||||
|
<p style="margin-top:6px; font-size:12px; color: var(--muted);">
|
||||||
|
This page is part of, and qualifies, the
|
||||||
|
<a href="/terms">Terms of Service</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">In short</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Everything published here is for <strong>educational and
|
||||||
|
informational purposes only</strong>.</li>
|
||||||
|
<li>Nothing on this site is a buy or sell recommendation, a personal
|
||||||
|
recommendation, or an inducement to deal in any financial
|
||||||
|
instrument.</li>
|
||||||
|
<li>{{ BRAND_NAME }} is not a regulated financial firm. It is not
|
||||||
|
authorised by the Financial Conduct Authority and is not a
|
||||||
|
registered investment adviser in any jurisdiction.</li>
|
||||||
|
<li>The output is not tailored to your circumstances, objectives,
|
||||||
|
tax position, or risk tolerance.</li>
|
||||||
|
<li>Past performance does not predict future returns. Investing
|
||||||
|
carries the risk of losing money, including the principal.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">About the AI output</h2>
|
||||||
|
<p>
|
||||||
|
The strategic log, indicator summaries, and portfolio analysis are
|
||||||
|
generated by large language models from publicly available market
|
||||||
|
data and news. They can be wrong, incomplete, or out of date. Numbers
|
||||||
|
can be misread. Models occasionally generate inaccurate or invented
|
||||||
|
information (often called “hallucinations”). Treat them
|
||||||
|
as a <em>prompt to think</em>, not as facts to act on.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The portfolio analysis is an interpretation of holdings <em>you
|
||||||
|
supplied</em>. It does not consider your overall wealth, debts, tax
|
||||||
|
position, or anything we don’t see. It is not personalised
|
||||||
|
advice.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">Before you act on anything</h2>
|
||||||
|
<p>
|
||||||
|
Consult a properly qualified, regulated financial adviser who knows
|
||||||
|
your full situation. Read the source documents (issuer accounts,
|
||||||
|
fund prospectus, etc.). If you are not in a position to lose the
|
||||||
|
money you would put at risk, do not put it at risk.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">Jurisdiction</h2>
|
||||||
|
<p>
|
||||||
|
{{ BRAND_NAME }} is operated from {{ OPERATOR_JURISDICTION }}. The
|
||||||
|
Service is not directed at, or intended for distribution to or use
|
||||||
|
by, any person in any jurisdiction where such distribution or use
|
||||||
|
would be contrary to local law or regulation.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
No part of this Service constitutes an offer or solicitation of
|
||||||
|
securities to any US person within the meaning of US securities law.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The Service is not directed at retail or professional clients in any
|
||||||
|
EU/EEA member state, nor in any jurisdiction where its provision
|
||||||
|
would require local licensing or registration. Where any output of
|
||||||
|
the Service could be construed as an “investment
|
||||||
|
recommendation” under Regulation (EU) 596/2014 (Market Abuse
|
||||||
|
Regulation) or its UK equivalent, it is non-personalised, produced
|
||||||
|
by a non-regulated source for educational purposes only, and the
|
||||||
|
operator (a) has no position in, or remuneration linked to, the
|
||||||
|
specific instruments mentioned in any individual piece of commentary,
|
||||||
|
and (b) is not a “relevant person” within MAR Art.
|
||||||
|
3(1)(34).
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">No warranty</h2>
|
||||||
|
<p>
|
||||||
|
The service is provided “as is” without warranties of any
|
||||||
|
kind. To the maximum extent permitted by applicable law, the operator
|
||||||
|
excludes liability for any loss arising from use of, or reliance on,
|
||||||
|
the content. See the <a href="/terms">Terms of Service</a> for the
|
||||||
|
full limitation of liability.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<p style="font-size: 12px; color: var(--muted);">
|
||||||
|
Questions about this disclaimer:
|
||||||
|
<a href="mailto:{{ OPERATOR_EMAIL }}">{{ OPERATOR_EMAIL }}</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
103
app/templates/landing.html
Normal file
103
app/templates/landing.html
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
{% extends "public_base.html" %}
|
||||||
|
{% block title %}{{ BRAND_NAME }} · {{ TAGLINE }}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<section class="hero">
|
||||||
|
<div class="hero__brand">{{ BRAND_NAME }}</div>
|
||||||
|
<h1 class="hero__headline">{{ TAGLINE }}</h1>
|
||||||
|
<p class="hero__subhead">
|
||||||
|
Built for investors who want to <strong>act rationally</strong> and
|
||||||
|
tune out the high-frequency noise that comes from treating markets
|
||||||
|
like a casino. We aggregate cross-asset news and macro signals,
|
||||||
|
then write a plain-English read of what the underlying fundamentals
|
||||||
|
justify versus what the crowd is doing. Refreshed hourly. A media
|
||||||
|
service, not a financial one.
|
||||||
|
</p>
|
||||||
|
<div class="hero__ctas">
|
||||||
|
{% if cu and (cu.user or cu.is_admin) %}
|
||||||
|
<a class="btn-primary" href="/">Open dashboard</a>
|
||||||
|
<a class="btn-secondary" href="/pricing">See pricing</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn-primary" href="/login">Sign up free</a>
|
||||||
|
<a class="btn-secondary" href="/pricing">See pricing</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="feature-grid">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-card__tag">News, aggregated</div>
|
||||||
|
<h3 class="feature-card__title">Headlines from across the macro universe</h3>
|
||||||
|
<p class="feature-card__body">
|
||||||
|
RSS and per-ticker feeds covering equities, rates, credit, FX,
|
||||||
|
commodities, and geopolitics. Every headline is auto-tagged by
|
||||||
|
theme so the noise stays as noise and the fundamentals-relevant
|
||||||
|
stuff is easy to find. Ingestion follows the trading calendar —
|
||||||
|
off-hours stay quiet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-card__tag">Macro signals</div>
|
||||||
|
<h3 class="feature-card__title">A curated cross-asset tape</h3>
|
||||||
|
<p class="feature-card__body">
|
||||||
|
A hand-picked set of indicators across every asset class, refreshed
|
||||||
|
hourly during market hours. Each group gets a short read that
|
||||||
|
explains what the move <em>means</em>, not what it was. Anchored
|
||||||
|
in earnings, policy, valuation — not chart patterns.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-card__tag">The hourly read</div>
|
||||||
|
<h3 class="feature-card__title">Rational vs irrational, every paragraph</h3>
|
||||||
|
<p class="feature-card__body">
|
||||||
|
We tie the day’s headlines and the cross-asset signals into
|
||||||
|
a single short interpretation. Each paragraph separates
|
||||||
|
<strong>rational drivers</strong> (earnings, policy, valuation)
|
||||||
|
from <strong>irrational ones</strong> (positioning, narrative,
|
||||||
|
flows) and names the gap. Two reading levels: novice and
|
||||||
|
intermediate. This is editorial commentary on public data —
|
||||||
|
not a forecast and not advice on any investment decision.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<p style="font-size: 13.5px; color: var(--muted);">
|
||||||
|
Paid users can also drop a Trading 212 pie CSV for an AI
|
||||||
|
sense-check on concentration, regime fit, and currency exposure.
|
||||||
|
Holdings stay in your browser by default; opt in to encrypted cloud
|
||||||
|
sync to restore on another device.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="not-strip">
|
||||||
|
<strong>What this isn’t.</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Not investment advice.</li>
|
||||||
|
<li>Not trading signals.</li>
|
||||||
|
<li>Not a day-trading tool.</li>
|
||||||
|
<li>No buy/sell calls, ever.</li>
|
||||||
|
<li>No chart-pattern predictions.</li>
|
||||||
|
<li>Not a regulated service.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<p style="font-size: 13px; color: var(--muted);">
|
||||||
|
By signing up you agree to our <a href="/terms">Terms</a> and
|
||||||
|
<a href="/privacy">Privacy notice</a>, and confirm you’ve read
|
||||||
|
the <a href="/disclaimer">financial disclaimer</a>.
|
||||||
|
</p>
|
||||||
|
<div class="hero__ctas" style="margin-top:8px;">
|
||||||
|
{% if cu and (cu.user or cu.is_admin) %}
|
||||||
|
<a class="btn-primary" href="/">Open dashboard</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn-primary" href="/login">Sign up free</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -41,6 +41,13 @@
|
||||||
</label>
|
</label>
|
||||||
<button type="submit">Send code</button>
|
<button type="submit">Send code</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<p class="auth-card__legal" style="margin-top:18px; font-size:11px; color: var(--muted); line-height:1.6;">
|
||||||
|
By signing in you agree to our
|
||||||
|
<a href="/terms">Terms</a> and
|
||||||
|
<a href="/privacy">Privacy notice</a>, and confirm you’ve read
|
||||||
|
the <a href="/disclaimer">financial disclaimer</a>.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
96
app/templates/pricing.html
Normal file
96
app/templates/pricing.html
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
{% extends "public_base.html" %}
|
||||||
|
{% block title %}{{ BRAND_NAME }} · Pricing{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h1 class="public-section__head">Pricing</h1>
|
||||||
|
<p>
|
||||||
|
Two tiers. The news aggregator and the hourly macro interpretation
|
||||||
|
are free for everyone — we want the read out where people can use
|
||||||
|
it. The paid tier extends the same editorial commentary to the
|
||||||
|
specific tickers in a portfolio you upload — an educational read
|
||||||
|
of public data, not advice on whether to hold them.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="tier-grid">
|
||||||
|
|
||||||
|
<div class="tier-card">
|
||||||
|
<div class="tier-card__name">Free</div>
|
||||||
|
<div class="tier-card__price">£0</div>
|
||||||
|
<div class="tier-card__price-hint">Forever. No card needed.</div>
|
||||||
|
<ul>
|
||||||
|
<li>News aggregator — auto-tagged by theme</li>
|
||||||
|
<li>Cross-asset macro signals across every asset class</li>
|
||||||
|
<li>Hourly AI interpretation of the news + the tape</li>
|
||||||
|
<li>Per-group cross-asset summaries</li>
|
||||||
|
<li>Novice / Intermediate reading levels</li>
|
||||||
|
<li class="tier-card__excluded">Portfolio import & analysis</li>
|
||||||
|
<li class="tier-card__excluded">Encrypted cloud sync</li>
|
||||||
|
</ul>
|
||||||
|
<div class="tier-card__cta">
|
||||||
|
{% if cu and (cu.user or cu.is_admin) %}
|
||||||
|
<a class="btn-secondary" href="/">Open dashboard</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn-primary" href="/login">Sign up free</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tier-card tier-card--featured">
|
||||||
|
<div class="tier-card__name">Paid</div>
|
||||||
|
<div class="tier-card__price">Coming soon</div>
|
||||||
|
<div class="tier-card__price-hint">Checkout opens with our payments rollout.</div>
|
||||||
|
<ul>
|
||||||
|
<li>Everything in Free</li>
|
||||||
|
<li>Portfolio import (Trading 212 CSV)</li>
|
||||||
|
<li>AI commentary on diversification, sector and currency concentration, and macro-regime context for the holdings you upload</li>
|
||||||
|
<li>Optional encrypted cloud sync across devices</li>
|
||||||
|
<li>Priority email when something material changes (later)</li>
|
||||||
|
</ul>
|
||||||
|
<div class="tier-card__cta">
|
||||||
|
{% if cu and (cu.user or cu.is_admin) %}
|
||||||
|
<a class="btn-secondary" href="/settings">Manage account</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn-primary" href="/login">Sign up — paid unlocks soon</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<p style="margin-top:14px; font-size:11.5px; color: var(--muted); font-style: italic; line-height:1.55;">
|
||||||
|
The portfolio feature does not produce buy, sell or hold
|
||||||
|
recommendations. It does not consider your wider finances, debts,
|
||||||
|
tax position or objectives. It is not regulated investment advice
|
||||||
|
or a personal recommendation under FSMA / FCA COBS.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">How the data is handled</h2>
|
||||||
|
<p>
|
||||||
|
Your portfolio holdings live in your browser’s local storage by
|
||||||
|
default. The server only learns which Yahoo tickers appear across the
|
||||||
|
user base — an anonymous union, with no link back to any specific
|
||||||
|
user.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you opt in to <strong>encrypted cloud sync</strong>, your pie is
|
||||||
|
encrypted in your browser with a PIN you choose, then sent to the
|
||||||
|
server. We add a second layer of encryption with a key only the
|
||||||
|
server holds. We never see your holdings as plaintext, and forgetting
|
||||||
|
the PIN means we can’t recover it for you. Full details on the
|
||||||
|
<a href="/privacy">privacy page</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section public-section--callout">
|
||||||
|
<p style="margin:0;">
|
||||||
|
<strong>Not investment advice.</strong> Every output here is an
|
||||||
|
interpretation of public data — not personalised advice, not a
|
||||||
|
recommendation, and not produced by a regulated entity. Read the full
|
||||||
|
<a href="/disclaimer">disclaimer</a> before relying on anything you see.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
311
app/templates/privacy.html
Normal file
311
app/templates/privacy.html
Normal file
|
|
@ -0,0 +1,311 @@
|
||||||
|
{% extends "public_base.html" %}
|
||||||
|
{% block title %}{{ BRAND_NAME }} · Privacy{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h1 class="public-section__head">Privacy notice</h1>
|
||||||
|
<p style="color: var(--muted); font-size: 13px;">
|
||||||
|
Last updated: 2026-05-24. The operator (data controller) is
|
||||||
|
{{ LEGAL_OPERATOR }}, {{ OPERATOR_JURISDICTION }}. Registered with
|
||||||
|
the UK Information Commissioner’s Office under reference
|
||||||
|
<strong>ZC098928</strong>. Questions:
|
||||||
|
<a href="mailto:{{ OPERATOR_EMAIL }}">{{ OPERATOR_EMAIL }}</a>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This page describes exactly what we collect, what we don’t,
|
||||||
|
where it lives, and how long we keep it. It is written from the code,
|
||||||
|
not from a template — every claim corresponds to an explicit
|
||||||
|
code path we’re happy to point a reviewer at.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">What we collect</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Your email address</strong>, when you sign in. We use it
|
||||||
|
only to send one-time login codes.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>An argon2 hash of each login code</strong>, plus expiry
|
||||||
|
and attempt counts. The plaintext code is sent to your inbox and
|
||||||
|
never written to disk on our side.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>A signed session cookie</strong> after you verify a code.
|
||||||
|
It contains your user id only and is signed so we can detect
|
||||||
|
tampering. Cookie is marked Secure and HttpOnly.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Anonymous ticker universe</strong>: when you upload a
|
||||||
|
portfolio CSV we record which Yahoo tickers appear, with
|
||||||
|
<em>no link</em> to your account. The same row would exist whether
|
||||||
|
any specific user holds the ticker or not — once a ticker is in
|
||||||
|
the universe, the row carries no signal as to whose import added it.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>If you opt in to encrypted cloud sync</strong>: an opaque
|
||||||
|
blob of bytes per user. The blob is your portfolio, encrypted in
|
||||||
|
your browser with a PIN you choose, then wrapped a second time on
|
||||||
|
the server with a key only the server holds. We can’t decrypt
|
||||||
|
the blob to plaintext without your PIN, and we can’t recover
|
||||||
|
your PIN if you forget it. By enabling cloud sync you give your
|
||||||
|
consent (UK-GDPR Art. 6(1)(a)) to this processing; you can
|
||||||
|
withdraw consent at any time by disabling sync in Settings, which
|
||||||
|
also removes the server-side blob.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Anonymised cost ledger</strong> of AI calls (model, tokens,
|
||||||
|
cost). No portfolio or personal data is attached to ledger rows.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Referral linkage</strong>: if you signed up via an invite
|
||||||
|
link, we record which existing user’s code you used so we can
|
||||||
|
apply the agreed referral credit later.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Job-run telemetry</strong>: success/failure timestamps for
|
||||||
|
the scheduled jobs that fetch market data and generate AI reads.
|
||||||
|
No user identifiers are attached.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">What we don’t collect</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Your portfolio holdings as plaintext on the server.</strong>
|
||||||
|
Parsed pies are returned to your browser and kept in
|
||||||
|
<code>localStorage</code>. The server’s view is the anonymous
|
||||||
|
ticker universe described above.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Third-party analytics or ad cookies.</strong> No Google
|
||||||
|
Analytics, no Hotjar, no Segment, no Facebook pixel, no LinkedIn
|
||||||
|
tag. (You can verify by viewing-source on any page.)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Browser fingerprints.</strong>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>IP-address joins to your user identity.</strong> IP
|
||||||
|
addresses are processed transiently by the reverse proxy for
|
||||||
|
security and access logging, retained for up to 30 days, and not
|
||||||
|
linked to your account record.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">Lawful basis (UK-GDPR Art. 6)</h2>
|
||||||
|
<p>We rely on the following lawful bases:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Performance of a contract</strong> (Art. 6(1)(b)) — for
|
||||||
|
operating your account, the sign-in flow, paid features, and the
|
||||||
|
mechanics of encrypted cloud sync.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Legitimate interests</strong> (Art. 6(1)(f)) — for the
|
||||||
|
anonymous ticker universe, the anonymised cost ledger, job-run
|
||||||
|
telemetry, and reverse-proxy access logs. Our interest is the
|
||||||
|
secure, abuse-resistant, cost-controlled operation of a free
|
||||||
|
public service, balanced against the minimal and de-identified
|
||||||
|
nature of the data.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Consent</strong> (Art. 6(1)(a)) — where you opt in to
|
||||||
|
encrypted cloud sync (and the related caching of a derived
|
||||||
|
encryption key in your browser’s <code>sessionStorage</code>).
|
||||||
|
You can withdraw consent at any time by disabling sync in
|
||||||
|
Settings; the cached key is cleared and the server-side blob is
|
||||||
|
removed.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">Automated decisions and profiling</h2>
|
||||||
|
<p>
|
||||||
|
The Service does not make decisions about you that produce legal or
|
||||||
|
similarly significant effects in an automated way (UK-GDPR Art. 22).
|
||||||
|
The AI portfolio analysis is editorial commentary on the holdings
|
||||||
|
you upload; it does not approve, reject or rank you, and you remain
|
||||||
|
the sole decision-maker about anything in your account.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">Cookies and local storage</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Session cookie</strong> — strictly necessary for keeping
|
||||||
|
you signed in (PECR reg. 6(4)). No prior consent required.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Local preferences</strong> — your chosen theme (light /
|
||||||
|
dark) and reading level (Novice / Intermediate) are stored in
|
||||||
|
<code>localStorage</code> on your device. They never leave the
|
||||||
|
browser.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Local portfolio + cached sync key</strong> — parsed pies
|
||||||
|
live in <code>localStorage</code> on your device. If you enable
|
||||||
|
cloud sync, the derived encryption key is cached in
|
||||||
|
<code>sessionStorage</code> so you don’t have to re-enter
|
||||||
|
your PIN on every navigation. This caching is performed only with
|
||||||
|
your consent (given when you enable sync); it is cleared when you
|
||||||
|
close the tab or disable sync.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">Where the data lives, and international transfers</h2>
|
||||||
|
<p>
|
||||||
|
The server runs in {{ OPERATOR_JURISDICTION }}. Data is stored in a
|
||||||
|
MariaDB database on the same host, backed up locally.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Two flows can take personal data outside the UK:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>SMTP</strong> for sending one-time login codes. Operator-hosted,
|
||||||
|
currently inside the UK; if that changes we will update this notice.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>AI provider calls</strong> for the strategic log, indicator
|
||||||
|
summaries, and (paid) portfolio analysis. Where the provider sits
|
||||||
|
outside the UK, we rely on the UK International Data Transfer
|
||||||
|
Agreement (IDTA) / the UK Addendum to the EU Standard Contractual
|
||||||
|
Clauses where no adequacy decision applies. Each outbound request
|
||||||
|
carries an explicit no-training opt-out header
|
||||||
|
(<code>X-OR-Allow-Training: false</code> on OpenRouter); see the
|
||||||
|
Third parties section below for the caveats.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">Retention</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Login codes</strong>: expire after a few minutes; row
|
||||||
|
remains briefly to enforce single-use, then is purged.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Session cookies</strong>: expire automatically; you can
|
||||||
|
sign out at any time to revoke.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Ticker universe</strong>: rows untouched for 60 days are
|
||||||
|
evicted by a nightly job. Active tickers remain.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Encrypted portfolio blob</strong>: kept until you disable
|
||||||
|
cloud sync (one click in Settings) or delete your account. We hold
|
||||||
|
one row per user; new uploads overwrite the previous blob.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Account</strong>: held until you ask us to delete it.
|
||||||
|
Email <a href="mailto:{{ OPERATOR_EMAIL }}">{{ OPERATOR_EMAIL }}</a>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Cost ledger and job telemetry</strong>: retained for
|
||||||
|
operational accounting; no personal data attached.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">Third parties</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>SMTP provider</strong>: an operator-hosted Mailu server
|
||||||
|
sends the one-time login codes. The provider sees your email
|
||||||
|
address and the code body (the code itself).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>AI provider(s)</strong>: DeepSeek (primary) with OpenRouter
|
||||||
|
as a fallback. They see the prompt for the strategic log, the
|
||||||
|
indicator summaries, and the portfolio analysis call — which
|
||||||
|
contains your holdings only when you press
|
||||||
|
“Generate AI analysis” on a paid plan, and only for the
|
||||||
|
duration of that single call. The portfolio analysis output is not
|
||||||
|
persisted on the server.
|
||||||
|
<br>
|
||||||
|
<strong>No-training opt-out.</strong> Every OpenRouter request
|
||||||
|
carries the <code>X-OR-Allow-Training: false</code> header, which
|
||||||
|
signals to OpenRouter and any compatible upstream that the prompt
|
||||||
|
must not be used to train or improve models. DeepSeek does not
|
||||||
|
currently expose a per-request opt-out; if you do not want your
|
||||||
|
holdings to leave our server at all, do not use the AI portfolio
|
||||||
|
analysis feature. We do not control retention or training policies
|
||||||
|
on the provider side beyond the headers we set — the provider’s
|
||||||
|
own published data policy is the binding statement on that point.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Market-data sources</strong>: Yahoo Finance and a small set
|
||||||
|
of public RSS feeds. We request prices and headlines; we don’t
|
||||||
|
send them any user identifier.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">Your rights (UK-GDPR)</h2>
|
||||||
|
<p>You have the right to:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Ask what personal data we hold about you (Art. 15, right of access).</li>
|
||||||
|
<li>Have inaccurate data corrected (Art. 16, rectification).</li>
|
||||||
|
<li>Have your account and associated data deleted (Art. 17, erasure).</li>
|
||||||
|
<li>Export the data you can recognise (Art. 20, portability): your
|
||||||
|
email, any active encrypted blob, your referral linkage.</li>
|
||||||
|
<li>Restrict processing (Art. 18).</li>
|
||||||
|
<li>Object specifically to processing carried out on the basis of
|
||||||
|
legitimate interests (Art. 21), including any direct marketing.</li>
|
||||||
|
<li>Withdraw consent at any time where processing is based on
|
||||||
|
consent (Art. 7(3)), e.g. by disabling cloud sync.</li>
|
||||||
|
<li>Lodge a complaint with the
|
||||||
|
<a href="https://ico.org.uk/" target="_blank" rel="noopener">Information Commissioner’s Office</a>
|
||||||
|
if you think we’re mishandling your data.</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Email <a href="mailto:{{ OPERATOR_EMAIL }}">{{ OPERATOR_EMAIL }}</a> to
|
||||||
|
exercise any of these.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">Children</h2>
|
||||||
|
<p>
|
||||||
|
The Service is not directed at, and is not intended for use by,
|
||||||
|
anyone under 18. Do not create an account if you are under 18. If
|
||||||
|
you believe a child has provided personal data to us, contact
|
||||||
|
<a href="mailto:{{ OPERATOR_EMAIL }}">{{ OPERATOR_EMAIL }}</a> and we
|
||||||
|
will delete it.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">Security incidents</h2>
|
||||||
|
<p>
|
||||||
|
If we discover a personal-data breach likely to result in a risk to
|
||||||
|
your rights and freedoms, we will notify the ICO within 72 hours of
|
||||||
|
becoming aware of it, as required by UK-GDPR Art. 33, and notify
|
||||||
|
affected users without undue delay where Art. 34 requires.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">Changes to this notice</h2>
|
||||||
|
<p>
|
||||||
|
Material changes will be flagged in-app and dated above. Trivial
|
||||||
|
edits (grammar, restructuring) won’t.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
64
app/templates/public_base.html
Normal file
64
app/templates/public_base.html
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{% block title %}{{ BRAND_NAME }}{% endblock %}</title>
|
||||||
|
<meta name="description" content="{{ TAGLINE }}">
|
||||||
|
{# Same flash-prevention theme bootstrap as the app shell. #}
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
var t = localStorage.getItem('cassandra.theme') || 'light';
|
||||||
|
document.documentElement.dataset.theme = t;
|
||||||
|
} catch (e) { document.documentElement.dataset.theme = 'light'; }
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', path='/css/cassandra.css') }}" />
|
||||||
|
</head>
|
||||||
|
<body class="public-page">
|
||||||
|
<div class="public-shell">
|
||||||
|
<header class="public-header">
|
||||||
|
<a class="public-header__brand" href="/" aria-label="{{ BRAND_NAME }} home">
|
||||||
|
{{ BRAND_NAME }}
|
||||||
|
</a>
|
||||||
|
<nav class="public-header__nav">
|
||||||
|
<a href="/pricing" class="{% if request.url.path == '/pricing' %}active{% endif %}">Pricing</a>
|
||||||
|
<a href="/about" class="{% if request.url.path == '/about' %}active{% endif %}">About</a>
|
||||||
|
{% if cu and (cu.user or cu.is_admin) %}
|
||||||
|
<a href="/" class="public-header__cta">Dashboard</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="/login" class="public-header__cta">Sign in / sign up</a>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="public-main">
|
||||||
|
{% block main %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="public-footer">
|
||||||
|
<div class="public-footer__inner">
|
||||||
|
<div class="public-footer__brand">
|
||||||
|
<strong>{{ BRAND_NAME }}</strong>
|
||||||
|
<span class="public-footer__tagline">{{ TAGLINE }}</span>
|
||||||
|
</div>
|
||||||
|
<nav class="public-footer__links">
|
||||||
|
<a href="/pricing">Pricing</a>
|
||||||
|
<a href="/about">About</a>
|
||||||
|
<a href="/terms">Terms</a>
|
||||||
|
<a href="/privacy">Privacy</a>
|
||||||
|
<a href="/disclaimer">Disclaimer</a>
|
||||||
|
</nav>
|
||||||
|
<div class="public-footer__meta">
|
||||||
|
© 2026 {{ LEGAL_OPERATOR }} ·
|
||||||
|
Not investment advice ·
|
||||||
|
{{ OPERATOR_JURISDICTION }} ·
|
||||||
|
ICO ZC098928 ·
|
||||||
|
Last reviewed 2026-05-24
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
214
app/templates/terms.html
Normal file
214
app/templates/terms.html
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
{% extends "public_base.html" %}
|
||||||
|
{% block title %}{{ BRAND_NAME }} · Terms of Service{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h1 class="public-section__head">Terms of Service</h1>
|
||||||
|
<p style="color: var(--muted); font-size: 13px;">
|
||||||
|
Last updated: 2026-05-24. {{ LEGAL_OPERATOR }} operates {{ BRAND_NAME }}
|
||||||
|
(the “Service”) from {{ OPERATOR_JURISDICTION }}.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">1. Acceptance</h2>
|
||||||
|
<p>
|
||||||
|
By creating an account or otherwise using the Service you agree to
|
||||||
|
these Terms and to the <a href="/privacy">Privacy notice</a> and
|
||||||
|
<a href="/disclaimer">Disclaimer</a>. If you do not agree, do not
|
||||||
|
use the Service.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">2. The Service</h2>
|
||||||
|
<p>
|
||||||
|
{{ BRAND_NAME }} provides a macro-strategy dashboard with curated
|
||||||
|
market data, news, and AI-generated commentary. Paid features include
|
||||||
|
portfolio import, AI portfolio analysis, and optional end-to-end
|
||||||
|
encrypted cloud sync of your portfolio. Feature lists, tiers, and
|
||||||
|
pricing are described on the <a href="/pricing">Pricing page</a> and
|
||||||
|
may change over time.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Nothing produced by the Service is investment advice. See the
|
||||||
|
<a href="/disclaimer">Disclaimer</a> for the full position.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">3. Accounts & authentication</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Accounts are tied to a working email address. Authentication is
|
||||||
|
by one-time codes sent to that address — no passwords.</li>
|
||||||
|
<li>You are responsible for the security of the email inbox you sign
|
||||||
|
up with. Anyone with access to that inbox can sign in.</li>
|
||||||
|
<li>One account per person. Do not share accounts or sign in on
|
||||||
|
behalf of someone else without their explicit permission.</li>
|
||||||
|
<li>You may delete your account at any time by writing to
|
||||||
|
<a href="mailto:{{ OPERATOR_EMAIL }}">{{ OPERATOR_EMAIL }}</a>.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">4. Acceptable use</h2>
|
||||||
|
<p>You agree not to:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Use automated tools to scrape, mirror, or repackage the Service
|
||||||
|
or any of its data, including market data and AI-generated
|
||||||
|
commentary, without prior written permission.</li>
|
||||||
|
<li>Use the Service, or any of its output, to ingest, train, fine-tune
|
||||||
|
or evaluate any machine-learning model without prior written
|
||||||
|
permission.</li>
|
||||||
|
<li>Abuse the one-time-code (OTP) system, including by submitting
|
||||||
|
emails you do not control.</li>
|
||||||
|
<li>Attempt to bypass authentication, paid-tier gating, or rate
|
||||||
|
limits.</li>
|
||||||
|
<li>Use the Service in any way that could damage, disable,
|
||||||
|
overburden, or impair it.</li>
|
||||||
|
<li>Use the Service to harass, defame, or threaten any person.</li>
|
||||||
|
<li>Reverse engineer, decompile, or otherwise extract source code,
|
||||||
|
except where applicable law expressly permits it.</li>
|
||||||
|
<li>Use the Service in a way that is unlawful in your jurisdiction.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">5. Paid plans</h2>
|
||||||
|
<p>
|
||||||
|
If and when paid plans become available, you will be told the
|
||||||
|
applicable fees at point of sale. Paid features remain active for as
|
||||||
|
long as the subscription is current or any time-bounded credit
|
||||||
|
granted to your account is still valid. You can cancel a paid
|
||||||
|
subscription at any time; cancellation takes effect at the end of
|
||||||
|
the current billing period unless otherwise stated.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Where the law gives you a 14-day right to cancel a subscription
|
||||||
|
(Consumer Contracts (Information, Cancellation and Additional
|
||||||
|
Charges) Regulations 2013, UK), that right applies. By starting to
|
||||||
|
use a paid feature immediately on purchase you agree we may begin
|
||||||
|
supplying the service within the cancellation period, and you
|
||||||
|
acknowledge that you lose the right to cancel in respect of any
|
||||||
|
digital content already delivered.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">6. Service availability</h2>
|
||||||
|
<p>
|
||||||
|
The Service is provided on a best-effort basis. There is no service
|
||||||
|
level agreement: outages, data delays, and feature changes may occur
|
||||||
|
without notice. Scheduled and unscheduled downtime is possible.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">7. Content & ownership</h2>
|
||||||
|
<p>
|
||||||
|
The Service’s code, design, indicator selection, and prompts
|
||||||
|
are owned or licensed by {{ LEGAL_OPERATOR }}. To the extent any
|
||||||
|
copyright subsists in the AI-generated commentary under UK law, it
|
||||||
|
vests in or is licensed to {{ LEGAL_OPERATOR }}. Market data is
|
||||||
|
sourced from third parties (e.g. Yahoo Finance and public RSS
|
||||||
|
feeds); their respective rights apply. You may view, save, and
|
||||||
|
quote short excerpts for personal use; you may not republish or
|
||||||
|
redistribute substantial portions, or use the Service’s
|
||||||
|
output to train machine-learning models, without prior written
|
||||||
|
permission.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Any portfolio you upload remains your data. The Service does not
|
||||||
|
persist your holdings as plaintext (see the
|
||||||
|
<a href="/privacy">Privacy notice</a>).
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">8. Suspension & termination</h2>
|
||||||
|
<p>
|
||||||
|
We may suspend or terminate access without notice for violation of
|
||||||
|
these Terms or for activity that risks the integrity, security, or
|
||||||
|
legality of the Service. You may stop using the Service at any time.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For paid subscribers, suspension for suspected breach will, where
|
||||||
|
reasonably practicable, be preceded by notice and an opportunity to
|
||||||
|
respond, unless immediate suspension is necessary to protect the
|
||||||
|
Service, its users, or any third party. If we terminate a paid
|
||||||
|
subscription for a breach you did not cause, we will refund the
|
||||||
|
unused portion of any prepaid fees on a pro-rata basis.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">9. No warranty</h2>
|
||||||
|
<p>
|
||||||
|
The Service is provided “as is” and “as
|
||||||
|
available”, without warranties of any kind, express or implied,
|
||||||
|
including merchantability, fitness for a particular purpose, accuracy,
|
||||||
|
and non-infringement, to the maximum extent permitted by law.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">10. Limitation of liability</h2>
|
||||||
|
<p>
|
||||||
|
To the maximum extent permitted by law, {{ LEGAL_OPERATOR }} is not
|
||||||
|
liable for any indirect, incidental, special, consequential, or
|
||||||
|
punitive damages, or for any investment loss or missed gain, arising
|
||||||
|
from your use of the Service. Subject to the carve-outs below, the
|
||||||
|
total aggregate liability of {{ LEGAL_OPERATOR }} to you for any
|
||||||
|
claim is capped at the greater of (a) the fees you have paid in the
|
||||||
|
12 months immediately preceding the claim, or (b) GBP 50.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Nothing in these Terms limits or excludes liability that cannot
|
||||||
|
lawfully be limited or excluded, including (i) death or personal
|
||||||
|
injury caused by negligence, (ii) fraud or fraudulent
|
||||||
|
misrepresentation, (iii) for consumers, any statutory rights under
|
||||||
|
Part 1 of the Consumer Rights Act 2015 (including the right to
|
||||||
|
services performed with reasonable care and skill), and (iv) any
|
||||||
|
other liability that cannot be excluded under applicable mandatory
|
||||||
|
law in your country of residence.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you are a consumer, the cap in the first paragraph of this
|
||||||
|
clause does not apply to claims for direct losses arising from a
|
||||||
|
breach of those statutory rights, which remain at the level the law
|
||||||
|
requires.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">11. Changes</h2>
|
||||||
|
<p>
|
||||||
|
These Terms may change. Material changes will be flagged in-app or
|
||||||
|
by email. Continued use after a change means you accept the updated
|
||||||
|
Terms. If you do not accept a material change, you may close your
|
||||||
|
account before the change takes effect; we will refund any prepaid
|
||||||
|
fees for the unused period.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">12. Governing law and jurisdiction</h2>
|
||||||
|
<p>
|
||||||
|
These Terms are governed by the laws of England and Wales. Subject
|
||||||
|
to any mandatory law of the consumer’s country of residence,
|
||||||
|
the courts of England and Wales have <strong>non-exclusive</strong>
|
||||||
|
jurisdiction over any dispute. UK consumers may also bring
|
||||||
|
proceedings in the courts of the part of the United Kingdom in
|
||||||
|
which they are habitually resident.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="public-section">
|
||||||
|
<h2 class="public-section__head">13. Contact</h2>
|
||||||
|
<p>
|
||||||
|
<a href="mailto:{{ OPERATOR_EMAIL }}">{{ OPERATOR_EMAIL }}</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -71,3 +71,7 @@ templates.env.globals["BRAND_NAME"] = branding.BRAND_NAME
|
||||||
templates.env.globals["BRAND_SHORT"] = branding.BRAND_SHORT
|
templates.env.globals["BRAND_SHORT"] = branding.BRAND_SHORT
|
||||||
templates.env.globals["SITE_URL"] = branding.SITE_URL
|
templates.env.globals["SITE_URL"] = branding.SITE_URL
|
||||||
templates.env.globals["APP_URL"] = branding.APP_URL
|
templates.env.globals["APP_URL"] = branding.APP_URL
|
||||||
|
templates.env.globals["TAGLINE"] = branding.TAGLINE
|
||||||
|
templates.env.globals["LEGAL_OPERATOR"] = branding.LEGAL_OPERATOR
|
||||||
|
templates.env.globals["OPERATOR_EMAIL"] = branding.OPERATOR_EMAIL
|
||||||
|
templates.env.globals["OPERATOR_JURISDICTION"] = branding.OPERATOR_JURISDICTION
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue