Files
ipx/web/index.html
rays de6b0a4ca5 ipx-web: the page in its own container, behind Caddy (#203)
web/build.mjs builds a static site, web/dist, with the icons and typeface
beside the pages; Dockerfile.web serves it with Caddy, which passes /api
and /media to ipx on the same origin, so cookies, live updates and
Cloudflare Access work as before. ipx serves no page any more: build.rs
and every include_str! of the web files are gone, and its image is built
without node.

What ipx did to the page as it served it is kept another way. Caddy asks
ipx before serving a page (forward_auth): /api/page-check sends a browser
not signed in to /login and a non-admin away from /admin, and refuses them
/admin.js; /api/login-check sends someone Access vouches for past the
form; a page asked for with ?token= goes to /api/signin, which sets the
cookie. The theme and the admin link, once written into the page per
person, are set by web/boot.js in <head> from the theme cookie and the
last visit, before the first frame, and /api/me brings the version and
the account's theme from before.

Caddy strips the Cf-Access-* headers from every peer but TUNNEL_PEER, the
address cloudflared's requests come from, which is the boundary
trusted_proxies drew; ipx then trusts only Caddy's address.

The browser suite runs Caddy in front of its daemon, so it tests what
production would run. Not deployed: production's cutover is on #203.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:06:53 +00:00

91 lines
4.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#0a1726">
<title>iPX</title>
<link rel="icon" type="image/png" sizes="128x128" id="favicon" href="/favicon.png" data-light="/favicon.png" data-dark="/favicon-dark.png">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="stylesheet" data-src="app.css">
<script src="/boot.js"></script>
</head>
<body>
<header id="topbar">
<button class="iconbtn" id="burger" title="Feeds" aria-label="Feeds" data-icon="menu"></button>
<!-- One group per thing acted on, in the order the panes read: feeds, then the selected item.
A phone hides the item group, which it has no table for. -->
<span id="applogo" title="iPX"><img class="logo onlight" src="/logo.svg" alt="iPX"><img class="logo ondark" src="/logo-dark.svg" alt="iPX"><img class="logo classic" src="/icon.png" alt="iPodderX"></span>
<div class="tgroup">
<button id="addFeed" title="Add a feed" aria-label="Add a feed" data-icon="plus"></button>
<button id="tbRemove" title="Unsubscribe from this feed" aria-label="Unsubscribe from this feed" data-icon="circleMinus" disabled></button>
<button id="scanAll" title="Check every feed for new items" aria-label="Check every feed for new items" data-icon="scan"></button>
</div>
<div class="tgroup item">
<button id="tbPlay" title="Play the selected item" aria-label="Play the selected item" data-icon="play" disabled></button>
<button id="tbRead" title="Mark the selected item read or unread" aria-label="Mark read or unread" data-icon="check" disabled></button>
<button id="tbFlag" title="Pin the selected item, so it is never deleted" aria-label="Pin" data-icon="pin" disabled></button>
</div>
<span class="grow"></span>
<input type="search" id="epSearch" placeholder="Search items…">
<div class="tgroup" id="admintools">
<button id="prefs" title="Settings" aria-label="Settings" data-icon="settings"></button>
<a id="admin" href="/admin" title="Admin: the server's settings, accounts and the log" aria-label="Admin" data-icon="admin"></a>
</div>
</header>
<div id="shell">
<aside id="sidebar">
<div class="searchwrap"><input type="search" id="feedFilter" placeholder="Filter feeds…"></div>
<div id="feedlist"></div>
<div class="sidefoot">
<div class="who"><span id="who"></span><button id="signout" title="Sign out" aria-label="Sign out" data-icon="signout"></button></div>
</div>
</aside>
<div id="main">
<div class="wrap" id="content"></div>
</div>
<div id="scrim" hidden></div>
</div>
<div id="status"><span id="count"></span></div>
<div id="player">
<div id="pnow">
<div id="partwrap"></div>
<div class="txt"><b><span class="eq" aria-hidden="true"><i></i><i></i><i></i></span><span id="ptitle"></span></b><small id="pfeed"></small></div>
</div>
<div id="pmid">
<div id="pbtns">
<button class="iconbtn" id="pback" title="Back 15 seconds (←)" aria-label="Back 15 seconds" data-icon="back"></button>
<button class="iconbtn" id="pplay" title="Play or pause (space)" aria-label="Play or pause" data-icon="play"></button>
<button class="iconbtn" id="pfwd" title="Forward 30 seconds (→)" aria-label="Forward 30 seconds" data-icon="fwd"></button>
</div>
<div id="seekrow">
<span id="pcur">0:00</span>
<input type="range" id="seek" min="0" max="1000" value="0">
<span id="pdur">0:00</span>
</div>
</div>
<div id="pright">
<button class="iconbtn" id="pdead" aria-pressed="false" aria-label="DeadAir" hidden></button>
<select id="rate" title="Speed">
<option value="0.8">0.8×</option><option value="1" selected>1×</option>
<option value="1.25">1.25×</option><option value="1.5">1.5×</option>
<option value="1.75">1.75×</option><option value="2">2×</option><option value="2.5">2.5×</option>
</select>
<input type="range" id="vol" min="0" max="100" value="100" title="Volume">
<button class="iconbtn" id="pclose" title="Close the player" aria-label="Close the player" data-icon="close"></button>
</div>
</div>
<div id="modal"><div class="card" id="modalCard"></div></div>
<div id="toasts"></div>
<!-- One element for both: a <video> plays an audio-only file exactly like <audio> does (same
HTMLMediaElement API), and it is the only tag that can also show a picture. Hidden unless
the current file is video -- see body.has-video below. -->
<video id="audio" preload="metadata" playsinline></video>
<script data-src="web/src"></script>
</body>
</html>