Skip to content
Open
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
5 changes: 5 additions & 0 deletions apps/backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ def normalize_log_level(cls, v: Any) -> str:
"http://localhost:3000",
"http://127.0.0.1:3000",
]
# Set EXTENSION_CORS_ORIGIN to your packed extension ID to enable the Chrome
# extension. Use the exact origin (e.g. "chrome-extension://abcdef...") for
# production, or "chrome-extension://.*" during local development. Disabled
# (None) by default so shared/server deployments are unaffected.
extension_cors_origin: str | None = None

@property
def effective_cors_origins(self) -> list[str]:
Expand Down
7 changes: 5 additions & 2 deletions apps/backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,16 @@ async def lifespan(app: FastAPI):
)

# CORS middleware - origins configurable via CORS_ORIGINS env var
app.add_middleware(
CORSMiddleware,
# Chrome extension origin opt-in via EXTENSION_CORS_ORIGIN (see config.py)
_cors_kwargs: dict = dict(
allow_origins=settings.effective_cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
if settings.extension_cors_origin:
_cors_kwargs["allow_origin_regex"] = settings.extension_cors_origin
app.add_middleware(CORSMiddleware, **_cors_kwargs)

# Include routers
app.include_router(health_router, prefix="/api/v1")
Expand Down
15 changes: 15 additions & 0 deletions apps/chrome-extension/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The extension allows users to configure a custom backendUrl in settings, but host_permissions only grants access to localhost and 127.0.0.1. In Manifest V3, fetch() from extension pages to origins not listed in host_permissions will be blocked by the browser. Either restrict the settings UI to only accept localhost URLs, or document that users must modify the manifest for non-local backends.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/chrome-extension/manifest.json, line 8:

<comment>The extension allows users to configure a custom `backendUrl` in settings, but `host_permissions` only grants access to `localhost` and `127.0.0.1`. In Manifest V3, `fetch()` from extension pages to origins not listed in `host_permissions` will be blocked by the browser. Either restrict the settings UI to only accept localhost URLs, or document that users must modify the manifest for non-local backends.</comment>

<file context>
@@ -0,0 +1,15 @@
+  "description": "Detect job descriptions on job boards and tailor your resume in one click.",
+  "permissions": ["activeTab", "storage", "scripting"],
+  "host_permissions": [
+    "http://localhost/*",
+    "http://127.0.0.1/*"
+  ],
</file context>

"manifest_version": 3,
"name": "Resume Matcher",
"version": "0.1.0",
"description": "Detect job descriptions on job boards and tailor your resume in one click.",
"permissions": ["activeTab", "storage", "scripting"],
"host_permissions": [
"http://localhost/*",
"http://127.0.0.1/*"
],
Comment on lines +6 to +10
"action": {
"default_popup": "popup/popup.html",
"default_title": "Resume Matcher"
}
}
286 changes: 286 additions & 0 deletions apps/chrome-extension/popup/popup.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}

body {
width: 360px;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif;
font-size: 13px;
background: #F0F0E8;
color: #000;
}

/* ── Header ───────────────────────────────────────────────────────────────── */
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-bottom: 1px solid #000;
}

.logo {
font-weight: 700;
font-size: 13px;
letter-spacing: 0.08em;
text-transform: uppercase;
}

#btn-settings-toggle {
width: auto;
background: none;
color: #000;
border: 1px solid #000;
padding: 2px 7px;
font-size: 14px;
margin: 0;
cursor: pointer;
border-radius: 0;
line-height: 1.4;
}
#btn-settings-toggle:hover { background: #e0e0d8; }

/* ── Sections ─────────────────────────────────────────────────────────────── */
section {
padding: 12px;
border-bottom: 1px solid #ccc;
}
section:last-of-type { border-bottom: none; }

h2 {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 10px;
}

/* ── Form elements ────────────────────────────────────────────────────────── */
label {
display: block;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 4px;
margin-top: 10px;
}
label:first-of-type { margin-top: 0; }

input[type='text'],
input[type='url'] {
width: 100%;
padding: 6px 8px;
border: 1px solid #000;
background: #fff;
font-family: 'Courier New', Courier, monospace;
font-size: 11px;
outline: none;
border-radius: 0;
}
input[type='text']:focus,
input[type='url']:focus {
outline: 2px solid #000;
outline-offset: 1px;
}

input[type='file'] {
width: 100%;
margin-top: 4px;
font-size: 11px;
cursor: pointer;
}

/* ── Buttons ──────────────────────────────────────────────────────────────── */
button {
display: block;
width: 100%;
margin-top: 8px;
padding: 8px 12px;
background: #000;
color: #fff;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
border: 1px solid #000;
cursor: pointer;
border-radius: 0;
}
button:hover:not(:disabled) { background: #222; }
button:disabled {
background: #888;
border-color: #888;
cursor: not-allowed;
}
button.secondary {
background: #F0F0E8;
color: #000;
margin-top: 6px;
}
button.secondary:hover { background: #e0e0d8; }

/* ── Utility ──────────────────────────────────────────────────────────────── */
.hint {
font-size: 11px;
color: #555;
line-height: 1.4;
margin-top: 4px;
}

.msg {
font-size: 11px;
padding: 6px 8px;
margin-top: 8px;
border: 1px solid #000;
line-height: 1.4;
}
.msg.error { border-color: #DC2626; color: #DC2626; }
.msg.success { border-color: #15803D; color: #15803D; }

.divider { height: 1px; background: #ccc; margin: 10px 0; }

.or-row {
text-align: center;
font-size: 11px;
color: #888;
margin: 8px 0;
}

/* ── Job info ─────────────────────────────────────────────────────────────── */
.job-title {
font-size: 13px;
font-weight: 700;
line-height: 1.3;
margin-bottom: 2px;
}
.job-company {
font-size: 11px;
color: #444;
margin-bottom: 6px;
}
.job-desc {
font-size: 11px;
color: #333;
line-height: 1.4;
max-height: 64px;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
}

/* ── Resume badge ─────────────────────────────────────────────────────────── */
#resume-badge {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
padding: 5px 8px;
border: 1px solid #ccc;
background: #fff;
}
.badge-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
white-space: nowrap;
}
.mono {
font-family: 'Courier New', Courier, monospace;
font-size: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

/* ── Result panel ─────────────────────────────────────────────────────────── */
.score-row {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 10px;
}
.score-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.score-value {
font-size: 30px;
font-weight: 700;
font-family: 'Courier New', Courier, monospace;
line-height: 1;
}
.score-value::after { content: '/100'; font-size: 13px; font-weight: 400; color: #555; }
.score-value.good { color: #15803D; }
.score-value.ok { color: #F97316; }
.score-value.bad { color: #DC2626; }

#result-keywords,
#result-recs {
margin-bottom: 10px;
}
.field-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 5px;
}

.chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.chip {
font-size: 10px;
font-family: 'Courier New', Courier, monospace;
padding: 2px 6px;
border: 1px solid #DC2626;
color: #DC2626;
background: #fff;
}

#recs-list {
list-style: none;
padding: 0;
}
#recs-list li {
font-size: 11px;
line-height: 1.4;
padding: 4px 0;
border-top: 1px solid #e0e0d8;
}
#recs-list li:first-child { border-top: none; }

/* ── Loading overlay ──────────────────────────────────────────────────────── */
#loading-overlay {
position: fixed;
inset: 0;
background: rgba(240, 240, 232, 0.92);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
#loading-overlay::before {
content: '';
width: 20px;
height: 20px;
border: 2px solid #000;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
Loading