Replace Astro hub build with standalone hub/index.html
CI / Python tests (push) Waiting to run
CI / Frontend build (push) Waiting to run

This commit is contained in:
Davide Scaini
2026-05-02 22:49:32 +02:00
parent 58def4bf02
commit 9540cdd6cb
3 changed files with 282 additions and 156 deletions
+279
View File
@@ -0,0 +1,279 @@
<!DOCTYPE html>
<html lang="it" style="background:#09090b">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bincio</title>
<script>
(function () {
var palettes = {
default: { accent: '#60a5fa', dim: 'rgba(96,165,250,0.15)' },
giro: { accent: '#f472b6', dim: 'rgba(244,114,182,0.15)' },
tour: { accent: '#facc15', dim: 'rgba(250,204,21,0.15)' },
vuelta: { accent: '#ef4444', dim: 'rgba(239,68,68,0.15)' },
};
var races = [
{ key: 'giro', start: [4, 8], end: [5, 1] },
{ key: 'tour', start: [5, 27], end: [6, 19] },
{ key: 'vuelta', start: [7, 15], end: [8, 6] },
];
function autoKey() {
var now = new Date(), y = now.getFullYear();
for (var i = 0; i < races.length; i++) {
var r = races[i];
if (now >= new Date(y, r.start[0], r.start[1]) &&
now < new Date(y, r.end[0], r.end[1] + 1)) return r.key;
}
return 'default';
}
var p = palettes[autoKey()];
document.documentElement.style.setProperty('--accent', p.accent);
document.documentElement.style.setProperty('--accent-dim', p.dim);
})();
</script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--accent: #60a5fa;
--accent-dim: rgba(96,165,250,0.15);
--bg: #09090b;
--bg-card: #18181b;
--border: #27272a;
--border-hover: #52525b;
--text: #ffffff;
--text-muted: #71717a;
--text-dim: #3f3f46;
--red: #f87171;
}
html, body {
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
-webkit-font-smoothing: antialiased;
}
body { visibility: hidden; }
.wrap {
max-width: 384px;
margin: 0 auto;
padding: 4rem 1rem 2rem;
}
.tagline {
text-align: center;
color: var(--text-dim);
font-size: 0.875rem;
font-style: italic;
line-height: 1.8;
margin-bottom: 2rem;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
text-align: center;
margin-bottom: 1.5rem;
}
label {
display: block;
font-size: 0.875rem;
color: var(--text-muted);
margin-bottom: 0.25rem;
}
input[type=text], input[type=password] {
width: 100%;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text);
font-size: 1rem;
outline: none;
transition: border-color 0.15s;
}
input[type=text]:focus, input[type=password]:focus {
border-color: var(--accent);
}
.field { margin-bottom: 1rem; }
.btn-primary {
width: 100%;
padding: 0.5rem;
border-radius: 0.5rem;
background: var(--accent);
color: #000;
font-weight: 600;
font-size: 1rem;
border: none;
cursor: pointer;
transition: opacity 0.15s;
margin-top: 0.25rem;
}
.btn-primary:hover { opacity: 0.85; }
.error {
font-size: 0.875rem;
color: var(--red);
margin-bottom: 0.75rem;
display: none;
}
.forgot {
text-align: center;
margin-top: 1.5rem;
}
.forgot a {
font-size: 0.875rem;
color: var(--text-muted);
text-decoration: none;
transition: color 0.15s;
}
.forgot a:hover { color: var(--text-dim); }
/* App cards */
.greeting {
text-align: center;
font-size: 0.875rem;
color: var(--text-muted);
margin-bottom: 1rem;
}
.cards { display: flex; flex-direction: column; gap: 0.75rem; }
.card {
display: block;
padding: 1rem 1.25rem;
border-radius: 0.75rem;
background: var(--bg-card);
border: 1px solid var(--border);
text-decoration: none;
transition: border-color 0.15s;
}
.card:hover { border-color: var(--border-hover); }
.card-title { font-weight: 600; color: var(--text); }
.card-sub { font-size: 0.75rem; color: var(--text-muted); margin-top: 0.125rem; }
.logout-wrap { text-align: center; margin-top: 2rem; }
.btn-logout {
font-size: 0.75rem;
color: var(--text-dim);
background: none;
border: none;
cursor: pointer;
transition: color 0.15s;
}
.btn-logout:hover { color: var(--text-muted); }
#hub-apps { display: none; }
</style>
</head>
<body>
<div class="wrap">
<div id="hub-login">
<p class="tagline">mangia<br>bevi<br>stai calmo<br>non strappare</p>
<h1>Bincio</h1>
<form id="login-form">
<div class="field">
<label for="handle">Handle</label>
<input id="handle" name="handle" type="text" autocomplete="username" placeholder="your handle" required>
</div>
<div class="field">
<label for="password">Password</label>
<input id="password" name="password" type="password" autocomplete="current-password" required>
</div>
<p id="login-error" class="error"></p>
<button type="submit" class="btn-primary">Sign in</button>
</form>
<p class="forgot">
<a href="https://activity.bincio.org/reset-password/">Forgot password?</a>
</p>
</div>
<div id="hub-apps">
<p id="hub-greeting" class="greeting"></p>
<p class="tagline">mangia<br>bevi<br>stai calmo<br>non strappare</p>
<div id="hub-cards" class="cards"></div>
<div class="logout-wrap">
<button id="hub-logout" class="btn-logout">Log out</button>
</div>
</div>
</div>
<script>
const ACTIVITY_URL = 'https://activity.bincio.org';
const WIKI_URL = 'https://wiki.bincio.org';
const loginDiv = document.getElementById('hub-login');
const appsDiv = document.getElementById('hub-apps');
const greeting = document.getElementById('hub-greeting');
const cardsDiv = document.getElementById('hub-cards');
const form = document.getElementById('login-form');
const errEl = document.getElementById('login-error');
function appCard(title, sub, href) {
const a = document.createElement('a');
a.className = 'card';
a.href = href;
a.innerHTML = `<p class="card-title">${title}</p><p class="card-sub">${sub}</p>`;
return a;
}
function showApps(user) {
loginDiv.style.display = 'none';
appsDiv.style.display = '';
greeting.textContent = 'Ciao ' + (user.display_name || user.handle);
cardsDiv.innerHTML = '';
if (user.activity_access)
cardsDiv.appendChild(appCard('BincioActivity', 'Tracks, strade e numeri', ACTIVITY_URL));
if (user.wiki_access)
cardsDiv.appendChild(appCard('BincioWiki', 'La memoria collettiva del gruppo', WIKI_URL));
}
function showLogin() {
appsDiv.style.display = 'none';
loginDiv.style.display = '';
}
fetch('/api/me', { credentials: 'include' })
.then(async r => {
document.body.style.visibility = '';
if (r.ok) showApps(await r.json());
})
.catch(() => { document.body.style.visibility = ''; });
form.addEventListener('submit', async e => {
e.preventDefault();
errEl.style.display = 'none';
const handle = form.querySelector('#handle').value.trim();
const password = form.querySelector('#password').value;
try {
const r = await fetch('/api/auth/login', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ handle, password }),
});
if (!r.ok) {
const d = await r.json().catch(() => ({}));
errEl.textContent = d.detail ?? 'Invalid credentials';
errEl.style.display = '';
return;
}
showApps(await r.json());
} catch {
errEl.textContent = 'Could not reach server';
errEl.style.display = '';
}
});
document.getElementById('hub-logout').addEventListener('click', async () => {
try { await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); } catch (_) {}
form.querySelector('#handle').value = '';
form.querySelector('#password').value = '';
showLogin();
});
</script>
</body>
</html>
+2 -6
View File
@@ -145,7 +145,7 @@ def prepare_serve() -> None:
# ── 4. Hand off to bincio dev ───────────────────────────────────────────────── # ── 4. Hand off to bincio dev ─────────────────────────────────────────────────
def start_dev(mobile: bool = False, hub: bool = False) -> None: def start_dev(mobile: bool = False) -> None:
section("Starting bincio dev") section("Starting bincio dev")
print() print()
print(" \033[1mCredentials\033[0m") print(" \033[1mCredentials\033[0m")
@@ -163,9 +163,6 @@ def start_dev(mobile: bool = False, hub: bool = False) -> None:
env = os.environ.copy() env = os.environ.copy()
env.setdefault("PUBLIC_WIKI_URL", os.environ.get("WIKI_DEV_URL", "http://localhost:4322")) env.setdefault("PUBLIC_WIKI_URL", os.environ.get("WIKI_DEV_URL", "http://localhost:4322"))
# --hub: simulate bincio.org hub mode (/ becomes login + app selector)
if hub:
env.setdefault("PUBLIC_ACTIVITY_URL", "http://localhost:4321")
try: try:
subprocess.run(cmd, cwd=PROJECT_DIR, env=env) subprocess.run(cmd, cwd=PROJECT_DIR, env=env)
@@ -193,7 +190,6 @@ def main() -> None:
parser.add_argument("--fresh", action="store_true", help="Wipe DATA_DIR before starting") parser.add_argument("--fresh", action="store_true", help="Wipe DATA_DIR before starting")
parser.add_argument("--no-dev", action="store_true", help="Stop after extract, skip bincio dev") parser.add_argument("--no-dev", action="store_true", help="Stop after extract, skip bincio dev")
parser.add_argument("--mobile", action="store_true", help="Bind API to 0.0.0.0 for local mobile testing") parser.add_argument("--mobile", action="store_true", help="Bind API to 0.0.0.0 for local mobile testing")
parser.add_argument("--hub", action="store_true", help="Simulate hub mode: / becomes login+app selector")
args = parser.parse_args() args = parser.parse_args()
raise_open_file_limit() raise_open_file_limit()
@@ -211,7 +207,7 @@ def main() -> None:
prepare_serve() prepare_serve()
if not args.no_dev: if not args.no_dev:
start_dev(mobile=args.mobile, hub=args.hub) start_dev(mobile=args.mobile)
else: else:
print(f"\n\033[32mDone.\033[0m Data ready at {DATA_DIR}") print(f"\n\033[32mDone.\033[0m Data ready at {DATA_DIR}")
print(f"Run: uv run bincio dev --data-dir {DATA_DIR}\n") print(f"Run: uv run bincio dev --data-dir {DATA_DIR}\n")
+1 -150
View File
@@ -4,71 +4,12 @@ import ActivityFeed from '../components/ActivityFeed.svelte';
import { readShardHandles, isInstancePrivate } from '../lib/manifest'; import { readShardHandles, isInstancePrivate } from '../lib/manifest';
const base = import.meta.env.BASE_URL; const base = import.meta.env.BASE_URL;
const activityUrl = import.meta.env.PUBLIC_ACTIVITY_URL ?? '';
const wikiUrl = import.meta.env.PUBLIC_WIKI_URL ?? '';
const shards = readShardHandles(); const shards = readShardHandles();
const isSingleUser = shards.length === 1 && !isInstancePrivate(); const isSingleUser = shards.length === 1 && !isInstancePrivate();
const singleHandle = isSingleUser ? shards[0].handle : null; const singleHandle = isSingleUser ? shards[0].handle : null;
--- ---
{activityUrl ? ( {isSingleUser ? (
<!-- Hub mode: bincio.org - login form / app selector -->
<Base title="Bincio" public={true}>
<div class="max-w-sm mx-auto mt-16 px-4"
id="hub-root"
data-activity-url={activityUrl}
data-wiki-url={wikiUrl}
style="visibility:hidden">
<!-- Login (shown when not authenticated) -->
<div id="hub-login">
<p class="text-center text-zinc-600 text-sm italic mb-8 leading-relaxed">
mangia<br/>bevi<br/>stai calmo<br/>non strappare
</p>
<h1 class="text-2xl font-bold text-white mb-6 text-center">Bincio</h1>
<form id="login-form" class="space-y-4">
<div>
<label class="block text-sm text-zinc-400 mb-1" for="handle">Handle</label>
<input id="handle" name="handle" type="text" autocomplete="username"
class="w-full px-3 py-2 rounded-lg bg-zinc-900 border border-zinc-700 text-white placeholder-zinc-500 focus:outline-none focus:border-[--accent]"
placeholder="your handle" required />
</div>
<div>
<label class="block text-sm text-zinc-400 mb-1" for="password">Password</label>
<input id="password" name="password" type="password" autocomplete="current-password"
class="w-full px-3 py-2 rounded-lg bg-zinc-900 border border-zinc-700 text-white focus:outline-none focus:border-[--accent]"
required />
</div>
<p id="login-error" class="text-red-400 text-sm hidden"></p>
<button type="submit"
class="w-full py-2 rounded-lg bg-[--accent] hover:opacity-90 text-white font-medium transition-opacity">
Sign in
</button>
</form>
<p class="text-center text-zinc-500 text-sm mt-6">
<a href="/reset-password/" class="hover:text-zinc-400 transition-colors">Forgot password?</a>
</p>
</div>
<!-- App selector (shown when authenticated) -->
<div id="hub-apps" style="display:none">
<p id="hub-greeting" class="text-center text-zinc-400 text-sm mb-4"></p>
<p class="text-center text-zinc-600 text-sm italic mb-8 leading-relaxed">
mangia<br/>bevi<br/>stai calmo<br/>non strappare
</p>
<div id="hub-cards" class="space-y-3"></div>
<p class="text-center mt-8">
<button id="hub-logout"
class="text-xs text-zinc-600 hover:text-zinc-400 transition-colors">
Log out
</button>
</p>
</div>
</div>
</Base>
) : isSingleUser ? (
<!-- Single-user: redirect / -> /u/{handle}/ --> <!-- Single-user: redirect / -> /u/{handle}/ -->
<meta http-equiv="refresh" content={`0;url=${base}u/${singleHandle}/`} /> <meta http-equiv="refresh" content={`0;url=${base}u/${singleHandle}/`} />
<script define:vars={{ base, singleHandle }}> <script define:vars={{ base, singleHandle }}>
@@ -81,93 +22,3 @@ const singleHandle = isSingleUser ? shards[0].handle : null;
<ActivityFeed {base} client:only="svelte" /> <ActivityFeed {base} client:only="svelte" />
</Base> </Base>
)} )}
<script>
const root = document.getElementById('hub-root') as HTMLElement | null;
if (!root) {
// Not in hub mode - nothing to do.
} else {
const activityUrl = root.dataset.activityUrl ?? '';
const wikiUrl = root.dataset.wikiUrl ?? '';
const loginDiv = document.getElementById('hub-login') as HTMLElement;
const appsDiv = document.getElementById('hub-apps') as HTMLElement;
const greeting = document.getElementById('hub-greeting') as HTMLElement;
const cardsDiv = document.getElementById('hub-cards') as HTMLElement;
const form = document.getElementById('login-form') as HTMLFormElement;
const errEl = document.getElementById('login-error') as HTMLElement;
function appCard(label: string, sub: string, href: string): HTMLAnchorElement {
const a = document.createElement('a');
a.href = href;
a.className = 'block rounded-xl bg-zinc-900 border border-zinc-800 px-5 py-4 hover:border-zinc-600 transition-colors';
a.innerHTML = `
<p class="font-semibold text-white">${label}</p>
<p class="text-xs text-zinc-500 mt-0.5">${sub}</p>
`;
return a;
}
function showApps(user: { handle: string; display_name: string; activity_access: boolean; wiki_access: boolean }) {
loginDiv.style.display = 'none';
appsDiv.style.display = '';
greeting.textContent = `Ciao ${user.display_name || user.handle}`;
cardsDiv.innerHTML = '';
if (user.activity_access && activityUrl) {
// If activityUrl is the same origin (dev --hub mode), go to the user page
// directly so we don't loop back to the hub root.
const activityHref = activityUrl === window.location.origin
? `${activityUrl}/u/${user.handle}/`
: activityUrl;
cardsDiv.appendChild(appCard('BincioActivity', 'Tracks, strade e numeri', activityHref));
}
if (user.wiki_access && wikiUrl)
cardsDiv.appendChild(appCard('BincioWiki', 'La memoria collettiva del gruppo', wikiUrl));
}
// Check if already authenticated
fetch('/api/me', { credentials: 'include' })
.then(async r => {
root.style.visibility = 'visible';
if (r.ok) showApps(await r.json());
// else: show login form (already visible)
})
.catch(() => { root.style.visibility = 'visible'; });
// Login form submit
form?.addEventListener('submit', async e => {
e.preventDefault();
const handle = (form.querySelector('#handle') as HTMLInputElement).value.trim();
const password = (form.querySelector('#password') as HTMLInputElement).value;
errEl.classList.add('hidden');
try {
const r = await fetch('/api/auth/login', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ handle, password }),
});
if (!r.ok) {
const d = await r.json().catch(() => ({}));
errEl.textContent = d.detail ?? 'Invalid credentials';
errEl.classList.remove('hidden');
return;
}
showApps(await r.json());
} catch {
errEl.textContent = 'Could not reach server';
errEl.classList.remove('hidden');
}
});
// Logout
document.getElementById('hub-logout')?.addEventListener('click', async () => {
try { await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); } catch (_) {}
appsDiv.style.display = 'none';
loginDiv.style.display = '';
(form.querySelector('#handle') as HTMLInputElement).value = '';
(form.querySelector('#password') as HTMLInputElement).value = '';
});
}
</script>