Compare commits

..

No commits in common. "master" and "v1.1.1" have entirely different histories.

16 changed files with 67 additions and 294 deletions

View file

@ -1,7 +1,6 @@
# ---- Database ---- # ---- Database ----
# Used by apps/api to connect to the postgres service defined in docker-compose.yml. # Used by apps/api to connect to the postgres service defined in docker-compose.yml.
# Postgres is not published to the host; all services run inside the compose network. DATABASE_URL=postgresql://jobhunt:***@localhost:5433/jobhunt
DATABASE_URL=postgresql://jobhunt:***@postgres:5432/jobhunt
# ---- LLM Gateway ---- # ---- LLM Gateway ----
# Primary provider (default: GLM-5.2 via ollama-cloud). # Primary provider (default: GLM-5.2 via ollama-cloud).

View file

@ -11,12 +11,7 @@ jobs:
api-tests: api-tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - uses: actions/checkout@v4
run: |
git init
git remote add origin https://x-access-token:${FORGEJO_TOKEN}@srvr.nu/git/hermes/jobhunt-platform.git
git fetch --depth 1 origin ${GITHUB_SHA}
git checkout FETCH_HEAD
- name: Set up Docker - name: Set up Docker
run: | run: |
docker --version docker --version
@ -29,12 +24,7 @@ jobs:
package-tests: package-tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - uses: actions/checkout@v4
run: |
git init
git remote add origin https://x-access-token:${FORGEJO_TOKEN}@srvr.nu/git/hermes/jobhunt-platform.git
git fetch --depth 1 origin ${GITHUB_SHA}
git checkout FETCH_HEAD
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
@ -63,27 +53,17 @@ jobs:
. .venv/bin/activate . .venv/bin/activate
uv pip install -e ".[dev]" uv pip install -e ".[dev]"
pytest -q pytest -q
- name: Run matching tests
run: |
cd packages/matching
uv venv
. .venv/bin/activate
uv pip install -e ".[dev]"
pytest -q
web-tests: web-tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - uses: actions/checkout@v4
run: |
git init
git remote add origin https://x-access-token:${FORGEJO_TOKEN}@srvr.nu/git/hermes/jobhunt-platform.git
git fetch --depth 1 origin ${GITHUB_SHA}
git checkout FETCH_HEAD
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "22" node-version: "22"
cache: npm
cache-dependency-path: apps/web/package-lock.json
- name: Install dependencies - name: Install dependencies
run: | run: |
cd apps/web cd apps/web
@ -95,4 +75,4 @@ jobs:
- name: Test - name: Test
run: | run: |
cd apps/web cd apps/web
npm test npm test

View file

