diff --git a/.gitignore b/.gitignore index 8a6192a26..d79f97935 100644 --- a/.gitignore +++ b/.gitignore @@ -1,50 +1,4 @@ -node_modules/ -dist/ -!extension/dist/ -*.tsbuildinfo -hosted-contract.json -plugin-command-manifest.json -.webcmd/ -.superpowers/ -.worktrees/ -.agents/* -!.agents/plugins/ -.agents/plugins/* -!.agents/plugins/marketplace.json -.mcp.json -*.log -.DS_Store +backend/node_modules/ +backend/uploads/ -# Local-only research, examples, and agent planning artifacts -autoresearch/ -cases/ -designs/ -docs/superpowers/ -instagram-test/ -llms.txt -sitemaps/ - -# Extensions & Secrets -*.pem -*.crx -*.zip -.envrc -.windsurf -.claude -.cortex - -# Database files -*.db -autoresearch-results.tsv - -# webcmd benchmarks (dataset comparison harness) -benchmarks/results/ -benchmarks/.venv/ -benchmarks/node_modules/ -benchmarks/**/__pycache__/ -benchmarks/.pytest_cache/ -benchmarks/pytest-cache-files-* -benchmarks/datasets/*.json -!benchmarks/datasets/Stealth_Webcmd.json -benchmarks/.playwright-cli/ -benchmarks/.playwright/ +backend/.env diff --git a/README.md b/README.md index e7f2c266c..d032c062d 100644 --- a/README.md +++ b/README.md @@ -1,160 +1,194 @@ -Webcmd โ€” stop paying agents to rediscover the web +# Application Rescue Agent ๐Ÿ›Ÿ +A hackathon MVP that helps students find relevant opportunities (internships, +scholarships, fellowships, competitions, grants, university programs) and +**rescues** their applications before deadlines, confusing forms, or missing +documents cost them the opportunity. -

- - NPM version - - - Documentation - - - License - - - Join the community on Discord - - - Follow AgentR on X - -

+> **Core flow:** Profile โ†’ Discover โ†’ Match โ†’ Apply with Rescue Agent โ†’ +> Analyze Form โ†’ Auto-fill โ†’ Detect Missing Items โ†’ Fix โ†’ Review โ†’ +> User Approval โ†’ Submit -# Webcmd +The Rescue Agent **never auto-submits**. It fills what it safely can, flags +what's missing, and waits for the student to click **Approve & Submit**. -**Self-learning browser infra for AI agents.** +--- -Webcmd learns the navigational context of websites as agents use them, then -turns that knowledge into local memory for faster, cheaper, more reliable -browser automation. The goal is simple: stop making agents rediscover the same -sites on every run and cut browser-agent token spend by up to 90%. +## Tech stack -Webcmd pairs live browser control with a self-learning memory layer: +| Layer | Tech | +|-----------|--------------------------------------------------| +| Frontend | React + Vite + Tailwind CSS + React Router | +| Backend | Node.js + Express | +| Database | MongoDB + Mongoose | +| Rescue engine | `webcmdService.js` โ€” DEMO mode (scripted) or REAL mode (Webcmd CLI) | -| Layer | Scenario | What Webcmd Helps With | -| --- | --- | --- | -| 0. Live browser control | The site is unfamiliar. | Use `webcmd browser` to inspect, click, type, extract, capture network calls, and complete the task in a real browser. | -| 1. Sitemap memory | The site is familiar, but the action space is not fully known. | Capture an agent-facing sitemap of observed pages, states, actions, workflows, APIs, pitfalls, and fallback paths. | +--- -## Demo +## Folder structure -https://github.com/user-attachments/assets/bdb65307-9e2a-4d58-9175-45d59528ae37 - -## Quick Start +``` +application-rescue-agent/ +โ”œโ”€โ”€ backend/ +โ”‚ โ”œโ”€โ”€ config/db.js # MongoDB connection +โ”‚ โ”œโ”€โ”€ models/ # User, Profile, Opportunity, Document, Application +โ”‚ โ”œโ”€โ”€ controllers/ # Route handlers +โ”‚ โ”œโ”€โ”€ routes/ # Express routers +โ”‚ โ”œโ”€โ”€ services/ +โ”‚ โ”‚ โ”œโ”€โ”€ matchEngine.js # Match % scoring +โ”‚ โ”‚ โ””โ”€โ”€ webcmdService.js # Webcmd integration (DEMO/REAL) +โ”‚ โ”œโ”€โ”€ seed/seed.js # Demo data seeder +โ”‚ โ”œโ”€โ”€ server.js +โ”‚ โ””โ”€โ”€ .env.example +โ””โ”€โ”€ frontend/ + โ”œโ”€โ”€ src/ + โ”‚ โ”œโ”€โ”€ api/api.js # Axios client for all backend endpoints + โ”‚ โ”œโ”€โ”€ components/ # Sidebar, cards, badges, progress bar, etc. + โ”‚ โ”œโ”€โ”€ pages/ # Dashboard, Profile, Discover, OpportunityDetails, + โ”‚ โ”‚ # Documents, Applications, ApplicationRescue + โ”‚ โ”œโ”€โ”€ App.jsx + โ”‚ โ””โ”€โ”€ index.css + โ”œโ”€โ”€ tailwind.config.js + โ””โ”€โ”€ vite.config.js +``` -### Agent prompt +--- -```text -Fetch and follow https://raw.githubusercontent.com/agentrhq/webcmd/main/start.md to set up Webcmd end to end. -``` +## Setup -### Manual +### Prerequisites +- Node.js 18+ +- A running MongoDB instance (local `mongod`, Docker, or a free MongoDB Atlas cluster) -Webcmd requires Node.js 20.6+. +### 1. Backend ```bash -npm install -g @agentrhq/webcmd -webcmd skills add +cd backend +npm install +cp .env.example .env +# edit .env if your MongoDB URI or port differ from the defaults +npm run seed # populates demo user, profile, 15 opportunities, and document vault +npm run dev # starts the API on http://localhost:5000 (nodemon) +# or: npm start ``` -When prompted, choose Claude, Codex, another supported harness, or a custom -skills path. That installs exactly one skill, `webcmd-browser`. +Verify it's alive: -Load or tag `webcmd-browser` only for live browser work, then describe the -outcome you want. Installation and setup commands do not require that skill. - -```text -Use webcmd to research the latest discussions about browser automation across Hacker News and Reddit, then return a concise comparison with source links. +```bash +curl http://localhost:5000/api/health ``` -## What You Can Ask - -- โ€œUse webcmd to research agentic browser automation on PubMed and return the title, authors, publication date, abstract, and URL for each result.โ€ -- โ€œUse webcmd to find active AI infrastructure companies in the YC company directory and return the company, batch, description, location, profile URL, and source links. Keep it read-only.โ€ -- โ€œUse webcmd to look up parts on Grainger by part number and return price, stock, minimum order quantity, lead time, and product URL.โ€ -- โ€œUse webcmd with my logged-in `work` profile to summarize unread LinkedIn messages from the last seven days and return the sender, subject or opening text, received time, and conversation URL.โ€ -- โ€œUse webcmd to check Grainger part prices and SAP Ariba purchase-order status, then return a combined summary.โ€ +### 2. Frontend -## See It in Action - -```text -Use webcmd with my logged-in `social` profile to collect my recent X bookmarks and return the author, text, and URL. +```bash +cd frontend +npm install +npm run dev # starts on http://localhost:5173 ``` -The agent uses the logged-in profile to complete the task in a real browser. -Along the way, Webcmd quietly retains useful navigation context so later agents -can avoid repeating the same exploration. - -## Where Webcmd Works - -Webcmd can work through authenticated browser sessions across research, social, -AI, shopping, and booking products. - -| Group | Supported surfaces | Representative outcomes | -| --- | --- | --- | -| research and communities | Hacker News, Reddit, PubMed | Compare current discussions, find primary research, and return concise summaries with source links. | -| social and professional | X/Twitter, LinkedIn, TikTok | Collect bookmarks, monitor public posts, or research people and creators with a named profile when needed. | -| AI tools | ChatGPT, Claude, Gemini, NotebookLM | Retrieve conversations, research outputs, notebooks, and generated materials from the tools you already use. | -| shopping and bookings | Amazon, Blinkit, Zepto, BigBasket, District, Practo | Compare products, availability, prices, appointments, events, and delivery options. | - -This list is illustrative. Webcmd can operate other websites through the same -live browser workflow. - -## How Self-Learning Works +The Vite dev server proxies `/api/*` requests to `http://localhost:5000` +(see `frontend/vite.config.js`), so just open **http://localhost:5173**. -How Webcmd learns: load memory, use the live web, keep useful learnings, and help the next agent - -Learning stays quiet and selective: the live browser is always truth, Webcmd -never explores just to learn, and a memory failure never blocks the task. First -access may use a Webcmd Cloud seed; subsequent learning stays local. - -For local, multi-step browser exploration, agents can send one sandboxed -Playwright-style program to an explicit browser session: +### 3. (Optional) Build for production ```bash -webcmd --profile work session create "Work Project" -f json -# id: work-project-k7 -webcmd --profile work --session work-project-k7 browser tabs -webcmd --profile work --session work-project-k7 browser run --file explore.js -printf 'return await page.title();' \ - | webcmd --profile work --session work-project-k7 browser run --stdin -webcmd --profile work session close work-project-k7 +cd frontend && npm run build # outputs to frontend/dist +cd backend && npm start ``` - -Profiles are cookie jars; Sessions are independent browser windows within a -profile, so Session IDs are immutable, Profile-scoped, and safe to reuse for -that Session's lifetime. Parallel agents should create separate Sessions. -Raw browser commands require an explicit readable Session ID. - -## Benchmarks - -On [BU Bench V1](https://github.com/browser-use/benchmark#bu-bench-v1), a -100-task browser automation benchmark, Webcmd recorded the highest accuracy and -lowest estimated controller cost per completed task, and fewest agent turns per -completed task in this comparison. - -![BU Bench V1 comparison: webcmd leads accuracy at 67%, cost per completed task at $0.255, and agent turns per completed task at 9.8](./benchmarks/charts/bu-bench-readme.svg) - -All tools used the same Pi controller, controller model, Codex `gpt-5.4` judge, -and CloakBrowser engine. This is a stronger judge than the original BU Bench -setup, whose [current runner uses Gemini 2.5 Flash](https://github.com/browser-use/benchmark/blob/main/run_eval.py#L37-L38). -Accuracy is passed tasks out of 100. Cost and agent turns are averaged over -completed tasks; cost excludes judge usage. See the -[benchmark report](./benchmarks/README.md) for category results, methodology, -architectural analysis, and reproduction steps. - -## Learn More - -Webcmd Cloud can run supported commands and browser sessions on hosted infrastructure. It is in active development and is not yet stable. - -- [Prompt Cookbook](https://webcmd.dev/docs/agent-prompts) -- [How Webcmd Works](https://webcmd.dev/docs/concepts) -- [Local or Cloud](https://webcmd.dev/docs/local-or-cloud) -- [Command Surface](https://webcmd.dev/docs/cli-reference) - -## Contributing - -See [CONTRIBUTING.md](./CONTRIBUTING.md). - -## License - -Released under the terms in [`LICENSE`](./LICENSE). +Serve `frontend/dist` with any static host, or add `express.static` to +`server.js` if you want the backend to serve the built frontend directly. + +--- + +## Environment variables (`backend/.env`) + +| Variable | Default | Notes | +|------------------|-------------------------------------------------------|-------| +| `MONGO_URI` | `mongodb://127.0.0.1:27017/application_rescue_agent` | Local or Atlas connection string | +| `PORT` | `5000` | Backend port | +| `WEBCMD_MODE` | `DEMO` | `DEMO` or `REAL` | +| `WEBCMD_CLI_PATH`| `webcmd` | Only used when `WEBCMD_MODE=REAL` | + +--- + +## Backend API reference + +| Method | Route | Description | +|--------|-------------------------------------|--------------| +| GET | `/api/health` | Health check + current Webcmd mode | +| GET | `/api/profile` | Get the student profile | +| POST | `/api/profile` | Create/update the student profile | +| GET | `/api/opportunities` | All opportunities with Match % | +| GET | `/api/opportunities/recommended` | Top opportunities sorted by Match % (`?limit=`) | +| GET | `/api/opportunities/:id` | Single opportunity detail | +| GET | `/api/documents` | Document vault status | +| POST | `/api/documents/upload` | `{ type, fileName }` โ†’ marks a document uploaded | +| GET | `/api/applications` | All applications for the student | +| POST | `/api/applications` | `{ opportunityId }` โ†’ start/resume a Rescue application | +| GET | `/api/applications/:id` | Single application detail | +| POST | `/api/applications/:id/analyze` | **Runs the Rescue Agent** โ€” opens the form, auto-fills, detects missing items | +| POST | `/api/applications/:id/submit` | Explicit, user-triggered submission (blocked until "Ready for Review") | + +--- + +## How matching works + +`services/matchEngine.js` computes an explainable **0โ€“100 Match %** per +opportunity, no ML required for the MVP: + +- Skills overlap โ€” 40 pts +- Interests overlap โ€” 20 pts +- CGPA eligibility โ€” 20 pts +- Preferred opportunity type โ€” 15 pts +- Has experience listed โ€” 5 pts + +## How the Rescue Agent works (`webcmdService.js`) + +This is the **only** file that knows about Webcmd, so the rest of the app +never changes when you swap modes: + +- **DEMO mode** (default): a deterministic, scripted analysis against a + fixed demo application form (Name, Email, University, Degree, Branch, + Skills, Resume, Transcript, SOP). It reads the student's Profile and + Document Vault, fills every field it safely can, and reports the rest as + missing. This is 100% reliable for a live hackathon demo โ€” no network + calls, no flakiness. +- **REAL mode**: shells out to an installed **Webcmd CLI** binary + (`webcmd analyze --url --profile --json`) to actually + open the target form in a browser and read/fill real fields. If the CLI + isn't installed, times out, or returns bad output, the service logs the + failure and **falls back to DEMO analysis** so the flow never breaks + mid-demo. Adjust the CLI invocation in `runRealAnalysis()` to match your + actual Webcmd CLI's interface. + +Toggle modes via `WEBCMD_MODE` in `backend/.env` โ€” no code changes needed. + +--- + +## Demo script (what to show the judges) + +1. **Profile** โ€” the seeded demo student (Ketna Sharma, B.Tech CS, VIT + Bhopal, CGPA 8.4) is already filled in. *"It knows me."* +2. **Discover** โ€” 15 real-feeling opportunities across 6 categories, each + with a computed Match %. Sort by best match. *"It finds the right + opportunity."* +3. Open an opportunity โ†’ **Apply with Rescue Agent**. +4. On the **Application Rescue** page, click **Run Rescue Agent** โ€” watch + the Webcmd activity log stream in, fields auto-fill from the profile and + document vault. *"It opens the application and fills it."* +5. **SOP** is missing on purpose โ†’ status shows **Missing Items**. + *"It finds what's missing."* +6. Click **Upload now** next to SOP (or go to the **Documents** vault) โ†’ + Rescue Agent re-runs automatically โ†’ status flips to **Ready for + Review**, completion hits 100%. *"It rescues it."* +7. Review the filled fields, then click **Approve & Submit** (with a + confirmation prompt). Status becomes **Submitted**. *"It gets my + approval, then submits it."* + +--- + +## Explicit non-goals (kept simple on purpose) + +No complex authentication, OCR, vector database, real-time scraping, +notifications, or microservices โ€” this is a focused 4-hour hackathon MVP. +The app operates as a single seeded demo user throughout (no login screen). diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 000000000..120240597 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,11 @@ +# MongoDB connection string (local or Atlas) +MONGO_URI=mongodb://127.0.0.1:27017/application_rescue_agent + +# Server port +PORT=5000 + +# Webcmd mode: DEMO (scripted, always reliable) or REAL (uses installed Webcmd CLI) +WEBCMD_MODE=DEMO + +# Only used if WEBCMD_MODE=REAL - path to the Webcmd CLI binary +WEBCMD_CLI_PATH=webcmd diff --git a/backend/config/db.js b/backend/config/db.js new file mode 100644 index 000000000..463423c4c --- /dev/null +++ b/backend/config/db.js @@ -0,0 +1,18 @@ +const mongoose = require("mongoose"); + +async function connectDB() { + const uri = process.env.MONGO_URI || "mongodb://127.0.0.1:27017/application_rescue_agent"; + + try { + await mongoose.connect(uri); + console.log(`[db] Connected to MongoDB -> ${uri}`); + } catch (err) { + console.error("[db] MongoDB connection failed:", err.message); + console.error( + "[db] Make sure MongoDB is running locally, or set MONGO_URI to an Atlas connection string in backend/.env" + ); + process.exit(1); + } +} + +module.exports = connectDB; diff --git a/backend/controllers/applicationController.js b/backend/controllers/applicationController.js new file mode 100644 index 000000000..3bf908f64 --- /dev/null +++ b/backend/controllers/applicationController.js @@ -0,0 +1,128 @@ +const Application = require("../models/Application"); +const Opportunity = require("../models/Opportunity"); +const Profile = require("../models/Profile"); +const Document = require("../models/Document"); +const { getDemoUser } = require("./profileController"); +const { ensureVault } = require("./documentController"); +const { computeMatchPercent } = require("../services/matchEngine"); +const webcmdService = require("../services/webcmdService"); + +exports.getApplications = async (req, res) => { + try { + const user = await getDemoUser(); + const apps = await Application.find({ user: user._id }) + .populate("opportunity") + .sort({ updatedAt: -1 }); + res.json(apps); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +exports.getApplicationById = async (req, res) => { + try { + const app = await Application.findById(req.params.id).populate("opportunity"); + if (!app) return res.status(404).json({ message: "Application not found" }); + res.json(app); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +// Start (or resume) a Rescue application for a given opportunity. +exports.startApplication = async (req, res) => { + try { + const { opportunityId } = req.body; + const user = await getDemoUser(); + + const opportunity = await Opportunity.findById(opportunityId); + if (!opportunity) return res.status(404).json({ message: "Opportunity not found" }); + + let app = await Application.findOne({ user: user._id, opportunity: opportunityId }); + + if (!app) { + const profile = await Profile.findOne({ user: user._id }); + const matchPercent = profile ? computeMatchPercent(profile, opportunity) : 0; + + app = await Application.create({ + user: user._id, + opportunity: opportunityId, + status: "Not Started", + matchPercent, + formFields: [], + missingFields: [], + completionPercent: 0, + webcmdLog: [], + }); + } + + const populated = await app.populate("opportunity"); + res.status(201).json(populated); + } catch (err) { + res.status(400).json({ message: err.message }); + } +}; + +// The core "Rescue" step: Webcmd opens the form, auto-fills what it can +// from the profile + document vault, and flags what's missing. +exports.analyzeApplication = async (req, res) => { + try { + const app = await Application.findById(req.params.id).populate("opportunity"); + if (!app) return res.status(404).json({ message: "Application not found" }); + + const user = await getDemoUser(); + const profile = await Profile.findOne({ user: user._id }); + const documents = await ensureVault(user._id); + + app.status = "Analyzing"; + await app.save(); + + const result = await webcmdService.analyzeApplication({ + profile, + documents, + opportunity: app.opportunity, + }); + + app.formFields = result.formFields; + app.missingFields = result.missingFields; + app.completionPercent = result.completionPercent; + app.status = result.status; // "Missing Items" or "Ready for Review" + app.webcmdLog.push(...result.log); + + await app.save(); + + const populated = await app.populate("opportunity"); + res.json(populated); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +// Explicit, user-triggered submission. Never happens automatically. +exports.submitApplication = async (req, res) => { + try { + const app = await Application.findById(req.params.id).populate("opportunity"); + if (!app) return res.status(404).json({ message: "Application not found" }); + + if (app.status !== "Ready for Review") { + return res.status(400).json({ + message: + "This application still has missing items. Resolve them and re-run analysis before submitting.", + }); + } + + app.status = "Submitted"; + app.submittedAt = new Date(); + app.webcmdLog.push({ + step: "submit", + message: "User approved and submitted the application.", + timestamp: new Date(), + }); + + await app.save(); + const populated = await app.populate("opportunity"); + res.json(populated); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; diff --git a/backend/controllers/documentController.js b/backend/controllers/documentController.js new file mode 100644 index 000000000..4b29f0b01 --- /dev/null +++ b/backend/controllers/documentController.js @@ -0,0 +1,130 @@ +const Document = require("../models/Document"); +const { getDemoUser } = require("./profileController"); + +const DOC_TYPES = ["Resume", "Transcript", "Certificates", "SOP"]; + +async function ensureVault(userId) { + const existing = await Document.find({ user: userId }); + const existingTypes = new Set(existing.map((d) => d.type)); + + const toCreate = DOC_TYPES + .filter((type) => !existingTypes.has(type)) + .map((type) => ({ + user: userId, + type, + status: "missing", + fileName: "", + filePath: "", + })); + + if (toCreate.length) { + await Document.insertMany(toCreate); + } + + return Document.find({ user: userId }).sort({ type: 1 }); +} + +exports.getDocuments = async (req, res) => { + try { + const user = await getDemoUser(); + const docs = await ensureVault(user._id); + + res.json(docs); + } catch (err) { + console.error("[documents] get error:", err); + res.status(500).json({ + message: err.message, + }); + } +}; +exports.removeDocument = async (req, res) => { + try { + const { type } = req.params; + + const user = await getDemoUser(); + + const doc = await Document.findOneAndUpdate( + { + user: user._id, + type, + }, + { + $set: { + status: "missing", + fileName: "", + filePath: "", + uploadedAt: null, + }, + }, + { new: true } + ); + + if (!doc) { + return res.status(404).json({ + message: "Document not found.", + }); + } + + res.json(doc); + } catch (err) { + console.error("[documents] remove error:", err); + + res.status(500).json({ + message: err.message, + }); + } +}; +exports.uploadDocument = async (req, res) => { + try { + const { type } = req.body; + + if (!DOC_TYPES.includes(type)) { + return res.status(400).json({ + message: `type must be one of ${DOC_TYPES.join(", ")}`, + }); + } + + if (!req.file) { + return res.status(400).json({ + message: "Please select a file to upload.", + }); + } + + const user = await getDemoUser(); + + const filePath = `/uploads/documents/${req.file.filename}`; + + const doc = await Document.findOneAndUpdate( + { + user: user._id, + type, + }, + { + $set: { + status: "uploaded", + fileName: req.file.originalname, + filePath, + uploadedAt: new Date(), + }, + }, + { + new: true, + upsert: true, + } + ); + + console.log( + `[documents] ${type} uploaded: ${req.file.originalname}` + ); + + res.json(doc); + } catch (err) { + console.error("[documents] upload error:", err); + + res.status(400).json({ + message: err.message, + }); + } +}; + +exports.ensureVault = ensureVault; \ No newline at end of file diff --git a/backend/controllers/opportunityController.js b/backend/controllers/opportunityController.js new file mode 100644 index 000000000..07e108dd8 --- /dev/null +++ b/backend/controllers/opportunityController.js @@ -0,0 +1,153 @@ +const { discoverOpportunities } = require("../services/opportunityDiscoveryService"); +const Opportunity = require("../models/Opportunity"); +const Profile = require("../models/Profile"); +const { computeMatchPercent } = require("../services/matchEngine"); +const { getDemoUser } = require("./profileController"); + +exports.getAllOpportunities = async (req, res) => { + try { + const now = new Date(); + +const opportunities = await Opportunity.find({ + $or: [ + { deadline: null }, + { deadline: { $gte: now } } + ] +}).sort({ deadline: 1 }); + + let profile = null; + try { + const user = await getDemoUser(); + profile = await Profile.findOne({ user: user._id }); + } catch (e) { + profile = null; + } + + const withMatch = opportunities.map((opp) => ({ + ...opp.toObject(), + matchPercent: profile ? computeMatchPercent(profile, opp) : null, + })); + + res.json(withMatch); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +exports.getOpportunityById = async (req, res) => { + try { + const opp = await Opportunity.findById(req.params.id); + if (!opp) return res.status(404).json({ message: "Opportunity not found" }); + + let profile = null; + const user = await getDemoUser(); + profile = await Profile.findOne({ user: user._id }); + + res.json({ + ...opp.toObject(), + matchPercent: profile ? computeMatchPercent(profile, opp) : null, + }); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +exports.discoverLiveOpportunities = async (req, res) => { + try { + console.log("[discovery] Starting live opportunity discovery..."); + + const user = await getDemoUser(); + + const profile = await Profile.findOne({ + user: user._id + }); + + if (!profile) { + return res.status(400).json({ + message: "Student profile not found" + }); + } + + const discovered = await discoverOpportunities(profile); + + const saved = []; + + for (const opportunity of discovered) { + const existing = await Opportunity.findOne({ + applyUrl: opportunity.applyUrl + }); + + if (existing) { + saved.push(existing); + continue; + } + + const created = await Opportunity.create(opportunity); + saved.push(created); + } + + const ranked = saved + .map((opp) => ({ + ...opp.toObject(), + matchPercent: computeMatchPercent( + profile, + opp + ) + })) + .sort( + (a, b) => + b.matchPercent - a.matchPercent + ); + + console.log( + `[discovery] ${ranked.length} opportunities ready` + ); + + res.json({ + success: true, + count: ranked.length, + opportunities: ranked + }); + + } catch (err) { + console.error( + "[discovery] Error:", + err + ); + + res.status(500).json({ + success: false, + message: err.message + }); + } +}; + +// Top recommended opportunities sorted by match %, highest first. +exports.getRecommended = async (req, res) => { + try { + const limit = parseInt(req.query.limit, 10) || 6; + const user = await getDemoUser(); + const profile = await Profile.findOne({ user: user._id }); + + const now = new Date(); + +const opportunities = await Opportunity.find({ + $or: [ + { deadline: null }, + { deadline: { $gte: now } } + ] +}); + + const ranked = opportunities + .map((opp) => ({ + ...opp.toObject(), + matchPercent: profile ? computeMatchPercent(profile, opp) : 0, + })) + .sort((a, b) => b.matchPercent - a.matchPercent) + .slice(0, limit); + + res.json(ranked); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; diff --git a/backend/controllers/profileController.js b/backend/controllers/profileController.js new file mode 100644 index 000000000..c34daaf90 --- /dev/null +++ b/backend/controllers/profileController.js @@ -0,0 +1,41 @@ +const Profile = require("../models/Profile"); +const User = require("../models/User"); + +// This MVP has a single demo user, so "the current user" = the first User doc. +async function getDemoUser() { + let user = await User.findOne(); + if (!user) { + user = await User.create({ name: "Demo Student", email: "demo.student@vitbhopal.ac.in" }); + } + return user; +} + +exports.getProfile = async (req, res) => { + try { + const user = await getDemoUser(); + const profile = await Profile.findOne({ user: user._id }); + if (!profile) return res.status(404).json({ message: "Profile not found. Create one first." }); + res.json(profile); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +exports.upsertProfile = async (req, res) => { + try { + const user = await getDemoUser(); + const payload = { ...req.body, user: user._id }; + + const profile = await Profile.findOneAndUpdate( + { user: user._id }, + { $set: payload }, + { new: true, upsert: true, runValidators: true } + ); + + res.json(profile); + } catch (err) { + res.status(400).json({ message: err.message }); + } +}; + +exports.getDemoUser = getDemoUser; diff --git a/backend/models/Application.js b/backend/models/Application.js new file mode 100644 index 000000000..628425343 --- /dev/null +++ b/backend/models/Application.js @@ -0,0 +1,48 @@ +const mongoose = require("mongoose"); + +const formFieldSchema = new mongoose.Schema( + { + key: { type: String, required: true }, // e.g. "name", "email", "sop" + label: { type: String, required: true }, // e.g. "Full Name" + value: { type: String, default: "" }, + filled: { type: Boolean, default: false }, + isDocument: { type: Boolean, default: false }, // true for Resume/Transcript/SOP style fields + }, + { _id: false } +); + +const webcmdLogEntrySchema = new mongoose.Schema( + { + step: { type: String, required: true }, + message: { type: String, required: true }, + timestamp: { type: Date, default: Date.now }, + }, + { _id: false } +); + +const applicationSchema = new mongoose.Schema( + { + user: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, + opportunity: { type: mongoose.Schema.Types.ObjectId, ref: "Opportunity", required: true }, + status: { + type: String, + enum: [ + "Not Started", + "Analyzing", + "Missing Items", + "Ready for Review", + "Submitted", + ], + default: "Not Started", + }, + matchPercent: { type: Number, default: 0 }, + formFields: [formFieldSchema], + missingFields: [{ type: String }], // keys of fields still empty + completionPercent: { type: Number, default: 0 }, + webcmdLog: [webcmdLogEntrySchema], + submittedAt: { type: Date }, + }, + { timestamps: true } +); + +module.exports = mongoose.model("Application", applicationSchema); diff --git a/backend/models/Document.js b/backend/models/Document.js new file mode 100644 index 000000000..b88b8f1e5 --- /dev/null +++ b/backend/models/Document.js @@ -0,0 +1,42 @@ +const mongoose = require("mongoose"); + +const documentSchema = new mongoose.Schema( + { + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + + type: { + type: String, + enum: ["Resume", "Transcript", "Certificates", "SOP"], + required: true, + }, + + fileName: { + type: String, + default: "", + }, + + filePath: { + type: String, + default: "", + }, + + status: { + type: String, + enum: ["uploaded", "missing"], + default: "missing", + }, + + uploadedAt: { + type: Date, + }, + }, + { timestamps: true } +); + +documentSchema.index({ user: 1, type: 1 }, { unique: true }); + +module.exports = mongoose.model("Document", documentSchema); \ No newline at end of file diff --git a/backend/models/Opportunity.js b/backend/models/Opportunity.js new file mode 100644 index 000000000..650a894c8 --- /dev/null +++ b/backend/models/Opportunity.js @@ -0,0 +1,108 @@ +const mongoose = require("mongoose"); + +const opportunitySchema = new mongoose.Schema( + { + title: { + type: String, + required: true, + }, + + organization: { + type: String, + required: true, + }, + + type: { + type: String, + enum: [ + "Internship", + "Scholarship", + "Fellowship", + "Competition", + "Grant", + "University Program", + ], + required: true, + }, + + description: { + type: String, + default: "", + }, + + // Optional because some live opportunities may not have + // a clearly visible deadline. + deadline: { + type: Date, + default: null, + }, + + eligibility: { + type: String, + default: "", + }, + + minCgpa: { + type: Number, + default: 0, + }, + + requiredSkills: [ + { + type: String, + }, + ], + + relatedInterests: [ + { + type: String, + }, + ], + + location: { + type: String, + default: "Remote", + }, + + stipendOrAmount: { + type: String, + default: "", + }, + + applyUrl: { + type: String, + default: "#", + }, + + requiredDocuments: [ + { + type: String, + }, + ], + + // Tells frontend whether this came from seed data + // or from real web discovery. + sourceType: { + type: String, + enum: ["demo", "live"], + default: "demo", + }, + + // Original webpage where the opportunity was discovered. + sourceUrl: { + type: String, + default: "", + }, + + // When Webcmd discovered this opportunity. + discoveredAt: { + type: Date, + default: null, + }, + }, + { + timestamps: true, + } +); + +module.exports = mongoose.model("Opportunity", opportunitySchema); \ No newline at end of file diff --git a/backend/models/Profile.js b/backend/models/Profile.js new file mode 100644 index 000000000..fa310ef2a --- /dev/null +++ b/backend/models/Profile.js @@ -0,0 +1,21 @@ +const mongoose = require("mongoose"); + +const profileSchema = new mongoose.Schema( + { + user: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true, unique: true }, + name: { type: String, required: true }, + email: { type: String, required: true }, + university: { type: String, default: "" }, + degree: { type: String, default: "" }, // e.g. B.Tech, B.Sc + branch: { type: String, default: "" }, // e.g. Computer Science + year: { type: Number, default: 1 }, // current year of study 1-5 + cgpa: { type: Number, default: 0 }, + skills: [{ type: String }], + interests: [{ type: String }], + experience: { type: String, default: "" }, // free text summary + preferredTypes: [{ type: String }], // Internship, Scholarship, Fellowship, Competition, Grant, University Program + }, + { timestamps: true } +); + +module.exports = mongoose.model("Profile", profileSchema); diff --git a/backend/models/User.js b/backend/models/User.js new file mode 100644 index 000000000..c797c304d --- /dev/null +++ b/backend/models/User.js @@ -0,0 +1,13 @@ +const mongoose = require("mongoose"); + +// Simple demo user - no auth/password hashing since this is a hackathon MVP. +// The seed script creates one demo user and the frontend always operates as this user. +const userSchema = new mongoose.Schema( + { + name: { type: String, required: true }, + email: { type: String, required: true, unique: true }, + }, + { timestamps: true } +); + +module.exports = mongoose.model("User", userSchema); diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 000000000..389fdd0d1 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1597 @@ +{ + "name": "application-rescue-agent-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "application-rescue-agent-backend", + "version": "1.0.0", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "mongoose": "^8.5.0", + "multer": "^2.3.0" + }, + "devDependencies": { + "nodemon": "^3.1.14" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.5.4.tgz", + "integrity": "sha512-05UC0jQsjKAOuXQ0H9Ud9vUTJpZIg+n/FinpR30tI5I8pY2inTfPOZ5OF/cg3Ce/N9MoD1xhRCeOsJtuTbFYlw==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.8", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz", + "integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.16.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.24.4", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.24.4.tgz", + "integrity": "sha512-DoV8hYy5ssoh/j99rhYzeS2BLLcLJZ+EGQYfe/Ln+yMorBy0wgKdlwJBj59zJ1N0MnEKQypbeqUYFB0FVThmYg==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.3.0.tgz", + "integrity": "sha512-cjNbm3sttszgZeGfJR124D+jFEfkXCVAsoPBmFn9X7UxmDSFHWqE2CoEj0vrmSpuAFnqWR1Szcm9QTsiHr60Xw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 000000000..eaf3e3f6f --- /dev/null +++ b/backend/package.json @@ -0,0 +1,22 @@ +{ + "name": "application-rescue-agent-backend", + "version": "1.0.0", + "description": "Backend API for Application Rescue Agent - helps students discover and rescue opportunity applications", + "main": "server.js", + "type": "commonjs", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js", + "seed": "node seed/seed.js" + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "mongoose": "^8.5.0", + "multer": "^2.3.0" + }, + "devDependencies": { + "nodemon": "^3.1.14" + } +} diff --git a/backend/routes/applications.js b/backend/routes/applications.js new file mode 100644 index 000000000..b8d76cbb7 --- /dev/null +++ b/backend/routes/applications.js @@ -0,0 +1,11 @@ +const express = require("express"); +const router = express.Router(); +const applicationController = require("../controllers/applicationController"); + +router.get("/", applicationController.getApplications); +router.post("/", applicationController.startApplication); +router.get("/:id", applicationController.getApplicationById); +router.post("/:id/analyze", applicationController.analyzeApplication); +router.post("/:id/submit", applicationController.submitApplication); + +module.exports = router; diff --git a/backend/routes/documents.js b/backend/routes/documents.js new file mode 100644 index 000000000..05b423eba --- /dev/null +++ b/backend/routes/documents.js @@ -0,0 +1,81 @@ +const express = require("express"); +const multer = require("multer"); +const path = require("path"); +const fs = require("fs"); + +const router = express.Router(); +const documentController = require("../controllers/documentController"); + +// Create upload directory +const uploadDir = path.join( + __dirname, + "..", + "uploads", + "documents" +); + +fs.mkdirSync(uploadDir, { recursive: true }); + +// Configure file storage +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, uploadDir); + }, + + filename: (req, file, cb) => { + const ext = path.extname(file.originalname); + + const safeType = String(req.body.type || "document") + .replace(/[^a-zA-Z0-9_-]/g, "_"); + + const uniqueName = + `${safeType}_${Date.now()}${ext}`; + + cb(null, uniqueName); + }, +}); + +// Allowed file types +const fileFilter = (req, file, cb) => { + const allowed = [ + ".pdf", + ".doc", + ".docx", + ]; + + const ext = path.extname(file.originalname).toLowerCase(); + + if (allowed.includes(ext)) { + cb(null, true); + } else { + cb( + new Error("Only PDF, DOC, and DOCX files are allowed."), + false + ); + } +}; + +const upload = multer({ + storage, + fileFilter, + limits: { + fileSize: 10 * 1024 * 1024, + }, +}); + + +router.get( + "/", + documentController.getDocuments +); + +router.post( + "/upload", + upload.single("file"), + documentController.uploadDocument +); +router.delete( + "/:type", + documentController.removeDocument +); +module.exports = router; \ No newline at end of file diff --git a/backend/routes/opportunities.js b/backend/routes/opportunities.js new file mode 100644 index 000000000..b3b455c68 --- /dev/null +++ b/backend/routes/opportunities.js @@ -0,0 +1,19 @@ +const express = require("express"); + +const router = express.Router(); + +const opportunityController = require("../controllers/opportunityController"); + +// Discover live opportunities from the web +router.post("/discover", opportunityController.discoverLiveOpportunities); + +// Get recommended opportunities +router.get("/recommended", opportunityController.getRecommended); + +// Get all opportunities +router.get("/", opportunityController.getAllOpportunities); + +// Get opportunity by ID +router.get("/:id", opportunityController.getOpportunityById); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/profile.js b/backend/routes/profile.js new file mode 100644 index 000000000..3243b1319 --- /dev/null +++ b/backend/routes/profile.js @@ -0,0 +1,9 @@ +const express = require("express"); +const router = express.Router(); +const profileController = require("../controllers/profileController"); + +router.get("/", profileController.getProfile); +router.post("/", profileController.upsertProfile); +router.put("/", profileController.upsertProfile); + +module.exports = router; diff --git a/backend/seed/seed.js b/backend/seed/seed.js new file mode 100644 index 000000000..ecdf70c85 --- /dev/null +++ b/backend/seed/seed.js @@ -0,0 +1,300 @@ +require("dotenv").config(); +const mongoose = require("mongoose"); +const connectDB = require("../config/db"); + +const User = require("../models/User"); +const Profile = require("../models/Profile"); +const Opportunity = require("../models/Opportunity"); +const Document = require("../models/Document"); +const Application = require("../models/Application"); + +function daysFromNow(days) { + const d = new Date(); + d.setDate(d.getDate() + days); + return d; +} + +const opportunities = [ + { + title: "Software Engineering Internship", + organization: "Nimbus Cloud Systems", + type: "Internship", + description: + "3-month backend internship working on distributed systems in Node.js and Go. Mentorship provided.", + deadline: daysFromNow(9), + eligibility: "B.Tech/B.E. students in 2nd year or above, CS/IT/related branches", + minCgpa: 7.0, + requiredSkills: ["JavaScript", "Node.js", "React", "MongoDB"], + relatedInterests: ["Web Development", "Backend Systems"], + location: "Remote", + stipendOrAmount: "โ‚น25,000/month", + applyUrl: "https://example.com/apply/nimbus-swe-intern", + requiredDocuments: ["Resume", "Transcript", "SOP"], + }, + { + title: "AI/ML Research Internship", + organization: "Vertex AI Labs", + type: "Internship", + description: "Work on applied NLP and computer vision projects with a research mentor.", + deadline: daysFromNow(14), + eligibility: "3rd/4th year students with ML coursework or projects", + minCgpa: 7.5, + requiredSkills: ["Python", "Machine Learning", "TensorFlow", "PyTorch"], + relatedInterests: ["AI/ML", "Research"], + location: "Hybrid - Bengaluru", + stipendOrAmount: "โ‚น30,000/month", + applyUrl: "https://example.com/apply/vertex-ai-intern", + requiredDocuments: ["Resume", "Transcript", "SOP"], + }, + { + title: "National Merit Scholarship for Engineering Students", + organization: "Ministry of Education Foundation", + type: "Scholarship", + description: "Merit-based scholarship covering tuition fees for high-performing engineering students.", + deadline: daysFromNow(21), + eligibility: "All B.Tech students with CGPA above 8.0", + minCgpa: 8.0, + requiredSkills: [], + relatedInterests: [], + location: "National (India)", + stipendOrAmount: "โ‚น1,00,000/year", + applyUrl: "https://example.com/apply/merit-scholarship", + requiredDocuments: ["Transcript", "SOP"], + }, + { + title: "Women in Computer Science Scholarship", + organization: "TechForward Foundation", + type: "Scholarship", + description: "Supporting women pursuing degrees in computer science and related fields.", + deadline: daysFromNow(30), + eligibility: "Female students enrolled in CS/IT programs", + minCgpa: 6.5, + requiredSkills: [], + relatedInterests: ["Computer Science"], + location: "National (India)", + stipendOrAmount: "โ‚น50,000", + applyUrl: "https://example.com/apply/wics-scholarship", + requiredDocuments: ["Transcript", "SOP", "Certificates"], + }, + { + title: "Global Innovation Fellowship", + organization: "Fenwick Institute", + type: "Fellowship", + description: "12-month fellowship supporting student-led projects with social or technical impact.", + deadline: daysFromNow(18), + eligibility: "Undergraduate students with a project idea and a working prototype", + minCgpa: 7.0, + requiredSkills: ["Project Management", "Product Thinking"], + relatedInterests: ["Entrepreneurship", "Social Impact"], + location: "Remote", + stipendOrAmount: "$2,000", + applyUrl: "https://example.com/apply/global-innovation-fellowship", + requiredDocuments: ["Resume", "SOP"], + }, + { + title: "Research Fellowship in Data Science", + organization: "Bhopal Institute of Advanced Studies", + type: "Fellowship", + description: "Assist faculty on a funded data science research project for one semester.", + deadline: daysFromNow(12), + eligibility: "Students with coursework in statistics or data science", + minCgpa: 7.5, + requiredSkills: ["Python", "Data Analysis", "SQL"], + relatedInterests: ["Data Science", "Research"], + location: "On-campus", + stipendOrAmount: "โ‚น10,000/month", + applyUrl: "https://example.com/apply/bias-research-fellowship", + requiredDocuments: ["Resume", "Transcript", "SOP"], + }, + { + title: "National Coding Championship 2026", + organization: "CodeArena", + type: "Competition", + description: "Individual competitive programming contest with cash prizes and internship offers.", + deadline: daysFromNow(6), + eligibility: "Open to all undergraduate students", + minCgpa: 0, + requiredSkills: ["Data Structures", "Algorithms", "C++"], + relatedInterests: ["Competitive Programming"], + location: "Online", + stipendOrAmount: "โ‚น2,00,000 prize pool", + applyUrl: "https://example.com/apply/national-coding-championship", + requiredDocuments: ["Resume"], + }, + { + title: "Smart India Hackathon", + organization: "Government of India (AICTE)", + type: "Competition", + description: "36-hour national hackathon solving real-world problem statements from ministries and industry.", + deadline: daysFromNow(25), + eligibility: "Teams of undergraduate students from AICTE-approved institutes", + minCgpa: 0, + requiredSkills: ["Full Stack Development", "Problem Solving"], + relatedInterests: ["Web Development", "Social Impact"], + location: "Multiple centers - India", + stipendOrAmount: "โ‚น1,00,000 per winning team", + applyUrl: "https://example.com/apply/sih", + requiredDocuments: ["Resume"], + }, + { + title: "UI/UX Design Challenge", + organization: "PixelCraft Studio", + type: "Competition", + description: "Design challenge for a real client brief judged by industry designers.", + deadline: daysFromNow(10), + eligibility: "Open to students with a design portfolio", + minCgpa: 0, + requiredSkills: ["UI/UX Design", "Figma"], + relatedInterests: ["Design", "Product"], + location: "Online", + stipendOrAmount: "โ‚น25,000", + applyUrl: "https://example.com/apply/pixelcraft-challenge", + requiredDocuments: ["Resume", "Certificates"], + }, + { + title: "Student Innovation Micro-Grant", + organization: "Bhopal Startup Council", + type: "Grant", + description: "Seed funding for early-stage student projects and prototypes.", + deadline: daysFromNow(16), + eligibility: "Student teams with a working prototype or MVP", + minCgpa: 0, + requiredSkills: ["Entrepreneurship", "Product Development"], + relatedInterests: ["Entrepreneurship", "Startups"], + location: "Madhya Pradesh, India", + stipendOrAmount: "โ‚น75,000", + applyUrl: "https://example.com/apply/bsc-micro-grant", + requiredDocuments: ["Resume", "SOP"], + }, + { + title: "Open Source Contribution Grant", + organization: "OpenSource Collective", + type: "Grant", + description: "Grant for students actively maintaining or contributing to open-source projects.", + deadline: daysFromNow(20), + eligibility: "Students with public GitHub contributions", + minCgpa: 0, + requiredSkills: ["Git", "Open Source", "JavaScript"], + relatedInterests: ["Open Source", "Web Development"], + location: "Remote", + stipendOrAmount: "$500", + applyUrl: "https://example.com/apply/osc-grant", + requiredDocuments: ["Resume", "SOP"], + }, + { + title: "Summer Research Program", + organization: "VIT Bhopal University", + type: "University Program", + description: "8-week on-campus research immersion program across CS, ECE and Mechanical departments.", + deadline: daysFromNow(11), + eligibility: "VIT Bhopal students, 2nd year and above", + minCgpa: 6.5, + requiredSkills: [], + relatedInterests: ["Research"], + location: "On-campus - VIT Bhopal", + stipendOrAmount: "Certificate + Stipend", + applyUrl: "https://example.com/apply/vit-summer-research", + requiredDocuments: ["Transcript"], + }, + { + title: "Semester Exchange Program - Singapore", + organization: "VIT Bhopal International Office", + type: "University Program", + description: "One semester student exchange with a partner university in Singapore.", + deadline: daysFromNow(28), + eligibility: "3rd year students with CGPA above 7.5 and no active backlogs", + minCgpa: 7.5, + requiredSkills: [], + relatedInterests: ["International Exposure"], + location: "Singapore", + stipendOrAmount: "Partial fee waiver", + applyUrl: "https://example.com/apply/vit-exchange-singapore", + requiredDocuments: ["Transcript", "SOP", "Certificates"], + }, + { + title: "Cybersecurity Bootcamp Fellowship", + organization: "SecureNet Foundation", + type: "Fellowship", + description: "6-week intensive cybersecurity bootcamp with certification and job referrals.", + deadline: daysFromNow(8), + eligibility: "Students interested in security, networking, or systems", + minCgpa: 6.0, + requiredSkills: ["Networking", "Linux", "Security"], + relatedInterests: ["Cybersecurity"], + location: "Remote", + stipendOrAmount: "Free + Certification", + applyUrl: "https://example.com/apply/securenet-fellowship", + requiredDocuments: ["Resume", "SOP"], + }, + { + title: "Cloud Computing Internship", + organization: "SkyForge Technologies", + type: "Internship", + description: "Internship focused on AWS infrastructure, CI/CD pipelines, and DevOps practices.", + deadline: daysFromNow(15), + eligibility: "3rd/4th year CS/IT students familiar with cloud basics", + minCgpa: 7.0, + requiredSkills: ["AWS", "Docker", "CI/CD"], + relatedInterests: ["Cloud Computing", "DevOps"], + location: "Remote", + stipendOrAmount: "โ‚น20,000/month", + applyUrl: "https://example.com/apply/skyforge-cloud-intern", + requiredDocuments: ["Resume", "Transcript", "SOP"], + }, +]; + +async function seed() { + await connectDB(); + + console.log("[seed] Clearing existing demo data..."); + await Promise.all([ + User.deleteMany({}), + Profile.deleteMany({}), + Opportunity.deleteMany({}), + Document.deleteMany({}), + Application.deleteMany({}), + ]); + + console.log("[seed] Creating demo user..."); + const user = await User.create({ + name: "Ketna Sharma", + email: "ketna.sharma@vitbhopal.ac.in", + }); + + console.log("[seed] Creating demo profile..."); + await Profile.create({ + user: user._id, + name: "Ketna Sharma", + email: "ketna.sharma@vitbhopal.ac.in", + university: "VIT Bhopal University", + degree: "B.Tech", + branch: "Computer Science", + year: 3, + cgpa: 8.4, + skills: ["JavaScript", "React", "Node.js", "Python", "Machine Learning", "MongoDB", "Git"], + interests: ["Web Development", "AI/ML", "Research", "Open Source"], + experience: + "Built two full-stack projects (CRM tool and campus canteen app), completed an ML coursework project on image classification.", + preferredTypes: ["Internship", "Fellowship", "Competition"], + }); + + console.log("[seed] Setting up document vault (SOP intentionally missing)..."); + await Document.insertMany([ + { user: user._id, type: "Resume", status: "uploaded", fileName: "Ketna_Sharma_Resume.pdf", uploadedAt: new Date() }, + { user: user._id, type: "Transcript", status: "uploaded", fileName: "Ketna_Sharma_Transcript.pdf", uploadedAt: new Date() }, + { user: user._id, type: "Certificates", status: "uploaded", fileName: "Ketna_Sharma_Certificates.pdf", uploadedAt: new Date() }, + { user: user._id, type: "SOP", status: "missing", fileName: "" }, + ]); + + console.log(`[seed] Inserting ${opportunities.length} demo opportunities...`); + await Opportunity.insertMany(opportunities); + + console.log("[seed] Done! Demo data is ready."); + await mongoose.connection.close(); + process.exit(0); +} + +seed().catch((err) => { + console.error("[seed] Failed:", err); + process.exit(1); +}); diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 000000000..4724177c7 --- /dev/null +++ b/backend/server.js @@ -0,0 +1,53 @@ +require("dotenv").config(); +const express = require("express"); +const cors = require("cors"); +const connectDB = require("./config/db"); +const path = require("path"); + +const profileRoutes = require("./routes/profile"); +const opportunityRoutes = require("./routes/opportunities"); +const documentRoutes = require("./routes/documents"); +const applicationRoutes = require("./routes/applications"); + +const app = express(); + +app.use(cors()); +app.use(express.json({ limit: "2mb" })); + +app.use( + "/uploads", + express.static(path.join(__dirname, "uploads")) +); + +app.get("/api/health", (req, res) => { + res.json({ + status: "ok", + service: "application-rescue-agent-backend", + webcmdMode: (process.env.WEBCMD_MODE || "DEMO").toUpperCase(), + time: new Date().toISOString(), + }); +}); + +app.use("/api/profile", profileRoutes); +app.use("/api/opportunities", opportunityRoutes); +app.use("/api/documents", documentRoutes); +app.use("/api/applications", applicationRoutes); + +// 404 fallback +app.use("/api", (req, res) => { + res.status(404).json({ message: `No route for ${req.method} ${req.originalUrl}` }); +}); + +// Basic error handler +app.use((err, req, res, next) => { + console.error(err); + res.status(500).json({ message: "Internal server error", detail: err.message }); +}); + +const PORT = process.env.PORT || 5000; + +connectDB().then(() => { + app.listen(PORT, () => { + console.log(`[server] Application Rescue Agent API running on http://localhost:${PORT}`); + }); +}); diff --git a/backend/services/matchEngine.js b/backend/services/matchEngine.js new file mode 100644 index 000000000..20716d53f --- /dev/null +++ b/backend/services/matchEngine.js @@ -0,0 +1,58 @@ +/** + * matchEngine.js + * Computes a 0-100 Match % between a student Profile and an Opportunity. + * Weighted, explainable scoring - no ML needed for the MVP. + * + * Skills overlap -> 40 pts + * Interests overlap -> 20 pts + * CGPA eligibility -> 20 pts + * Preferred type match -> 15 pts + * Has experience listed -> 5 pts + */ + +function normalize(list) { + return (list || []).map((s) => String(s).trim().toLowerCase()).filter(Boolean); +} + +function overlapScore(profileList, oppList, weight) { + const opp = normalize(oppList); + if (opp.length === 0) return weight; // nothing specifically required -> free points + const profileSet = new Set(normalize(profileList)); + const matched = opp.filter((item) => profileSet.has(item)); + return Math.round((matched.length / opp.length) * weight); +} + +function computeMatchPercent(profile, opportunity) { + if (!profile) return 0; + + let score = 0; + + score += overlapScore(profile.skills, opportunity.requiredSkills, 40); + score += overlapScore(profile.interests, opportunity.relatedInterests, 20); + + // CGPA eligibility + const minCgpa = opportunity.minCgpa || 0; + if (minCgpa === 0) { + score += 20; + } else if ((profile.cgpa || 0) >= minCgpa) { + score += 20; + } else { + const ratio = Math.max(0, (profile.cgpa || 0) / minCgpa); + score += Math.round(ratio * 20); + } + + // Preferred opportunity type + const preferred = normalize(profile.preferredTypes); + if (preferred.length === 0 || preferred.includes(opportunity.type.toLowerCase())) { + score += 15; + } + + // Experience listed at all + if (profile.experience && profile.experience.trim().length > 0) { + score += 5; + } + + return Math.max(0, Math.min(100, score)); +} + +module.exports = { computeMatchPercent }; diff --git a/backend/services/opportunityDiscoveryService.js b/backend/services/opportunityDiscoveryService.js new file mode 100644 index 000000000..ec0f0756b --- /dev/null +++ b/backend/services/opportunityDiscoveryService.js @@ -0,0 +1,1037 @@ +const { spawn } = require("child_process"); + +const WEBCMD_CLI_PATH = process.env.WEBCMD_CLI_PATH || "webcmd"; +const WEBCMD_TIMEOUT = Number(process.env.WEBCMD_TIMEOUT || 60000); + +function runCommand(args, input = "") { + return new Promise((resolve, reject) => { + const child = spawn(WEBCMD_CLI_PATH, args, { + shell: true, + windowsHide: true, + }); + + let stdout = ""; + let stderr = ""; + + child.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + child.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + const timer = setTimeout(() => { + child.kill(); + reject(new Error("Webcmd command timed out")); + }, WEBCMD_TIMEOUT); + + child.on("close", (code) => { + clearTimeout(timer); + + if (code !== 0) { + reject( + new Error(`Webcmd failed (${code}): ${stderr || stdout}`) + ); + return; + } + + resolve(stdout); + }); + + if (input) { + child.stdin.write(input); + } + + child.stdin.end(); + }); +} + +function parseWebcmdOutput(raw) { + const text = String(raw || "").trim(); + + try { + return JSON.parse(text); + } catch (_) {} + + const startObject = text.indexOf("{"); + const endObject = text.lastIndexOf("}"); + + if (startObject !== -1 && endObject > startObject) { + try { + return JSON.parse( + text.slice(startObject, endObject + 1) + ); + } catch (_) {} + } + + return null; +} + +async function createSession() { + const output = await runCommand([ + "session", + "create", + "application-rescue", + "-f", + "json", + ]); + + const parsed = parseWebcmdOutput(output); + + const sessionId = + parsed?.id || + parsed?.session?.id || + parsed?.result?.id; + + if (!sessionId) { + throw new Error( + `Could not get Webcmd session ID: ${output}` + ); + } + + console.log(`[discovery] Created Webcmd session: ${sessionId}`); + + return sessionId; +} + +async function closeSession(sessionId) { + try { + await runCommand([ + "session", + "close", + sessionId, + ]); + + console.log(`[discovery] Closed session: ${sessionId}`); + } catch (_) { + // Ignore close errors + } +} + +async function runBrowser(sessionId, script) { + const output = await runCommand( + [ + "--session", + sessionId, + "browser", + "run", + "--stdin", + ], + script + ); + + const parsed = parseWebcmdOutput(output); + + if (!parsed || parsed.ok === false) { + throw new Error( + `Browser command failed: ${output}` + ); + } + + return parsed.result; +} + +/* --------------------------------------------------------- + SEARCH QUERIES +--------------------------------------------------------- */ + +function buildSearchQueries(profile = {}) { + const skills = Array.isArray(profile.skills) + ? profile.skills.join(" ") + : ""; + + const interests = Array.isArray(profile.interests) + ? profile.interests.join(" ") + : ""; + + const degree = + profile.degree || "computer science"; + + const year = + profile.year || "student"; + + return [ + `${degree} internships 2026 ${skills}`, + `computer science scholarships 2026 ${year}`, + `student fellowships 2026 technology ${interests}`, + `student competitions hackathons 2026 ${skills}`, + `grants student programs 2026 technology`, + ]; +} + +/* --------------------------------------------------------- + URL HANDLING +--------------------------------------------------------- */ + +function extractRealUrl(url) { + if (!url) { + return null; + } + + try { + const parsed = new URL(url); + + // DuckDuckGo redirect URL + if ( + parsed.hostname.includes("duckduckgo.com") && + parsed.searchParams.has("uddg") + ) { + const target = + parsed.searchParams.get("uddg"); + + return target + ? decodeURIComponent(target) + : null; + } + + return url; + } catch (_) { + return null; + } +} + +/* --------------------------------------------------------- + CLASSIFICATION +--------------------------------------------------------- */ + +function classifyOpportunity(title, url) { + const text = + `${title} ${url}`.toLowerCase(); + + if ( + text.includes("scholarship") || + text.includes("scholarships") + ) { + return "Scholarship"; + } + + if ( + text.includes("fellowship") || + text.includes("fellowships") + ) { + return "Fellowship"; + } + + if ( + text.includes("hackathon") || + text.includes("competition") || + text.includes("contest") + ) { + return "Competition"; + } + + if ( + text.includes("grant") || + text.includes("grants") + ) { + return "Grant"; + } + + if ( + text.includes("university program") || + text.includes("student program") + ) { + return "University Program"; + } + + return "Internship"; +} + +function isUsefulOpportunity(title, url) { + const text = + `${title} ${url}`.toLowerCase(); + + const positiveKeywords = [ + "intern", + "internship", + "scholarship", + "fellowship", + "hackathon", + "competition", + "contest", + "grant", + "student program", + "university program", + "opportunity", + ]; + + const negativeKeywords = [ + "coursera article", + "blog", + "guide", + "salary", + "course", + "tutorial", + ]; + + const hasPositive = + positiveKeywords.some((word) => + text.includes(word) + ); + + const hasNegative = + negativeKeywords.some((word) => + text.includes(word) + ); + + return hasPositive && !hasNegative; +} + +/* --------------------------------------------------------- + ORGANIZATION +--------------------------------------------------------- */ + +function getOrganization(url) { + try { + const hostname = + new URL(url).hostname.replace( + /^www\./, + "" + ); + + const parts = hostname.split("."); + + if (parts.length >= 2) { + return parts[parts.length - 2] + .replace(/[-_]/g, " ") + .replace(/\b\w/g, (c) => + c.toUpperCase() + ); + } + + return hostname; + } catch (_) { + return "Unknown Organization"; + } +} + +/* --------------------------------------------------------- + SEARCH WEB +--------------------------------------------------------- */ + +async function searchWeb(sessionId, query) { + console.log( + `[discovery] Searching: ${query}` + ); + + const script = ` + const query = ${JSON.stringify(query)}; + + const searchUrl = + "https://html.duckduckgo.com/html/?q=" + + encodeURIComponent(query) + + "&kl=in-en"; + + await page.goto(searchUrl, { + waitUntil: "domcontentloaded" + }); + + await page.waitForTimeout(1500); + + const links = await page.locator("a").evaluateAll( + links => + links.map(a => ({ + title: (a.innerText || "").trim(), + url: a.href + })) + ); + + return JSON.stringify({ + query, + results: links + }); + `; + + const result = + await runBrowser(sessionId, script); + + let data = result; + + if (typeof result === "string") { + try { + data = JSON.parse(result); + } catch (_) {} + } + + const results = + Array.isArray(data?.results) + ? data.results + : []; + + console.log( + `[discovery] Found ${results.length} raw results` + ); + + return results; +} + +/* --------------------------------------------------------- + LIVE DEADLINE EXTRACTION +--------------------------------------------------------- */ + +function parseDateString(value) { + if (!value) { + return null; + } + + const cleaned = + String(value) + .replace(/\s+/g, " ") + .trim(); + + // Examples: + // 30 September 2026 + // September 30, 2026 + // 30/09/2026 + // 30-09-2026 + // 2026-09-30 + + const patterns = [ + /\b\d{1,2}\s+(January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}\b/i, + + /\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},?\s+\d{4}\b/i, + + /\b\d{1,2}[\/\-]\d{1,2}[\/\-]\d{4}\b/, + + /\b\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}\b/, + ]; + + for (const pattern of patterns) { + const match = cleaned.match(pattern); + + if (match) { + const date = new Date(match[0]); + + if ( + !Number.isNaN(date.getTime()) && + date.getFullYear() >= 2025 + ) { + return date; + } + } + } + + return null; +} + +async function extractLiveDetails( + sessionId, + url +) { + console.log( + `[discovery] Opening opportunity page: ${url}` + ); + + try { + const script = ` + const targetUrl = ${JSON.stringify(url)}; + + await page.goto(targetUrl, { + waitUntil: "domcontentloaded", + timeout: 30000 + }); + + await page.waitForTimeout(2000); + + const title = + await page.title().catch(() => ""); + + const bodyText = + await page.locator("body").innerText() + .catch(() => ""); + + const metaDescription = + await page.locator( + 'meta[name="description"]' + ).getAttribute("content") + .catch(() => ""); + + const h1 = + await page.locator("h1").first() + .innerText() + .catch(() => ""); + + return JSON.stringify({ + title, + h1, + bodyText: bodyText.slice(0, 30000), + metaDescription + }); + `; + + const result = + await runBrowser(sessionId, script); + + let data = result; + + if (typeof result === "string") { + try { + data = JSON.parse(result); + } catch (_) {} + } + + const bodyText = + String(data?.bodyText || ""); + + + const combinedText = + `${data?.title || ""} + ${data?.h1 || ""} + ${bodyText}`; + + const deadline = + extractDeadlineFromText(combinedText); + + return { + title: + data?.h1 || + data?.title || + null, + + description: + data?.metaDescription || + "", + + deadline, + }; + } catch (error) { + console.error( + `[discovery] Could not open ${url}:`, + error.message + ); + + return { + title: null, + description: "", + deadline: null, + }; + } +} + +/* --------------------------------------------------------- + DEADLINE TEXT DETECTION +--------------------------------------------------------- */ + +function extractDeadlineFromText(text) { + if (!text) { + return null; + } + + const normalized = String(text) + .replace(/\s+/g, " ") + .trim(); + + /* + * We first look around deadline-related words. + */ + const deadlineKeywords = [ + "application deadline", + "applications deadline", + "deadline", + "apply by", + "last date to apply", + "last day to apply", + "application closes", + "applications close", + "closing date", + "submission deadline", + "deadline to apply", + ]; + + const lowerText = normalized.toLowerCase(); + + for (const keyword of deadlineKeywords) { + let start = 0; + + while (true) { + const index = lowerText.indexOf( + keyword, + start + ); + + if (index === -1) { + break; + } + + /* + * Look at roughly 250 characters after + * the deadline label. + */ + const section = normalized.slice( + index, + index + 300 + ); + + const date = findDateInText(section); + + if (date) { + return date; + } + + start = index + keyword.length; + } + } + + /* + * Some websites don't explicitly put the word + * "deadline" immediately before the date. + * + * So check common date formats in the page. + */ + return findDateInText(normalized); +} +function findDateInText(text) { + if (!text) { + return null; + } + + const datePatterns = [ + /* + * 30 September 2026 + */ + /\b(0?[1-9]|[12]\d|3[01])\s+(January|February|March|April|May|June|July|August|September|October|November|December)\s+(202[5-9])\b/gi, + + /* + * September 30, 2026 + */ + /\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+(0?[1-9]|[12]\d|3[01]),?\s+(202[5-9])\b/gi, + + /* + * 30/09/2026 + */ + /\b(0?[1-9]|[12]\d|3[01])[\/\-](0?[1-9]|1[0-2])[\/\-](202[5-9])\b/g, + + /* + * 2026-09-30 + */ + /\b(202[5-9])[\/\-](0?[1-9]|1[0-2])[\/\-](0?[1-9]|[12]\d|3[01])\b/g, + + /* + * 30.09.2026 + */ + /\b(0?[1-9]|[12]\d|3[01])\.(0?[1-9]|1[0-2])\.(202[5-9])\b/g, + ]; + + for (const pattern of datePatterns) { + const match = pattern.exec(text); + + if (!match) { + continue; + } + + const dateText = match[0]; + + const parsed = parseFlexibleDate( + dateText + ); + + if (parsed) { + return parsed; + } + } + + return null; +} +function parseFlexibleDate(value) { + if (!value) { + return null; + } + + const cleaned = value.trim(); + + /* + * YYYY-MM-DD + */ + let match = cleaned.match( + /^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/ + ); + + if (match) { + const year = Number(match[1]); + const month = Number(match[2]); + const day = Number(match[3]); + + return createValidDate( + year, + month, + day + ); + } + + /* + * DD/MM/YYYY or DD-MM-YYYY + */ + match = cleaned.match( + /^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})$/ + ); + + if (match) { + const day = Number(match[1]); + const month = Number(match[2]); + const year = Number(match[3]); + + return createValidDate( + year, + month, + day + ); + } + + /* + * DD.MM.YYYY + */ + match = cleaned.match( + /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/ + ); + + if (match) { + const day = Number(match[1]); + const month = Number(match[2]); + const year = Number(match[3]); + + return createValidDate( + year, + month, + day + ); + } + + /* + * "30 September 2026" + */ + match = cleaned.match( + /^(\d{1,2})\s+(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{4})$/i + ); + + if (match) { + const day = Number(match[1]); + const month = monthNumber(match[2]); + const year = Number(match[3]); + + return createValidDate( + year, + month, + day + ); + } + + /* + * "September 30, 2026" + */ + match = cleaned.match( + /^(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),?\s+(\d{4})$/i + ); + + if (match) { + const month = monthNumber(match[1]); + const day = Number(match[2]); + const year = Number(match[3]); + + return createValidDate( + year, + month, + day + ); + } + + return null; +} + +function monthNumber(month) { + const months = { + january: 1, + february: 2, + march: 3, + april: 4, + may: 5, + june: 6, + july: 7, + august: 8, + september: 9, + october: 10, + november: 11, + december: 12, + }; + + return months[ + String(month).toLowerCase() + ]; +} + +function createValidDate( + year, + month, + day +) { + if ( + !year || + !month || + !day || + year < 2025 || + month < 1 || + month > 12 || + day < 1 || + day > 31 + ) { + return null; + } + + const date = new Date( + year, + month - 1, + day + ); + + /* + * Prevent invalid dates such as + * 31/02/2026. + */ + if ( + date.getFullYear() !== year || + date.getMonth() !== month - 1 || + date.getDate() !== day + ) { + return null; + } + + return date; +} + +/* --------------------------------------------------------- + NORMALIZE SEARCH RESULT +--------------------------------------------------------- */ + +function normalizeResult(result, query) { + const title = + String(result.title || "").trim(); + + const realUrl = + extractRealUrl(result.url); + + if (!title || !realUrl) { + return null; + } + + if (!/^https?:\/\//i.test(realUrl)) { + return null; + } + + if (!isUsefulOpportunity(title, realUrl)) { + return null; + } + + return { + title, + + organization: + getOrganization(realUrl), + + type: + classifyOpportunity( + title, + realUrl + ), + + description: + `Live opportunity discovered from web search for "${query}".`, + + deadline: null, + + eligibility: + "Check official opportunity page", + + minCgpa: null, + + requiredSkills: [], + + relatedInterests: [], + + location: + "See official opportunity page", + + stipendOrAmount: null, + + applyUrl: realUrl, + + sourceUrl: realUrl, + + sourceType: "live", + + discoveredAt: new Date(), + + requiredDocuments: [], + }; +} + +/* --------------------------------------------------------- + DEDUPLICATION +--------------------------------------------------------- */ + +function dedupeOpportunities( + opportunities +) { + const map = new Map(); + + for (const opportunity of opportunities) { + if (!opportunity?.applyUrl) { + continue; + } + + const key = + opportunity.applyUrl + .replace(/\/$/, "") + .toLowerCase(); + + if (!map.has(key)) { + map.set(key, opportunity); + } + } + + return [...map.values()]; +} + +/* --------------------------------------------------------- + MAIN DISCOVERY +--------------------------------------------------------- */ + +async function discoverOpportunities( + profile = {} +) { + const sessionId = + await createSession(); + + try { + const queries = + buildSearchQueries(profile); + + const allOpportunities = []; + + for (const query of queries) { + try { + const results = + await searchWeb( + sessionId, + query + ); + + for (const result of results) { + const opportunity = + normalizeResult( + result, + query + ); + + if (opportunity) { + allOpportunities.push( + opportunity + ); + } + } + } catch (error) { + console.error( + `[discovery] Query failed: ${query}`, + error.message + ); + } + } + + let unique = + dedupeOpportunities( + allOpportunities + ); + + console.log( + `[discovery] Unique opportunities before live extraction: ${unique.length}` + ); + + /* + Open the actual opportunity pages. + + Limit to 15 pages so one discovery request + does not take excessively long. + */ + + const opportunitiesToInspect = + unique.slice(0, 15); + + const enriched = []; + + for ( + const opportunity of opportunitiesToInspect + ) { + try { + const liveDetails = + await extractLiveDetails( + sessionId, + opportunity.sourceUrl + ); + + if (liveDetails.title) { + opportunity.title = + liveDetails.title; + } + + if (liveDetails.description) { + opportunity.description = + liveDetails.description; + } + + /* + IMPORTANT: + Only use a deadline that was actually + found on the opportunity page. + */ + +if (liveDetails.deadline) { + opportunity.deadline = liveDetails.deadline; +} else { + opportunity.deadline = null; +} + +// Do not show expired opportunities +if (opportunity.deadline) { + const deadline = new Date(opportunity.deadline); + + if (!isNaN(deadline.getTime()) && deadline < new Date()) { + console.log( + `[discovery] Skipping expired opportunity: ${opportunity.title}` + ); + continue; + } +} + +enriched.push(opportunity); + + console.log( + `[discovery] ${opportunity.title} | Deadline: ${ + opportunity.deadline + ? opportunity.deadline.toISOString() + : "Not specified" + }` + ); + } catch (error) { + console.error( + `[discovery] Detail extraction failed:`, + error.message + ); + + enriched.push(opportunity); + } + } + + unique = enriched; + + console.log( + `[discovery] Final live opportunities: ${unique.length}` + ); + + return unique.slice(0, 30); + } finally { + await closeSession(sessionId); + } +} + +module.exports = { + discoverOpportunities, +}; \ No newline at end of file diff --git a/backend/services/webcmdService.js b/backend/services/webcmdService.js new file mode 100644 index 000000000..29e4c195a --- /dev/null +++ b/backend/services/webcmdService.js @@ -0,0 +1,182 @@ +/** + * webcmdService.js + * ------------------------------------------------------------------ + * Integration layer for "Webcmd" - the CLI/tool used by the Rescue + * Agent to open a target application form, read its fields, and + * drive auto-fill. This module is intentionally the ONLY place that + * knows about Webcmd, so swapping DEMO <-> REAL never touches the + * rest of the codebase. + * + * Modes (set via WEBCMD_MODE in backend/.env): + * DEMO - fully scripted, deterministic, no external process. + * Guarantees the hackathon demo works with zero flakiness. + * REAL - shells out to the installed Webcmd CLI binary to open a + * real browser/page, read the DOM, and fill fields. If the + * binary isn't found or errors, we log it and gracefully + * fall back to DEMO logic so the flow never hard-crashes + * mid-demo. + * ------------------------------------------------------------------ + */ + +const { execFile } = require("child_process"); + +const MODE = (process.env.WEBCMD_MODE || "DEMO").toUpperCase(); +const WEBCMD_CLI_PATH = process.env.WEBCMD_CLI_PATH || "webcmd"; + +// The canonical demo application form. In DEMO mode this is what the +// Rescue Agent "opens" and reads fields from. Every field here maps to +// the Application Details form shown on the frontend (see DemoForm.jsx). +const DEMO_FORM_SCHEMA = [ + { key: "name", label: "Full Name", isDocument: false, source: "profile.name" }, + { key: "email", label: "Email", isDocument: false, source: "profile.email" }, + { key: "university", label: "University", isDocument: false, source: "profile.university" }, + { key: "degree", label: "Degree", isDocument: false, source: "profile.degree" }, + { key: "branch", label: "Branch", isDocument: false, source: "profile.branch" }, + { key: "skills", label: "Skills", isDocument: false, source: "profile.skills" }, + { key: "resume", label: "Resume", isDocument: true, source: "document.Resume" }, + { key: "transcript", label: "Transcript", isDocument: true, source: "document.Transcript" }, + { key: "sop", label: "Statement of Purpose (SOP)", isDocument: true, source: "document.SOP" }, +]; + +function getFormSchema() { + return DEMO_FORM_SCHEMA; +} + +function resolveValue(source, profile, documentsByType) { + if (source.startsWith("profile.")) { + const field = source.split(".")[1]; + const val = profile ? profile[field] : null; + if (Array.isArray(val)) return val.join(", "); + return val || ""; + } + if (source.startsWith("document.")) { + const docType = source.split(".")[1]; + const doc = documentsByType[docType]; + if (doc && doc.status === "uploaded") return doc.fileName || `${docType}.pdf`; + return ""; + } + return ""; +} + +/** + * Core DEMO-mode analysis. Deterministic, synchronous logic that + * "fills what it safely can" from the student's profile + document + * vault, and flags anything left empty as missing. + */ +function runDemoAnalysis({ profile, documents }) { + const documentsByType = {}; + (documents || []).forEach((d) => { + documentsByType[d.type] = d; + }); + + const log = []; + log.push(step("open_form", "Webcmd opened the application form in DEMO mode.")); + log.push(step("read_fields", `Detected ${DEMO_FORM_SCHEMA.length} fields on the form.`)); + + const formFields = DEMO_FORM_SCHEMA.map((field) => { + const value = resolveValue(field.source, profile, documentsByType); + const filled = Boolean(value); + return { + key: field.key, + label: field.label, + value, + filled, + isDocument: field.isDocument, + }; + }); + + const filledFields = formFields.filter((f) => f.filled).map((f) => f.label); + const missingFields = formFields.filter((f) => !f.filled).map((f) => f.label); + + log.push( + step( + "autofill", + filledFields.length + ? `Auto-filled from profile & document vault: ${filledFields.join(", ")}.` + : "No fields could be auto-filled yet." + ) + ); + + if (missingFields.length) { + log.push(step("detect_missing", `Missing before submission: ${missingFields.join(", ")}.`)); + } else { + log.push(step("detect_missing", "No missing fields or documents detected.")); + } + + const completionPercent = Math.round((filledFields.length / formFields.length) * 100); + const status = missingFields.length === 0 ? "Ready for Review" : "Missing Items"; + + log.push(step("complete", `Form completion is ${completionPercent}%. Status: ${status}.`)); + + return { formFields, missingFields, completionPercent, status, log }; +} + +function step(name, message) { + return { step: name, message, timestamp: new Date() }; +} + +/** + * REAL mode: attempts to call the actual Webcmd CLI. Expected contract + * (adjust to match your installed Webcmd CLI's real interface): + * + * webcmd analyze --url --profile --json + * + * The CLI is expected to print a JSON object to stdout with the shape: + * { formFields: [...], missingFields: [...], completionPercent, status, log } + * + * If the CLI is missing, times out, or returns bad JSON, we log the + * failure and fall back to DEMO analysis so the rescue flow still + * completes for the user. + */ +function runRealAnalysis({ profile, documents, opportunity }) { + return new Promise((resolve) => { + const args = [ + "analyze", + "--url", + opportunity.applyUrl || "about:blank", + "--profile", + JSON.stringify(profile || {}), + "--json", + ]; + + execFile(WEBCMD_CLI_PATH, args, { timeout: 15000 }, (error, stdout) => { + if (error) { + console.warn(`[webcmdService] REAL mode failed (${error.message}). Falling back to DEMO.`); + const fallback = runDemoAnalysis({ profile, documents }); + fallback.log.unshift( + step("real_mode_fallback", "Webcmd CLI unavailable/failed - fell back to DEMO analysis.") + ); + return resolve(fallback); + } + + try { + const parsed = JSON.parse(stdout); + resolve(parsed); + } catch (parseErr) { + console.warn("[webcmdService] Could not parse Webcmd CLI output. Falling back to DEMO."); + const fallback = runDemoAnalysis({ profile, documents }); + fallback.log.unshift( + step("real_mode_fallback", "Webcmd CLI output was invalid JSON - fell back to DEMO analysis.") + ); + resolve(fallback); + } + }); + }); +} + +/** + * Public entry point used by the applications controller. + * Always resolves - never throws - so a demo run never breaks mid-flow. + */ +async function analyzeApplication({ profile, documents, opportunity }) { + if (MODE === "REAL") { + return runRealAnalysis({ profile, documents, opportunity }); + } + return runDemoAnalysis({ profile, documents }); +} + +module.exports = { + MODE, + getFormSchema, + analyzeApplication, +}; diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 000000000..3a12b32f5 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,18 @@ + + + + + + Application Rescue Agent + + + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 000000000..582e0826c --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2959 @@ +{ + "name": "application-rescue-agent-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "application-rescue-agent-frontend", + "version": "1.0.0", + "dependencies": { + "axios": "^1.7.4", + "lucide-react": "^0.436.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.41", + "tailwindcss": "^3.4.10", + "vite": "^5.4.2" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.4", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.4.tgz", + "integrity": "sha512-q7j5geK7xs3UJSdm9/iytUNclBnLmYx1EnSeCFXHPeutdqgIMeFeHtUZgS3EhlKxdBEAu8OwtJCwmLrEzpSs7Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.2.tgz", + "integrity": "sha512-Xa6RDoWa+hNiX6PgsljlH6W75RaONx3y6PVlbLhkEWW+GaPQ3dP5gwbL/erAzQHWwkvW5UxdD5l87Qx2FAQ/4A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.2.tgz", + "integrity": "sha512-vNASxsghMfQ5s+v3PrpnJd+ryL/26lxCCaGI+sDJ7VzmHiYXIrrVltsDhaawxLM1WcoMU2oYlbPHLaYQtBzhcg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.2.tgz", + "integrity": "sha512-0dWDjmlrpZAgjPD/aPzUDhBW8APLRjAni5bOrM76wiiZm+E+KTMVKNhAzaTBohz8UyO2fKNAl0+fygbe2HZXOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.2.tgz", + "integrity": "sha512-N58uktcwzk3+qT4KHEuNdIxX1N01RWrkfVoml69EAbSaNDL+sbNVLx2RMl4Qd23lpA0fgPvyh5hHb4weD5WKmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.2.tgz", + "integrity": "sha512-HWF2zH8EAp2scWRpt2PGe6iUGz7zi04waXsdRr3zb4DWCk2ImIo5FZu0jjmD53nP/DGSvnW0e7/1ToCNZs2lZw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.2.tgz", + "integrity": "sha512-MkvcwHMnzPSMOQEwB6wHnLzmc+hT8BGc5bW/Mhmjjgx3wbj6VBnlc47XsK74kD0K9MikFfXpQqyz4NUXaUW62A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.2.tgz", + "integrity": "sha512-xe1bCKPJaKsD0tfd7Rb6bGfUogJTpKbTEEthsfdb7hTfTRNJVQTdirabQx0o6ERVba/smkM720soMY+0QnrlSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.2.tgz", + "integrity": "sha512-yOM7LdK0p6gk6+Q773OEwtlsikT1TL3yMmYsTtRlDRPha5vV2DC5x7LqRWDr6f3cSYNMKVqxzffXv8ivxNBIFQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.2.tgz", + "integrity": "sha512-qiWuJJV3DybA2IfzvRimeKXGrGuVPv1zobSY/26KnP3HbV0VcNb3ECzgvtbvF3xjSMkcooou6HASXZuLdjnhpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.2.tgz", + "integrity": "sha512-akcZquRzCY/KpUoZAMBhGf7oi4LmXq1BzRA5CPAC3rkUf28Y/sAYV3jSL+JKd7cwEyFvR5G0XVZ0gaMedP+60A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.2.tgz", + "integrity": "sha512-fNwYHrPyYyxauPzX/cpYw8Z7LQpp+DGA0KCoswA0aVFBpmdMil9XgjB8V3Ny64Ihu797+GKcuJqnsOKEmor7fA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.2.tgz", + "integrity": "sha512-XfvsgzR7DZqREdst7K1Mj3ilSUM5xLAHJcIMDFPKdxTs9q5VHOT8aMA+a683fqBu7DQl8+Sd9HCsQYL8EMY9qA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.2.tgz", + "integrity": "sha512-Pp7gVZggEFlbcuztay+/U0gVG9S1XAh8i7I1Re/htbAzo43P5wHZHw6pTyzotISqlKohoh9RpIfnOz3RbemK1w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.2.tgz", + "integrity": "sha512-zkgL2xff6i7u5hau/m6FGeS8gRkLEdgLw522WGmdWWlLd9btmNl3S80mcEjtGq+kvgUekQ3+BOYLLLcPlS2LIA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.2.tgz", + "integrity": "sha512-qOheJomrkVCbbHFJ7L3J97cnhfogKqguAQphv26+3ZsAQIF1L19b+dArl//s8rjJHJLz9byykyM8NBP4nmSa1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.2.tgz", + "integrity": "sha512-XlxLD54wQhH3FciCgMofxBw27NzUe818gJH410qWvc41UT0ZFcgxVjyX5/EK8MPTupjeVWqN5oy+9pCA9mqfCA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.2.tgz", + "integrity": "sha512-vdryWeRb2bLJZf0Fv/W8se6nvsHe2PkTCxV0meheK3nQE+G90VCJcke51Miy1yQRsfm2uqIyjXOu4wmUzbTtkQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.2.tgz", + "integrity": "sha512-bcq2h2pkKmH2po4cZV8VWzO4lL40STyu/nLoFpYMQp9C2tCVNTdcVv86MwSsn3D5s1FBe2Ty1atqvVAUTMimNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.2.tgz", + "integrity": "sha512-EGoo5DMVMRkTId8fuTDaoxVlR5ZTsKULUezRjd9gCw5eeY+DjCvDpZAOlNUvKPGX+7rS1RWx6j+yOpNPx0cUgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.2.tgz", + "integrity": "sha512-MErl12k7BFHZG1TI9QF/3lSSZARzq9KgNy/FjnqFMCkv+N4RSSzoUCA5h2mqHX4Mox3WaTVKblyzhQ1zRb2ZuQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.2.tgz", + "integrity": "sha512-ILs8k07Wh4p0PsNY4wYLEaXZKMOpVhrG5QDB0yHhGhuzOfDlnyHN6sflL4El/MpUP1y8uY2lUZrv4oBS6pTT3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.2.tgz", + "integrity": "sha512-hKgB3nz/TKD3Wv78XEsyXzQsNjvhOHmwKQTvXADGOyU/cIClZDO7DsoggbdmJDPGp5V80tA3Vfv61PaKTLH3LA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.2.tgz", + "integrity": "sha512-T4wf1mudIDxN8Q/CWIBJC1u5gQUc+r5mPvlwoSbIvNkyVTP2TAFeobEmst5AQ4gMyAz4sSByVdoTDfvTmGK/8g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.2.tgz", + "integrity": "sha512-tC3IY7qoaD9Ll3/8WJQn49j5V2f/NuI9S41NOE2iM5MPs3sPIvOkVToLcz/7Bz4pyF7PSvrtwu8I/pUrGOSecQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.2.tgz", + "integrity": "sha512-6NHnk/K3eq2ZFYcU1X8g67s9qIJRCOTT92gwLMVBp08dB2uuuwI1/Q/empzL2Bfr2f2WRLJVwpp90RmacQyFkw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.6.tgz", + "integrity": "sha512-HiH4oYNc5+DQEx/b8FPfMj+WHH/WWUBmbN5r1Uf/ixtfyFkk+wGIlfJT/RD5eAyzlwkeYRtZUCI8qrEi26pl2g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.9", + "caniuse-lite": "^1.0.30001810", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.22", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.22.tgz", + "integrity": "sha512-pWc4w51fBFd7mav43/zKRC+RI6f4yfzQoVlfvE8dECePyfkn1bzLp01Fj0QACcyCZyFhiEMyD2qScfKRWgWibA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.427", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.427.tgz", + "integrity": "sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.436.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.436.0.tgz", + "integrity": "sha512-N292bIxoqm1aObAg0MzFtvhYwgQE6qnIOWx/GLj5ONgcTPH6N0fD9bVq/GfdeC9ZORBXozt/XeEKDpiB3x3vlQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.6.tgz", + "integrity": "sha512-5HfK7k5im7LTOB0EqCQmfvy4C13G92Ssj1VTmouTK3AJvyjKTnFuCV0vcMAD/JS+JC4DvDIBRrlAeJIFjh5VWg==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.6", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.6.tgz", + "integrity": "sha512-0RHKZz7wwffvkU+2MFVT2NnjK44ssLEV+m0CAJaS2Ksmorrwj7WxH00jO0SOCW26/tINUnJHToXblDs33I38YQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.4", + "react-router": "6.30.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.2.tgz", + "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.2.tgz", + "integrity": "sha512-l5eyksV4tPBj6lJyEa37YzIOCSOV7lkZzEHUdpjWZbtD7wTcFYmEYXSgm5bT4vV+dZLb9rBG1W9GROOG4NS4Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.2", + "@rollup/rollup-android-arm64": "4.63.2", + "@rollup/rollup-darwin-arm64": "4.63.2", + "@rollup/rollup-darwin-x64": "4.63.2", + "@rollup/rollup-freebsd-arm64": "4.63.2", + "@rollup/rollup-freebsd-x64": "4.63.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.2", + "@rollup/rollup-linux-arm-musleabihf": "4.63.2", + "@rollup/rollup-linux-arm64-gnu": "4.63.2", + "@rollup/rollup-linux-arm64-musl": "4.63.2", + "@rollup/rollup-linux-loong64-gnu": "4.63.2", + "@rollup/rollup-linux-loong64-musl": "4.63.2", + "@rollup/rollup-linux-ppc64-gnu": "4.63.2", + "@rollup/rollup-linux-ppc64-musl": "4.63.2", + "@rollup/rollup-linux-riscv64-gnu": "4.63.2", + "@rollup/rollup-linux-riscv64-musl": "4.63.2", + "@rollup/rollup-linux-s390x-gnu": "4.63.2", + "@rollup/rollup-linux-x64-gnu": "4.63.2", + "@rollup/rollup-linux-x64-musl": "4.63.2", + "@rollup/rollup-openbsd-x64": "4.63.2", + "@rollup/rollup-openharmony-arm64": "4.63.2", + "@rollup/rollup-win32-arm64-msvc": "4.63.2", + "@rollup/rollup-win32-ia32-msvc": "4.63.2", + "@rollup/rollup-win32-x64-gnu": "4.63.2", + "@rollup/rollup-win32-x64-msvc": "4.63.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 000000000..dc3f922d4 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "application-rescue-agent-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.7.4", + "lucide-react": "^0.436.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.41", + "tailwindcss": "^3.4.10", + "vite": "^5.4.2" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 000000000..2aa7205d4 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 000000000..6c014e110 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,50 @@ +import { Routes, Route, useLocation } from "react-router-dom"; +import Sidebar from "./components/Sidebar.jsx"; +import Topbar from "./components/Topbar.jsx"; + +import Dashboard from "./pages/Dashboard.jsx"; +import Profile from "./pages/Profile.jsx"; +import Discover from "./pages/Discover.jsx"; +import OpportunityDetails from "./pages/OpportunityDetails.jsx"; +import Documents from "./pages/Documents.jsx"; +import Applications from "./pages/Applications.jsx"; +import ApplicationRescue from "./pages/ApplicationRescue.jsx"; + +const TITLES = { + "/": "Dashboard", + "/profile": "Your Profile", + "/discover": "Discover Opportunities", + "/documents": "Document Vault", + "/applications": "Your Applications", +}; + +function pageTitle(pathname) { + if (TITLES[pathname]) return TITLES[pathname]; + if (pathname.startsWith("/discover/")) return "Opportunity Details"; + if (pathname.startsWith("/applications/")) return "Application Rescue"; + return "Application Rescue Agent"; +} + +export default function App() { + const location = useLocation(); + + return ( +
+ +
+ +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+
+ ); +} diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js new file mode 100644 index 000000000..6189afb37 --- /dev/null +++ b/frontend/src/api/api.js @@ -0,0 +1,67 @@ +import axios from "axios"; + +const client = axios.create({ + baseURL: "/api", + headers: { "Content-Type": "application/json" }, +}); + +export const api = { + health: () => client.get("/health").then((r) => r.data), + + // Profile + getProfile: () => client.get("/profile").then((r) => r.data), + saveProfile: (data) => client.post("/profile", data).then((r) => r.data), + + // Opportunities + getOpportunities: () => client.get("/opportunities").then((r) => r.data), + + getRecommended: (limit = 6) => + client.get(`/opportunities/recommended?limit=${limit}`).then((r) => r.data), + + getOpportunity: (id) => + client.get(`/opportunities/${id}`).then((r) => r.data), + + // Discover live opportunities from the web + discoverOpportunities: () => + client.post("/opportunities/discover").then((r) => r.data), + + //documents +getDocuments: () => client.get("/documents").then((r) => r.data), + +uploadDocument: (type, file) => { + const formData = new FormData(); + + formData.append("type", type); + formData.append("file", file); + + return client + .post("/documents/upload", formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }) + .then((r) => r.data); +}, + +removeDocument: (type) => + client + .delete(`/documents/${encodeURIComponent(type)}`) + .then((r) => r.data), + + // Applications + getApplications: () => client.get("/applications").then((r) => r.data), + + getApplication: (id) => + client.get(`/applications/${id}`).then((r) => r.data), + + startApplication: (opportunityId) => + client.post("/applications", { opportunityId }).then((r) => r.data), + + analyzeApplication: (id) => + client.post(`/applications/${id}/analyze`).then((r) => r.data), + + submitApplication: (id) => + client.post(`/applications/${id}/submit`).then((r) => r.data), +}; + +export default api; \ No newline at end of file diff --git a/frontend/src/components/MatchBadge.jsx b/frontend/src/components/MatchBadge.jsx new file mode 100644 index 000000000..3a617075f --- /dev/null +++ b/frontend/src/components/MatchBadge.jsx @@ -0,0 +1,16 @@ +export default function MatchBadge({ percent }) { + const value = Math.round(percent ?? 0); + + let tone = "bg-navy-50 text-navy-500 ring-navy-100"; + if (value >= 80) tone = "bg-ok-50 text-ok-700 ring-ok-300/50"; + else if (value >= 55) tone = "bg-rescue-50 text-rescue-700 ring-rescue-300/50"; + else tone = "bg-danger-50 text-danger-500 ring-danger-500/20"; + + return ( + + {value}% match + + ); +} diff --git a/frontend/src/components/OpportunityCard.jsx b/frontend/src/components/OpportunityCard.jsx new file mode 100644 index 000000000..9f551d0e7 --- /dev/null +++ b/frontend/src/components/OpportunityCard.jsx @@ -0,0 +1,68 @@ +import { useNavigate } from "react-router-dom"; +import { CalendarClock, MapPin, Coins } from "lucide-react"; +import MatchBadge from "./MatchBadge.jsx"; + +const TYPE_COLORS = { + Internship: "bg-navy-50 text-navy-700", + Scholarship: "bg-ok-50 text-ok-700", + Fellowship: "bg-rescue-50 text-rescue-700", + Competition: "bg-warn-50 text-warn-500", + Grant: "bg-navy-100 text-navy-700", + "University Program": "bg-navy-50 text-navy-500", +}; + +function daysUntil(dateStr) { + const diff = Math.ceil((new Date(dateStr) - new Date()) / (1000 * 60 * 60 * 24)); + return diff; +} + +export default function OpportunityCard({ opportunity }) { + const navigate = useNavigate(); + const days = daysUntil(opportunity.deadline); + const urgent = days <= 10; + + return ( + + ); +} diff --git a/frontend/src/components/Panel.jsx b/frontend/src/components/Panel.jsx new file mode 100644 index 000000000..1ab34a9f5 --- /dev/null +++ b/frontend/src/components/Panel.jsx @@ -0,0 +1,34 @@ +export function Panel({ children, className = "", accent }) { + const accentBorder = accent ? `border-l-4 ${accent}` : "border border-navy-100"; + return ( +
+ {children} +
+ ); +} + +export function Loader({ label = "Loading..." }) { + return ( +
+ + + + {label} +
+ ); +} + +export function EmptyState({ icon: Icon, title, description, action }) { + return ( +
+ {Icon && ( +
+ +
+ )} +

{title}

+ {description &&

{description}

} + {action &&
{action}
} +
+ ); +} diff --git a/frontend/src/components/ProgressBar.jsx b/frontend/src/components/ProgressBar.jsx new file mode 100644 index 000000000..3685f1ca5 --- /dev/null +++ b/frontend/src/components/ProgressBar.jsx @@ -0,0 +1,23 @@ +export default function ProgressBar({ percent = 0, label, tone = "rescue" }) { + const value = Math.max(0, Math.min(100, Math.round(percent))); + + const barColor = + tone === "ok" ? "bg-ok-500" : tone === "danger" ? "bg-danger-500" : "bg-rescue-500"; + + return ( +
+ {label && ( +
+ {label} + {value}% +
+ )} +
+
+
+
+ ); +} diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx new file mode 100644 index 000000000..55c37b458 --- /dev/null +++ b/frontend/src/components/Sidebar.jsx @@ -0,0 +1,65 @@ +import { NavLink } from "react-router-dom"; +import { + LayoutDashboard, + UserRound, + Compass, + FolderClock, + FileStack, + LifeBuoy, +} from "lucide-react"; + +const links = [ + { to: "/", label: "Dashboard", icon: LayoutDashboard, end: true }, + { to: "/profile", label: "Profile", icon: UserRound }, + { to: "/discover", label: "Discover", icon: Compass }, + { to: "/documents", label: "Documents", icon: FileStack }, + { to: "/applications", label: "Applications", icon: FolderClock }, +]; + +export default function Sidebar() { + return ( + + ); +} diff --git a/frontend/src/components/StatusBadge.jsx b/frontend/src/components/StatusBadge.jsx new file mode 100644 index 000000000..ec6d41fb8 --- /dev/null +++ b/frontend/src/components/StatusBadge.jsx @@ -0,0 +1,18 @@ +const STYLES = { + "Not Started": "bg-navy-50 text-navy-500 ring-navy-100", + Analyzing: "bg-rescue-50 text-rescue-700 ring-rescue-300/50", + "Missing Items": "bg-warn-50 text-warn-500 ring-warn-500/20", + "Ready for Review": "bg-ok-50 text-ok-700 ring-ok-300/50", + Submitted: "bg-navy-900 text-white ring-navy-900", + uploaded: "bg-ok-50 text-ok-700 ring-ok-300/50", + missing: "bg-danger-50 text-danger-500 ring-danger-500/20", +}; + +export default function StatusBadge({ status }) { + const style = STYLES[status] || "bg-navy-50 text-navy-500 ring-navy-100"; + return ( + + {status} + + ); +} diff --git a/frontend/src/components/Topbar.jsx b/frontend/src/components/Topbar.jsx new file mode 100644 index 000000000..5880585f2 --- /dev/null +++ b/frontend/src/components/Topbar.jsx @@ -0,0 +1,38 @@ +import { NavLink } from "react-router-dom"; +import { LayoutDashboard, UserRound, Compass, FolderClock, FileStack } from "lucide-react"; + +const links = [ + { to: "/", label: "Dashboard", icon: LayoutDashboard, end: true }, + { to: "/profile", label: "Profile", icon: UserRound }, + { to: "/discover", label: "Discover", icon: Compass }, + { to: "/documents", label: "Documents", icon: FileStack }, + { to: "/applications", label: "Applications", icon: FolderClock }, +]; + +export default function Topbar({ title }) { + return ( +
+
+

{title}

+
+ +
+ ); +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 000000000..83771a403 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,50 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body, +#root { + height: 100%; +} + +body { + @apply bg-paper text-ink font-body antialiased; +} + +h1, +h2, +h3, +h4 { + @apply font-display text-navy-900; +} + +::selection { + @apply bg-rescue-100; +} + +/* Custom scrollbar - subtle, not decorative-heavy */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} +::-webkit-scrollbar-thumb { + background: #cfd8e6; + border-radius: 999px; +} +::-webkit-scrollbar-track { + background: transparent; +} + +@keyframes rescue-pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.45; + } +} +.animate-rescue-pulse { + animation: rescue-pulse 1.4s ease-in-out infinite; +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 000000000..8a91f187d --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import App from "./App.jsx"; +import "./index.css"; + +ReactDOM.createRoot(document.getElementById("root")).render( + + + + + +); diff --git a/frontend/src/pages/ApplicationRescue.jsx b/frontend/src/pages/ApplicationRescue.jsx new file mode 100644 index 000000000..313d11ab4 --- /dev/null +++ b/frontend/src/pages/ApplicationRescue.jsx @@ -0,0 +1,242 @@ +import { useEffect, useState, useCallback } from "react"; +import { useParams, Link } from "react-router-dom"; +import { + ArrowLeft, + LifeBuoy, + CheckCircle2, + CircleAlert, + UploadCloud, + Send, + Terminal, +} from "lucide-react"; +import api from "../api/api.js"; +import { Panel, Loader } from "../components/Panel.jsx"; +import StatusBadge from "../components/StatusBadge.jsx"; +import ProgressBar from "../components/ProgressBar.jsx"; + +export default function ApplicationRescue() { + const { id } = useParams(); + const [app, setApp] = useState(null); + const [loading, setLoading] = useState(true); + const [analyzing, setAnalyzing] = useState(false); + const [uploadingType, setUploadingType] = useState(null); + const [submitting, setSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(""); + const [visibleLogCount, setVisibleLogCount] = useState(0); + + const load = useCallback(async () => { + setLoading(true); + const data = await api.getApplication(id); + setApp(data); + setVisibleLogCount(data.webcmdLog?.length || 0); + setLoading(false); + }, [id]); + + useEffect(() => { + load(); + }, [load]); + + async function handleAnalyze() { + setAnalyzing(true); + setSubmitError(""); + setVisibleLogCount(0); + try { + const updated = await api.analyzeApplication(id); + setApp(updated); + // Reveal the Webcmd log line by line for a "live agent" feel. + const total = updated.webcmdLog.length; + let i = 0; + const interval = setInterval(() => { + i += 1; + setVisibleLogCount(i); + if (i >= total) clearInterval(interval); + }, 400); + } finally { + setAnalyzing(false); + } + } + + async function handleUploadMissingDoc(label) { + setUploadingType(label); + try { + await api.uploadDocument(label, `${label.replace(/\s+/g, "_")}.pdf`); + const updated = await api.analyzeApplication(id); + setApp(updated); + setVisibleLogCount(updated.webcmdLog.length); + } finally { + setUploadingType(null); + } + } + + async function handleSubmit() { + const confirmed = window.confirm( + "Submit this application now? This action cannot be undone from within the Rescue Agent." + ); + if (!confirmed) return; + + setSubmitting(true); + setSubmitError(""); + try { + const updated = await api.submitApplication(id); + setApp(updated); + } catch (err) { + setSubmitError(err?.response?.data?.message || "Could not submit. Please try again."); + } finally { + setSubmitting(false); + } + } + + if (loading) return ; + if (!app) return

Application not found.

; + + const opp = app.opportunity; + const hasRun = app.formFields && app.formFields.length > 0; + const missingDocFields = (app.formFields || []).filter((f) => f.isDocument && !f.filled); + const readyToSubmit = app.status === "Ready for Review"; + const submitted = app.status === "Submitted"; + + return ( +
+ + + Back to Applications + + + +
+
+

+ Application Rescue +

+

{opp?.title}

+

{opp?.organization}

+
+ +
+ +
+ +
+ + {!submitted && ( + + )} +
+ + {hasRun && ( + +

+ + Webcmd activity log +

+
    + {app.webcmdLog.slice(0, visibleLogCount).map((entry, idx) => ( +
  1. + + {entry.message} +
  2. + ))} +
+
+ )} + + {hasRun && ( + +

Form fields

+
+ {app.formFields.map((field) => ( +
+
+

{field.label}

+ {field.filled ? ( + + ) : ( + + )} +
+

+ {field.filled ? field.value : "Missing"} +

+ + {!field.filled && field.isDocument && ( + + )} +
+ ))} +
+ + {missingDocFields.length === 0 && app.completionPercent < 100 && ( +

+ Some fields still need attention - update your profile, then re-run the Rescue Agent. +

+ )} +
+ )} + + {hasRun && ( + +
+
+

+ {submitted + ? "Application submitted" + : readyToSubmit + ? "Ready for your review" + : "Waiting on missing items"} +

+

+ {submitted + ? `Submitted on ${new Date(app.submittedAt).toLocaleString()}.` + : readyToSubmit + ? "Everything is filled in. Review the fields above, then submit when you're ready." + : "Resolve the missing items above and re-run the Rescue Agent to unlock submission."} +

+ {submitError &&

{submitError}

} +
+ + {!submitted && ( + + )} +
+
+ )} +
+ ); +} diff --git a/frontend/src/pages/Applications.jsx b/frontend/src/pages/Applications.jsx new file mode 100644 index 000000000..970873b25 --- /dev/null +++ b/frontend/src/pages/Applications.jsx @@ -0,0 +1,73 @@ +import { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { FolderClock, ArrowRight } from "lucide-react"; +import api from "../api/api.js"; +import { Panel, Loader, EmptyState } from "../components/Panel.jsx"; +import StatusBadge from "../components/StatusBadge.jsx"; +import ProgressBar from "../components/ProgressBar.jsx"; +import MatchBadge from "../components/MatchBadge.jsx"; + +export default function Applications() { + const [applications, setApplications] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function load() { + setLoading(true); + const data = await api.getApplications(); + setApplications(data); + setLoading(false); + } + load(); + }, []); + + if (loading) return ; + + if (applications.length === 0) { + return ( + + + Discover opportunities + + } + /> + + ); + } + + return ( +
+ {applications.map((app) => ( + + +
+
+
+

{app.opportunity?.title}

+ +
+

{app.opportunity?.organization}

+
+ +
+
+
+ + +
+
+
+ + ))} +
+ ); +} diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx new file mode 100644 index 000000000..0eed808fa --- /dev/null +++ b/frontend/src/pages/Dashboard.jsx @@ -0,0 +1,178 @@ +import { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { Sparkles, FileWarning, CalendarClock, ArrowRight } from "lucide-react"; +import api from "../api/api.js"; +import { Panel, Loader, EmptyState } from "../components/Panel.jsx"; +import OpportunityCard from "../components/OpportunityCard.jsx"; +import StatusBadge from "../components/StatusBadge.jsx"; + +export default function Dashboard() { + const [profile, setProfile] = useState(null); + const [recommended, setRecommended] = useState([]); + const [applications, setApplications] = useState([]); + const [documents, setDocuments] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + + async function load() { + setLoading(true); + const results = await Promise.allSettled([ + api.getProfile(), + api.getRecommended(4), + api.getApplications(), + api.getDocuments(), + ]); + if (cancelled) return; + + if (results[0].status === "fulfilled") setProfile(results[0].value); + if (results[1].status === "fulfilled") setRecommended(results[1].value); + if (results[2].status === "fulfilled") setApplications(results[2].value); + if (results[3].status === "fulfilled") setDocuments(results[3].value); + setLoading(false); + } + + load(); + return () => { + cancelled = true; + }; + }, []); + + if (loading) return ; + + const missingDocs = documents.filter((d) => d.status === "missing"); + const inProgress = applications.filter( + (a) => a.status === "Missing Items" || a.status === "Analyzing" || a.status === "Not Started" + ); + const readyToSubmit = applications.filter((a) => a.status === "Ready for Review"); + + return ( +
+ {profile && ( + +
+
+

Welcome back,

+

{profile.name}

+

+ {profile.degree} in {profile.branch} ยท {profile.university} +

+
+ + + Find opportunities for me + +
+
+ )} + +
+ + + d.type).join(", ") || "All documents uploaded"} + /> +
+ +
+
+

