diff --git a/docs/ARCHITECTURE_EVOLUTION.md b/docs/ARCHITECTURE_EVOLUTION.md new file mode 100644 index 0000000..686b051 --- /dev/null +++ b/docs/ARCHITECTURE_EVOLUTION.md @@ -0,0 +1,53 @@ +# Jobbjakt Architecture Evolution + +## Guiding constraints + +- Preserve the existing single-master-profile model. +- Keep job applications and their workflow at the centre of the domain. +- Make AI providers replaceable and optional. +- Add SaaS foundations without prematurely building a billing platform or team product. +- Keep feature slices small while reducing the current controller concentration. + +## Target boundaries + +```text +Web UI + -> Application APIs + -> Application workflow domain + -> Career profile and document domain + -> Discovery provider adapters + -> Connected-account adapters + -> AI provider boundary + -> Entitlements and usage policy +``` + +### Career profile and documents + +`ApplicationUser.ProfileCvStructureJson` remains the canonical master profile during the transition. A later migration may normalize high-change sections (projects and portfolio items) into user-owned tables, but generated CVs and application-specific drafts must remain immutable copies with a profile-version reference. + +### Discovery + +Create an `IJobDiscoveryProvider` contract with normalized query and result types. Providers own authentication, source-specific filters, terms-aware attribution, and mapping. The UI and import flow must not know provider payload shapes. Cache public results briefly, do not persist an external job until the user imports it. + +### AI + +Extract AI prompts and model transport from controllers behind task-focused interfaces: CV extraction, CV tailoring, draft generation, interview preparation, and summarization. Each request records provider, model, input profile version, job context, and user review state. Local and online providers implement the same boundary. + +### Entitlements + +Introduce account-level plan, entitlement, and usage-ledger records before payment integration. Product code checks named capabilities (for example `ai.tailor_cv`, `cv.premium_theme`, `storage.attachments`) rather than payment-provider state. This supports trials, manual access, subscriptions, and future organisation plans. + +## Incremental engineering work + +1. Add targeted feature services beside the existing controllers for every touched feature; do not attempt a big-bang controller rewrite. +2. Replace local-only preferences that affect behaviour with user-owned persisted settings. +3. Replace the in-memory CV queue with a database-backed work item before horizontal hosting. +4. Use PostgreSQL for hosted multi-tenant deployment; retain SQLite for self-hosted/single-user deployments. +5. Add provider health, job processing, AI usage, and entitlement audit telemetry before paid hosting. + +## Explicit non-goals for the first SaaS release + +- Teams/organisations and shared candidate data. +- Auto-apply automation or scraping-proxy infrastructure. +- A browser-extension marketplace release. +- A billing-provider integration before commercial policy is decided. diff --git a/docs/PRODUCT_EVOLUTION_PLAN.md b/docs/PRODUCT_EVOLUTION_PLAN.md new file mode 100644 index 0000000..0d35147 --- /dev/null +++ b/docs/PRODUCT_EVOLUTION_PLAN.md @@ -0,0 +1,81 @@ +# Jobbjakt Product Evolution Plan + +> Confirmed 2026-07-15. This document is the product decision record for the career-management evolution. + +## Product position + +Jobbjakt is a job-tracking and career-management product. The application workflow remains the core loop: + +```text +Discover opportunity -> Track application -> Prepare application -> Follow up -> Learn and progress +``` + +Career profile, CV generation, cover letters, portfolio, interview preparation, and discovery support this loop. They must not displace it or turn Jobbjakt into a CV-builder-first product. + +## Confirmed decisions + +| Decision | Direction | +| --- | --- | +| Primary discovery market | Nordics and Europe; global later | +| Discovery sources | Permitted provider APIs and existing URL capture; no scraping or browser extension in the first release | +| Career workspace | Dedicated top-level navigation item | +| Add-job flow | Wizard; create the application after job review, then keep preparation steps optional and resumable | +| Portfolio | Lightweight links and project items connected to the master career profile | +| AI | Retain local and online capability behind a provider boundary and explicit user controls | +| SaaS priority | Hosted subscriptions and feature entitlements before organisations/teams | +| Brand | Keep the Jobbjakt name | + +## Target information architecture + +| Area | Purpose | Initial destinations | +| --- | --- | --- | +| Dashboard | Daily direction | priorities, follow-ups, recent activity | +| Applications | Core tracker | list, board, companies, application workspace | +| Career Workspace | Reusable career source of truth | master profile, CV builder, portfolio, interview preparation | +| Discover Jobs | Find then act | provider search, saved searches later, import into tracker | +| Settings | Product preferences | general, follow-ups, notifications, connected accounts, data | +| Account | Identity and security | personal details, password, providers, sessions, 2FA | + +## Delivery roadmap + +### Phase 1 — Workflow clarity + +1. Create the Career Workspace and move CV-facing functionality out of Account Profile. +2. Move Gmail, Outlook, and IMAP management to Settings > Connected Accounts. +3. Reorganise Settings: remove normal-user export/import controls, hide admin-only status controls, and improve follow-up explanations. +4. Simplify application workspace navigation into task-oriented sections. + +### Phase 2 — Application preparation + +1. Replace the dense Add Job modal with a six-step wizard. +2. URL capture and extraction lead the flow; manual details remain an easy fallback. +3. Save the application after review; CV, cover letter, portfolio, and documents are explicitly optional. +4. Link job-specific CV copies and draft material to the application without mutating the master profile. + +### Phase 3 — Career workspace completion + +1. Strengthen the structured master career profile: experience, education, skills, projects, certifications, languages, achievements, and portfolio links. +2. Turn the existing template system into a guided three-step CV builder: theme, preview/customise, save defaults. +3. Keep five professional templates and persisted visual preferences; add drag-and-drop ordering after the underlying configuration is durable. + +### Phase 4 — Nordic/European discovery + +1. Introduce a provider-adapter contract and a normalized result model. +2. Implement approved public/provider APIs beginning with Nordic sources; add European sources only when their terms permit product use. +3. Provide search, filters, source attribution, map/distance enrichment where data permits, and one-click import to the wizard. +4. Add fuzzy role matching locally through a role/skill synonym catalog before any external AI ranking. + +### Phase 5 — Hosted SaaS foundations + +1. Add account-level entitlement and usage records independent of a billing provider. +2. Gate premium themes, AI allowances, storage, and discovery quotas through policy checks. +3. Integrate a billing provider only after pricing, tax, trial, cancellation, and support policy decisions are recorded. +4. Move restart-sensitive CV processing to durable storage before multi-instance hosting. + +## Success measures + +- A new user can create a tracked application from a URL in under two minutes. +- A user can find their master profile, create a CV, and manage email connections without visiting Account Profile. +- Optional preparation never blocks capture of an opportunity. +- Every AI-generated output visibly states its source profile/job context and requires user review before it is saved or sent. +- Discovery always preserves source attribution and sends users to the original job advert to apply. diff --git a/job-tracker-ui/src/App.tsx b/job-tracker-ui/src/App.tsx index 18407a2..81ea16c 100644 --- a/job-tracker-ui/src/App.tsx +++ b/job-tracker-ui/src/App.tsx @@ -16,6 +16,7 @@ import ShieldIcon from "@mui/icons-material/Shield"; import SearchIcon from "@mui/icons-material/Search"; import MailOutlineIcon from "@mui/icons-material/MailOutline"; import MemoryIcon from "@mui/icons-material/Memory"; +import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined"; import { Navigate, Route, Routes, useLocation, useNavigate, createBrowserRouter, RouterProvider } from "react-router-dom"; @@ -47,6 +48,8 @@ const SettingsView = lazy(() => import("./components/SettingsView")); const RemindersView = lazy(() => import("./components/RemindersView")); const QuickCommandDialog = lazy(() => import("./components/QuickCommandDialog")); const ProfilePage = lazy(() => import("./views/ProfilePage")); +const CareerWorkspacePage = lazy(() => import("./views/CareerWorkspacePage")); +const ConnectedAccountsPage = lazy(() => import("./views/ConnectedAccountsPage")); const AdminAuditPage = lazy(() => import("./views/AdminAuditPage")); const AdminUsersPage = lazy(() => import("./views/AdminUsersPage")); const AdminSystemPage = lazy(() => import("./views/AdminSystemPage")); @@ -78,6 +81,8 @@ function breadcrumbsFor(path: string, t: (k: any) => string): string[] { if (path.startsWith("/trash")) return [t("home"), t("trash")]; if (path.startsWith("/settings")) return [t("home"), t("settings")]; if (path.startsWith("/profile")) return [t("home"), t("account"), t("profile")]; + if (path.startsWith("/career")) return [t("home"), "Career Workspace"]; + if (path.startsWith("/settings/connected-accounts")) return [t("home"), t("settings"), "Connected accounts"]; if (path.startsWith("/admin/audit")) return [t("home"), t("admin"), t("auditLog")]; if (path.startsWith("/admin/users")) return [t("home"), t("admin"), t("users")]; if (path.startsWith("/admin/system")) return [t("home"), t("admin"), t("system")]; @@ -95,6 +100,8 @@ function titleFor(path: string, t: (k: any) => string): string { if (path.startsWith("/trash")) return t("trash"); if (path.startsWith("/settings")) return t("settings"); if (path.startsWith("/profile")) return t("profile"); + if (path.startsWith("/career")) return "Career Workspace"; + if (path.startsWith("/settings/connected-accounts")) return "Connected accounts"; if (path.startsWith("/admin/audit")) return t("auditLog"); if (path.startsWith("/admin/users")) return t("users"); if (path.startsWith("/admin/system")) return t("systemStatus"); @@ -234,6 +241,7 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo { to: "/companies", label: t("companies"), icon: , section: t("manage") }, { to: "/correspondence", label: "Correspondence", icon: , section: t("manage") }, { to: "/correspondence/review", label: "Gmail review", icon: , section: t("manage") }, + { to: "/career", label: "Career Workspace", icon: , section: t("manage") }, { to: "/trash", label: t("trash"), icon: , section: t("manage") }, ]; @@ -314,11 +322,13 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo } /> } /> } /> + } /> } /> } /> } /> } /> } /> + } /> } /> diff --git a/job-tracker-ui/src/components/SettingsView.tsx b/job-tracker-ui/src/components/SettingsView.tsx index 9215806..bd8a310 100644 --- a/job-tracker-ui/src/components/SettingsView.tsx +++ b/job-tracker-ui/src/components/SettingsView.tsx @@ -198,6 +198,9 @@ export default function SettingsView({ + + + @@ -226,7 +229,6 @@ export default function SettingsView({ - diff --git a/job-tracker-ui/src/views/CareerWorkspacePage.tsx b/job-tracker-ui/src/views/CareerWorkspacePage.tsx new file mode 100644 index 0000000..bd7a16e --- /dev/null +++ b/job-tracker-ui/src/views/CareerWorkspacePage.tsx @@ -0,0 +1,22 @@ +import React from "react"; + +import { Alert, Box, Paper, Typography } from "@mui/material"; + +import ProfilePage from "./ProfilePage"; + +export default function CareerWorkspacePage() { + return ( + + + Career Workspace + + Maintain the master career profile that powers your CVs, tailored application material, and future portfolio outputs. + + + + Your master profile is the source of truth. Job-specific CV drafts remain separate and never overwrite it. + + + + ); +} diff --git a/job-tracker-ui/src/views/ConnectedAccountsPage.tsx b/job-tracker-ui/src/views/ConnectedAccountsPage.tsx new file mode 100644 index 0000000..d5fed1c --- /dev/null +++ b/job-tracker-ui/src/views/ConnectedAccountsPage.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import { Box, Button, Paper, Typography } from "@mui/material"; +import { useNavigate } from "react-router-dom"; + +import EmailProviderConnections from "../components/EmailProviderConnections"; + +export default function ConnectedAccountsPage() { + const navigate = useNavigate(); + + return ( + + + Connected accounts + + Connect the inboxes Jobbjakt may use to identify recruiter correspondence. Connecting an inbox never sends a message on your behalf. + + + + + + ); +} diff --git a/job-tracker-ui/src/views/ProfilePage.tsx b/job-tracker-ui/src/views/ProfilePage.tsx index a651ebc..3d204ce 100644 --- a/job-tracker-ui/src/views/ProfilePage.tsx +++ b/job-tracker-ui/src/views/ProfilePage.tsx @@ -13,7 +13,6 @@ import MicrosoftAuthCard from "../components/MicrosoftAuthCard"; import AuthStatusCard from "../components/AuthStatusCard"; import TwoFactorSettingsCard from "../components/TwoFactorSettingsCard"; import SessionsSettingsCard from "../components/SessionsSettingsCard"; -import EmailProviderConnections from "../components/EmailProviderConnections"; import CropImageDialog from "../components/CropImageDialog"; import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; @@ -227,7 +226,7 @@ function FieldReviewNote({ metadata }: { metadata?: StructuredCvFieldMetadata }) ); } -export default function ProfilePage() { +export default function ProfilePage({ careerOnly = false }: { careerOnly?: boolean }) { const { toast } = useToast(); const { t } = useI18n(); const cvInputRef = useRef(null); @@ -553,7 +552,7 @@ export default function ProfilePage() { - {t("profileTitle")} + {careerOnly ? "Master career profile" : t("profileTitle")} {me?.userName || me?.displayName || fullName || me?.email || "-"} {headline || t("profileHeadlinePlaceholder")} @@ -566,37 +565,38 @@ export default function ProfilePage() { - - - - - - + {!careerOnly ? <> + + + + : null} - + {!careerOnly ? {t("profileAccountSection")} {!isLocal ? ( {t("profileReadOnlyInfo")} ) : null} - + : null} - setDisplayName(e.target.value)} disabled={!isLocal} fullWidth /> - setUserName(e.target.value)} disabled={!isLocal} fullWidth /> - setFirstName(e.target.value)} disabled={!isLocal} fullWidth /> - setLastName(e.target.value)} disabled={!isLocal} fullWidth /> - setEmail(e.target.value)} disabled={!isLocal} fullWidth /> - + setDisplayName(e.target.value)} disabled={!isLocal} fullWidth /> + setUserName(e.target.value)} disabled={!isLocal} fullWidth /> + setFirstName(e.target.value)} disabled={!isLocal} fullWidth /> + setLastName(e.target.value)} disabled={!isLocal} fullWidth /> + setEmail(e.target.value)} disabled={!isLocal} fullWidth /> + setHeadline(e.target.value)} helperText={t("profileHeadlineHelp")} fullWidth - /> + /> + : null} - + {t("profileMasterCv")} @@ -1318,16 +1318,17 @@ export default function ProfilePage() { - + {!careerOnly ? {t("profileChangePassword")} {!isLocal ? {t("profilePasswordLocalOnly")} : null} - + : null} - setCurrentPassword(e.target.value)} disabled={!isLocal} fullWidth /> - setNewPassword(e.target.value)} disabled={!isLocal} fullWidth /> + {!careerOnly ? <> + setCurrentPassword(e.target.value)} disabled={!isLocal} fullWidth /> + setNewPassword(e.target.value)} disabled={!isLocal} fullWidth /> - + - + + : null} - {isLocal ? : null} - {isLocal ? : null} + {!careerOnly && isLocal ? : null} + {!careerOnly && isLocal ? : null} ); }