@ -1,145 +0,0 @@
name: Deploy to Production
on:
workflow_dispatch:
inputs:
version:
description: 'Leave as "auto" to bump from latest git tag, or enter a specific version (e.g. v0.1.2)'
required: false
default: 'auto'
type: string
jobs:
deploy:
name: Build and deploy
runs-on: ubuntu-latest
steps:
- name: Checkout repository
run: |
git init
git remote add origin https://x-access-token:${FORGEJO_TOKEN}@srvr.nu/git/hermes/jobhunt-platform.git
git fetch --depth 1 origin ${GITHUB_SHA}
git checkout FETCH_HEAD
- name: Resolve version
run: |
INPUT_VERSION="${{ github.event.inputs.version }}"
if [ -z "$INPUT_VERSION" ] || [ "$INPUT_VERSION" = "auto" ]; then
git fetch --tags origin
LATEST=$(git tag --list 'v*' --sort=-v:refname | head -1)
if [ -z "$LATEST" ]; then LATEST="v0.0.0"; fi
BASE="${LATEST#v}"
MAJOR=$(echo "$BASE" | cut -d. -f1)
MINOR=$(echo "$BASE" | cut -d. -f2)
PATCH=$(echo "$BASE" | cut -d. -f3)
PATCH=$(( ${PATCH:-0} + 1 ))
VERSION="v${MAJOR:-0}.${MINOR:-0}.${PATCH}"
echo "Latest tag: $LATEST → auto-bumped to $VERSION"
else
VERSION="$INPUT_VERSION"
echo "Using manual version: $VERSION"
fi
if ! echo "$VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "ERROR: resolved version '$VERSION' is not valid semver (expected vX.Y.Z)"
exit 1
fi
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Tag version
run: |
git tag -d ${{ env.VERSION }} 2>/dev/null || true
git push origin --delete ${{ env.VERSION }} 2>/dev/null || true
git tag ${{ env.VERSION }}
git push origin ${{ env.VERSION }}
- name: Write production .env
env:
LLM_PRIMARY_KEY: ${{ secrets.LLM_PRIMARY_KEY }}
run: |
{
printf 'DATABASE_URL=%s\n' 'postgresql://jobhunt:jobhunt@postgres:5432/jobhunt'
printf 'LLM_PRIMARY_BASE_URL=%s\n' 'https://ollama.com/v1'
printf 'LLM_PRIMARY_KEY=%s\n' "$LLM_PRIMARY_KEY"
printf 'LLM_PRIMARY_MODEL=%s\n' 'glm-5.2'
printf 'LLM_CHEAP_MODEL=%s\n' 'glm-5.2'
printf 'LLM_STRONG_MODEL=%s\n' 'glm-5.2'
printf 'VITE_API_BASE=%s\n' '/api'
} > .env
- name: Build and start production stack
run: |
docker compose -p jobhunt -f docker-compose.prod.yml down
docker compose -p jobhunt -f docker-compose.prod.yml up --build -d
- name: Health checks with rollback
run: |
echo "Waiting for services to start..."
sleep 15
API_OK=false
for i in 1 2 3 4 5 6 7 8 9 10; do
if docker run --rm --network jobhunt_default curlimages/curl:8.5.0 \
-sf http://jobhunt-api:8000/api/health > /dev/null; then
echo "API is healthy"
API_OK=true
break
fi
echo "API check attempt $i failed, retrying in 5s..."
sleep 5
done
WEB_OK=false
for i in 1 2 3 4 5; do
if docker run --rm --network jobhunt_default curlimages/curl:8.5.0 \
-sf http://jobhunt-web/ > /dev/null; then
echo "Frontend is serving"
WEB_OK=true
break
fi
echo "Frontend check attempt $i failed, retrying in 5s..."
sleep 5
done
if [ "$API_OK" != "true" ] || [ "$WEB_OK" != "true" ]; then
echo ""
echo "═══════════════════════════════════════════════════"
echo " HEALTH CHECK FAILED — DIAGNOSTICS"
echo "═══════════════════════════════════════════════════"
echo ""
docker compose -p jobhunt -f docker-compose.prod.yml ps
echo ""
echo "--- API logs ---"
docker logs jobhunt-api 2>&1 | tail -80 || true
echo ""
echo "--- Postgres logs ---"
docker logs jobhunt-postgres 2>&1 | tail -30 || true
echo ""
echo "═══════════════════════════════════════════════════"
echo " ROLLING BACK DEPLOYMENT"
echo "═══════════════════════════════════════════════════"
echo ""
docker compose -p jobhunt -f docker-compose.prod.yml down
echo ""
echo "Rolled back. Containers stopped. DB volume preserved."
echo "Read API logs above to find the root cause before redeploying."
exit 1
fi
- name: Seed demo data (idempotent)
run: |
docker run --rm --network jobhunt_default curlimages/curl:8.5.0 \
-sf -X POST http://jobhunt-api:8000/api/concierge/seed-demo || \
echo "WARN: demo seed failed (non-fatal)"
- name: Print deploy status
run: |
echo ""
echo "═══════════════════════════════════════════════════"
echo " Deployed ${{ env.VERSION }} to production"
echo "═══════════════════════════════════════════════════"
echo ""
docker compose -p jobhunt -f docker-compose.prod.yml ps
echo ""
echo "Web UI: http://tocke:8085"
echo "API: http://tocke:8000/api/health"
echo ""

View file

