Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions batch/batch-runner.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ DRY_RUN=false
RETRY_FAILED=false
START_FROM=0
MAX_RETRIES=2
MIN_SCORE=0

usage() {
cat <<'USAGE'
Expand All @@ -41,6 +42,7 @@ Options:
--retry-failed Only retry offers marked as "failed" in state
--start-from N Start from offer ID N (skip earlier IDs)
--max-retries N Max retry attempts per offer (default: 2)
--min-score N Minimum score required to save the report (default: 0)
-h, --help Show this help

Files:
Expand Down Expand Up @@ -73,6 +75,7 @@ while [[ $# -gt 0 ]]; do
--retry-failed) RETRY_FAILED=true; shift ;;
--start-from) START_FROM="$2"; shift 2 ;;
--max-retries) MAX_RETRIES="$2"; shift 2 ;;
--min-score) MIN_SCORE="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1"; usage; exit 1 ;;
esac
Expand Down Expand Up @@ -334,6 +337,7 @@ process_offer() {
local esc_url esc_jd_file esc_report_num esc_date esc_id
esc_url="${url//\\/\\\\}"
esc_url="${esc_url//|/\\|}"
esc_url="${esc_url//&/\\&}"
esc_jd_file="${jd_file//\\/\\\\}"
esc_jd_file="${esc_jd_file//|/\\|}"
esc_report_num="${report_num//|/\\|}"
Expand Down Expand Up @@ -365,13 +369,22 @@ process_offer() {
# Try to extract score from worker output
local score="-"
local score_match
score_match=$(grep -oP '"score":\s*[\d.]+' "$log_file" 2>/dev/null | head -1 | grep -oP '[\d.]+' || true)
score_match=$(grep -oE '"score":\s*[0-9.]+' "$log_file" 2>/dev/null | head -1 | grep -oE '[0-9.]+' || true)
if [[ -n "$score_match" ]]; then
score="$score_match"
fi

update_state "$id" "$url" "completed" "$started_at" "$completed_at" "$report_num" "$score" "-" "$retries"
echo " ✅ Completed (score: $score, report: $report_num)"
# Check minimum score threshold
if [[ "$score" != "-" ]] && awk -v score="$score" -v min="$MIN_SCORE" 'BEGIN{exit !(score >= min)}'; then
update_state "$id" "$url" "completed" "$started_at" "$completed_at" "$report_num" "$score" "-" "$retries"
echo " ✅ Completed (score: $score, report: $report_num)"
elif [[ "$score" != "-" ]]; then
update_state "$id" "$url" "skipped" "$started_at" "$completed_at" "$report_num" "$score" "Score $score below min $MIN_SCORE" "$retries"
echo " ⏭️ Skipped (score: $score < min $MIN_SCORE, report: $report_num)"
else
update_state "$id" "$url" "completed" "$started_at" "$completed_at" "$report_num" "$score" "-" "$retries"
echo " ✅ Completed (score: -, report: $report_num)"
fi
else
retries=$((retries + 1))
local error_msg
Expand Down Expand Up @@ -401,7 +414,7 @@ print_summary() {
return
fi

local total=0 completed=0 failed=0 pending=0
local total=0 completed=0 failed=0 pending=0 skipped=0
local score_sum=0 score_count=0

while IFS=$'\t' read -r sid _ sstatus _ _ _ sscore _ _; do
Expand All @@ -415,11 +428,12 @@ print_summary() {
fi
;;
failed) failed=$((failed + 1)) ;;
skipped) skipped=$((skipped + 1)) ;;
*) pending=$((pending + 1)) ;;
esac
done < "$STATE_FILE"

echo "Total: $total | Completed: $completed | Failed: $failed | Pending: $pending"
echo "Total: $total | Completed: $completed | Skipped: $skipped | Failed: $failed | Pending: $pending"

if (( score_count > 0 )); then
local avg
Expand Down
14 changes: 13 additions & 1 deletion check-liveness.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@
import { chromium } from 'playwright';
import { readFile } from 'fs/promises';
import { classifyLiveness } from './liveness-core.mjs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';

async function checkUrl(page, url) {
if (!url.startsWith('http://') && !url.startsWith('https://')) {
return { result: 'expired', reason: 'Invalid URL protocol' };
}

try {
const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });

Expand Down Expand Up @@ -80,7 +86,13 @@ async function main() {

let urls;
if (args[0] === '--file') {
const text = await readFile(args[1], 'utf-8');
const filePath = resolve(args[1]);
const rootDir = dirname(fileURLToPath(import.meta.url));
if (!filePath.startsWith(rootDir)) {
console.error('Security Error: Path traversal attempt blocked for --file');
process.exit(1);
}
const text = await readFile(filePath, 'utf-8');
urls = text.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
} else {
urls = args;
Expand Down
75 changes: 73 additions & 2 deletions dashboard/internal/ui/screens/viewer.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,86 @@ type ViewerModel struct {
theme theme.Theme
}

// formatTables aligns markdown tables properly
func formatTables(lines []string) []string {
var out []string
var tableLines []string

flushTable := func() {
if len(tableLines) == 0 {
return
}
var colWidths []int
var rows [][]string
for _, tl := range tableLines {
cols := strings.Split(tl, "|")
rows = append(rows, cols)
for i, c := range cols {
// Don't count separator lines for max width
if strings.Contains(c, "---") {
continue
}
w := len([]rune(strings.TrimSpace(c)))
if i >= len(colWidths) {
colWidths = append(colWidths, w)
} else if w > colWidths[i] {
colWidths[i] = w
}
}
}
for _, cols := range rows {
var formattedCols []string
for i, c := range cols {
if i == 0 || i == len(cols)-1 {
if strings.TrimSpace(c) == "" {
formattedCols = append(formattedCols, "")
continue
}
}
w := 0
if i < len(colWidths) {
w = colWidths[i]
}
c = strings.TrimSpace(c)
if strings.Contains(c, "---") {
if w < 3 { w = 3 }
formattedCols = append(formattedCols, strings.Repeat("─", w+2))
} else {
pad := w - len([]rune(c))
if pad < 0 { pad = 0 }
formattedCols = append(formattedCols, " "+c+strings.Repeat(" ", pad)+" ")
}
}
out = append(out, strings.Join(formattedCols, "|"))
}
tableLines = nil
}

for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "|") && strings.HasSuffix(trimmed, "|") {
tableLines = append(tableLines, trimmed)
} else {
flushTable()
out = append(out, line)
}
}
flushTable()
return out
}

// NewViewerModel creates a new file viewer for the given path.
func NewViewerModel(t theme.Theme, path, title string, width, height int) ViewerModel {
content, err := os.ReadFile(path)
if err != nil {
content = []byte("Error reading file: " + err.Error())
}

lines := strings.Split(string(content), "\n")
lines = formatTables(lines)

return ViewerModel{
lines: strings.Split(string(content), "\n"),
lines: lines,
title: title,
width: width,
height: height,
Expand Down Expand Up @@ -257,7 +328,7 @@ func (m ViewerModel) styleLine(line string) string {
Render(line)
}
// Table headers/separators
if strings.HasPrefix(trimmed, "|") && strings.Contains(trimmed, "---") {
if strings.HasPrefix(trimmed, "|") && (strings.Contains(trimmed, "---") || strings.Contains(trimmed, "─")) {
return lipgloss.NewStyle().
Foreground(m.theme.Overlay).
Render(line)
Expand Down
21 changes: 18 additions & 3 deletions dedup-tracker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,26 @@ function normalizeRole(role) {
.trim();
}

const STOPWORDS = new Set([
'senior', 'junior', 'lead', 'manager', 'director', 'intern', 'remote',
'hybrid', 'onsite', 'engineer', 'developer', 'staff', 'principal',
'london', 'york', 'francisco', 'seattle', 'boston', 'austin', 'berlin',
'paris', 'sydney', 'toronto', 'chicago', 'usa', 'uk', 'eu',
'level', 'tier', 'parttime', 'fulltime', 'contract', 'role', 'team'
]);

function roleMatch(a, b) {
const wordsA = normalizeRole(a).split(/\s+/).filter(w => w.length > 3);
const wordsB = normalizeRole(b).split(/\s+/).filter(w => w.length > 3);
const wordsA = normalizeRole(a).split(/\s+/).filter(w => w.length > 3 && !STOPWORDS.has(w));
const wordsB = normalizeRole(b).split(/\s+/).filter(w => w.length > 3 && !STOPWORDS.has(w));

if (wordsA.length === 0 || wordsB.length === 0) return false;

const overlap = wordsA.filter(w => wordsB.some(wb => wb.includes(w) || w.includes(wb)));
return overlap.length >= 2;

const ratioA = overlap.length / wordsA.length;
const ratioB = overlap.length / wordsB.length;

return overlap.length >= 2 || (overlap.length >= 1 && Math.max(ratioA, ratioB) >= 0.5);
}

function parseScore(s) {
Expand Down
4 changes: 2 additions & 2 deletions docs/CUSTOMIZATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ Key sections:
- **compensation**: Target range, minimum, currency
- **location**: Country, timezone, visa status, on-site availability

## Target Roles (modes/_shared.md)
## Target Roles (modes/_profile.md)

The archetype table in `_shared.md` determines how offers are scored and CVs are framed. Edit the table to match YOUR career targets:
The archetype table in `_profile.md` determines how offers are scored and CVs are framed. Edit the table to match YOUR career targets:

```markdown
| Archetype | Thematic axes | What they buy |
Expand Down
13 changes: 11 additions & 2 deletions generate-pdf.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,17 @@ async function generatePDF() {
process.exit(1);
}

inputPath = resolve(inputPath);
outputPath = resolve(outputPath);
function enforceSafePath(p) {
const resolvedPath = resolve(p);
if (!resolvedPath.startsWith(__dirname)) {
console.error(`Security Error: Path traversal attempt blocked for path: ${p}`);
process.exit(1);
}
return resolvedPath;
}

inputPath = enforceSafePath(inputPath);
outputPath = enforceSafePath(outputPath);

// Validate format
const validFormats = ['a4', 'letter'];
Expand Down