Adds nullable JobSearchSessionId to PageFetchEntity and FetchPageRequest.
cv-search-job passes session.Id on every fetch so all Playwright page
loads for a job search session can be traced back to their session.
Includes index on JobSearchSessionId for efficient lookup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Captures the IP when the user submits the CV match form and stores it on
the token, giving a full audit trail: token holds the match-site IP,
session holds the email link-click IP.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously always stored empty string; now stores the full page text
returned by page-fetcher-api, which is already in scope at save time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Captures client IP at job-search link-click time and threads it through to the session.
Both Email and ClientIpAddress are copied from session to each result row during processing.
Closes#47
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Threads the caller's email and client IP through the match pipeline so
every Results row records who triggered the match and from where.
Closes#45
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
page-fetcher-api always uses Playwright (networkidle by default), so the
per-provider flag that chose between headless and plain HTTP is obsolete.
- Removed from JobProviderEntity, CvSearchDbContext, JobProviderConfig, JobTokenService
- HtmlJobSearcher no longer passes WaitFor (uses page-fetcher-api default)
- EF migration drops the column from cvSearch.JobProviders
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Refit 10.1.6 signing certificate was revoked; upgraded to 11.0.1 in Directory.Packages.props
- cv-matcher-api/Dockerfile and cv-search-job/Dockerfile were missing COPY steps
for page-fetcher-api-models (added in this feature branch)
All 8 images now build cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the spurious Api segment to match the pattern used by all other
models projects: CvMatcher.Models.*, Rag.Models.*, PageFetcher.Models.*.
Updated all consumers: email-api, api, cv-search-job.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes inconsistency where email-api used EmailApi.* and page-fetcher-api
used PageFetcherApi.*, while cv-matcher-api and rag-api use the generic
Api.* namespace. All four API projects now follow the same pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings classes now live in the -models project alongside DTOs and client
interfaces, eliminating the Settings/ folder from both API projects.
- SmtpSettings: email-api/Settings/ → email-api-models/Settings/ (namespace EmailApi.Models.Settings)
- PageFetcherSettings: page-fetcher-api/Settings/ → page-fetcher-api-models/Settings/ (namespace PageFetcher.Models.Settings)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Server-side-only settings (internal config not needed by callers) belong in
the API project itself, not in the models project. PageFetcherSettings
(DefaultWaitFor, TimeoutSeconds, MaxTextChars) mirrors SmtpSettings in
email-api/Settings/ — callers never reference these.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings class now lives in Apis/page-fetcher-api-models/Settings/ with
namespace PageFetcher.Models.Settings, matching how EmailApiSettings is
placed in email-api-models/Settings/.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings classes belong in Settings/ with namespace PageFetcherApi.Settings,
not Services/. Matches the SmtpSettings placement in email-api.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use http://page-fetcher-api:8080 (the Compose service key) for Docker DNS
resolution, consistent with all other internal service URLs (rag-api,
email-api, cv-matcher-api).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces page-fetcher-api, a new internal ASP.NET Core service that
centralises all web-page fetching through a single Playwright (headless
Chromium) browser instance. All fetches are persisted to the pageFetcher
SQL schema for auditing.
New projects:
- Apis/page-fetcher-api-models: FetchPageRequest, FetchPageResponse, IPageFetcherApiClient
- Apis/page-fetcher-data: PageFetchDbContext, PageFetchEntity, InitialSchema migration (schema: pageFetcher)
- Apis/page-fetcher-api: PlaywrightBrowserService (singleton), PageFetcherService, PageController
Changes to existing services:
- cv-matcher-api: JobTextExtractor now calls IPageFetcherApiClient instead of HttpClient
- cv-search-job: HtmlJobSearcher uses IPageFetcherApiClient (removes inline Playwright);
CvSearchJobTask fetches individual job pages and applies keyword pre-filter before
LLM call; passes pre-fetched JobDescription to cv-matcher-api to skip re-fetch
- common: add PageFetcherApiSettings
- docker-compose.yml, build.yml: add new service + env vars for callers
Closes#43
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After resolving relative hrefs against the base search URL, some ejobs.ro
links were producing file:/// URIs (e.g. file:///user/locuri-de-munca/...).
These were sent to cv-matcher-api and rejected with HTTP 400, causing 0 matches.
Added a scheme guard after URI resolution to skip any URL that is not
http:// or https://, preventing malformed URLs from reaching the matcher.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After resolving relative hrefs against the base search URL, some ejobs.ro
links were producing file:/// URIs (e.g. file:///user/locuri-de-munca/...).
These were sent to cv-matcher-api and rejected with HTTP 400, causing 0 matches.
Added a scheme guard after URI resolution to skip any URL that is not
http:// or https://, preventing malformed URLs from reaching the matcher.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Show the candidate's location in the scan summary block of the results email
alongside keywords and providers, for both en and ro templates.
- CvSearchEmailSender.SendResultsAsync accepts location and passes it to BuildScanSummary
- BuildScanSummary passes {{location}} to the template (falls back to '-' when absent)
- CvSearchJobTask passes session.Location to SendResultsAsync
- New migration AddLocationToScanSummaryTemplate updates both language variants of
email.search-results.scan-summary to include a 'Location / Locație căutată' row
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes#41
- Add RequireKeywordInAnchor per-provider flag (default true); set false for
ejobs.ro and bestjobs.eu so Stage 2 anchor-text filter is skipped — their
search URL already filters by relevance server-side
- Update AI system prompts (en + ro) to extract concise job-board-friendly
keywords (role title + key tech, not abstract concepts) and candidate location
- Propagate location through JobMatchResponse -> CreateJobSearchTokenRequest ->
JobSearchTokenEntity -> JobSearchSessionEntity
- Add {location} and {location-slug} substitution in HtmlJobSearcher
- Update provider SearchUrlTemplates to include location:
ejobs.ro: /locuri-de-munca/{location-slug}?q={keywords}
bestjobs.eu: /ro/locuri-de-munca-in-{location-slug}?keywords={keywords}
linkedin.com: ?keywords={keywords}&location={location}
- Three new migrations: AddRequireKeywordInAnchorAndLocation,
ImproveKeywordsAndAddLocation, AddLocationToProviders
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UseInternalApiKeyProtection was registered after UseSwaggerInDevelopment,
allowing unauthenticated access to /swagger. Swapped order to match
rag-api and cv-matcher-api.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
email-data references shared-data but the email-api Dockerfile never
copied it into the build context, causing MSB9008 during Docker build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Serilog.Settings.Configuration cannot deserialize NetworkCredential or
MailKit's SecureSocketOptions from JSON, causing an InvalidOperationException
in the binder and preventing containers from starting.
Fix: remove Email from the WriteTo JSON array entirely and wire it in code
inside ConfigureJsonSerilog using a dedicated SerilogEmail:* config section.
The sink is skipped when From/To/Host are absent, so local dev is unaffected.
Also renames the docker-compose env vars from the verbose
Serilog__WriteTo__2__Args__* prefix to the clean SerilogEmail__* prefix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
HTTP and Playwright fetch failures in HtmlJobSearcher are now logged at
Error so that Serilog's email sink triggers an alert when a job provider
is unreachable. Per-URL match failures remain at Warning (expected noise).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Serilog.Sinks.Email v4 renamed all configuration parameters from their
v2 names. The old names were silently ignored, so no error alert emails
were ever sent.
Parameter renames applied across all 6 appsettings.json and docker-compose:
fromEmail → from
toEmail → to
mailServer → host
networkCredential → credentials
enableSsl: true → connectionSecurity: StartTls
emailSubject → subject
outputTemplate → body
batchPostingLimit / period removed (v4 batching uses a separate overload)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 requests / 1 min per IP. docker-compose.yml wired with ${VAR:-default}.
Staging and production .env files updated locally (gitignored).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Require captchaToken query param on initial (non-range) download requests
- Range requests (HTTP resume) bypass captcha — they are continuations of an already-validated download
- Add download rate limit policy: 5 requests / 1 min per IP (configured in .env)
- Inject ICaptchaVerifier; action name is file_download
UI change required: execute grecaptcha.execute(siteKey, {action: 'file_download'})
before triggering the download and append ?captchaToken=<token> to the URL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These templates belong to the myAi schema (myai-data) and are read by
CvMatcherController via ITemplateService. The email-data copies were
never read by any code — removing them to avoid confusion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The job-search status page HTML wrapper was baked into a static helper
method in CvMatcherController. Extracted to a new template key
html.job-search.shell (*) with {{title}} and {{message}} placeholders.
Added to AddTemplates seed and a new AddHtmlJobSearchShell migration
for existing DBs. Controller now calls _templates.Render() for all paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The LLM JSON shape was missing the keywords array so res.Keywords was
always empty, causing "none detected" in job search emails. Both en/ro
prompts now include "keywords" in the required JSON shape so the LLM
extracts relevant job-search terms from the CV/job pair.
Note: the cvMatcher.CvMatchResults cache must be cleared on existing DBs
so cached responses (which lack keywords) are not served.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace div/CSS-class approach with nested table layout so the 600px
container is enforced via HTML attributes, not a <style> block that
Outlook strips. Also removes border-radius and display:inline-block.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace div-based layouts with table-based HTML throughout (max-width/border-radius/display:inline-block ignored by Outlook)
- email.match.body: width:100% table with per-cell borders and fixed 130px label column
- email.match.job-search-footer: table-based button with bgcolor attribute
- email.search-results.empty: div replaced with full-width table
- email.search-results.body: remove div wrapper around items
- Add email.search-results.scan-summary and email.search-results.item templates
- CvSearchEmailSender: remove all hardcoded HTML; render via IEmailTemplateService
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Changed table width from 100% to max-width: 500px with margin: 0 auto
- Applies to both English and Romanian email.match.body templates
- Table now narrower and centered in email
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Convert email.match.body, email.match.job-search-footer, email.search-results.body, and email.search-results.empty templates from plain text to proper HTML format in InitialSchema migration
- Update EmailApiEmailSender.BuildMatchEmailBody() to work with HTML templates instead of plain text
- Add WebUtility.HtmlEncode() for security when inserting dynamic content (summary)
- Templates now use semantic HTML tags (table, h2, h3, ul, li, p, div, hr, a) instead of plain text with newlines
- All 32 email template variants (16 keys × 2 languages) and 8 html.job-search.* templates seeded via migration
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Remove html-shell entries from InitialSchema Seed() method
- Create new AddHtmlShellTemplates migration to insert html-shell templates
- Prevents duplicate key errors from having same data in two migrations
- InitialSchema seeds 32 templates (16 keys × 2 languages)
- AddHtmlShellTemplates seeds 2 html-shell templates (start, end)
- Total: 34 templates after both migrations run
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add email.html-shell.start: Opening HTML wrapper with blue header and MyAi.ro branding
- Add email.html-shell.end: Closing HTML wrapper with footer
- These templates wrap HTML email bodies before sending via SmtpEmailDispatcher
- Language key set to '*' (language-agnostic)
- Ensures email shell templates are seeded automatically on fresh database initialization
Fixes the "Email template not found: key='email.html-shell.start'" error that prevented email sending.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Email migration includes seed data for 14 templates (en, ro)
- CV matcher migration includes seed data for 2 AI prompts (en, ro)
- Tables are created successfully by migrations
- Issue: migrationBuilder.Sql() statements not being executed by EF Core
- Workaround needed: Current seeding approach not working automatically
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Changed configuration error handling to throw InvalidOperationException instead of silently using fallback values. This ensures:
1. Missing email templates (critical config) → 500 error to UI
2. Missing AI prompts (critical config) → 500 error to UI
3. Clear error messages indicating config issue
4. Prompts administrators to check database seeding
Services updated:
- EmailTemplateService.Get() throws for missing template
- CvMatcherService.ScorePairAsync() throws for missing AI prompt
This prevents silent failures with degraded service quality and makes it obvious to users that the system has a configuration problem that needs fixing.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Refactored the AI prompt system to use proper language-specific prompts (en and ro) instead of a single wildcard prompt with runtime {{languageName}} placeholder substitution.
Benefits:
- Language-specific instructions optimized for each language
- Better control over LLM behavior per language
- Cleaner code without placeholder substitution
- Easier to maintain and update prompts per language
Changes:
- Updated cvMatcher InitialSchema migration to seed en and ro prompts separately
- Modified CvMatcherService to retrieve language-specific prompts directly
- Removed LanguageName() helper method (no longer needed)
- Added fallback prompts in service for safety
The English and Romanian prompts now include specific JSON examples in their respective languages, ensuring the LLM understands the expected output format for each language variant.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Added seeding of the ai.cv-match.system-prompt to the AiPrompts table. This prompt is retrieved by CvMatcherService when scoring CV-job pairs with the LLM. The {{languageName}} placeholder is substituted at runtime based on the requested language.
The prompt has a fallback in the service code, but seeding it ensures the proper version is used and avoids relying on the hardcoded fallback.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
EF Core migration scaffolding doesn't recognize InsertData calls made through local functions in manually-edited migrations. Changed to use raw SQL INSERT statements with migrationBuilder.Sql() to directly populate the Templates table with all required email.* and html.job-search.* templates (en+ro).
This ensures templates are present when EmailTemplateService loads the cache, preventing 'Email template not found' warnings and enabling proper email rendering for CV match results and job search pages.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
When matching CVs, the system was finding no templates for email rendering because the email.Templates table was empty. The templates were seeded to the myAi schema, not the email schema.
Added seeding of all required email.* and html.job-search.* templates (en+ro) to the email-data InitialSchema migration. This ensures templates are automatically populated when the migration runs.
Templates seeded:
- email.match.subject, .body, .job-search-footer (en+ro)
- email.search-results.subject, .body, .empty (en+ro)
- html.job-search.started.*, .already-used.*, .expired.*, .invalid.*, .error.* (en+ro)
This fixes the issue where EmailTemplateService would log "Email template not found" warnings and return template keys as fallback text, causing match result emails to fail rendering.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Deleted all incremental migrations and regenerated fresh InitialSchema migrations
that contain the complete, correct schema from the start:
- CvMatcher: InitialSchema with 3-column unique constraint on Results
(CvDocumentId, JobDocumentId, Language)
- Email: InitialSchema with Templates table (consolidated from EmailTemplates)
This creates a cleaner migration history and faster fresh deployments. Since there
is no production data, consolidation is safe and improves maintainability.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This migration renames the EmailTemplates table to Templates in the email schema.
The migration was scaffolded by EF Core with manual RenameTable commands added.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Refactoring:
- Rename EmailApiDbContext class to EmailDbContext for consistency with other DbContext naming
- Rename DbSet property from EmailTemplates to Templates
- Rename table from EmailTemplates to Templates
- Update all references in Program.cs files (email-api, api, cv-search-job)
- Update all migration files and model snapshot
- Fix cv-search-job migrations assembly name: email-api-data → email-data
This improves naming consistency across the solution.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
When a match result is inserted concurrently between the existence check and the
database insert, log a warning to help with diagnostics while gracefully handling
the idempotent duplicate key violation.
The Results table had a unique constraint on (CvDocumentId, JobDocumentId) but the code
expects uniqueness on (CvDocumentId, JobDocumentId, Language). When matching the same CV
against the same job in different languages, this caused duplicate key violations.
Changes:
- Updated CvMatcherDbContext to define 3-column unique index including Language
- Generated proper EF Core migration to drop 2-column index and create 3-column index
- Updated ModelSnapshot to reflect new 3-column index definition
- Added exception handling in SaveMatchAsync to gracefully handle any race conditions
where duplicate key violations could occur between the existence check and insert
The migration will be automatically applied on container startup via db.Database.Migrate().
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
The unique constraint on cvMatcher.Results was defined as (CvDocumentId, JobDocumentId)
but the code checks for (CvDocumentId, JobDocumentId, Language). This mismatch caused
duplicate key violations when matching the same CV+Job in different languages.
Update the constraint to (CvDocumentId, JobDocumentId, Language) to allow different
languages for the same CV+Job pair while preventing true duplicates.
Resolves: Duplicate key constraint violations on concurrent/repeated match requests
ejobs.ro migrated to a Nuxt SPA - plain HTTP GET returns only the JS
bundle. This change equips cv-search-job with a headless Chromium
(Playwright 1.60) so it can fully render SPA pages before extracting
job links.
- Add UseHeadlessBrowser flag to JobProviderEntity, JobProviderConfig,
and CvSearchDbContext; map it in JobTokenService.ToConfig so the flag
is included in the session provider-config snapshot
- Migration: add UseHeadlessBrowser column; fix ejobs.ro search URL
(remove /user/ prefix that caused 404) and set UseHeadlessBrowser=true
- HtmlJobSearcher: detect flag and dispatch to FetchWithPlaywrightAsync;
plain-HTTP path is unchanged; NetworkIdle timeout falls back to partial
content rather than failing outright
- Dockerfile: download Playwright Chromium in the SDK build stage via
npx; copy browser binaries to the final image; install Chromium system
libs (Ubuntu noble t64 variants)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Individual job listings on bestjobs.eu use /loc-de-munca/{slug} URLs.
The seeded JobLinkContains value /ro/locuri-de-munca/ matched only the
category navigation links (Vanzari, Inginerie, Management...), so
zero job URLs passed the stage-1 href filter and the scraper returned
nothing. Migration updates the stored record (Id=2) to /loc-de-munca/.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
string.Join("") produced no whitespace between inline-block spans,
causing keywords to visually merge in email clients that collapse margins.
Switched to string.Join(" ") and zeroed left margin on each badge so
they wrap cleanly without a gap on the first item.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deleted stale directories and stray .csproj files that were never added
to the solution after project renames:
- Apis/cv-search-models/ (renamed → cv-search-data)
- Apis/myai-models/ (renamed → myai-data)
- Apis/shared-models/ (empty leftover)
- Apis/cv-search-data/cv-search-models.csproj (stray old csproj)
- Apis/myai-data/myai-models.csproj (stray old csproj)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two migration files had literal schema strings that were missed in earlier passes:
- cv-search-data AddJobSearchTables: two CreateIndex calls used "cvSearch"
- rag-data InitialRagSchema: FK principalSchema used "rag"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Piggybacks keyword extraction onto the existing CV-to-job LLM call —
no extra API calls. The system prompt now instructs the model to return
8-12 English job-search terms (job titles, technologies, skills, domains)
in a new `keywords` field alongside the existing score/summary fields.
Keywords flow: LLM JSON → JobMatchResponse.Keywords → CreateJobSearchTokenRequest →
JobSearchTokenEntity.Keywords (stored comma-separated) → JobSearchSessionEntity.Keywords
(copied at session-creation time, no RAG call needed).
Changes:
- Add Keywords to JobMatchResponse, CreateJobSearchTokenRequest, JobSearchTokenEntity
- IJobTokenService.CreateTokenAsync now accepts IReadOnlyList<string> keywords
- JobTokenService: store keywords on token; TriggerStartAsync reads token.Keywords
instead of fetching CV text from RAG — removes IRagApiClient dependency
- Remove heuristic ExtractKeywords method
- Migration AddKeywordsToJobSearchTokens: adds Keywords column to cvSearch.JobSearchTokens
- Migration UpdateCvMatchSystemPromptKeywords: updates ai.cv-match.system-prompt seed
to include keywords in the JSON shape
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PDF text extraction often stores all content without newlines. The previous
line-based splitter would produce one line > 200 chars which was filtered out,
yielding empty keywords. Replace with word-level sampling of the first 2000
chars, splitting on whitespace and common delimiters, skipping phone fragments,
emails, and URLs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Down migration was referencing "emailApi" literal instead of MigrationConstants.SchemaName,
which would have dropped the wrong schema on rollback. Also fix stale comment in DbContext.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace hardcoded "cvSearch" string literals with MigrationConstants.SchemaName
in the Up, InsertData, and Down methods, consistent with all other migrations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Provider config is no longer read from appsettings or env vars.
All three providers (ejobs.ro, bestjobs.eu, linkedin.com) are seeded
into cvSearch.JobProviders by the AddJobProviders migration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JobTokenService.CreateTokenAsync queries cvSearch.JobProviders for any
enabled row; returns null (no token created) when the table is empty or
all providers are disabled. TriggerStartAsync snapshots enabled providers
from DB at session-start time, preserving the existing snapshot contract.
CvMatcherController guards link-building on a non-null TokenId so the
"Start a job search" CTA is omitted from match emails when no providers
are configured.
JobSearchSettings.Providers list removed — provider config now lives
exclusively in the DB. CvSearchJobTask.GetProviders falls back to an
empty list with a warning (snapshot should always be populated from DB).
Closes#35
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New JobProviderEntity persists provider config (name, URL template,
link filter, initial keywords, max results, display order) in the DB
instead of appsettings. Migration seeds three disabled defaults:
ejobs.ro, bestjobs.eu, and linkedin.com.
Closes#35
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add funnel-level logging to HtmlJobSearcher (total anchors found,
stage-1 href-filter count, stage-2 keyword-filter count) and warn
when the keyword list is empty. Log the full search URL and response
size to catch silent HTTP failures or bot-block pages.
In CvSearchJobTask, log keywords and active providers at session start,
per-provider URL counts after each scrape, and every scored URL with its
verdict (ACCEPTED / rejected) at Information level.
Add a scan summary block to the results email (both non-empty and
empty-results paths) showing the CV keywords used as chips and the
comma-separated list of providers scanned.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix schema name references in migration Designer.cs and ModelSnapshot files.
Previously these files contained hardcoded 'emailApi' schema name instead of
using MigrationConstants.SchemaName constant. This was causing EF Core to
detect pending model changes and fail migrations.
Changes:
- 20260528100000_CreateEmailTemplates.Designer.cs: Use MigrationConstants.SchemaName
- 20260528130652_SeedEmailTemplates.Designer.cs: Use MigrationConstants.SchemaName
- EmailApiDbContextModelSnapshot.cs: Use MigrationConstants.SchemaName and updated namespace
Also updated entity namespace references from EmailApi.Data to Email.Data.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Update all Dockerfile COPY commands to reference the renamed email-data project
instead of email-api-data. This resolves Docker build failures introduced by the
email-api-data → email-data rename.
- Apis/api/Dockerfile: Update lines 8 and 20
- Apis/email-api/Dockerfile: Update lines 6 and 17
- Jobs/cv-search-job/Dockerfile: Update lines 10 and 23
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Rename project folder Apis/email-api-data → Apis/email-data
- Rename csproj file: email-api-data.csproj → email-data.csproj
- Update csproj properties: AssemblyName and RootNamespace (email-data, Email.Data)
- Update C# namespaces: EmailApi.Data → Email.Data across all email-data files
- Update project references in api.csproj and email-api.csproj
- Update migration assembly references in api/Program.cs and email-api/Program.cs
- Update cv-search-job references to use email-data project and Email.Data namespace
- Update solution file to reference new email-data project path
- Remove hardcoded schema name from SmtpEmailDispatcher, use template service instead
This maintains consistency with other data project naming convention (no service-type suffix).
All tests passing, build succeeds.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Delete cv-search-models/Data (duplicate of cv-search-data/Data)
- Delete myai-models/Data (duplicate of myai-data/Data)
- DbContext and Entities belong only in -data projects, not -models
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Update MigrationConstants.SchemaName in email-api-data from 'emailApi' to 'email'
- All migrations automatically use the new schema name via MigrationConstants reference
- Aligns with naming convention: 'email', 'rag', 'cvMatcher', 'cvSearch', 'myAi'
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Move IRagRepository, EfRagRepository, and VectorSerializer from rag-api/Data to rag-data/Repositories
- Add rag-api-models ProjectReference to rag-data.csproj for model type availability
- Delete rag-api/Data folder (no longer needed; all data access is now in rag-data)
- This aligns RAG with email-api and other services: all data code in the data project
Pattern: rag-api (API logic) → rag-data (repository, EF entities, migrations)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Added inline comments throughout myai.css to:
- Clarify complex CSS selectors (input:not selectors, specificity explanations)
- Explain design patterns (.is-invalid error state pattern)
- Document focus and error states
- Describe layout decisions (sticky result panel, hamburger dropdown)
- Clarify responsive breakpoints and what changes at each
- Explain the relationship between CSS and JS (e.g., .is-open, .is-invalid)
Comments are strategic and concise—added to complex/non-obvious sections
without bloating the file. All CSS rules remain unchanged—purely additive
documentation.
Key sections now have better context:
- Form field selectors and state handling
- Hamburger menu responsive behavior
- Result panel sticky positioning strategy
- Responsive grid layout changes at 900px and 560px
- Cookie banner and loader overlay behavior
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Refactored legal.js from 135 → 124 lines (8% reduction) by:
- Removing local browserLang() and getLang() that are now in utils
- Simplifying to focus on page-specific injection logic
Kept legal page-specific functionality:
- Local LANG_KEY storage for page language preference
- injectTopbar() with language switcher buttons
- injectFooter() with language-aware copyright and legal links
- Event delegation for language link clicks
- DOMContentLoaded handler
Added clear JSDoc comments explaining the injection pattern and
how legal pages dynamically reuse common UI elements while supporting
language switching via event delegation.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Refactored cv-matcher.js from 257 → 213 lines (17% reduction) by:
- Removing duplicate helper functions now in utils/form-helpers.js
- Removing duplicate i18n logic now in utils/i18n.js
- Removing API loading code now in utils/api.js
Kept CV matcher-specific logic:
- CV file input change handler
- Async CV upload and match flow with two captcha tokens
- Match result rendering with score badge and lists
- escapeHtml() XSS prevention utility
- $(window).on('load') to load reCaptcha
All function calls updated to use window.MyAi.* utilities for consistency.
Added detailed JSDoc comments explaining the two-step async flow.
Updated cv-matcher/index.html to load all utilities before cv-matcher.js.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Refactored main.js from 544 → 266 lines (51% reduction) by:
- Removing duplicate functions now in utils/form-helpers.js
- Removing duplicate i18n logic now in utils/i18n.js
- Removing API loading code now in utils/api.js
- Removing cookie consent handlers now in modules/cookie-consent.js
Kept only page-specific form handlers:
- Contact form submission with reCaptcha
- Subscribe form submission with reCaptcha
- Language switcher initialization
- Footer year and version display
All calls now use window.MyAi.* utilities for consistency.
Updated index.html to load all utilities before main.js.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Create reusable utility modules to eliminate duplication across main.js,
cv-matcher.js, and legal.js:
- js/utils/form-helpers.js: showFieldError, clearFieldErrors, isValidEmail,
extractApiError — shared form validation and error handling
- js/utils/i18n.js: currentLang, t, applyLanguage, updateLegalLinks,
browserLang — shared translation and language switching
- js/utils/api.js: checkApiLive, getRecaptchaWebKey, getGoogleTagManagerId,
loadGoogleTagManager — shared API configuration loading
- js/modules/cookie-consent.js: getConsent, setConsent, initConsent,
setupConsentHandlers — cookie banner and consent management
All utilities exposed on window.MyAi namespace for use by existing pages.
Full JSDoc headers and inline comments for maintainability.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Each DbContext now explicitly configures its migration history table to use
the schema-qualified name pattern [schemaName].[_Migrations]:
- [cvMatcher].[_Migrations] for CvMatcherDbContext
- [emailApi].[_Migrations] for EmailApiDbContext
- [cvSearch].[_Migrations] for CvSearchDbContext
- [rag].[_Migrations] for RagDbContext
- [myAi].[_Migrations] for MyAiDbContext
This is done via OnConfiguring() with UseSqlServer().MigrationsHistoryTable(name, schema).
Removed incorrect rename migrations that were created due to misunderstanding
of the proper EF Core configuration approach.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Rename EmailApiDbContext MigrationTableName from '_EmailApiMigrations' to '_Migrations'
- Rename MyAiDbContext MigrationTableName from '_MyAiMigrations' to '_Migrations'
- Add migrations to rename tables in database: emailApi._EmailApiMigrations → emailApi._Migrations, myAi._MyAiMigrations → myAi._Migrations
- Aligns with naming convention used in other schemas (cvMatcher, cvSearch, rag)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Remove Seed() call from CreateEmailTemplates Up() method to prevent
duplicate key violation when applying SeedEmailTemplates migration.
The original migration was attempting to seed data during schema creation,
but data seeding is now handled by the separate SeedEmailTemplates migration
(20260528130652). Keeping both Seed() calls caused PRIMARY KEY violation on
(email.html-shell.start, *) when the second migration tried to insert
already-existing templates.
This maintains the migration order: schema creation first, then data seeding
in a separate, dedicated migration.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Create SeedEmailTemplates migration (20260528130652) with all email templates
- Add Microsoft.EntityFrameworkCore.Design to email-api.csproj for EF migrations
- Add EmailApiDbContext registration and migration support to email-api Program.cs
- Configure IEmailTemplateRepository and IEmailTemplateService in email-api
- All 14 email templates now seeded in emailApi schema (HTML shells, CV match, job search)
- Templates include proper placeholder support ({{score}}, {{count}}, {{jobLabel}}, etc.)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Both api and cv-search-job need to connect to email-api for sending emails.
Add EmailApi section to their appsettings.json with BaseUrl and InternalApiKey
placeholders. Environment variables from docker-compose will populate these at runtime.
Also add EmailApi credentials to docker-compose/.env:
- EmailApi__BaseUrl=http://email-api:8080
- EmailApi__InternalApiKey=<shared key>
- EmailApi__RequireApiKey=true
This ensures both services can authenticate and call the email-api service.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Updated general-dev-workflow skill to stress that for Docker projects,
BOTH build success AND successful container startup are required before
considering a change complete.
Key additions:
- Docker container startup verification is mandatory, not optional
- Containers must have status 'Up', not 'Restarting' or 'Exited'
- Check container logs for errors during startup
- Missing config files, invalid env vars, DB issues only show at runtime
- Startup failures block production deployments
- Updated Phase 4 checkpoint to include container startup validation
Lesson from email-api issue: missing appsettings.json caused restart loop
that had no visible logs during build. This must be caught in Phase 4
before code review and prevents production issues.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Merges refactor/web-cleanup-31 into main with all infrastructure fixes:
- Directory.Packages.props handling in all Dockerfiles
- Missing email-api-data dependencies in build configs
- Missing appsettings.json for email-api
- myai-smoke-test skill for automated end-to-end testing
- general-dev-workflow skill updates for build verification
All 7 containers building and running successfully.
Closes#33
The email-api service was missing its configuration file, which is required
for Serilog logging setup, database connection, and SMTP settings.
Created appsettings.json with:
- Serilog configuration for console, file, and email logging
- Database connection settings
- SMTP configuration for email sending
- Internal API key configuration
- File storage path configuration
This fixes the container crash loop caused by missing configuration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Updated Phase 4 (Test) to include mandatory build verification:
- dotnet build for .NET projects
- docker compose --build for Docker projects
- Catch missing dependencies and configuration issues early
- Prevent build failures during code review and CI/CD
Also added tip about verifying builds before opening PR, and updated
Phase 4 checkpoint to include successful build requirement.
Lesson learned from docker build issues: catching these early saves
reviewers and CI/CD time, and prevents 'works on my machine' problems.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The email-api service was missing from the CI/CD build pipeline. Added:
- EMAIL_API_IMAGE environment variable
- Build step for email-api Dockerfile
- Push step for email-api image to registry
This ensures email-api images are built and pushed alongside other services
during the staging build workflow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The api.csproj references both email-api-data and email-api-models, but the
Dockerfile was not copying them. This caused compilation warnings and potential
build failures.
Added COPY commands for both projects before restore and publish steps.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The cv-search-job.csproj references both email-api-data and email-api-models,
but the Dockerfile was not copying them into the build context. This caused
compilation errors about missing EmailApi namespace types.
Added COPY commands for both projects before restore and publish steps.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The email-api.csproj references email-api-data as a project dependency,
but the Dockerfile was not copying it into the build context. This caused
'Skipping project' warnings during restore/publish.
Added COPY commands for both .csproj (before restore) and source directory
(before publish) to include email-api-data in the Docker build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The explicitly added versions were conflicting with the centralized version
definitions in Directory.Packages.props. Removed all explicit versions from:
- web/web.csproj
- Jobs/cv-cleanup-job/cv-cleanup-job.csproj
- Jobs/cv-search-job/cv-search-job.csproj
NuGet will now resolve versions from Directory.Packages.props which has the
canonical version definitions for the entire solution.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Docker builds were failing because the centralized package version
management file (Directory.Packages.props) was not being copied into the
build context. This file is required for NuGet to resolve package versions
in projects that don't specify explicit versions.
Updated all Dockerfiles to copy Directory.Packages.props before running
dotnet restore:
- Apis/api/Dockerfile
- Apis/cv-matcher-api/Dockerfile
- Apis/rag-api/Dockerfile
- Jobs/cv-cleanup-job/Dockerfile
- Jobs/cv-search-job/Dockerfile
- web/Dockerfile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The PackageReference items were missing version specifications:
- Microsoft.VisualStudio.Azure.Containers.Tools.Targets (added 1.21.0)
- Yarp.ReverseProxy (added 2.2.0)
This fixes the NuGet error NU1015 that prevented Docker builds from
successfully restoring package dependencies.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The smoke test now correctly includes docker-compose.override.yml which
configures local image builds instead of pulling from remote registry.
This fixes the 'failed to resolve reference' error when building containers
locally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates a new skill that automates smoke testing of the myAi CV Matcher:
- Starts Docker Compose and waits for app health check
- Uploads CV.pdf and job description via Selenium WebDriver
- Verifies CV analysis results display (score, strengths, gaps)
- Confirms match email was sent by checking container logs
- Returns pass/fail summary with any failures detailed
Includes SKILL.md documentation and run_smoke_test.py automation script
with hardcoded test data (CV file path, job description). Can be extended
to test against different CVs/job descriptions via environment variables.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
legal.js now:
- Dynamically injects a common topbar (logo + language switcher) on all 6 pages
- Dynamically injects a language-aware footer (EN vs RO copyright text)
- Detects page language and builds appropriate language links
- Uses event delegation for language links (works on injected elements)
- Persists language preference to localStorage
All 6 legal HTML pages now:
- Removed the hardcoded topbar div (12 lines of identical HTML per file)
- Removed the hardcoded footer div (7 lines of HTML with language-specific content)
- Total savings: 114 lines of duplicated HTML across 6 pages
- Pages are 38% smaller (60 lines → 37 lines core content)
Closes#31
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract CV matcher form logic from main.js into dedicated cv-matcher.js:
- CV file upload with validation
- Job URL/description input
- reCaptcha token retrieval for upload and match actions
- Match result rendering with score badge, strengths, gaps, evidence
- Helper functions: extractApiError, escapeHtml, showFieldError, etc.
Depends on jQuery, i18n.js, and shared utilities from main.js.
Closes#31
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract i18n dictionary from main.js into dedicated i18n.js module.
Sets window.MyAi.i18n with 228 keys across English and Romanian.
Main.js will consume this via t(key) helper function.
Closes#31
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove all 7 inline style="display:none;" attributes from loaders/cookie elements
(now handled by CSS with .loader-overlay { display: none } and
.loader-overlay.loader-visible { display: flex })
- Remove orphan footer-legal class (unused in CSS) from footer-links divs
- Add <script src="/js/i18n.js"></script> before main.js on both pages
- Add <script src="/js/cv-matcher.js"></script> after main.js on cv-matcher page
jQuery 4.0 now detects CSS display:none correctly via getComputedStyle,
so class-based visibility (.loader-visible) works seamlessly.
Closes#31
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add section block comments throughout for navigability
- Merge two duplicate @media (max-width:900px) blocks into one
- Remove dead .ai-mark rule (replaced by <img> logo, never rendered)
- Move .cookie-overlay, .cookie-manage to display:none by default
(removes need for inline style="display:none;" on those elements)
- Add .loader-overlay.loader-visible{display:flex} so JS can use
class toggling instead of .css('display','flex') — works correctly
with jQuery 4.0's getComputedStyle-based visibility detection
- Consolidate brand-mark, console-line, nav-actions rules next to
their related blocks (were scattered at end of file)
Closes#31
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bootstrap CSS (232 KB) and Bootstrap JS (39 KB) were loaded for only
4 utility classes (.btn-sm, .btn-dark, .btn-warning, .shadow).
Replace with 4 targeted rules in myai.css and delete both files.
style.css (784 lines) was unreferenced by any HTML page — delete it.
Closes#31
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The vendor file was misnamed jquery-1.12.4.min.js but contained v3.6.1.
Replaced with the correctly-named jquery-4.0.0.min.js (78 KB, up from 89 KB).
Compatibility check: none of the removed jQuery 4.0 APIs are used in
main.js — no $.isArray, $.isFunction, $.trim, $.type, $.parseJSON, etc.
All ajax/deferred/DOM methods in use ($.ajax, $.when, .done/.fail/.always,
.on, .prop, .css, .addClass etc.) remain in the full 4.0 build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- UseJsonExceptionHandler now maps InvalidOperationException to 400 (was 500),
so upstream business-rule rejections reach the browser as actionable messages.
- CvMatcherController forwards Refit 4xx bodies from cv-matcher-api instead
of swallowing them in a generic 502.
- ErrorResponse.Score removed; CaptchaController puts the score in Detail.
- Frontend extractApiError helper reads the server Error/error/title field for
4xx responses and falls back to a generic i18n string for 5xx / missing body.
- All four failure handlers (CV upload, CV match, contact form, subscribe form)
updated to use extractApiError with the correct rate-limit i18n key.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Update CLAUDE.md: replace incorrect 'no XML doc on internal code' rule
with the correct convention (XML doc on all public methods and
non-trivial private/protected helpers)
- Restore /// <summary> on FileDownloadController private helpers
(HandleRangeRequest, StreamRangeAsync)
- Add full XML doc to all service contracts:
ICaptchaVerifier, IEmailSender, ICvMatcherService, IJobTextExtractor,
IJobTokenService, IDocumentClassifier, IRagService, ITextChunker,
ITextExtractor, IEmailTemplateService, ITemplateService
- Add /// <summary> and /// <inheritdoc /> to all concrete service classes
and their methods: RecaptchaVerifier, EmailApiEmailSender,
SmtpEmailDispatcher, CvMatcherService, JobTextExtractor, JobTokenService,
RagService, DocumentClassifier, TextChunker, TextExtractor,
HtmlJobSearcher, CvSearchEmailSender, CvSearchJobTask,
EmailTemplateService, DbTemplateService
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- EmailController: add class summary, full SwaggerResponse/ProducesResponseType
for 400 and 500, and Description on SwaggerOperation
- ContactController: fix terse "Failed." error message to
"Could not process subscription."
- FileDownloadController: remove redundant XML <response code> tags from
the public action doc block; convert private-method /// <summary> to //
(project convention: no XML doc on internal code)
- CvMatcherService: remove two dead commented-out blocks (old email send
and BuildEmailBody helper)
- JobTokenService: comment the phone/contact-line regex filter in
ExtractKeywords
- DocumentClassifier: comment the keyword-frequency scoring approach and
the confidence formula
- TextChunker: comment the sliding-window step (chunkSize - overlap)
- CvSearchJobTask: comment the GdprConsent = true rationale and the
BuildCvFileName sanitisation logic
- HtmlJobSearcher: comment GetLeftPart(UriPartial.Path) query-strip dedup
Closes#26
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ProjectReference to email-api-data; remove myai-data reference
- Program.cs: register EmailApiDbContext (no migrate), IEmailTemplateRepository
(scoped), IEmailTemplateService (singleton); remove MyAiDbContext +
ITemplateService registrations and their migration call
- CvSearchEmailSender: inject IEmailTemplateService; replace
_config["Contact:ToEmail"] with GetOperatorCopy("email.search-results.subject")
for operator copy logic; remove IConfiguration injection
- docker-compose: remove Contact__ToEmail from cv-search-job service block;
add Database__* env vars to email-api service (needed for EmailApiDbContext)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docker-compose: add email-api service (internal, no ports)
with Smtp__* + FileStorage__Path + Files volume mount
- api + cv-search-job: remove Smtp__* vars, add EmailApi__BaseUrl
and EmailApi__InternalApiKey; add depends_on: email-api
- .sln: move email-api-models to Models virtual folder
- CLAUDE.md: add email-api/email-api-models to layout, update
service dependency diagram and internal API key table
Closes#22
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Upgrades 8 email body templates from plain text to styled HTML.
Templates: email.match.body, email.match.job-search-footer,
email.search-results.body, email.search-results.empty (en + ro each).
All use inline CSS only (Gmail-compatible). Branded #2c5282 accent.
Closes#22
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CvSearchEmailSender now injects IEmailApiClient instead of IConfiguration+MailKit
- BuildBody updated to produce styled HTML job cards
- CvSearchJobTask passes only filename (not full path) to email sender
- IEmailApiClient registered in Program.cs with EmailApi:BaseUrl + InternalApiKey
- MailKit removed from cv-search-job.csproj
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- EmailApiEmailSender calls email-api via IEmailApiClient Refit client
- HTML bodies built inline for contact/subscribe/file-download emails
- match and job-search emails use DB templates (rendered in caller)
- SmtpSettings moved from api-models to email-api (kept in Models.Settings namespace)
- MailKit removed from api.csproj
- SmtpEmailSender deleted; IEmailSender interface unchanged
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New internal service that centralises SMTP email sending.
- email-api-models: SendEmailRequest DTO, IEmailApiClient Refit interface, EmailApiSettings
- email-api: SmtpEmailDispatcher (MailKit), EmailController (POST /api/email/send),
branded HTML shell wrapper, shared-Files-volume attachment support
- Protected by X-Internal-Api-Key via UseInternalApiKeyProtection()
- No exposed Docker port — internal network only (http://email-api:8080)
Closes#22
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stores the canonical general-dev-workflow skill definition alongside the
codebase so it travels with the project and can be evolved via normal PRs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Changed CORS allowed origin from localhost:5000 to localhost:5140
- Updated docker-compose.override.yml port mapping from 5000:8080 to 5140:8080
- Aligns local development port with staging (myai.easysoft.ro) and production (myai.ro) Caddy reverse proxy configuration on port 5140
The docker-compose refactor moved port 5000:8080 to the override file.
Caddy on staging routes myai.easysoft.ro → localhost:5000, so the port
must be present in the deployment compose. Restoring it as WEB_PORT env var
(default 5000) and adding missing watchtower label.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
api now registers MyAiDbContext for template loading and needs
Database__* connection string vars like the other DB-connected services.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New Apis/myai-models project: MyAiDbContext (schema myAi), TemplateEntity,
ITemplateService, DbTemplateService with 10-min in-memory cache
- Seeds EN+RO variants for all user-facing templates (match email, job search
results email, HTML status pages, AI system prompt)
- Match result email now sent in user's UI language (en/ro)
- Job search results email now respects session language
- Language propagates: MatchJobRequest -> token -> session -> email
- Add Language column to JobSearchTokens and JobSearchSessions (default 'en')
- All three Dockerfiles updated to include myai-models in build context
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The <p> wrapping the copyright line had default browser margins (1em top/bottom)
which offset it above the sibling flex items despite align-items: center.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The frontend sends the active language code (currentLang()) with every match
request. CvMatcherService injects a language instruction into the system prompt
so the LLM returns summary, strengths, gaps, recommendations, and evidence in
the correct language. The match result cache (CvMatchResults) now includes
Language as part of the lookup key so Romanian and English results are stored
and retrieved independently. Existing cached rows default to 'en'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Every public action now has <summary>, <param>, and <returns> XML docs
plus matching SwaggerOperation/SwaggerResponse attributes with typed response
descriptions. Class-level summaries added to CvController, JobSearchController,
and RagController. Explanatory inline comments removed from FileDownloadController
per project conventions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uses GetApplicationVersion(Assembly.GetExecutingAssembly()) — the same
timestamp-based version already logged at startup and baked into the
assembly via the csproj <Version> property. Removes the minimal-API
/version endpoint from web/Program.cs and reverts the web Dockerfile
APP_VERSION build-arg (no longer needed).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Setting APP_VERSION in docker-compose with a :-unknown fallback would override
the version baked into the image at build time. The CI already embeds it via
--build-arg APP_VERSION=1.0.<commit-count>, so compose should stay silent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exposes GET /version endpoint in the web container (reads APP_VERSION env var).
CI computes the version as 1.0.<git-commit-count> and passes it via --build-arg at build time.
Both index.html and cv-matcher/index.html show the version in the footer via a JS fetch.
docker-compose passes APP_VERSION through to the running container.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude Code stores session files under .claude/ — these are local tooling
artifacts and should not be tracked in the repository.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docker-compose.yml is now the single file for Portainer (staging and prod).
Uses registry images with ${IMAGE_TAG:-staging}, ${LOGS_PATH:-/opt/myai/logs},
and ${FILES_PATH:-/opt/myai/files} so the same file works for all environments.
- docker-compose.override.yml adds build context, ports, and env_file for local dev
and is auto-merged by "docker compose up" (no extra flags needed).
- .env.template documents IMAGE_TAG, LOGS_PATH, FILES_PATH alongside existing vars.
- docker-compose.dcproj updated so override file nests under docker-compose.yml in
Solution Explorer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dotnet restore failed in CI because cv-search-models.csproj was added as
a ProjectReference but not copied into the Docker build context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New cv-search-models shared library: EF entities + CvSearchDbContext for cvSearch schema (JobSearchTokens, JobSearchSessions, JobSearchResults tables)
- New cv-search-job worker service: polls DB for pending sessions, scrapes job boards via configurable HTML scraping, runs LLM scoring via cv-matcher-api, emails ranked results
- cv-matcher-api: JobTokenService creates one-time tokens; JobSearchController handles link clicks and creates sessions
- api: proxies job-search start endpoint, appends job search link to match result email
- CI workflow updated to build and push myai-cv-search-job:staging image
- CLAUDE.md documentation added for all affected services
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>