feat: implement YAML-based configuration, .env support, and customizable prompt templates for AI analysis
This commit is contained in:
+14
-3
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -43,7 +43,7 @@ const jobListHTML = `{{ define "jobs" }}
|
||||
<tr>
|
||||
<th class="px-4 py-3" data-sort="title">Job Title & 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 & 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>
|
||||
|
||||
@@ -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]">
|
||||
|
||||
Reference in New Issue
Block a user