@ -175,10 +175,10 @@ def _mock_cv_tailor(prompt: str) -> dict[str, Any]:
change_log = [] change_log = []
for b in bullets[:2]: for b in bullets[:2]:
tailored.append(f"{b} (tailored for this posting)") tailored.append(f"{b} (tailored for this posting)")
change_log.append({"action": "rephrased", "detail": f"Emphasized relevance of {b[:60]}"}) change_log.append({"action": "rephrased", "detail": f"Emphasized relevance of: {b[:60]}..."})
for b in bullets[2:]: for b in bullets[2:]:
tailored.append(b) tailored.append(b)
change_log.append({"action": "kept", "detail": f"Retained as-is {b[:60]}"}) change_log.append({"action": "kept", "detail": f"Retained as-is: {b[:60]}..."})
return { return {
"tailored_cv": {"summary": bullets[0][:160], "bullets": tailored}, "tailored_cv": {"summary": bullets[0][:160], "bullets": tailored},
"change_log": change_log, "change_log": change_log,

View file

@ -1151,9 +1151,9 @@ def seed_demo() -> Any:
profile = repo_profile.update_profile({ profile = repo_profile.update_profile({
"full_name": "Demo Demosson", "full_name": "Demo Demosson",
"email": "demo@example.com", "email": "demo@example.com",
"location": "Malmö", "location": "Malmo",
"headline": "Software Developer", "headline": "Software Developer",
"summary": "Experienced developer looking for opportunities in Skåne.", "summary": "Experienced developer looking for opportunities in Skane.",
"languages": [{"code": "sv", "level": "native"}, {"code": "en", "level": "fluent"}], "languages": [{"code": "sv", "level": "native"}, {"code": "en", "level": "fluent"}],
}) })
if profile is None: if profile is None:
@ -1161,7 +1161,7 @@ def seed_demo() -> Any:
# Create demo CV sections # Create demo CV sections
demo_sections = [ demo_sections = [
{"kind": "experience", "title": "Backend Developer", "org": "TechSkåne AB", "bullets": ["Built REST APIs", "Improved performance by 30%"], "tags": ["python", "fastapi"]}, {"kind": "experience", "title": "Backend Developer", "org": "TechSkane AB", "bullets": ["Built REST APIs", "Improved performance by 30%"], "tags": ["python", "fastapi"]},
{"kind": "experience", "title": "Junior Developer", "org": "Lund Software", "bullets": ["Maintained web apps"], "tags": ["javascript"]}, {"kind": "experience", "title": "Junior Developer", "org": "Lund Software", "bullets": ["Maintained web apps"], "tags": ["javascript"]},
{"kind": "education", "title": "MSc Computer Science", "org": "Lunds Universitet", "bullets": ["Distributed systems specialization"], "tags": ["algorithms"]}, {"kind": "education", "title": "MSc Computer Science", "org": "Lunds Universitet", "bullets": ["Distributed systems specialization"], "tags": ["algorithms"]},
{"kind": "skills", "title": "Technical Skills", "bullets": ["Python", "PostgreSQL", "Docker", "FastAPI"], "tags": ["python", "docker"]}, {"kind": "skills", "title": "Technical Skills", "bullets": ["Python", "PostgreSQL", "Docker", "FastAPI"], "tags": ["python", "docker"]},
@ -1176,12 +1176,12 @@ def seed_demo() -> Any:
# -- Create 6 standalone demo postings with varied states -- # -- Create 6 standalone demo postings with varied states --
demo_postings = [ demo_postings = [
{"company": "Skåne Tech AB", "title": "Senior Python Developer", "location": "Malmö", "url": "https://example.com/af/1", "state": "scored", "score": 85}, {"company": "Skane Tech AB", "title": "Senior Python Developer", "location": "Malmo", "url": "https://example.com/af/1", "state": "scored", "score": 85},
{"company": "Lund Systems", "title": "Fullstack Engineer", "location": "Lund", "url": "https://example.com/af/2", "state": "discovered", "score": None}, {"company": "Lund Systems", "title": "Fullstack Engineer", "location": "Lund", "url": "https://example.com/af/2", "state": "discovered", "score": None},
{"company": "Copenhagen Digital", "title": "Backend Developer", "location": "Copenhagen", "url": "https://example.com/af/3", "state": "approved", "score": 70}, {"company": "Copenhagen Digital", "title": "Backend Developer", "location": "Copenhagen", "url": "https://example.com/af/3", "state": "approved", "score": 70},
{"company": "Helsingborg IT", "title": "DevOps Engineer", "location": "Helsingborg", "url": "https://example.com/af/4", "state": "sent", "score": 65}, {"company": "Helsingborg IT", "title": "DevOps Engineer", "location": "Helsingborg", "url": "https://example.com/af/4", "state": "sent", "score": 65},
{"company": "Malmö Startup", "title": "Software Engineer", "location": "Malmö", "url": "https://example.com/af/5", "state": "rejected", "score": 30}, {"company": "Malmo Startup", "title": "Software Engineer", "location": "Malmo", "url": "https://example.com/af/5", "state": "rejected", "score": 30},
{"company": "Ängelholm Tech", "title": "Data Engineer", "location": "Ängelholm", "url": "https://example.com/af/6", "state": "discovered", "score": None}, {"company": "Angelholm Tech", "title": "Data Engineer", "location": "Angelholm", "url": "https://example.com/af/6", "state": "discovered", "score": None},
] ]
for dp in demo_postings: for dp in demo_postings:
@ -1258,7 +1258,7 @@ def seed_demo() -> Any:
url=f"https://example.com/af/deadline/{j+1}", url=f"https://example.com/af/deadline/{j+1}",
company=f"Deadline Corp {j+1}", company=f"Deadline Corp {j+1}",
title=f"Urgent Developer Role {j+1}", title=f"Urgent Developer Role {j+1}",
location="Göteborg", location="Goteborg",
description="Urgent hire for a developer with deadline approaching.", description="Urgent hire for a developer with deadline approaching.",
raw={}, raw={},
) )
@ -1290,7 +1290,7 @@ def seed_demo() -> Any:
url="https://example.com/af/interview/1", url="https://example.com/af/interview/1",
company="Festina Digital AB", company="Festina Digital AB",
title="Full Stack Developer", title="Full Stack Developer",
location="Malmö", location="Malmo",
description="Full stack developer with React, Python, and cloud experience.", description="Full stack developer with React, Python, and cloud experience.",
raw={}, raw={},
) )
@ -1398,8 +1398,8 @@ def seed_demo() -> Any:
create_email_suggestion( create_email_suggestion(
application_id=interviewing_app_id, application_id=interviewing_app_id,
mailbox_from="hr@festina-demo.se", mailbox_from="hr@festina-demo.se",
subject="Fråga om din erfarenhet", subject="Fraga om din erfarenhet",
snippet="Vi har några frågor om din bakgrund inom Python...", snippet="Vi har nagra fragor om din bakgrund inom Python...",
classification="question", classification="question",
state_proposal=None, state_proposal=None,
received_at=datetime.now(timezone.utc) - timedelta(hours=1), received_at=datetime.now(timezone.utc) - timedelta(hours=1),

View file

@ -66,10 +66,8 @@ export interface Application {
notes: string notes: string
state_changed_at: string state_changed_at: string
created_at: string created_at: string
// joined posting info (flat fields from GET /applications) // joined posting info (from GET /applications)
company?: string | null posting?: JobPosting
title?: string | null
location?: string | null
} }
export type ArtifactKind = 'cv' | 'cover_letter' | 'email' | 'other' export type ArtifactKind = 'cv' | 'cover_letter' | 'email' | 'other'
@ -264,21 +262,14 @@ export interface Cluster {
postings: ClusterPosting[] postings: ClusterPosting[]
} }
export interface TailorKeywordCoverage {
ratio: number
matched: string[]
missing: string[]
}
export interface TailorChangeLogEntry { export interface TailorChangeLogEntry {
action?: string section: string
section?: string change: string
detail?: string bullet_id: string
change?: string
} }
export interface TailorCvResponse { export interface TailorCvResponse {
artifact_id: string artifact_id: string
change_log: TailorChangeLogEntry[] change_log: TailorChangeLogEntry[]
keyword_coverage: TailorKeywordCoverage keyword_coverage: number
} }

