mirror of
https://github.com/computerim/impactflow-discovery.git
synced 2026-08-27 05:00:37 +00:00
c4fc1cccd7
Build the boundary the ImpactFlow core time-tracker plugs into. A task maps to a foundation — one of the six stable profile elements (love, strength, mission, vocation, short_term, long_term) — so the tracker can ask "which goal does this build toward?" and post the answer back to Vision. - Models + migration 006: task_mapping (one row per logged time entry). - app/services/foundations.py: the six foundations + a pure, testable work-pattern aggregator (rollup) and a plain-language summary. - app/routers/integration.py (user-scoped; tracker calls as the user or via X-API-Key): GET /foundations, POST/GET /task-mappings, GET /work-patterns?days=N (per-foundation minutes/share/neglected). - Reminder engine now pulls from real work patterns: CheckinCoach takes an optional work-pattern summary (last 14 days) and reflects where time has gone against the person's own words — an observation, never a verdict. - Frontend: dashboard.html (time per foundation + neglected); linked from profile.html. Documented the core-tracker integration contract in the README. Phase 4 completes the Vision module's roadmap on the Discovery side; the core tracker integrates by calling these endpoints. Tests: 86 passing (added pure-aggregator, integration-endpoint, and work-pattern-into-check-in tests; run in-container). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
282 lines
9.4 KiB
HTML
282 lines
9.4 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 — Your Profile</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>
|
||
<div class="wrap">
|
||
<div class="brand">ImpactFlow · Your Direction</div>
|
||
<div id="content">
|
||
<p>Loading your profile…</p>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Plain-language description of each enneagram triad. We never show the
|
||
// type number on this page — only the pattern.
|
||
const TRIAD_INFO = {
|
||
gut: {
|
||
label: "You lead with instinct and will",
|
||
text:
|
||
"You move through the world from your gut. You sense what's right before you can fully explain it, and you're driven to act, to protect what matters, and to set things right. Autonomy and integrity are non-negotiable for you, and you'd rather meet a problem head-on than wait for permission.",
|
||
},
|
||
heart: {
|
||
label: "You lead with feeling and connection",
|
||
text:
|
||
"You navigate through emotion and relationship. You're finely attuned to how things land — for you and for the people around you — and you care deeply about being genuinely seen for who you are. Your energy comes alive in connection, recognition, and making others feel that they matter.",
|
||
},
|
||
head: {
|
||
label: "You lead with thought and perception",
|
||
text:
|
||
"You meet the world through your mind. You like to understand before you commit — gathering information, mapping possibilities, and anticipating what's ahead so you can feel prepared and secure. Your gift is seeing patterns and thinking your way toward clarity others miss.",
|
||
},
|
||
};
|
||
|
||
const IKIGAI = [
|
||
{ key: "love_summary", title: "What you love", icon: "♥" },
|
||
{ key: "strength_summary", title: "What you're good at", icon: "★" },
|
||
{ key: "mission_summary", title: "What the world needs", icon: "◆" },
|
||
{ key: "vocation_summary", title: "What you can be paid for", icon: "$" },
|
||
];
|
||
|
||
function confClass(level) {
|
||
if (level === "high") return "high";
|
||
if (level === "medium") return "medium";
|
||
return "low";
|
||
}
|
||
|
||
function dot(level) {
|
||
const c = confClass(level);
|
||
const title = `Confidence: ${level || "low"}`;
|
||
return `<span class="dot ${c}" title="${title}"></span>`;
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
if (s == null) return "";
|
||
return String(s)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">");
|
||
}
|
||
|
||
// The editable prose. Order/labels mirror the read view. The AI's
|
||
// structural read (triad/type/wing/variant) is shown but not edited here.
|
||
const GOAL_FIELDS = [
|
||
{ key: "short_term_goals", title: "Next 6–12 months" },
|
||
{ key: "long_term_goals", title: "Next 3–5 years" },
|
||
];
|
||
|
||
const content = document.getElementById("content");
|
||
let currentProfile = null;
|
||
|
||
async function load() {
|
||
try {
|
||
const res = await authedFetch("/discovery/profile/me");
|
||
if (!res.ok) throw new Error("Profile not found");
|
||
currentProfile = await res.json();
|
||
} catch (err) {
|
||
content.innerHTML = `<div class="error-box">Could not load your profile: ${escapeHtml(
|
||
err.message
|
||
)}</div>`;
|
||
return;
|
||
}
|
||
renderRead();
|
||
}
|
||
|
||
function renderRead() {
|
||
const profile = currentProfile;
|
||
const conf = profile.confidence || {};
|
||
const triad = TRIAD_INFO[profile.triad] || TRIAD_INFO.gut;
|
||
|
||
const ikigaiCards = IKIGAI.map(
|
||
(item) => `
|
||
<div class="card">
|
||
<h3>${dot(conf.ikigai)} ${item.icon} ${item.title}</h3>
|
||
<p>${escapeHtml(profile[item.key]) || "—"}</p>
|
||
</div>`
|
||
).join("");
|
||
|
||
const locked = profile.locked;
|
||
|
||
// Goals are the person's own articulated direction. Only show the
|
||
// section if at least one horizon has content.
|
||
const goals = GOAL_FIELDS.filter(
|
||
(g) => profile[g.key] && profile[g.key].trim()
|
||
);
|
||
const goalsBlock = goals.length
|
||
? `
|
||
<p class="section-label">Where you're headed</p>
|
||
<div class="ikigai-grid">${goals
|
||
.map(
|
||
(g) => `
|
||
<div class="card">
|
||
<h3>→ ${g.title}</h3>
|
||
<p>${escapeHtml(profile[g.key])}</p>
|
||
</div>`
|
||
)
|
||
.join("")}</div>
|
||
`
|
||
: "";
|
||
|
||
// Unlocked profiles can be edited and then affirmed; locked profiles are
|
||
// final and show neither control as actionable.
|
||
const actions = locked
|
||
? `<div class="confirm-row">
|
||
<button class="btn-primary confirmed" disabled>Profile confirmed ✓</button>
|
||
</div>`
|
||
: `<div class="nav">
|
||
<button class="btn-ghost" id="editBtn">Edit my words</button>
|
||
<button class="btn-primary" id="confirmBtn">This is me</button>
|
||
</div>
|
||
<p class="back-link" style="text-align:center;margin-top:18px">
|
||
<a href="/static/reflect.html">Not quite right? Talk it through with your coach →</a>
|
||
</p>`;
|
||
|
||
const coachingLink =
|
||
`<p class="back-link" style="text-align:center;margin-top:10px">
|
||
<a href="/static/coaching.html">Coaching preferences & check-ins →</a>
|
||
</p>
|
||
<p class="back-link" style="text-align:center;margin-top:10px">
|
||
<a href="/static/dashboard.html">Where your time goes →</a>
|
||
</p>`;
|
||
|
||
content.innerHTML = `
|
||
<div class="profile-narrative">${escapeHtml(
|
||
profile.overlap_narrative
|
||
)}</div>
|
||
|
||
<p class="section-label">Where your four circles meet</p>
|
||
<div class="ikigai-grid">${ikigaiCards}</div>
|
||
|
||
<div class="triad-block">
|
||
<h2>${dot(conf.triad)} ${triad.label}</h2>
|
||
<p>${triad.text}</p>
|
||
</div>
|
||
|
||
${goalsBlock}
|
||
${actions}
|
||
${coachingLink}
|
||
`;
|
||
|
||
if (locked) return;
|
||
|
||
document
|
||
.getElementById("editBtn")
|
||
.addEventListener("click", renderEdit);
|
||
|
||
const btn = document.getElementById("confirmBtn");
|
||
btn.addEventListener("click", async () => {
|
||
btn.disabled = true;
|
||
try {
|
||
const res = await authedFetch("/discovery/profile/me/confirm", {
|
||
method: "PUT",
|
||
});
|
||
if (!res.ok) throw new Error("confirm failed");
|
||
currentProfile.locked = true;
|
||
renderRead();
|
||
} catch (err) {
|
||
btn.disabled = false;
|
||
btn.textContent = "Try again";
|
||
}
|
||
});
|
||
}
|
||
|
||
function editField(key, label, value) {
|
||
return `
|
||
<div class="edit-field">
|
||
<label for="edit_${key}">${label}</label>
|
||
<textarea class="edit" id="edit_${key}">${escapeHtml(
|
||
value
|
||
)}</textarea>
|
||
</div>`;
|
||
}
|
||
|
||
function renderEdit() {
|
||
const profile = currentProfile;
|
||
|
||
const ikigaiFields = IKIGAI.map((item) =>
|
||
editField(item.key, `${item.icon} ${item.title}`, profile[item.key])
|
||
).join("");
|
||
const goalFields = GOAL_FIELDS.map((g) =>
|
||
editField(g.key, `→ ${g.title}`, profile[g.key])
|
||
).join("");
|
||
|
||
content.innerHTML = `
|
||
<p class="section-label">Edit your words</p>
|
||
<p class="edit-help">These are your words to own. Revise anything that
|
||
doesn't sound like you, then save.</p>
|
||
|
||
${editField(
|
||
"overlap_narrative",
|
||
"Where it all comes together",
|
||
profile.overlap_narrative
|
||
)}
|
||
|
||
<div class="ikigai-grid">${ikigaiFields}</div>
|
||
<div class="ikigai-grid">${goalFields}</div>
|
||
|
||
<div id="editError"></div>
|
||
<div class="nav">
|
||
<button class="btn-ghost" id="cancelBtn">Cancel</button>
|
||
<button class="btn-primary" id="saveBtn">Save changes</button>
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById("cancelBtn").addEventListener("click", renderRead);
|
||
document.getElementById("saveBtn").addEventListener("click", saveEdits);
|
||
}
|
||
|
||
const EDITABLE_KEYS = [
|
||
"overlap_narrative",
|
||
...IKIGAI.map((i) => i.key),
|
||
...GOAL_FIELDS.map((g) => g.key),
|
||
];
|
||
|
||
async function saveEdits() {
|
||
const saveBtn = document.getElementById("saveBtn");
|
||
saveBtn.disabled = true;
|
||
|
||
const payload = {};
|
||
EDITABLE_KEYS.forEach((key) => {
|
||
payload[key] = document.getElementById(`edit_${key}`).value;
|
||
});
|
||
|
||
try {
|
||
const res = await authedFetch("/discovery/profile/me", {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
if (!res.ok) {
|
||
const detail = await res
|
||
.json()
|
||
.catch(() => ({ detail: "Could not save changes" }));
|
||
throw new Error(detail.detail || "Could not save changes");
|
||
}
|
||
currentProfile = await res.json();
|
||
renderRead();
|
||
} catch (err) {
|
||
saveBtn.disabled = false;
|
||
document.getElementById(
|
||
"editError"
|
||
).innerHTML = `<div class="error-box">${escapeHtml(
|
||
err.message
|
||
)}</div>`;
|
||
}
|
||
}
|
||
|
||
load();
|
||
</script>
|
||
</body>
|
||
</html>
|