Files
impactflow_discovery/app/static/discovery.html
T
Joel Salmon 33674f92f4 Complete Phase 1: goals, cookie auth, profile editing
Close the remaining Phase 1 DoD gaps and reconcile the browser flow with
the auth layer.

Goals (5 -> 7 prompts):
- Add near-term (6-12mo) and long-term (3-5yr) goal prompts; collect raw
  text on the conversation and store AI-articulated goal summaries on the
  profile. Extractor articulates the person's own stated goals (mirror,
  not compass) and never fabricates. Alembic 003 adds the four columns.

Cookie-based browser sessions (fixes frontend<->auth desync):
- OAuth callback now sets httpOnly session cookies and redirects into the
  app instead of returning JSON. get_current_user gains a cookie fallback
  (X-API-Key -> Bearer -> cookie). refresh/logout read the refresh cookie
  and set/clear cookies. New shared auth.js (authedFetch) sends cookies and
  silently refreshes on 401. Static pages drop the bogus user_id and call
  the correct /me endpoints.

Profile editing (read/edit/affirm):
- PATCH /discovery/profile/me edits the prose (Ikigai summaries, overlap
  narrative, goals); owner-scoped, partial update, 409 when locked. Edit
  mode in profile.html with Save/Cancel.

Also: bump default model to claude-sonnet-4-6, align ports to 8011
(OAuth redirect, CORS), add COOKIE_SECURE/POST_LOGIN_REDIRECT config, and
refresh the README to match the shipped behavior.

Tests: 33 passing (added cookie-auth, profile-edit, goal-extraction cases;
factored a shared app_client fixture into conftest.py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:26:08 -05:00

197 lines
6.2 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ImpactFlow — Self Discovery</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Playfair+Display:wght@500;600&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/static/style.css" />
<script src="/static/auth.js"></script>
</head>
<body>
<!-- Question flow -->
<div class="wrap" id="flow">
<div class="brand">ImpactFlow · Self Discovery</div>
<div class="progress" id="progress">1 of 5</div>
<h2 class="prompt-title" id="promptTitle"></h2>
<p class="prompt-text" id="promptText"></p>
<textarea
id="answer"
placeholder="Take your time. Tell the whole story…"
></textarea>
<div class="nav">
<button class="btn-ghost" id="backBtn">Back</button>
<button class="btn-primary" id="nextBtn">Next</button>
</div>
</div>
<!-- Loading state -->
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Reading your story…</p>
</div>
<script>
const PROMPTS = [
{
key: "prompt_alive",
title: "The Alive Moment",
text:
"Tell me about a time you felt most alive and useful. What were you doing, who was involved, and what made it matter?",
},
{
key: "prompt_friction",
title: "The Friction Moment",
text:
"Describe a situation where something felt deeply wrong or unfair. What was it, and what did you do about it?",
},
{
key: "prompt_pull",
title: "The Natural Pull",
text:
"What do you find yourself doing or thinking about in your free time, even when you're supposed to be doing something else?",
},
{
key: "prompt_recognition",
title: "The Recognition Moment",
text:
"When have you felt most seen or valued — and what were you being recognized for?",
},
{
key: "prompt_future",
title: "The Future Pull",
text:
"If you knew you couldn't fail and money wasn't a factor, what would you spend the next five years building or doing?",
},
{
key: "prompt_goals_short",
title: "The Near Horizon",
text:
"Looking at the next 6 to 12 months, what do you most want to make progress on or accomplish? Say it in your own words.",
},
{
key: "prompt_goals_long",
title: "The Long Horizon",
text:
"Now stretch out 3 to 5 years. What do you want to have built, become, or changed by then?",
},
];
// The signed-in user is resolved server-side from the session cookie, so
// the page no longer mints or tracks a user id of its own.
const answers = new Array(PROMPTS.length).fill("");
let index = 0;
let conversationId = null;
const el = {
progress: document.getElementById("progress"),
title: document.getElementById("promptTitle"),
text: document.getElementById("promptText"),
answer: document.getElementById("answer"),
back: document.getElementById("backBtn"),
next: document.getElementById("nextBtn"),
flow: document.getElementById("flow"),
loading: document.getElementById("loading"),
};
function render() {
const p = PROMPTS[index];
el.progress.textContent = `${index + 1} of ${PROMPTS.length}`;
el.title.textContent = p.title;
el.text.textContent = p.text;
el.answer.value = answers[index];
el.back.style.visibility = index === 0 ? "hidden" : "visible";
el.next.textContent =
index === PROMPTS.length - 1 ? "Submit" : "Next";
el.answer.focus();
}
function saveCurrent() {
answers[index] = el.answer.value;
}
el.back.addEventListener("click", () => {
saveCurrent();
if (index > 0) {
index -= 1;
render();
}
});
el.next.addEventListener("click", async () => {
saveCurrent();
if (index < PROMPTS.length - 1) {
index += 1;
render();
} else {
await submit();
}
});
async function startConversation() {
const res = await authedFetch("/discovery/start", { method: "POST" });
if (!res.ok) throw new Error("Could not start conversation");
const data = await res.json();
conversationId = data.conversation_id;
}
async function submit() {
el.flow.style.display = "none";
el.loading.classList.add("active");
try {
if (!conversationId) await startConversation();
const body = {};
PROMPTS.forEach((p, i) => {
body[p.key] = answers[i];
});
const respondRes = await authedFetch(
`/discovery/${conversationId}/respond`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}
);
if (!respondRes.ok) throw new Error("Could not save responses");
const completeRes = await authedFetch(
`/discovery/${conversationId}/complete`,
{ method: "POST" }
);
if (!completeRes.ok) {
const detail = await completeRes
.json()
.catch(() => ({ detail: "Extraction failed" }));
throw new Error(detail.detail || "Extraction failed");
}
window.location.href = "/static/profile.html";
} catch (err) {
el.loading.classList.remove("active");
el.flow.style.display = "block";
el.flow.innerHTML =
`<div class="error-box"><strong>Something went wrong.</strong><br/>` +
`${err.message}<br/><br/>Your answers are still here — ` +
`please try submitting again.</div>` +
`<div class="nav"><span></span>` +
`<button class="btn-primary" onclick="location.reload()">Reload</button></div>`;
}
}
// Kick off a conversation as soon as the page loads so the id is ready.
startConversation().catch(() => {
/* will retry on submit */
});
render();
</script>
</body>
</html>