View file

@ -34,9 +34,11 @@ function makeApp(): Application {
notes: '', notes: '',
state_changed_at: '2026-01-01T00:00:00Z', state_changed_at: '2026-01-01T00:00:00Z',
created_at: '2026-01-01T00:00:00Z', created_at: '2026-01-01T00:00:00Z',
company: 'Acme', posting: {
title: 'Engineer', id: 'j-1', source: 'manual_url', external_id: null, url: 'http://x',
location: 'Remote' company: 'Acme', title: 'Engineer', location: 'Remote', description: '',
raw: {}, fetched_at: '2026-01-01T00:00:00Z'
}
} }
} }
@ -58,10 +60,10 @@ function makeTailorResult(): TailorCvResponse {
return { return {
artifact_id: 'art-tailor-1', artifact_id: 'art-tailor-1',
change_log: [ change_log: [
{ action: 'experience', detail: 'Reordered to highlight Python backend work' }, { section: 'experience', change: 'Reordered to highlight Python backend work', bullet_id: 'b-1' },
{ action: 'skills', detail: 'Moved Docker and Kubernetes higher' } { section: 'skills', change: 'Moved Docker and Kubernetes higher', bullet_id: 'b-5' }
], ],
keyword_coverage: { ratio: 0.75, matched: ['python', 'docker'], missing: ['kubernetes'] } keyword_coverage: 0.75
} }
} }

