feat: add sales-first Rank landing
This commit is contained in:
@@ -51,6 +51,25 @@ const profiles = {
|
||||
},
|
||||
};
|
||||
const state = { ideas: [], milestones: [], activity: [], activeId: null, selected: null, profileId: localStorage.getItem('rank-profile') || 'mvp', undo: null, recentPlacement: null };
|
||||
const sampleBacklog = {
|
||||
format: 'prioritix-feature-set-v1',
|
||||
name: 'Messy SaaS launch backlog',
|
||||
features: [
|
||||
{ title: 'Mobile sorting flow feels clumsy', description: 'Users can capture ideas, but the phone flow makes prioritizing feel like work instead of relief.', labels: ['Mobile', 'Activation'], impact: 9, effort: 4, confidence: 8, urgency: 8 },
|
||||
{ title: 'Team voting on every feature', description: 'Stakeholders want input, but voting may create politics before the product has a clear decision model.', labels: ['Collaboration'], impact: 7, effort: 7, confidence: 4, urgency: 5 },
|
||||
{ title: 'Export roadmap for sales calls', description: 'Turn sorted decisions into something founders can share with clients or internal buyers.', labels: ['Sales', 'Export'], impact: 8, effort: 5, confidence: 7, urgency: 7 },
|
||||
{ title: 'Dark mode polish', description: 'Nice for taste, but unlikely to decide whether anyone trusts the core prioritization.', labels: ['Polish'], impact: 4, effort: 3, confidence: 8, urgency: 2 },
|
||||
{ title: 'AI explains why an idea moved', description: 'Every ranking should show a reason, risk, and what evidence would change the decision.', labels: ['Trust', 'AI'], impact: 9, effort: 6, confidence: 6, urgency: 8 }
|
||||
]
|
||||
};
|
||||
function loadSampleBacklog(){
|
||||
const input = $('#featureSetInput');
|
||||
if(!input) return;
|
||||
input.value = JSON.stringify(sampleBacklog, null, 2);
|
||||
document.querySelector('#feature-sets')?.scrollIntoView({ behavior:'smooth', block:'start' });
|
||||
toast('Sample backlog loaded. Import it or replace it with your own chaos.');
|
||||
}
|
||||
|
||||
const $ = (sel, root=document) => root.querySelector(sel);
|
||||
const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel));
|
||||
const featureDeck = $('#featureDeck'); const sortingGrid = $('#sortingGrid'); const timeline = $('#timeline');
|
||||
@@ -168,6 +187,7 @@ async function reorderOnTimeline(id,e){ const line=e.currentTarget; const idea=s
|
||||
function openDetail(id){ const idea=state.ideas.find(i=>i.id===id); if(!idea) return; state.selected=id; detailForm.title.value=idea.title||''; detailForm.description.value=idea.description||''; detailForm.labels.value=(idea.labels||[]).join(', '); detailForm.impact.value=idea.impact??5; detailForm.effort.value=idea.effort??5; detailForm.confidence.value=idea.confidence??5; detailForm.urgency.value=idea.urgency??5; detailForm.notes.value=idea.notes||''; $('#detailCategory').textContent=categoryOf(idea); $('.detail-head .chip').textContent=zoneFor(idea).label; detail.classList.add('open'); detail.setAttribute('aria-hidden','false'); }
|
||||
function closeDetail(){ detail.classList.remove('open'); detail.setAttribute('aria-hidden','true'); state.selected=null; }
|
||||
async function archiveIdea(id=state.selected){ if(!id) return; const previous=state.ideas.find(i=>i.id===id); try{ await api(`/api/ideas/${id}`,{method:'PATCH',body:{archived:true,status:'remove'}}); state.ideas = state.ideas.filter(i=>i.id!==id); closeDetail(); render(); toast('Removed', previous ? () => undoIdea({...previous, archived:false}) : null); } catch(error){ toast(error.message); } }
|
||||
$('#sampleBacklog')?.addEventListener('click', loadSampleBacklog);
|
||||
$('#featureSetFile')?.addEventListener('change', async e => {
|
||||
const file = e.currentTarget.files?.[0];
|
||||
if(!file) return;
|
||||
|
||||
+61
-14
@@ -4,9 +4,9 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#061B33" />
|
||||
<title>Prioritix — Feature Prioritization</title>
|
||||
<title>Rank — Work your ideas before they work you</title>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="stylesheet" href="/styles.css?v=prioritix-20260522-5" />
|
||||
<link rel="stylesheet" href="/styles.css?v=rank-sales-20260523-1" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
@@ -22,11 +22,58 @@
|
||||
</aside>
|
||||
|
||||
<main class="workspace">
|
||||
<section class="sales-hero" aria-label="Rank product promise">
|
||||
<div class="hero-copy">
|
||||
<span class="eyebrow">For scattered founders, product teams, and backlog-heavy brains</span>
|
||||
<h1>Your backlog is not a roadmap.</h1>
|
||||
<p class="hero-lede">Rank is a nice-looking tool that understands your chaos. Paste messy ideas and work them into a clear build order before they start working you.</p>
|
||||
<div class="hero-actions">
|
||||
<a class="primary-cta" href="#feature-sets">Paste my messy backlog</a>
|
||||
<button class="secondary-cta" type="button" id="sampleBacklog">Try sample backlog</button>
|
||||
</div>
|
||||
<div class="trust-row" aria-label="What Rank gives you">
|
||||
<span>Build now</span>
|
||||
<span>Validate next</span>
|
||||
<span>Park safely</span>
|
||||
<span>Explain why</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="proof-card" aria-label="Before and after example">
|
||||
<div class="proof-column messy">
|
||||
<span>Before</span>
|
||||
<h2>Messy idea pile</h2>
|
||||
<ul>
|
||||
<li>AI onboarding coach</li>
|
||||
<li>Team voting</li>
|
||||
<li>Dark mode</li>
|
||||
<li>Stripe export</li>
|
||||
<li>Better mobile sorting</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="proof-arrow" aria-hidden="true">→</div>
|
||||
<div class="proof-column clear">
|
||||
<span>After</span>
|
||||
<h2>Defendable build order</h2>
|
||||
<ol>
|
||||
<li><b>Build now</b><small>Mobile sorting removes the biggest usage friction.</small></li>
|
||||
<li><b>Validate next</b><small>Team voting needs proof before it becomes process theatre.</small></li>
|
||||
<li><b>Park</b><small>Dark mode is nice, not the reason anyone buys.</small></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="approach-strip" aria-label="How Rank approaches you">
|
||||
<article><strong>No dashboard first.</strong><span>Proof before controls. You see the transformation before learning the system.</span></article>
|
||||
<article><strong>No fake AI mysticism.</strong><span>Every decision needs a plain reason, risk, and next evidence.</span></article>
|
||||
<article><strong>No surprise gate.</strong><span>Work the ideas first; export, save, and deeper workflows come after value is obvious.</span></article>
|
||||
</section>
|
||||
|
||||
<header class="topbar" id="home">
|
||||
<button class="menu-button" type="button" aria-label="Menu">☰</button>
|
||||
<div class="project-title">
|
||||
<h1>Rank Prioritization Studio</h1>
|
||||
<p><strong>Project goal:</strong> capture, sort, and turn rough feature ideas into a visible roadmap.</p>
|
||||
<h1>Rank idea workbench</h1>
|
||||
<p><strong>Project goal:</strong> turn rough feature ideas into a clear, defendable build order.</p>
|
||||
</div>
|
||||
<div class="profile-card">
|
||||
<span>Sorting profile</span>
|
||||
@@ -47,25 +94,25 @@
|
||||
<div class="mobile-statusbar"><span>9:41</span><span>Rank</span></div>
|
||||
<div class="mobile-project-card">
|
||||
<span class="eyebrow">Project overview</span>
|
||||
<h2>Rank Roadmap</h2>
|
||||
<p>Create a clear feature timeline from rough ideas, imports, and agent suggestions.</p>
|
||||
<h2>Work your ideas</h2>
|
||||
<p>Paste the chaos. Sort what to build now, validate next, and park without guilt.</p>
|
||||
<div id="mobileHomeProgress" class="mobile-home-progress"></div>
|
||||
</div>
|
||||
<div class="mobile-flow-cards">
|
||||
<a href="#prioritize"><span>☆</span><strong>Active Feature Sorting</strong><small>Prioritize using the selected profile.</small></a>
|
||||
<a href="#roadmap"><span>▱</span><strong>Timeline Roadmap</strong><small>Review and reorder the product timeline.</small></a>
|
||||
<a href="#feature-sets"><span>⇪</span><strong>Import Feature Set</strong><small>Paste or upload a feature backlog.</small></a>
|
||||
<a href="#feature-sets"><span>⇪</span><strong>Paste messy backlog</strong><small>Drop in rough ideas and turn them into order.</small></a>
|
||||
<a href="#review"><span>✓</span><strong>Completion Review</strong><small>Check sorted progress and export.</small></a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="capture-strip" aria-label="Quick capture">
|
||||
<form id="ideaForm" autocomplete="off">
|
||||
<label class="capture-title">New feature
|
||||
<input id="title" name="title" maxlength="180" placeholder="Smart requirement sorting, mobile dark mode, dependency warnings…" required />
|
||||
<label class="capture-title">One messy idea
|
||||
<input id="title" name="title" maxlength="180" placeholder="Mobile sorting feels clumsy, team voting, Stripe export…" required />
|
||||
</label>
|
||||
<label>Brief
|
||||
<input id="description" name="description" maxlength="500" placeholder="One sentence. Keep sorting focused." />
|
||||
<input id="description" name="description" maxlength="500" placeholder="Why does this matter, and what would it change?" />
|
||||
</label>
|
||||
<label>Category
|
||||
<input id="labels" name="labels" placeholder="UI / UX" />
|
||||
@@ -81,9 +128,9 @@
|
||||
|
||||
<section class="feature-set-panel" id="feature-sets" aria-label="Feature set import and export">
|
||||
<div class="feature-set-copy">
|
||||
<div class="section-label">Feature set import / export</div>
|
||||
<h2>Upload or paste a whole feature set.</h2>
|
||||
<p>Use <strong>Prioritix Feature Set v1</strong>: JSON with a top-level <code>features</code> array. Required: <code>title</code>. Optional: <code>description</code>, <code>labels</code>, <code>impact</code>, <code>effort</code>, <code>confidence</code>, <code>urgency</code>, <code>status</code>, <code>milestoneId</code>, <code>rank</code>, <code>notes</code>. Export uses the same format, including the sorted setup.</p>
|
||||
<div class="section-label">Paste the chaos</div>
|
||||
<h2>Drop in the rough list. Rank turns it into a build order.</h2>
|
||||
<p>Start with a messy backlog, stakeholder requests, or five half-formed ideas. Rank helps you decide what to build now, what needs evidence, what can wait, and what is just noise wearing a nice hat. JSON import still works if you need it.</p>
|
||||
</div>
|
||||
<div class="feature-set-actions">
|
||||
<textarea id="featureSetInput" spellcheck="false" placeholder='{
|
||||
@@ -172,6 +219,6 @@
|
||||
</aside>
|
||||
|
||||
<div class="toast" id="toast" hidden></div>
|
||||
<script src="/app.js?v=prioritix-20260522-5" type="module"></script>
|
||||
<script src="/app.js?v=rank-sales-20260523-1" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -28,3 +28,31 @@ button,input,textarea{font:inherit} button{cursor:pointer} a{color:inherit;text-
|
||||
@media(max-width:680px){
|
||||
:root{--radius:20px;color-scheme:light}.app-shell{display:block}.workspace{padding:12px 12px calc(104px + env(safe-area-inset-bottom));max-width:430px}.topbar{display:none}.mobile-home{display:block;padding:0 0 12px}.mobile-statusbar{height:34px;display:flex;justify-content:space-between;align-items:center;color:var(--navy-950);font-weight:900;padding:0 6px}.mobile-project-card{background:linear-gradient(180deg,var(--navy-950),#082856);color:white;border-radius:0 0 28px 28px;padding:22px 18px 24px;margin:0 -12px 12px;box-shadow:0 18px 45px rgba(6,27,51,.18)}.eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#A7D8FF;font-weight:900}.mobile-project-card h2{font-size:26px;letter-spacing:-.04em;margin:8px 0}.mobile-project-card p{margin:0;color:#D9E8FF;line-height:1.45}.mobile-home-progress{margin-top:18px}.mobile-home-progress strong{display:block}.mobile-home-progress span{display:block;color:#C6D8EF;font-size:12px;margin-top:7px}.mobile-flow-cards{display:grid;gap:10px}.mobile-flow-cards a{display:grid;grid-template-columns:42px 1fr;gap:10px;align-items:center;background:white;border:1px solid var(--border);border-radius:18px;padding:14px;box-shadow:var(--shadow-soft)}.mobile-flow-cards span{width:34px;height:34px;display:grid;place-items:center;border-radius:11px;background:#EEF4FF;color:var(--blue-600);font-size:18px;grid-row:1/3}.mobile-flow-cards strong{display:block;color:var(--navy-950);grid-column:2}.mobile-flow-cards small{display:block;color:var(--slate-600);margin-top:3px;grid-column:2}.sidebar{display:block!important;position:fixed;left:0;right:0;top:auto;bottom:0;height:auto;min-height:74px;padding:7px 10px calc(8px + env(safe-area-inset-bottom));background:rgba(255,255,255,.96);backdrop-filter:blur(18px);color:var(--slate-700);border-top:1px solid var(--border);border-right:0;box-shadow:0 -16px 40px rgba(16,24,40,.12);z-index:80}.brand-mark,.collapse{display:none}.sidebar nav{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}.sidebar a{color:var(--slate-600);padding:7px 4px;border-radius:14px;font-size:10px}.sidebar a span{font-size:19px}.sidebar a.active,.sidebar a:target{background:#EEF4FF;color:var(--blue-600);box-shadow:none}.capture-strip{border-radius:22px;padding:14px;margin:12px 0}.capture-strip form{grid-template-columns:1fr!important}.capture-strip button{border-radius:14px;background:linear-gradient(135deg,var(--blue-600),var(--purple-600));height:48px}.feature-set-panel{border-radius:22px;padding:14px;max-height:94px;overflow:hidden;transition:max-height .2s ease}.feature-set-panel:focus-within,.feature-set-panel:hover{max-height:760px}.feature-set-copy h2{font-size:17px}.feature-set-copy p{display:none}.sorting-layout{display:grid;grid-template-columns:1fr!important;gap:14px;margin-top:14px}.active-column,.grid-column{background:white;border:1px solid var(--border);border-radius:22px;padding:15px;box-shadow:var(--shadow-soft)}.section-label{font-size:11px;color:var(--slate-600)}.feature-deck{min-height:174px}.feature-card{width:100%;min-height:162px;border-radius:22px;box-shadow:0 16px 36px rgba(16,24,40,.12)}.feature-card h2{font-size:20px}.deck-actions{width:100%;grid-template-columns:1fr 1fr}.deck-actions button{border-radius:14px}.sorting-grid{grid-template-columns:1fr 1fr!important;gap:10px}.zone{min-height:132px;border-radius:20px;padding:12px}.zone-icon{font-size:28px}.zone strong{font-size:13px}.zone span{display:none}.drop-hint{width:30px;height:30px;margin-top:9px;border-radius:10px}.shortcut-hint{display:none}.utility-row{position:static;bottom:auto;z-index:auto;margin:12px 0;border-radius:22px;grid-template-columns:repeat(3,1fr)!important;box-shadow:0 18px 48px rgba(16,24,40,.18)}.utility-row button{display:grid;grid-template-columns:1fr;text-align:center;padding:12px 8px;border-bottom:0!important;border-right:1px solid var(--border);gap:4px}.utility-row button:last-child{border-right:0}.utility-row span{grid-row:auto;font-size:24px}.utility-row small{display:none}.timeline-panel,.backlog-panel,.review-panel{border-radius:22px;padding:16px}.timeline-head{display:block}.timeline-head p{font-size:12px}.zoom-controls{display:none}.timeline{padding:14px 2px 4px;min-height:auto;overflow:visible}.detail{inset:0;width:100vw;height:100dvh;right:auto;top:auto;bottom:auto;border-radius:0;border:0;padding:18px 16px calc(104px + env(safe-area-inset-bottom));transform:translateX(100%)}.detail.open{transform:translateX(0)}.detail-head{position:sticky;top:-18px;background:white;z-index:2;padding:12px 0 10px}.detail-title{font-size:24px!important}.detail-actions{grid-template-columns:1fr 1fr 1fr}.review-stats,.review-actions{grid-template-columns:1fr}.toast{left:12px;right:12px;bottom:92px;transform:none;justify-content:space-between;border-radius:16px}.placed-card{max-width:132px;padding:7px}.placed-card strong{font-size:11px!important}
|
||||
}
|
||||
|
||||
|
||||
/* Sales-first Rank layer: proof before controls, pretty before cockpit. */
|
||||
.sales-hero{
|
||||
position:relative;display:grid;grid-template-columns:minmax(320px,0.95fr) minmax(420px,1.05fr);gap:22px;align-items:stretch;
|
||||
margin:0 0 18px;padding:24px;border:1px solid rgba(11,99,246,.16);border-radius:28px;
|
||||
background:
|
||||
radial-gradient(circle at 8% 8%,rgba(69,184,255,.26),transparent 30rem),
|
||||
radial-gradient(circle at 92% 12%,rgba(124,58,237,.18),transparent 28rem),
|
||||
linear-gradient(135deg,rgba(255,255,255,.94),rgba(246,248,251,.82));
|
||||
box-shadow:0 24px 70px rgba(6,27,51,.12);overflow:hidden;
|
||||
}
|
||||
.sales-hero::after{content:"";position:absolute;inset:auto -80px -120px auto;width:320px;height:320px;background:radial-gradient(circle,rgba(11,99,246,.18),transparent 68%);pointer-events:none}.hero-copy,.proof-card{position:relative;z-index:1}.hero-copy{display:flex;flex-direction:column;justify-content:center;padding:8px}.sales-hero .eyebrow{display:inline-flex;width:max-content;max-width:100%;padding:7px 10px;border:1px solid rgba(11,99,246,.18);border-radius:999px;background:rgba(255,255,255,.70);color:#0B63F6;font-size:11px;font-weight:950;text-transform:uppercase;letter-spacing:.075em}.sales-hero h1{margin:16px 0 10px;font-size:clamp(38px,5.4vw,72px);line-height:.92;letter-spacing:-.07em;color:var(--navy-950)}.hero-lede{margin:0;max-width:720px;color:#344054;font-size:clamp(18px,2vw,23px);line-height:1.32}.hero-actions{display:flex;gap:12px;flex-wrap:wrap;margin:24px 0 18px}.primary-cta,.secondary-cta{display:inline-flex;align-items:center;justify-content:center;min-height:48px;padding:0 18px;border-radius:999px;font-weight:950;box-shadow:0 12px 28px rgba(11,99,246,.18)}.primary-cta{background:linear-gradient(135deg,#0B63F6,#7C3AED);color:white}.secondary-cta{border:1px solid rgba(6,27,51,.14);background:white;color:var(--navy-950)}.trust-row{display:flex;gap:8px;flex-wrap:wrap}.trust-row span{padding:8px 10px;border:1px solid rgba(6,27,51,.10);border-radius:999px;background:rgba(255,255,255,.64);color:#344054;font-size:12px;font-weight:850}.proof-card{display:grid;grid-template-columns:1fr auto 1.08fr;gap:12px;align-items:center}.proof-column{min-height:100%;border:1px solid rgba(6,27,51,.10);border-radius:22px;background:rgba(255,255,255,.82);box-shadow:0 16px 42px rgba(16,24,40,.08);padding:18px}.proof-column span{display:inline-flex;margin-bottom:8px;color:#667085;font-size:11px;text-transform:uppercase;letter-spacing:.08em;font-weight:950}.proof-column h2{margin:0 0 12px;font-size:22px;letter-spacing:-.04em;color:var(--navy-950)}.proof-column ul,.proof-column ol{margin:0;padding:0;list-style:none;display:grid;gap:9px}.proof-column li{border:1px solid rgba(6,27,51,.08);border-radius:14px;background:#fff;padding:10px 11px;color:#344054}.proof-column.clear li{display:grid;gap:3px}.proof-column.clear b{color:#061B33}.proof-column.clear small{color:#667085;line-height:1.35}.proof-arrow{width:42px;height:42px;display:grid;place-items:center;border-radius:999px;background:var(--navy-950);color:white;font-weight:950;font-size:22px;box-shadow:0 14px 28px rgba(6,27,51,.24)}.approach-strip{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:0 0 18px}.approach-strip article{border:1px solid rgba(6,27,51,.10);border-radius:20px;background:rgba(255,255,255,.76);box-shadow:var(--shadow-soft);padding:16px}.approach-strip strong{display:block;color:var(--navy-950);font-size:15px}.approach-strip span{display:block;margin-top:5px;color:#667085;line-height:1.45;font-size:13px}.feature-set-panel{background:linear-gradient(135deg,#061B33,#0b2a53 54%,#123f75)!important;border-radius:24px!important}.feature-set-copy h2{font-size:clamp(24px,2.6vw,36px)!important;line-height:1.02}.feature-set-copy p{font-size:15px!important}.capture-title{color:var(--navy-950)}
|
||||
@media(max-width:1050px){.sales-hero{grid-template-columns:1fr}.proof-card{grid-template-columns:1fr}.proof-arrow{margin:auto;transform:rotate(90deg)}.approach-strip{grid-template-columns:1fr}}
|
||||
@media(max-width:680px){.sales-hero{padding:18px 14px;border-radius:26px;margin-bottom:12px}.sales-hero .eyebrow{width:auto;font-size:10px}.sales-hero h1{font-size:42px}.hero-lede{font-size:17px}.hero-actions{display:grid}.primary-cta,.secondary-cta{width:100%}.proof-column{padding:14px}.proof-column.messy li:nth-child(n+4){display:none}.approach-strip{display:none}.feature-set-panel{border-radius:22px!important}.feature-set-copy h2{font-size:25px!important}}
|
||||
|
||||
|
||||
/* Mobile overflow fix for the sales hero. */
|
||||
@media(max-width:680px){
|
||||
html,body{overflow-x:hidden;width:100%}
|
||||
.workspace{width:100%;max-width:none;overflow-x:hidden}
|
||||
.sales-hero,.hero-copy,.proof-card,.proof-column{min-width:0;max-width:100%}
|
||||
.sales-hero{width:100%;margin-left:0;margin-right:0;box-sizing:border-box;overflow:hidden}
|
||||
.sales-hero .eyebrow{display:block;width:100%;line-height:1.25;white-space:normal}
|
||||
.sales-hero h1{font-size:38px;line-height:.98;overflow-wrap:anywhere}
|
||||
.hero-lede{overflow-wrap:anywhere}
|
||||
.primary-cta,.secondary-cta{white-space:normal;text-align:center;box-sizing:border-box}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,15 @@ const milestonesTableId = process.env.RANK_MILESTONES_TABLE_ID || 'milestones';
|
||||
const activityTableId = process.env.RANK_ACTIVITY_TABLE_ID || 'activity';
|
||||
const agentToken = process.env.RANK_AGENT_TOKEN || process.env.PRIORITY_AGENT_TOKEN || '';
|
||||
const appVersion = process.env.APP_VERSION || 'rank-local';
|
||||
const appwriteTimeoutMs = Number(process.env.APPWRITE_TIMEOUT_MS || 7000);
|
||||
|
||||
function withTimeout(promise, label = 'operation') {
|
||||
let timer;
|
||||
const timeout = new Promise((_, reject) => {
|
||||
timer = setTimeout(() => reject(new Error(`${label} timed out after ${appwriteTimeoutMs}ms`)), appwriteTimeoutMs);
|
||||
});
|
||||
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
||||
}
|
||||
|
||||
if (!endpoint || !projectId || !apiKey) {
|
||||
console.warn('[rank] Missing Appwrite configuration; /api/health will report degraded.');
|
||||
@@ -169,7 +178,10 @@ app.get('/api/health', async (_req, res) => {
|
||||
const health = { ok: false, app: 'rank', version: appVersion, appwriteConfigured: Boolean(endpoint && projectId && apiKey), appwriteReachable: false, tableReachable: false };
|
||||
try {
|
||||
if (health.appwriteConfigured) {
|
||||
const probe = await tables.listRows({ databaseId, tableId: ideasTableId, queries: [Query.limit(1)] });
|
||||
const probe = await withTimeout(
|
||||
tables.listRows({ databaseId, tableId: ideasTableId, queries: [Query.limit(1)] }),
|
||||
'Appwrite health probe'
|
||||
);
|
||||
rowsFrom(probe);
|
||||
health.appwriteReachable = true;
|
||||
health.tableReachable = true;
|
||||
@@ -182,11 +194,11 @@ app.get('/api/health', async (_req, res) => {
|
||||
});
|
||||
|
||||
app.get('/api/bootstrap', async (_req, res) => {
|
||||
const [ideas, milestones, activity] = await Promise.all([
|
||||
const [ideas, milestones, activity] = await withTimeout(Promise.all([
|
||||
tables.listRows({ databaseId, tableId: ideasTableId, queries: [Query.equal('archived', false), Query.orderDesc('score'), Query.orderAsc('rank'), Query.limit(100)] }),
|
||||
tables.listRows({ databaseId, tableId: milestonesTableId, queries: [Query.equal('active', true), Query.orderAsc('position'), Query.limit(50)] }),
|
||||
tables.listRows({ databaseId, tableId: activityTableId, queries: [Query.orderDesc('$createdAt'), Query.limit(18)] }).catch(() => ({ rows: [], documents: [] })),
|
||||
]);
|
||||
]), 'Appwrite bootstrap');
|
||||
res.json({
|
||||
version: appVersion,
|
||||
ideas: rowsFrom(ideas).map(publicIdea),
|
||||
|
||||
Reference in New Issue
Block a user