diff --git a/src/applypilot/view.py b/src/applypilot/view.py index ff42fec..2e84577 100644 --- a/src/applypilot/view.py +++ b/src/applypilot/view.py @@ -76,7 +76,8 @@ def generate_dashboard(output_path: str | None = None) -> str: jobs = conn.execute(""" SELECT url, title, salary, description, location, site, strategy, full_description, application_url, detail_error, - fit_score, score_reasoning + fit_score, score_reasoning, + applied_at, apply_status, apply_error, last_attempted_at FROM jobs WHERE fit_score >= 5 ORDER BY fit_score DESC, site, title @@ -178,8 +179,68 @@ def generate_dashboard(output_path: str | None = None) -> str: if apply_url: apply_html = f'Apply' + # Auto-apply command button (only for jobs not yet applied) + raw_url = j["url"] or "" + auto_apply_cmd = f"applypilot apply --url {raw_url}" + + # Applied indicator + was_applied = j["apply_status"] == "applied" and j["applied_at"] + applied_banner = "" + applied_attr = "" + if was_applied: + try: + from datetime import datetime as _dt + applied_dt = _dt.fromisoformat(j["applied_at"].replace("Z", "+00:00")) + applied_date_str = applied_dt.strftime("%b %-d, %Y") + except (ValueError, AttributeError): + applied_date_str = j["applied_at"][:10] + applied_banner = f'
✓ Applied on {applied_date_str}
' + applied_attr = ' data-applied="true"' + + # Failed indicator + _status_reasons = { + "expired": "Job posting expired", + "captcha": "CAPTCHA blocked", + "login_issue": "Login required", + "not_eligible_location": "Location not eligible", + "not_eligible_salary": "Salary not eligible", + "already_applied": "Already applied", + "account_required": "Account required", + "not_a_job_application": "Not a job posting", + "unsafe_permissions": "Unsafe permissions", + "unsafe_verification": "Unsafe verification", + "sso_required": "SSO required", + "site_blocked": "Site blocked", + "cloudflare_blocked": "Cloudflare blocked", + "failed": "Application failed", + } + was_failed = ( + j["apply_status"] and j["apply_status"] != "applied" + and j["last_attempted_at"] + ) + failed_banner = "" + if was_failed: + try: + from datetime import datetime as _dt + failed_dt = _dt.fromisoformat(j["last_attempted_at"].replace("Z", "+00:00")) + failed_date_str = failed_dt.strftime("%b %-d, %Y") + except (ValueError, AttributeError): + failed_date_str = j["last_attempted_at"][:10] + short_reason = ( + escape((j["apply_error"] or "")[:60]) or + _status_reasons.get(j["apply_status"], j["apply_status"].replace("_", " ").title()) + ) + failed_banner = f'
✗ Failed on {failed_date_str} · {short_reason}
' + + card_extra_class = "" + if was_applied: + card_extra_class = " job-card--applied" + elif was_failed: + card_extra_class = " job-card--failed" + job_sections += f""" -
+
+ {applied_banner}{failed_banner}
{score} {title} @@ -189,7 +250,10 @@ def generate_dashboard(output_path: str | None = None) -> str: {f'
{escape(reasoning)}
' if reasoning else ''}

{desc_preview}...

{"
Full Description (" + f'{desc_len:,}' + " chars)
" + full_desc_html + "
" if j["full_description"] else ""} - +
""" if current_score is not None: @@ -291,6 +355,26 @@ def generate_dashboard(output_path: str | None = None) -> str: .hidden {{ display: none !important; }} .job-count {{ color: #94a3b8; font-size: 0.85rem; margin-bottom: 1rem; }} + /* Auto-apply button */ + .auto-apply-btn {{ background: transparent; border: 1px solid #6366f1; color: #818cf8; padding: 0.3rem 0.8rem; + border-radius: 6px; cursor: pointer; font-size: 0.78rem; font-weight: 600; transition: all 0.15s; white-space: nowrap; }} + .auto-apply-btn:hover {{ background: #6366f122; color: #a5b4fc; border-color: #a5b4fc; }} + .auto-apply-btn.copied {{ background: #064e3b; border-color: #10b981; color: #6ee7b7; }} + + /* Applied indicator */ + .job-card--applied {{ border-left-color: #10b981 !important; background: #0d2b1e; }} + .job-card--applied:hover {{ box-shadow: 0 4px 16px #10b98133; }} + .applied-banner {{ background: #10b981; color: #022c22; font-size: 0.75rem; font-weight: 700; + padding: 0.3rem 0.75rem; margin: -1rem -1rem 0.75rem -1rem; border-radius: 7px 7px 0 0; + letter-spacing: 0.03em; }} + + /* Failed indicator */ + .job-card--failed {{ border-left-color: #ef4444 !important; background: #1f0f0f; }} + .job-card--failed:hover {{ box-shadow: 0 4px 16px #ef444433; }} + .failed-banner {{ background: #7f1d1d; color: #fca5a5; font-size: 0.75rem; font-weight: 700; + padding: 0.3rem 0.75rem; margin: -1rem -1rem 0.75rem -1rem; border-radius: 7px 7px 0 0; + letter-spacing: 0.03em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }} + @media (max-width: 768px) {{ .summary {{ grid-template-columns: repeat(2, 1fr); }} .score-section {{ grid-template-columns: 1fr; }} @@ -319,6 +403,7 @@ def generate_dashboard(output_path: str | None = None) -> str: Search: +
@@ -339,10 +424,39 @@ def generate_dashboard(output_path: str | None = None) -> str: