ontoref/crates/ontoref-daemon/templates/pages/career.html

260 lines
11 KiB
HTML
Raw Normal View History

feat: domain extension system, VCS abstraction, personal/provisioning domains, web subpages Domain extension system (ADR-012): bash-layer dispatch activates repo_kind-conditional CLI domains. install.nu copies domains/ tree; short_alias wrappers generated (personal, prov). ore help and describe capabilities domain-aware. personal domain (PersonalOntology): career skills/talks/publications/positioning, CFP pipeline (Watching→Delivered), opportunities lifecycle, content pipeline, Sessionize integration. Daemon pages: /career, /personal. provisioning domain (DevWorkspace/Mixed): FSM state, next transitions, connections graph, gates, workspace card, capabilities, backlog. Daemon page: /provisioning. VCS abstraction layer (ADR-013): reflection/modules/vcs.nu — uniform jj/git API via filesystem detection (.jj/ vs .git/). opmode.nu and git-event.nu migrated off ^git. reflection/bin/jjw.nu — jj + ontoref + Radicle agent workspace lifecycle. jjw-ncl-merge.nu registered as jj merge tool for .ontology/ NCL conflicts. init-repo.nu for new_project mode. jj/rad not in ontoref requirements — belong in orchestration project manifests. 'Framework RepoKind: ontology/schemas/manifest.ncl gains 'Framework variant; ontoref self-identifies as framework — no domain activates for the protocol itself. Web presence: personal.html and provisioning.html domain subpages. index.html gains "Project Types — Domain Extensions" section with type cards and subpage links. Nav compacted (Arch/Prov labels, solid backdrop-filter background). on+re: vcs-abstraction (adrs: adr-013) and agent-workspace-orchestration Practice nodes; 21 manifest capabilities; state.ncl catalysts updated.
2026-04-07 23:08:29 +01:00
{% extends "base.html" %}
{% import "macros/ui.html" as m %}
{% block title %}Career — {{ slug }} — Ontoref{% endblock title %}
{% block nav_career %}active{% endblock nav_career %}
{% block nav_group_domain %}active{% endblock nav_group_domain %}
{% block content %}
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold">Career</h1>
<p class="text-base-content/50 text-sm mt-1">Skills · Experience · Talks · Positioning</p>
</div>
</div>
<!-- Tabs -->
<div role="tablist" class="tabs tabs-bordered mb-4">
<a role="tab" class="tab tab-active" onclick="switchTab('skills', this)">Skills</a>
<a role="tab" class="tab" onclick="switchTab('experience', this)">Experience</a>
<a role="tab" class="tab" onclick="switchTab('talks', this)">Talks</a>
<a role="tab" class="tab" onclick="switchTab('positioning', this)">Positioning</a>
</div>
<!-- Skills tab -->
<div id="tab-skills">
<div class="flex flex-wrap gap-2 mb-3">
<input id="skill-filter" type="text" placeholder="Filter by name…"
class="input input-sm input-bordered flex-1 min-w-48 font-mono" oninput="renderSkills()">
<select id="skill-tier" class="select select-sm select-bordered" onchange="renderSkills()">
<option value="">All tiers</option>
<option value="Expert">Expert</option>
<option value="Advanced">Advanced</option>
<option value="Intermediate">Intermediate</option>
<option value="Foundational">Foundational</option>
</select>
</div>
<div class="overflow-x-auto">
<table class="table table-sm w-full bg-base-200 rounded-lg">
<thead>
<tr class="text-base-content/50 text-xs uppercase tracking-wider">
<th>Name</th><th>Tier</th><th>Proficiency</th><th>Years</th>
</tr>
</thead>
<tbody id="skills-body"></tbody>
</table>
</div>
</div>
<!-- Experience tab -->
<div id="tab-experience" class="hidden space-y-4">
<div id="experience-cards"></div>
</div>
<!-- Talks tab -->
<div id="tab-talks" class="hidden">
<div class="flex flex-wrap gap-2 mb-3">
<input id="talk-filter" type="text" placeholder="Filter by title, event…"
class="input input-sm input-bordered flex-1 min-w-48 font-mono" oninput="renderTalks()">
<select id="talk-status" class="select select-sm select-bordered" onchange="renderTalks()">
<option value="">All statuses</option>
<option value="Idea">Idea</option>
<option value="Proposed">Proposed</option>
<option value="Accepted">Accepted</option>
<option value="Delivered">Delivered</option>
<option value="Archived">Archived</option>
</select>
</div>
<div class="overflow-x-auto">
<table class="table table-sm w-full bg-base-200 rounded-lg">
<thead>
<tr class="text-base-content/50 text-xs uppercase tracking-wider">
<th>Title</th><th>Event</th><th>Date</th><th>Status</th><th>Links</th>
</tr>
</thead>
<tbody id="talks-body"></tbody>
</table>
</div>
</div>
<!-- Positioning tab -->
<div id="tab-positioning" class="hidden grid grid-cols-1 lg:grid-cols-2 gap-4">
<div id="positioning-cards"></div>
</div>
<!-- Detail modal -->
<dialog id="detail-modal" class="modal">
<div class="modal-box w-full max-w-2xl">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-3 top-3"></button>
</form>
<div id="modal-content" class="text-sm space-y-3"></div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
<script>
const SKILLS = {{ skills_json | safe }};
const EXPERIENCES = {{ experiences_json | safe }};
const TALKS = {{ talks_json | safe }};
const POSITIONING = {{ positioning_json | safe }};
const TIER_BADGE = {
Expert: 'badge-primary', Advanced: 'badge-success',
Intermediate: 'badge-warning', Foundational: 'badge-ghost',
};
const TALK_BADGE = {
Idea: 'badge-ghost', Proposed: 'badge-info', Accepted: 'badge-warning',
Delivered: 'badge-success', Archived: 'badge-ghost',
};
function tierBadge(t) {
return `<span class="badge badge-xs font-mono ${TIER_BADGE[t] ?? 'badge-ghost'}">${t}</span>`;
}
function talkBadge(s) {
return `<span class="badge badge-xs font-mono ${TALK_BADGE[s] ?? 'badge-ghost'}">${s}</span>`;
}
function linkChip(lk) {
return `<a href="${lk.url}" target="_blank" class="badge badge-xs badge-outline font-mono">${lk.kind}${lk.label ? ': ' + lk.label : ''}</a>`;
}
function profBar(p) {
const pct = Math.round(p * 100);
return `<div class="flex items-center gap-2">
<div class="w-20 bg-base-300 rounded-full h-1.5">
<div class="bg-primary rounded-full h-1.5" style="width:${pct}%"></div>
</div>
<span class="font-mono text-xs text-base-content/50">${pct}%</span>
</div>`;
}
// ── Skills ────────────────────────────────────────────────────────────────────
let visibleSkills = [...SKILLS].sort((a, b) => b.proficiency - a.proficiency);
function renderSkills() {
const text = document.getElementById('skill-filter').value.toLowerCase();
const tier = document.getElementById('skill-tier').value;
const base = [...SKILLS].sort((a, b) => b.proficiency - a.proficiency);
visibleSkills = base.filter(s => {
const tm = !text || s.name.toLowerCase().includes(text) || s.id.toLowerCase().includes(text);
const tm2 = !tier || s.tier === tier;
return tm && tm2;
});
const tbody = document.getElementById('skills-body');
tbody.innerHTML = visibleSkills.length === 0
? '<tr><td colspan="4" class="text-center text-base-content/30 py-6">No skills</td></tr>'
: visibleSkills.map(s => `
<tr>
<td class="text-sm font-medium">${s.name}</td>
<td>${tierBadge(s.tier)}</td>
<td>${profBar(s.proficiency)}</td>
<td class="font-mono text-xs text-base-content/50">${s.years > 0 ? s.years + 'y' : '—'}</td>
</tr>`).join('');
}
// ── Experience ────────────────────────────────────────────────────────────────
function renderExperience() {
const container = document.getElementById('experience-cards');
container.innerHTML = EXPERIENCES.length === 0
? '<p class="text-base-content/30 text-sm py-6">No experiences</p>'
: EXPERIENCES.map(e => `
<div class="bg-base-200 rounded-lg p-4 mb-4 space-y-2">
<div class="flex items-start justify-between gap-2 flex-wrap">
<div>
<p class="font-bold">${e.position}</p>
<p class="text-sm text-primary font-medium">
${e.company_url ? `<a href="${e.company_url}" target="_blank" class="link link-hover">${e.company}</a>` : e.company}
</p>
</div>
<div class="text-right text-xs text-base-content/50 font-mono flex-shrink-0">
<p>${e.date_start} — ${e.date_end}</p>
${e.location ? `<p>${e.location}</p>` : ''}
</div>
</div>
${e.description ? `<p class="text-sm text-base-content/70">${e.description}</p>` : ''}
${(e.achievements||[]).length ? `
<ul class="text-xs text-base-content/60 space-y-1 pl-3">
${e.achievements.map(a => `<li class="list-disc">${a}</li>`).join('')}
</ul>` : ''}
${(e.tools||[]).length ? `
<div class="flex flex-wrap gap-1">
${e.tools.map(t => `<span class="badge badge-xs badge-outline font-mono">${t}</span>`).join('')}
</div>` : ''}
</div>`).join('');
}
// ── Talks ─────────────────────────────────────────────────────────────────────
let visibleTalks = TALKS;
function renderTalks() {
const text = document.getElementById('talk-filter').value.toLowerCase();
const status = document.getElementById('talk-status').value;
visibleTalks = TALKS.filter(t => {
const tm = !text || (t.title||'').toLowerCase().includes(text) || (t.event||'').toLowerCase().includes(text);
const sm = !status || t.status === status;
return tm && sm;
});
const tbody = document.getElementById('talks-body');
tbody.innerHTML = visibleTalks.length === 0
? '<tr><td colspan="5" class="text-center text-base-content/30 py-6">No talks</td></tr>'
: visibleTalks.map((t, i) => `
<tr class="hover cursor-pointer" onclick="showTalkDetail(${i})">
<td class="text-sm font-medium">${t.title}</td>
<td class="text-sm text-base-content/60">${t.event || '—'}</td>
<td class="font-mono text-xs text-base-content/50">${t.date || '—'}</td>
<td>${talkBadge(t.status)}</td>
<td class="text-xs">${(t.links||[]).length ? `<span class="badge badge-xs badge-outline">${(t.links||[]).length}</span>` : '—'}</td>
</tr>`).join('');
}
function showTalkDetail(i) {
const t = visibleTalks[i];
if (!t) return;
const links = (t.links||[]).map(linkChip).join(' ');
document.getElementById('modal-content').innerHTML = `
<div class="flex items-baseline gap-2 flex-wrap">
${talkBadge(t.status)}
<span class="font-mono text-xs text-base-content/40">${t.date || ''} ${t.location ? '· ' + t.location : ''}</span>
</div>
<h2 class="text-lg font-bold">${t.title}</h2>
<p class="text-sm text-base-content/60 font-medium">${t.event || ''}</p>
${t.description ? `<p class="text-base-content/70 text-sm leading-relaxed">${t.description}</p>` : ''}
${links ? `<div class="flex flex-wrap gap-1">${links}</div>` : ''}
${(t.linked_nodes||[]).length ? `<p class="text-xs text-base-content/40 font-mono">nodes: ${t.linked_nodes.join(', ')}</p>` : ''}
`;
document.getElementById('detail-modal').showModal();
}
// ── Positioning ───────────────────────────────────────────────────────────────
function renderPositioning() {
const container = document.getElementById('positioning-cards');
container.innerHTML = POSITIONING.length === 0
? '<p class="text-base-content/30 text-sm py-6">No positioning strategies</p>'
: POSITIONING.map(p => `
<div class="bg-base-200 rounded-lg p-4 space-y-2">
<p class="font-bold text-sm">${p.name}</p>
<p class="text-xs text-base-content/50">Target: ${p.target || '—'}</p>
<p class="text-sm text-base-content/70 leading-relaxed">${p.core_message}</p>
${p.note ? `<p class="text-xs text-base-content/40 italic">${p.note}</p>` : ''}
</div>`).join('');
}
// ── Tabs ──────────────────────────────────────────────────────────────────────
function switchTab(name, el) {
['skills','experience','talks','positioning'].forEach(t => {
document.getElementById('tab-' + t).classList.toggle('hidden', t !== name);
});
document.querySelectorAll('[role="tab"]').forEach(t => t.classList.remove('tab-active'));
el.classList.add('tab-active');
}
renderSkills();
renderExperience();
renderTalks();
renderPositioning();
</script>
{% endblock content %}