diff --git a/docs/deferred-inputs.md b/docs/deferred-inputs.md new file mode 100644 index 0000000..21dc4ff --- /dev/null +++ b/docs/deferred-inputs.md @@ -0,0 +1,20 @@ +# Deferred inputs + +Updated: 2026-07-31 + +## Stripe billing — Phase 7.5 + +Needed from the operator before implementation can finish: + +- Add `STRIPE_SECRET_KEY` and `STRIPE_PUBLISHABLE_KEY` to the production environment. +- Create the recurring Premium price and add `STRIPE_PRICE_PREMIUM`. +- Create the billing webhook and add `STRIPE_WEBHOOK_SECRET`. +- Do not store or paste secret values in the repository or chat. + +## Production key rotation — Phase 1.4 + +Confirm the DataProtection keys exposed in git history have been rotated on the production host. + +## Progress + +Phases 2 through 7.9 are complete except Stripe billing. Public CV sharing, premium themes, AI call and token limits, storage limits, NAV discovery, provider market dual-write, CORS hardening, the prospect workflow, and migration-history reconciliation are pushed to `main`. diff --git a/job-tracker-ui/src/ai-usage-card.test.tsx b/job-tracker-ui/src/ai-usage-card.test.tsx new file mode 100644 index 0000000..ec7a7e6 --- /dev/null +++ b/job-tracker-ui/src/ai-usage-card.test.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import AiUsageCard from "./components/AiUsageCard"; +import { api } from "./api"; + +jest.mock("./api", () => ({ api: { get: jest.fn() } })); + +test("shows monthly AI call and token limits", async () => { + (api.get as jest.Mock).mockResolvedValue({ + data: { + currentMonth: { calls: 4, estimatedTokens: 12000 }, + plan: "free", + monthlyCallLimit: 25, + monthlyTokenLimit: 100000, + }, + }); + + render(); + + expect(await screen.findByText("4 of 25 generations this month")).toBeInTheDocument(); + expect(screen.getByText("12,000 of 100,000 estimated tokens")).toBeInTheDocument(); + expect(screen.getByLabelText("Monthly AI generations used")).toHaveAttribute("aria-valuenow", "16"); +}); diff --git a/job-tracker-ui/src/components/AiUsageCard.tsx b/job-tracker-ui/src/components/AiUsageCard.tsx new file mode 100644 index 0000000..2d7de81 --- /dev/null +++ b/job-tracker-ui/src/components/AiUsageCard.tsx @@ -0,0 +1,47 @@ +import React, { useEffect, useState } from "react"; +import { Alert, Box, LinearProgress, Paper, Skeleton, Stack, Typography } from "@mui/material"; +import { api } from "../api"; + +type Usage = { + currentMonth: { calls: number; estimatedTokens: number }; + plan: string; + monthlyCallLimit: number; + monthlyTokenLimit: number; +}; + +export default function AiUsageCard() { + const [usage, setUsage] = useState(null); + const [failed, setFailed] = useState(false); + + useEffect(() => { + let active = true; + api.get("/ai/usage") + .then((response) => { if (active) setUsage(response.data); }) + .catch(() => { if (active) setFailed(true); }); + return () => { active = false; }; + }, []); + + if (failed) return AI usage is temporarily unavailable.; + if (!usage) return ; + + const callsPercent = Math.min(100, usage.currentMonth.calls / usage.monthlyCallLimit * 100); + const tokensPercent = Math.min(100, usage.currentMonth.estimatedTokens / usage.monthlyTokenLimit * 100); + + return ( + + + AI usage + {usage.plan} plan + + + {usage.currentMonth.calls.toLocaleString()} of {usage.monthlyCallLimit.toLocaleString()} generations this month + + + + {usage.currentMonth.estimatedTokens.toLocaleString()} of {usage.monthlyTokenLimit.toLocaleString()} estimated tokens + + + Limits reset at the start of each calendar month. + + ); +} diff --git a/job-tracker-ui/src/components/SettingsView.tsx b/job-tracker-ui/src/components/SettingsView.tsx index bd8a310..ecb6c5a 100644 --- a/job-tracker-ui/src/components/SettingsView.tsx +++ b/job-tracker-ui/src/components/SettingsView.tsx @@ -22,6 +22,7 @@ import ImportExportJobs from "./ImportExportJobs"; import RulesSettingsCard from "./RulesSettingsCard"; import BackupCard from "./BackupCard"; import QuickCaptureCard from "./QuickCaptureCard"; +import AiUsageCard from "./AiUsageCard"; import { ThemeModePref } from "../themePrefs"; import { useI18n } from "../i18n/I18nProvider"; @@ -197,6 +198,7 @@ export default function SettingsView({ +