View file

@ -34,9 +34,11 @@ function makeApp(): Application {
notes: '', notes: '',
state_changed_at: '2026-01-01T00:00:00Z', state_changed_at: '2026-01-01T00:00:00Z',
created_at: '2026-01-01T00:00:00Z', created_at: '2026-01-01T00:00:00Z',
company: 'Acme', posting: {
title: 'Engineer', id: 'j-1', source: 'manual_url', external_id: null, url: 'http://x',
location: 'Remote' company: 'Acme', title: 'Engineer', location: 'Remote', description: '',
raw: {}, fetched_at: '2026-01-01T00:00:00Z'
}
} }
} }

View file

@ -52,22 +52,17 @@ const severityClass: Record<string, string> = {
low: 'bg-blue-50 border-blue-200' low: 'bg-blue-50 border-blue-200'
} }
const coverageRatio = computed(() => {
if (!tailorResult.value) return 0
const kc = tailorResult.value.keyword_coverage
return typeof kc === 'object' && kc !== null ? (kc.ratio ?? 0) : Number(kc) || 0
})
const coverageColor = computed(() => { const coverageColor = computed(() => {
if (!tailorResult.value) return 'bg-gray-300' if (!tailorResult.value) return 'bg-gray-300'
const c = coverageRatio.value const c = tailorResult.value.keyword_coverage
if (c >= 0.7) return 'bg-green-500' if (c >= 0.7) return 'bg-green-500'
if (c >= 0.4) return 'bg-yellow-500' if (c >= 0.4) return 'bg-yellow-500'
return 'bg-red-500' return 'bg-red-500'
}) })
const coveragePercent = computed(() => { const coveragePercent = computed(() => {
return Math.round(coverageRatio.value * 100) if (!tailorResult.value) return 0
return Math.round(tailorResult.value.keyword_coverage * 100)
}) })
const downloadUrl = computed(() => { const downloadUrl = computed(() => {
@ -145,7 +140,7 @@ async function sendOutbox() {
if (!approval.value || !canSend.value) return if (!approval.value || !canSend.value) return
sending.value = true sending.value = true
try { try {
await api.outboxSend(approval.value.id, { to: application.value?.company ?? '' }) await api.outboxSend(approval.value.id, { to: application.value?.posting?.company ?? '' })
toast.push('Sent successfully', 'success') toast.push('Sent successfully', 'success')
} catch (err) { } catch (err) {
let msg = 'Send failed' let msg = 'Send failed'
@ -200,8 +195,8 @@ onMounted(loadData)
<template v-if="!loading && application"> <template v-if="!loading && application">
<!-- Posting info --> <!-- Posting info -->
<section class="bg-white rounded-lg border border-gray-200 p-4"> <section class="bg-white rounded-lg border border-gray-200 p-4">
<div class="font-semibold text-lg">{{ application.company ?? 'Unknown' }}</div> <div class="font-semibold text-lg">{{ application.posting?.company ?? 'Unknown' }}</div>
<div class="text-gray-600">{{ application.title ?? 'No title' }}</div> <div class="text-gray-600">{{ application.posting?.title ?? 'No title' }}</div>
<div class="text-sm text-gray-500 mt-1"> <div class="text-sm text-gray-500 mt-1">
State: <span class="capitalize font-medium">{{ application.state }}</span> State: <span class="capitalize font-medium">{{ application.state }}</span>
<span v-if="application.score != null" class="ml-3">Score: {{ application.score }}</span> <span v-if="application.score != null" class="ml-3">Score: {{ application.score }}</span>
@ -250,8 +245,8 @@ onMounted(loadData)
:key="i" :key="i"
class="border-b border-gray-100 py-1" class="border-b border-gray-100 py-1"
> >
<span class="font-medium text-gray-700">{{ entry.action || entry.section || 'change' }}:</span> <span class="font-medium text-gray-700">{{ entry.section }}:</span>
<span class="text-gray-600 ml-1">{{ entry.detail || entry.change }}</span> <span class="text-gray-600 ml-1">{{ entry.change }}</span>
</li> </li>
</ul> </ul>
</div> </div>

View file

@ -30,9 +30,11 @@ function makeApp(id: string, state: string, company: string): Application {
notes: '', notes: '',
state_changed_at: '2026-01-01T00:00:00Z', state_changed_at: '2026-01-01T00:00:00Z',
created_at: '2026-01-01T00:00:00Z', created_at: '2026-01-01T00:00:00Z',
company, posting: {
title: 'Engineer', id: 'j-' + id, source: 'manual_url', external_id: null, url: 'http://x',
location: 'Remote' company, title: 'Engineer', location: 'Remote', description: '',
raw: {}, fetched_at: '2026-01-01T00:00:00Z'
}
} }
} }

View file

@ -31,9 +31,11 @@ function fixture(): Application[] {
notes: '', notes: '',
state_changed_at: '2026-01-01T00:00:00Z', state_changed_at: '2026-01-01T00:00:00Z',
created_at: '2026-01-01T00:00:00Z', created_at: '2026-01-01T00:00:00Z',
company: 'Acme', posting: {
title: 'Engineer', id: 'j-1', source: 'manual_url', external_id: null, url: 'http://x',
location: 'Remote' company: 'Acme', title: 'Engineer', location: 'Remote', description: '',
raw: {}, fetched_at: '2026-01-01T00:00:00Z'
}
}, },
{ {
id: 'app-2', id: 'app-2',
@ -44,9 +46,11 @@ function fixture(): Application[] {
notes: '', notes: '',
state_changed_at: '2026-01-01T00:00:00Z', state_changed_at: '2026-01-01T00:00:00Z',
created_at: '2026-01-01T00:00:00Z', created_at: '2026-01-01T00:00:00Z',
company: 'Globex', posting: {
title: 'Manager', id: 'j-2', source: 'linkedin', external_id: null, url: 'http://y',
location: 'Malmo' company: 'Globex', title: 'Manager', location: 'Malmo', description: '',
raw: {}, fetched_at: '2026-01-01T00:00:00Z'
}
} }
] ]
} }

