Files
thomasandClaude Opus 4.8 9418750061 Jobsuche als Feature der App: Suchprofil pro Benutzer statt Cron-Prompt
Die Jobsuche lag in zwei Host-Skripten (jobsuche-cron.sh, jobsuche-remote-cron.sh)
aus der Ein-Benutzer-Zeit: Die Suchkriterien (sieben Staedte, Rollen, Buzzwords)
standen fest im Prompt, und importiert wurde mit EINEM globalen API-Token aus der
.env - der zufaellig dem Admin gehoerte. Auf der Multi-User-Plattform ist beides
hinfaellig.

Jetzt legt jeder Benutzer sein Suchprofil selbst fest (/jobsuche):
- Modus: regional / 100 % Remote / beides
- Staedte (die erste gilt als Wohnort und wird hoechstpriorisiert, max. 12)
- Feinschliff: zusaetzliche Begriffe, Ausschluesse
- Zeitplan: Wochentage + Uhrzeit, plus Button "Jetzt suchen"
Rollen und Technologien bleiben abgeleitet - aus dem Lebenslauf des Benutzers
(basis_dokumente), nicht aus einer gepflegten Liste. Admins koennen Profil und
Zeitplan eines Benutzers ueber /jobsuche?user=<id> mitpflegen (Link im Admin-Panel).

Aufteilung App/Host: Der Container hat weder claude noch ollama. Die App reiht
Laeufe daher nur in die Warteschlange ein (Tabelle suchlaeufe); der neue Runner
auf dem Host (scripts/jobsuche-runner.js, Cron alle 5 Min) arbeitet sie ab, baut
den Prompt je Benutzer aus dessen Profil + Lebenslauf und laeuft mit DESSEN
Zugangsdaten:
- BEWERBUNG_API_KEY = eigener API-Token des Benutzers (wird beim ersten Lauf
  automatisch erzeugt), damit Treffer im richtigen Konto landen und durch dieselbe
  Dedup-/Blacklist-Logik gehen,
- ANTHROPIC_BASE_URL/AUTH_TOKEN = eigener Ollama-Key des Benutzers ueber die
  Anthropic-kompatible Schnittstelle von Ollama Cloud (https://ollama.com/v1/messages,
  verifiziert: gueltiger Key -> 200, ungueltiger -> 401). Damit zahlt jeder seine
  eigene Suche, statt alles ueber die Host-Subscription zu buchen
  (JOBSUCHE_KI_AUTH=host stellt das alte Verhalten wieder her).

Ohne eigenen Ollama-Key oder ohne Lebenslauf bricht der Lauf mit klarer Meldung ab
statt still nichts zu tun; die Oberflaeche warnt vorab. Kein Stapeln: solange ein
Lauf offen ist, erzeugt ein weiterer Klick keinen zweiten. Verwaiste Laeufe
(Prozess weg) werden nach Zeitlimit als Fehler freigegeben.

Verifiziert mit zwei Benutzern: Zugriffsschutz (fremdes Profil -> 403), Speichern,
Warteschlange, automatische Key-Erzeugung, Prompt-Aufbau aus Profil + CV,
Ergebnis-Ruecklauf in die Oberflaeche, Fehlerpfade und die Faelligkeitslogik des
Zeitplans (Tag/Uhrzeit/bereits gelaufen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 23:28:56 +02:00

55 lines
3.2 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<% if (!laeufe.length) { %>
<p class="text-sm text-gray-500 dark:text-gray-400">Noch kein Suchlauf durchgeführt.</p>
<% } else { %>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="text-left text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
<th class="py-2 pr-4 font-medium">Zeitpunkt</th>
<th class="py-2 pr-4 font-medium">Auslöser</th>
<th class="py-2 pr-4 font-medium">Status</th>
<th class="py-2 pr-4 font-medium">Ergebnis</th>
</tr>
</thead>
<tbody>
<% laeufe.forEach(function (l) {
var badge = {
angefordert: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-200',
laeuft: 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300',
fertig: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300',
fehler: 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300'
}[l.status] || 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-200';
var text = {
angefordert: 'wartet', laeuft: 'läuft…', fertig: 'fertig', fehler: 'Fehler'
}[l.status] || l.status;
var zeit = l.gestartet_at || l.angefordert_at;
%>
<tr class="border-b border-gray-100 dark:border-gray-700/60">
<td class="py-2 pr-4 text-gray-700 dark:text-gray-300 whitespace-nowrap">
<%= zeit ? new Date(zeit + 'Z').toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short' }) : '' %>
</td>
<td class="py-2 pr-4 text-gray-500 dark:text-gray-400">
<%= l.ausloeser === 'zeitplan' ? 'Zeitplan' : 'manuell' %>
</td>
<td class="py-2 pr-4">
<span class="inline-block px-2 py-0.5 rounded text-xs font-medium <%= badge %>"><%= text %></span>
</td>
<td class="py-2 pr-4 text-gray-700 dark:text-gray-300">
<% if (l.status === 'fertig') { %>
<span class="font-medium text-green-700 dark:text-green-400"><%= l.neu || 0 %> neu</span>,
<%= l.dubletten || 0 %> Dubletten, <%= l.verworfen || 0 %> verworfen
<% } else if (l.status === 'fehler') { %>
<span class="text-red-700 dark:text-red-400" title="<%= l.fehler || '' %>">
<%= (l.fehler || 'Unbekannter Fehler').slice(0, 80) %>
</span>
<% } else { %>
<span class="text-gray-400 dark:text-gray-500"></span>
<% } %>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } %>