feat: implement YAML-based configuration, .env support, and customizable prompt templates for AI analysis

This commit is contained in:
2026-08-22 10:17:38 -07:00
parent 7ec91f384f
commit 0de312edbd
30 changed files with 895 additions and 211 deletions
+14 -3
View File
@@ -137,7 +137,7 @@ func (s *Server) handleCreateTarget(w http.ResponseWriter, r *http.Request) {
logger.Log.Infow("Triggered /targets (Create)", "component", "API", "name", name, "platform", platform)
_, err := s.db.CreateTarget(name, targetURL, platform)
_, err := s.db.CreateTarget(name, targetURL, platform, true)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -232,7 +232,18 @@ func (s *Server) handleAnalyzeJob(w http.ResponseWriter, r *http.Request) {
logger.Log.Infow("Triggered /jobs/{id}/analyze", "component", "API", "job_id", id)
// Enqueue analyze task instead of synchronous processing
job, err := s.db.GetJobByID(id)
if err == nil && job != nil {
filterScore := 0.0
score := 0.0
if job.Analysis != nil {
filterScore = job.Analysis.FilterScore
score = job.Analysis.Score
}
_ = s.db.CreateJobAnalysis(id, filterScore, score, true, "PENDING")
}
// Enqueue analyze task
payload := worker.AnalyzePayload{RawJobID: int64(id)}
err = s.db.EnqueueTask("ANALYZE", payload)
if err != nil {
@@ -241,7 +252,7 @@ func (s *Server) handleAnalyzeJob(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusAccepted)
_, _ = w.Write([]byte(`<span class="text-xs px-2 py-1 bg-yellow-100 text-yellow-800 rounded">Analysis Queued</span>`))
_, _ = w.Write([]byte(`<span class="text-xs px-2.5 py-1 bg-amber-500/20 text-amber-300 border border-amber-500/30 rounded-lg font-semibold">🔄 AI Queued</span>`))
}
func (s *Server) handleOptOut(w http.ResponseWriter, r *http.Request) {
+1 -1
View File
@@ -33,7 +33,7 @@ func TestHandleDeleteTarget(t *testing.T) {
}
defer func() { _ = database.Close() }()
target, err := database.CreateTarget("Test Target", "https://example.com/test-jobs", "google_jobs")
target, err := database.CreateTarget("Test Target", "https://example.com/test-jobs", "google_jobs", true)
if err != nil {
t.Fatalf("failed to create target: %v", err)
}
+2 -2
View File
@@ -101,8 +101,8 @@ const applyTableHTML = `{{ define "applyTable" }}
<!-- Score -->
<td class="px-4 py-3" data-sort-val="{{ if .Analysis }}{{ printf "%.0f" .Analysis.Score }}{{ else }}-1{{ end }}">
{{ if .Analysis }}
<span class="font-bold {{ if .Analysis.IsQualified }}text-emerald-400{{ else }}text-slate-500{{ end }}">
{{ printf "%.0f" .Analysis.Score }}
<span class="font-bold {{ if .Analysis.IsQualified }}text-emerald-400{{ else }}text-slate-400{{ end }}">
{{ if eq .Analysis.Status "PARSED" }}🤖 {{ printf "%.0f" .Analysis.Score }}{{ else }}⚡ {{ printf "%.0f" .Analysis.FilterScore }}{{ end }}
</span>
{{ else }}
<span class="text-slate-600">–</span>
+69 -18
View File
@@ -43,7 +43,7 @@ const jobListHTML = `{{ define "jobs" }}
<tr>
<th class="px-4 py-3" data-sort="title">Job Title &amp; Company</th>
<th class="px-4 py-3 text-center">Link</th>
<th class="px-4 py-3" data-sort="status">Analysis</th>
<th class="px-4 py-3" data-sort="status">Status</th>
<th class="px-4 py-3" data-sort="score">Score &amp; Qualification</th>
<th class="px-4 py-3">Reason / Tags</th>
<th class="px-4 py-3" data-sort="date">Last Updated</th>
@@ -86,9 +86,13 @@ const jobListHTML = `{{ define "jobs" }}
<td class="px-4 py-3" data-sort-val="{{ if .Analysis }}{{ .Analysis.Status }}{{ else }}UNANALYZED{{ end }}">
{{ if .Analysis }}
{{ if eq .Analysis.Status "PARSED" }}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">PARSED</span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">AI PARSED</span>
{{ else if eq .Analysis.Status "PENDING" }}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-amber-500/20 text-amber-400 border border-amber-500/30">PENDING</span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-amber-500/20 text-amber-400 border border-amber-500/30">QUEUED</span>
{{ else if eq .Analysis.Status "FILTERED_OUT" }}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-slate-700/40 text-slate-400 border border-slate-600/30">LOW FIT</span>
{{ else if eq .Analysis.Status "REJECTED" }}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-rose-500/20 text-rose-400 border border-rose-500/30">REJECTED</span>
{{ else if eq .Analysis.Status "DONE" }}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-emerald-500/20 text-emerald-400 border border-emerald-500/30">DONE</span>
{{ else }}
@@ -102,14 +106,32 @@ const jobListHTML = `{{ define "jobs" }}
<!-- Score & Qualification -->
<td class="px-4 py-3" data-sort-val="{{ if .Analysis }}{{ printf "%.0f" .Analysis.Score }}{{ else }}-1{{ end }}">
{{ if .Analysis }}
{{ if .Analysis.IsQualified }}
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-black border {{ scoreBadgeClass .Analysis.Score }}">
★ {{ printf "%.0f" .Analysis.Score }}
</span>
{{ if eq .Analysis.Status "PARSED" }}
{{ if .Analysis.IsQualified }}
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-black border {{ scoreBadgeClass .Analysis.Score }}" title="AI Match Score (DeepSeek)">
🤖 {{ printf "%.0f" .Analysis.Score }}
</span>
{{ else }}
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-rose-500/10 text-rose-400 border border-rose-500/20" title="AI Score: {{ printf "%.0f" .Analysis.Score }}">
✗ {{ printf "%.0f" .Analysis.Score }} Not Qual
</span>
{{ end }}
{{ else if eq .Analysis.Status "PENDING" }}
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-amber-500/10 text-amber-300 border border-amber-500/30" title="Keyword Fit: {{ printf "%.0f" .Analysis.FilterScore }} - Waiting for AI">
⚡ {{ printf "%.0f" .Analysis.FilterScore }}
</span>
{{ else if eq .Analysis.Status "FILTERED_OUT" }}
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-medium bg-slate-800 text-slate-400 border border-slate-700" title="Keyword Fit: {{ printf "%.0f" .Analysis.FilterScore }}">
⚡ {{ printf "%.0f" .Analysis.FilterScore }}
</span>
{{ else if eq .Analysis.Status "REJECTED" }}
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-rose-500/10 text-rose-400 border border-rose-500/20">
🚫 Disqualified
</span>
{{ else }}
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-rose-500/10 text-rose-400 border border-rose-500/20">
✗ Not Qualified
</span>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-slate-700/50 text-slate-400 border border-slate-600/30">
{{ printf "%.0f" .Analysis.Score }}
</span>
{{ end }}
{{ else }}
<span class="text-slate-500 text-[11px]">–</span>
@@ -138,6 +160,8 @@ const jobListHTML = `{{ define "jobs" }}
{{ $missing := parseJSON .Analysis.MissingHighlights }}
{{ if $missing }}
<span class="text-[10px] text-rose-400/90 leading-snug line-clamp-2" title="{{ index $missing 0 }}">{{ index $missing 0 }}</span>
{{ else if .Analysis.Summary }}
<span class="text-[10px] text-slate-400/90 leading-snug line-clamp-2" title="{{ .Analysis.Summary }}">{{ .Analysis.Summary }}</span>
{{ end }}
{{ end }}
{{ end }}
@@ -232,23 +256,50 @@ const jobDetailHTML = `{{ define "jobDetail" }}
</div>
{{ if .Analysis }}
<!-- Score + Status Grid -->
<!-- Dual Score Breakdown Grid -->
<div class="grid grid-cols-2 gap-3">
<!-- AI Match Score Card -->
<div class="p-4 rounded-xl bg-slate-800/60 border border-slate-700/60">
<span class="text-[10px] text-slate-400 uppercase tracking-wider font-semibold block">Match Score</span>
{{ if .Analysis.IsQualified }}
<span class="text-2xl font-black text-emerald-400 mt-1 block">{{ printf "%.1f" .Analysis.Score }} <span class="text-sm font-medium text-slate-400">/ 100</span></span>
<div class="flex items-center justify-between">
<span class="text-[10px] text-indigo-300 uppercase tracking-wider font-bold">🤖 AI Match Score</span>
<span class="text-[10px] font-semibold px-2 py-0.5 rounded {{ if eq .Analysis.Status "PARSED" }}bg-indigo-500/20 text-indigo-300 border border-indigo-500/30{{ else }}bg-amber-500/20 text-amber-300 border border-amber-500/30{{ end }}">
{{ .Analysis.Status }}
</span>
</div>
{{ if eq .Analysis.Status "PARSED" }}
{{ if .Analysis.IsQualified }}
<span class="text-2xl font-black text-emerald-400 mt-1.5 block">{{ printf "%.1f" .Analysis.Score }} <span class="text-xs font-normal text-slate-400">/ 100</span></span>
<span class="text-[10px] text-emerald-400/90 font-medium">✓ Qualified for interview profile</span>
{{ else }}
<span class="text-2xl font-black text-rose-400 mt-1.5 block">{{ printf "%.1f" .Analysis.Score }} <span class="text-xs font-normal text-slate-400">/ 100</span></span>
<span class="text-[10px] text-rose-400 font-medium">✗ Does not meet core fit threshold</span>
{{ end }}
{{ else }}
<span class="text-base font-bold text-rose-400 mt-1 block">Not Qualified</span>
<span class="text-lg font-bold text-amber-300 mt-1.5 block">Pending Evaluation</span>
<span class="text-[10px] text-slate-400">DeepSeek LLM analysis queued</span>
{{ end }}
</div>
<!-- Keyword Relevance Card -->
<div class="p-4 rounded-xl bg-slate-800/60 border border-slate-700/60">
<span class="text-[10px] text-slate-400 uppercase tracking-wider font-semibold block">Analysis Status</span>
<span class="text-sm font-bold text-indigo-300 mt-1 block uppercase">{{ .Analysis.Status }}</span>
<div class="flex items-center justify-between">
<span class="text-[10px] text-amber-400 uppercase tracking-wider font-bold">⚡ Keyword Fit</span>
<span class="text-[10px] text-slate-400">Pre-filter rule</span>
</div>
<span class="text-2xl font-black text-white mt-1.5 block">{{ printf "%.1f" .Analysis.FilterScore }} <span class="text-xs font-normal text-slate-400">/ 100</span></span>
<span class="text-[10px] text-slate-400">Lexical match against filters.yaml</span>
</div>
</div>
<!-- Highlights -->
<!-- AI Executive Summary -->
{{ if .Analysis.Summary }}
<div class="p-3.5 rounded-xl bg-indigo-950/40 border border-indigo-500/30">
<span class="text-[10px] font-bold text-indigo-300 uppercase tracking-wider block mb-1">💬 AI Executive Assessment</span>
<p class="text-xs text-indigo-100/90 leading-relaxed">{{ .Analysis.Summary }}</p>
</div>
{{ end }}
<!-- Highlights & Gaps -->
<div class="space-y-4">
{{ if .Analysis.IsQualified }}
<div>
+1 -1
View File
@@ -63,7 +63,7 @@ const kanbanCardHTML = `{{ define "kanbanCard" }}
</div>
{{ if .Analysis }}
<div class="text-[10px] text-slate-400">
Score: <strong class="{{ scoreBadgeClass .Analysis.Score }}">{{ printf "%.0f" .Analysis.Score }}</strong>
Score: <strong class="{{ scoreBadgeClass .Analysis.Score }}">{{ if eq .Analysis.Status "PARSED" }}🤖 {{ else }}⚡ {{ end }}{{ printf "%.0f" .Analysis.Score }}</strong>
</div>
{{ end }}
<div class="flex items-center justify-between pt-2 border-t border-slate-700/50 text-[10px]">