View file

@ -171,9 +171,9 @@ onMounted(loadApplications)
class="inline-block w-2 h-2 rounded-full bg-orange-500 flex-shrink-0" class="inline-block w-2 h-2 rounded-full bg-orange-500 flex-shrink-0"
title="Follow-up nudge pending" title="Follow-up nudge pending"
></span> ></span>
<div class="font-medium text-sm truncate">{{ app.company ?? 'Unknown' }}</div> <div class="font-medium text-sm truncate">{{ app.posting?.company ?? 'Unknown' }}</div>
</div> </div>
<div class="text-xs text-gray-500 truncate">{{ app.title ?? 'No title' }}</div> <div class="text-xs text-gray-500 truncate">{{ app.posting?.title ?? 'No title' }}</div>
<div v-if="app.score != null" class="text-xs text-green-700 mt-1"> <div v-if="app.score != null" class="text-xs text-green-700 mt-1">
Score: {{ app.score }} Score: {{ app.score }}
</div> </div>

View file

@ -153,7 +153,7 @@ onMounted(loadPostings)
<!-- Fetch form (Arbetsformedlingen connector) --> <!-- Fetch form (Arbetsformedlingen connector) -->
<section class="bg-white rounded-lg border border-gray-200 p-4 space-y-3"> <section class="bg-white rounded-lg border border-gray-200 p-4 space-y-3">
<h2 class="font-semibold">Fetch from Arbetsförmedlingen</h2> <h2 class="font-semibold">Fetch from Arbetsformedlingen</h2>
<div class="flex gap-2"> <div class="flex gap-2">
<input <input
v-model="fetchQuery" v-model="fetchQuery"