Recommended for you

+ + View all + +
+ + {recommended.length === 0 ? ( + + + Complete profile + + } + /> + + ) : ( +
+ {recommended.map((opp) => ( + + ))} +
+ )} +
+ + {applications.length > 0 && ( +
+

Recent applications

+ +
    + {applications.slice(0, 5).map((app) => ( +
  • + +
    +

    {app.opportunity?.title}

    +

    {app.opportunity?.organization}

    +
    + + +
  • + ))} +
+
+
+ )} +
+ ); +} + +function StatCard({ icon: Icon, label, value, tone, hint }) { + const toneClasses = { + ok: "bg-ok-50 text-ok-700", + rescue: "bg-rescue-50 text-rescue-700", + danger: "bg-danger-50 text-danger-500", + }[tone]; + + return ( + +
+
+ +
+
+

{value}

+

{label}

+

{hint}

+
+
+
+ ); +} diff --git a/frontend/src/pages/Discover.jsx b/frontend/src/pages/Discover.jsx new file mode 100644 index 000000000..3d315b8f6 --- /dev/null +++ b/frontend/src/pages/Discover.jsx @@ -0,0 +1,223 @@ +import { useEffect, useMemo, useState } from "react"; +import { Search, SlidersHorizontal, Globe } from "lucide-react"; +import api from "../api/api.js"; +import { Loader, EmptyState, Panel } from "../components/Panel.jsx"; +import OpportunityCard from "../components/OpportunityCard.jsx"; + +const TYPES = [ + "All", + "Internship", + "Scholarship", + "Fellowship", + "Competition", + "Grant", + "University Program", +]; + +const SORTS = [ + { key: "match", label: "Best match" }, + { key: "deadline", label: "Deadline soonest" }, +]; + +export default function Discover() { + const [opportunities, setOpportunities] = useState([]); + const [loading, setLoading] = useState(true); + const [query, setQuery] = useState(""); + const [type, setType] = useState("All"); + const [sort, setSort] = useState("match"); + + // Live discovery states + const [discovering, setDiscovering] = useState(false); + const [discoveryMessage, setDiscoveryMessage] = useState(""); + + // Load existing opportunities + useEffect(() => { + async function load() { + try { + setLoading(true); + + const data = await api.getOpportunities(); + + setOpportunities(data); + } catch (error) { + console.error("Failed to load opportunities:", error); + } finally { + setLoading(false); + } + } + + load(); + }, []); + + // Search the real web for new opportunities + async function handleLiveDiscovery() { + try { + setDiscovering(true); + setDiscoveryMessage("Searching the web for opportunities..."); + + const data = await api.discoverOpportunities(); + + if (data.opportunities) { + setOpportunities(data.opportunities); + } + + setDiscoveryMessage( + `Found ${data.count || 0} live opportunities!` + ); + } catch (error) { + console.error("Live discovery error:", error); + + setDiscoveryMessage( + "Could not find live opportunities. Please try again." + ); + } finally { + setDiscovering(false); + } + } + + // Filter and sort opportunities + const filtered = useMemo(() => { + let list = opportunities; + + // Filter by type + if (type !== "All") { + list = list.filter((o) => o.type === type); + } + + // Search + if (query.trim()) { + const q = query.toLowerCase(); + + list = list.filter( + (o) => + (o.title || "").toLowerCase().includes(q) || + (o.organization || "").toLowerCase().includes(q) || + (o.requiredSkills || []).some((s) => + s.toLowerCase().includes(q) + ) + ); + } + + // Sort + list = [...list].sort((a, b) => { + if (sort === "deadline") { + // Put opportunities without a deadline at the end + if (!a.deadline && !b.deadline) return 0; + if (!a.deadline) return 1; + if (!b.deadline) return -1; + + return new Date(a.deadline) - new Date(b.deadline); + } + + return (b.matchPercent ?? 0) - (a.matchPercent ?? 0); + }); + + return list; + }, [opportunities, query, type, sort]); + + return ( +
+ + {/* Search + Sort */} +
+
+ + + setQuery(e.target.value)} + placeholder="Search by title, organization, or skill..." + className="w-full text-sm focus:outline-none" + /> +
+ +
+ + + +
+
+ + {/* Live Web Discovery Button */} +
+ +
+ + {/* Discovery status */} + {discoveryMessage && ( +

+ {discoveryMessage} +

+ )} + + {/* Opportunity Type Filters */} +
+ {TYPES.map((t) => ( + + ))} +
+
+ + {/* Loading */} + {loading ? ( + + ) : filtered.length === 0 ? ( + + + + ) : ( +
+ {filtered.map((opp) => ( +
+ {/* LIVE badge */} + {opp.sourceType === "live" && ( +
+ + + LIVE + +
+ )} + + +
+ ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Documents.jsx b/frontend/src/pages/Documents.jsx new file mode 100644 index 000000000..190514a19 --- /dev/null +++ b/frontend/src/pages/Documents.jsx @@ -0,0 +1,302 @@ +import { useEffect, useRef, useState } from "react"; +import { + FileText, + GraduationCap, + Award, + PenLine, + UploadCloud, + CheckCircle2, +} from "lucide-react"; + +import api from "../api/api.js"; +import { Panel, Loader } from "../components/Panel.jsx"; +import StatusBadge from "../components/StatusBadge.jsx"; + +const ICONS = { + Resume: FileText, + Transcript: GraduationCap, + Certificates: Award, + SOP: PenLine, +}; + +const DESCRIPTIONS = { + Resume: + "Your latest resume or CV, used for internships, competitions, and grants.", + + Transcript: + "Official academic transcript with your CGPA and grades.", + + Certificates: + "Course, workshop, or achievement certificates that strengthen your application.", + + SOP: + "A Statement of Purpose tailored to why you're a strong fit for an opportunity.", +}; + +export default function Documents() { + const [documents, setDocuments] = useState([]); + const [loading, setLoading] = useState(true); + const [uploadingType, setUploadingType] = useState(null); + + const fileInputRefs = useRef({}); + + useEffect(() => { + load(); + }, []); + + async function load() { + try { + setLoading(true); + + const data = await api.getDocuments(); + + setDocuments(data); + } catch (error) { + console.error("Failed to load documents:", error); + } finally { + setLoading(false); + } + } + + + function openFilePicker(type) { + const input = fileInputRefs.current[type]; + + if (input) { + input.click(); + } + } + + async function handleFileChange(type, event) { + const file = event.target.files?.[0]; + + // User cancelled file picker + if (!file) {async function handleRemove(type) { + const confirmed = window.confirm( + `Remove your ${type}? You can upload it again later.` + ); + + if (!confirmed) return; + + try { + setUploadingType(type); + + await api.removeDocument(type); + + await load(); + } catch (error) { + console.error("Remove failed:", error); + + alert( + error?.response?.data?.message || + "Could not remove the document." + ); + } finally { + setUploadingType(null); + } +} + return; + } + + // Validate file type + const allowedTypes = [ + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ]; + + const allowedExtensions = [".pdf", ".doc", ".docx"]; + + const extension = file.name + .substring(file.name.lastIndexOf(".")) + .toLowerCase(); + + if ( + !allowedTypes.includes(file.type) && + !allowedExtensions.includes(extension) + ) { + alert("Please upload a PDF, DOC, or DOCX file."); + event.target.value = ""; + return; + } + + // 10 MB limit + if (file.size > 10 * 1024 * 1024) { + alert("File size must be less than 10 MB."); + event.target.value = ""; + return; + } + + try { + setUploadingType(type); + + await api.uploadDocument(type, file); + + await load(); + } catch (error) { + console.error("Upload failed:", error); + + alert( + error?.response?.data?.message || + "File upload failed. Please try again." + ); + } finally { + setUploadingType(null); + + // Allows selecting the same file again + event.target.value = ""; + } + } + + if (loading) { + return ; + } + + return ( +
+ + +

+ Document Vault +

+ +

+ Keep these up to date so the Rescue Agent can auto-fill + applications instantly. +

+
+ +
+ + {documents.map((doc) => { + const Icon = ICONS[doc.type] || FileText; + + const isMissing = doc.status === "missing"; + + const isUploading = + uploadingType === doc.type; + + return ( + + +
+ +
+ +
+ +
+ +
+ +

+ {doc.type} +

+ + + +
+ +

+ {DESCRIPTIONS[doc.type]} +

+ + {doc.status === "uploaded" ? ( +
+ +

+ + + + {doc.fileName} + +

+ +
+ + + + + +
+ +
+ ) : ( + + )} + + {/* Hidden file input */} + { + fileInputRefs.current[doc.type] = + element; + }} + type="file" + accept=".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document" + className="hidden" + onChange={(event) => + handleFileChange( + doc.type, + event + ) + } + /> + +
+
+ +
+ ); + })} + +
+ +

+ Supported formats: PDF, DOC, DOCX ยท Maximum size: 10 MB +

+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/OpportunityDetails.jsx b/frontend/src/pages/OpportunityDetails.jsx new file mode 100644 index 000000000..41d963e92 --- /dev/null +++ b/frontend/src/pages/OpportunityDetails.jsx @@ -0,0 +1,206 @@ +import { useEffect, useState } from "react"; +import { useParams, useNavigate, Link } from "react-router-dom"; +import { + CalendarClock, + MapPin, + Coins, + ArrowLeft, + LifeBuoy, + ListChecks, +} from "lucide-react"; +import api from "../api/api.js"; +import { Panel, Loader } from "../components/Panel.jsx"; +import MatchBadge from "../components/MatchBadge.jsx"; + +export default function OpportunityDetails() { + const { id } = useParams(); + const navigate = useNavigate(); + + const [opportunity, setOpportunity] = useState(null); + const [loading, setLoading] = useState(true); + const [starting, setStarting] = useState(false); + + useEffect(() => { + async function load() { + setLoading(true); + + const data = await api.getOpportunity(id); + + setOpportunity(data); + setLoading(false); + } + + load(); + }, [id]); + + async function handleStartRescue() { + setStarting(true); + + try { + const app = await api.startApplication(id); + navigate(`/applications/${app._id}`); + } finally { + setStarting(false); + } + } + + // Safely format the deadline + function formatDeadline(deadline) { + // No deadline available + if (!deadline) { + return "Not specified"; + } + + const date = new Date(deadline); + + // Invalid date or Unix epoch date + if (isNaN(date.getTime()) || date.getFullYear() <= 1970) { + return "Not specified"; + } + + return date.toLocaleDateString("en-IN", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + } + + if (loading) { + return ; + } + + if (!opportunity) { + return

Opportunity not found.

; + } + + return ( +
+ + + Back to Discover + + + +
+
+ + {opportunity.type} + + +

+ {opportunity.title} +

+ +

+ {opportunity.organization} +

+
+ + {opportunity.matchPercent !== null && ( + + )} +
+ +

+ {opportunity.description} +

+ +
+ + + + + +
+
+ + +

+ Eligibility +

+ +

+ {opportunity.eligibility || "Check official opportunity page"} +

+ + {opportunity.minCgpa > 0 && ( +

+ Minimum CGPA required: {opportunity.minCgpa} +

+ )} +
+ + +

+ + Required documents +

+ +
+ {opportunity.requiredDocuments?.map((doc) => ( + + {doc} + + ))} +
+
+ + +
+
+

+ Ready to apply? +

+ +

+ The Rescue Agent will open the form, auto-fill what it can from + your profile, and flag anything missing before you submit. +

+
+ + +
+
+
+ ); +} + +function InfoBlock({ icon: Icon, label, value }) { + return ( +
+

+ + {label} +

+ +

+ {value} +

+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Profile.jsx b/frontend/src/pages/Profile.jsx new file mode 100644 index 000000000..31ea27c0f --- /dev/null +++ b/frontend/src/pages/Profile.jsx @@ -0,0 +1,221 @@ +import { useEffect, useState } from "react"; +import { Save, CheckCircle2 } from "lucide-react"; +import api from "../api/api.js"; +import { Panel, Loader } from "../components/Panel.jsx"; + +const OPPORTUNITY_TYPES = [ + "Internship", + "Scholarship", + "Fellowship", + "Competition", + "Grant", + "University Program", +]; + +const emptyForm = { + name: "", + email: "", + university: "", + degree: "", + branch: "", + year: 1, + cgpa: "", + skills: "", + interests: "", + experience: "", + preferredTypes: [], +}; + +export default function Profile() { + const [form, setForm] = useState(emptyForm); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [saved, setSaved] = useState(false); + + useEffect(() => { + async function load() { + try { + const profile = await api.getProfile(); + setForm({ + ...profile, + skills: (profile.skills || []).join(", "), + interests: (profile.interests || []).join(", "), + }); + } catch (e) { + // No profile yet - keep the empty form + } finally { + setLoading(false); + } + } + load(); + }, []); + + function updateField(key, value) { + setForm((f) => ({ ...f, [key]: value })); + setSaved(false); + } + + function toggleType(type) { + setForm((f) => { + const has = f.preferredTypes.includes(type); + return { + ...f, + preferredTypes: has + ? f.preferredTypes.filter((t) => t !== type) + : [...f.preferredTypes, type], + }; + }); + setSaved(false); + } + + async function handleSave(e) { + e.preventDefault(); + setSaving(true); + try { + const payload = { + ...form, + year: Number(form.year) || 1, + cgpa: Number(form.cgpa) || 0, + skills: form.skills + .split(",") + .map((s) => s.trim()) + .filter(Boolean), + interests: form.interests + .split(",") + .map((s) => s.trim()) + .filter(Boolean), + }; + await api.saveProfile(payload); + setSaved(true); + } finally { + setSaving(false); + } + } + + if (loading) return ; + + return ( +
+ +

Basic information

+
+ updateField("name", v)} required /> + updateField("email", v)} + required + /> + updateField("university", v)} + /> + updateField("degree", v)} /> + updateField("branch", v)} /> + updateField("year", v)} + /> + updateField("cgpa", v)} + /> +
+
+ + +

Skills & interests

+
+ updateField("skills", v)} + placeholder="e.g. JavaScript, React, Python, Machine Learning" + /> + updateField("interests", v)} + placeholder="e.g. Web Development, AI/ML, Research" + /> +
+ +