View file

@ -223,7 +223,7 @@ function prev() {
<div v-if="step === 2" class="bg-white rounded-lg border border-gray-200 p-6 space-y-4"> <div v-if="step === 2" class="bg-white rounded-lg border border-gray-200 p-6 space-y-4">
<h2 class="font-semibold text-lg">Fetch Job Postings</h2> <h2 class="font-semibold text-lg">Fetch Job Postings</h2>
<p class="text-sm text-gray-600"> <p class="text-sm text-gray-600">
Search for job postings from the Arbetsförmedlingen connector. New postings will be added to your applications. Search for job postings from the Arbetsformedlingen connector. New postings will be added to your applications.
</p> </p>
<div class="space-y-2"> <div class="space-y-2">
<label class="block"> <label class="block">

View file

@ -1,57 +0,0 @@
# Production stack for jobhunt-platform.
# Built and started by .forgejo/workflows/deploy.yml on the host docker daemon.
# Web UI is published on http://<host>:8085, API on :8000. Postgres is internal only.
services:
postgres:
image: postgres:16
container_name: jobhunt-postgres
environment:
POSTGRES_USER: jobhunt
POSTGRES_PASSWORD: jobhunt
POSTGRES_DB: jobhunt
volumes:
- jobhunt_pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U jobhunt -d jobhunt"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
api:
build:
context: .
dockerfile: apps/api/Dockerfile.test
image: jobhunt-api
container_name: jobhunt-api
entrypoint: uvicorn app.main:app --host 0.0.0.0 --port 8000
env_file: .env
environment:
DATABASE_URL: postgresql://jobhunt:jobhunt@postgres:5432/jobhunt
working_dir: /app/apps/api
ports:
- "8000:8000"
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
web:
build:
context: .
dockerfile: apps/web/Dockerfile
args:
# Baked into the SPA at build time. Relative /api goes through the
# nginx proxy in apps/web/nginx.conf -> http://api:8000/api/
VITE_API_BASE: /api
image: jobhunt-web
container_name: jobhunt-web
ports:
- "8085:80"
depends_on:
- api
restart: unless-stopped
volumes:
jobhunt_pgdata:
name: jobhunt_pgdata

View file

@ -8,8 +8,8 @@ services:
POSTGRES_USER: jobhunt POSTGRES_USER: jobhunt
POSTGRES_PASSWORD: jobhunt POSTGRES_PASSWORD: jobhunt
POSTGRES_DB: jobhunt POSTGRES_DB: jobhunt
# No host port publishing: CI/tests run inside the compose network, and on ports:
# this host 5433 is already taken by bilhej-postgres-prod. - "5433:5432"
volumes: volumes:
- jobhunt_pgdata:/var/lib/postgresql/data - jobhunt_pgdata:/var/lib/postgresql/data
healthcheck: healthcheck: