Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1cf49b0a2 | ||
|
|
1ab41bf3ec | ||
|
|
f3cfacfd11 | ||
|
|
8db07e5d21 | ||
|
|
38a9a11772 | ||
|
|
042d83f6d6 | ||
|
|
6d063caada | ||
|
|
043efc0dd3 | ||
|
|
429e3d9785 | ||
|
|
ad6ba2199a | ||
|
|
59e501cda8 | ||
|
|
18f6e2732e | ||
|
|
6ae0c91539 |
@@ -0,0 +1,24 @@
|
||||
# Agent Rules
|
||||
|
||||
## Model Selection Advisor
|
||||
|
||||
Whenever you receive a new user request or begin a new task, you MUST invoke the `model_advisor` skill to evaluate the optimal model and tool strategy before proceeding with any significant planning or code modifications.
|
||||
## OKF Knowledge Base Access
|
||||
|
||||
Whenever the user asks you to search, query, access, or update their "knowledge", "OKF", or "Wissensdatenbank", you MUST use the custom `ctx-guard` CLI tool.
|
||||
The tool is executed from the Orchestrator's virtual environment: `/home/andre/coden/Orchestrator/.venv/bin/ctx-guard knowledge`.
|
||||
|
||||
**Available commands:**
|
||||
- `search`: e.g., `/home/andre/coden/Orchestrator/.venv/bin/ctx-guard knowledge search "query" --limit 5`
|
||||
- `capture`: e.g., `/home/andre/coden/Orchestrator/.venv/bin/ctx-guard knowledge capture "Note" --context privat`
|
||||
- `status`: e.g., `/home/andre/coden/Orchestrator/.venv/bin/ctx-guard knowledge status`
|
||||
- `ingest`: e.g., `/home/andre/coden/Orchestrator/.venv/bin/ctx-guard knowledge ingest`
|
||||
|
||||
Always run this command directly using your `run_command` tool to retrieve or update knowledge before answering the user's queries.
|
||||
|
||||
## PR Workflow Rule
|
||||
|
||||
All code changes or configurations made by agents MUST go through a Pull Request / Merge Request.
|
||||
- You are not allowed to do quicksaves directly to the main branch.
|
||||
- You must create a new branch, commit your changes, push to the remote, and open a PR.
|
||||
- After creating the PR, you must review it for viability and only merge it once confirmed.
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"last_updated": "2026-07-16T20:12:30Z",
|
||||
"providers": {
|
||||
"codex": {
|
||||
"models": []
|
||||
},
|
||||
"antigravity": {
|
||||
"models": []
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# check_quotas.sh - Modular script to check quotas
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
check_codex_quota() {
|
||||
if command -v codex &> /dev/null; then
|
||||
echo "[Codex Quota]"
|
||||
codex quota || echo "Failed to fetch Codex quota"
|
||||
fi
|
||||
}
|
||||
|
||||
check_agy_quota() {
|
||||
if command -v agy &> /dev/null; then
|
||||
echo "[Antigravity Quota]"
|
||||
agy quota || echo "Failed to fetch Antigravity quota"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "--- Current Quotas ---"
|
||||
check_codex_quota
|
||||
check_agy_quota
|
||||
echo "----------------------"
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# install_clis.sh - Install configured CLIs
|
||||
set -e
|
||||
|
||||
echo "Starting CLI installations..."
|
||||
|
||||
echo "Checking codex..."
|
||||
if command -v codex &> /dev/null; then
|
||||
echo "codex is already installed. Version: $(codex --version 2>/dev/null || echo 'unknown')"
|
||||
else
|
||||
echo "Installing codex via curl..."
|
||||
curl -fsSL https://chatgpt.com/codex/install.sh | sh
|
||||
echo "codex installed successfully."
|
||||
fi
|
||||
|
||||
echo "Checking agy (antigravity)..."
|
||||
if command -v agy &> /dev/null; then
|
||||
echo "agy is already installed."
|
||||
else
|
||||
echo "Warning: agy is not installed. Please install it manually."
|
||||
fi
|
||||
|
||||
echo "All CLIs are set up."
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# setup_apis.sh - Trigger browser login flows
|
||||
set -e
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
CLIS=("codex" "agy")
|
||||
|
||||
echo "Starting API Configuration via Browser Login..."
|
||||
|
||||
for cli in "${CLIS[@]}"; do
|
||||
echo "Configuring $cli..."
|
||||
if command -v "$cli" &> /dev/null; then
|
||||
if [ "$cli" == "agy" ]; then
|
||||
"$cli" auth
|
||||
else
|
||||
"$cli" login
|
||||
fi
|
||||
echo "$cli configured."
|
||||
else
|
||||
echo "Warning: $cli not installed. Please run install_clis.sh first."
|
||||
fi
|
||||
done
|
||||
echo "API setup complete."
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# update_models_config.sh - Fetches latest models and quotas
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_FILE="$DIR/../models_config.json"
|
||||
|
||||
echo "{" > "$CONFIG_FILE"
|
||||
echo " \"last_updated\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"," >> "$CONFIG_FILE"
|
||||
|
||||
# Logic to update config
|
||||
echo " \"providers\": {" >> "$CONFIG_FILE"
|
||||
|
||||
# Codex Models
|
||||
echo " \"codex\": {" >> "$CONFIG_FILE"
|
||||
if command -v codex &> /dev/null; then
|
||||
echo " \"models\": $(codex models --json 2>/dev/null || echo '[]')" >> "$CONFIG_FILE"
|
||||
else
|
||||
echo " \"models\": []" >> "$CONFIG_FILE"
|
||||
fi
|
||||
echo " }," >> "$CONFIG_FILE"
|
||||
|
||||
# Antigravity Models
|
||||
echo " \"antigravity\": {" >> "$CONFIG_FILE"
|
||||
if command -v agy &> /dev/null; then
|
||||
echo " \"models\": $(agy models --json 2>/dev/null || echo '[]')" >> "$CONFIG_FILE"
|
||||
else
|
||||
echo " \"models\": []" >> "$CONFIG_FILE"
|
||||
fi
|
||||
echo " }" >> "$CONFIG_FILE"
|
||||
|
||||
echo " }" >> "$CONFIG_FILE"
|
||||
echo "}" >> "$CONFIG_FILE"
|
||||
|
||||
echo "Models config updated at $CONFIG_FILE"
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: model_advisor
|
||||
description: Evaluates a task and orchestrates execution across available CLI models based on cost, quotas, and strengths.
|
||||
---
|
||||
|
||||
# Instructions
|
||||
|
||||
You have been invoked to act as a central orchestrator. You must evaluate the task, check available models and quotas, and delegate the work via CLI.
|
||||
|
||||
## Step 1: Check Quotas & Models
|
||||
Run the following scripts from your workspace:
|
||||
- `.agents/scripts/check_quotas.sh`
|
||||
- Review `.agents/models_config.json` to see currently available models.
|
||||
|
||||
## Step 2: Evaluate the Task
|
||||
Consider the following criteria:
|
||||
1. **Complexity & Context**: Does it require massive context (Gemini Pro) or deep nuance (Claude Sonnet)?
|
||||
2. **Cost & Quota**: Are quotas low? Use cheaper models if the task is simple.
|
||||
|
||||
## Step 3: Output a Recommendation & Delegate
|
||||
If the task is best suited for a CLI-driven model (e.g., Codex or Antigravity with a specific model), you MUST proactively write and execute the terminal command to delegate the task.
|
||||
|
||||
Example output:
|
||||
> [!TIP]
|
||||
> **Model Advisor Recommendation**: The quota for Antigravity is healthy. I am delegating this refactoring task to Claude 3.5 Sonnet via the CLI.
|
||||
|
||||
Example command you might run:
|
||||
```bash
|
||||
agy run --model claude-3-5-sonnet "refactor the payment gateway"
|
||||
```
|
||||
|
||||
If the task is best suited for the *current* IDE model, advise the user to proceed normally without CLI delegation. If it's suited for an IDE model you are *not* currently using, advise the user to switch models manually in the IDE.
|
||||
|
||||
## Step 4: Proceed
|
||||
Once the CLI command completes, or if you decided to handle it locally, complete the user's request.
|
||||
@@ -106,3 +106,6 @@ All filenames must be OneDrive-safe:
|
||||
- No single quotes or backticks
|
||||
- Replace umlauts in filenames: ä→ae, ö→oe, ü→ue, ß→ss
|
||||
- Use hyphens instead of special characters
|
||||
|
||||
## Agent Restrictions
|
||||
- ONLY the human user is allowed to use `sudo`. Agents MUST NOT attempt to use `sudo` or request `sudo` access.
|
||||
|
||||
@@ -28,11 +28,28 @@ On this machine, PowerShell is the shell. Use `;` to chain commands, NOT `&&` (w
|
||||
## Key Decisions
|
||||
|
||||
- **Bahn repos stay untouched** as independent remotes. The monorepo organizes them but doesn't own them.
|
||||
- **External repos are READ-ONLY in the monorepo.** Never modify code, add tests, or create MRs for: aisupport, Confluence_Bot, O2C-Harness, db-planet-mcp-server, wissensdatenbank. Only COMPONENT.md (monorepo wrapper metadata) is allowed.
|
||||
- **Jury-Voting** is dhive (not privat). Will be transferred to dhive GitLab later.
|
||||
- **NoteGraph** needs a complete redo — superseded by the monorepo knowledge store.
|
||||
- **symphony** is a read-only reference (pinned).
|
||||
- Secrets go in `.secrets` (gitignored) or encrypted via git-crypt per context.
|
||||
|
||||
## Ownership Rules
|
||||
|
||||
| Repo | Owner | Editable? |
|
||||
|------|-------|-----------|
|
||||
| bahn/aisupport | DB GitLab (Team) | ❌ read-only |
|
||||
| bahn/Analyse-O2C-C2S | Andre (DB GitLab) | ✅ |
|
||||
| bahn/awesome-bahn-mcp-servers | monorepo-internal | ✅ |
|
||||
| bahn/Confluence_Bot | DB GitLab (Team) | ❌ read-only |
|
||||
| bahn/db-planet-mcp-server | DB GitLab (Team) | ❌ read-only |
|
||||
| bahn/O2C-Harness | DB GitLab (Team) | ❌ read-only |
|
||||
| bahn/project-audit | Andre (DB GitLab) | ✅ |
|
||||
| bahn/wissensdatenbank | DB GitLab (Team) | ❌ read-only (subtree) |
|
||||
| dhive/* | Andre (GitHub) | ✅ |
|
||||
| privat/* | Andre (GitHub) | ✅ |
|
||||
| shared/* | Andre (GitHub/internal) | ✅ |
|
||||
|
||||
## Python Environment
|
||||
|
||||
The monorepo CLI tool is at `shared/tools/monorepo-cli/`. Tests run with:
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# New Device Setup Guide
|
||||
|
||||
This guide explains how to get your full Monorepo, AI Orchestrator, CLI agents, and Knowledge Base (`ctx-guard`) up and running on a brand new device.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Ensure the following tools are installed on your new device:
|
||||
- `git`
|
||||
- `python3.12` or newer
|
||||
- `pipx`
|
||||
- `curl`
|
||||
|
||||
## Step 1: Clone the Monorepo
|
||||
|
||||
Clone the Orchestrator to your preferred workspace directory (e.g., `~/coden`):
|
||||
```bash
|
||||
mkdir -p ~/coden
|
||||
cd ~/coden
|
||||
git clone https://git.d-hive.de/ankn/Orchestrator.git
|
||||
cd Orchestrator
|
||||
```
|
||||
|
||||
## Step 2: Set up the Knowledge CLI (`ctx-guard`)
|
||||
|
||||
The `ctx-guard` tool requires a Python virtual environment.
|
||||
|
||||
```bash
|
||||
cd ~/coden/Orchestrator
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
# Install the monorepo-cli package in editable mode
|
||||
cd shared/tools/monorepo-cli
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
To register the new machine to your federation topology, run the built-in onboard command:
|
||||
```bash
|
||||
# Replace <machine_name> with your device name
|
||||
ctx-guard onboard <machine_name> "privat,dhive,bahn,shared"
|
||||
```
|
||||
|
||||
## Step 3: Install the AI CLIs
|
||||
|
||||
Install the Codex CLI:
|
||||
```bash
|
||||
curl -fsSL https://chatgpt.com/codex/install.sh | sh
|
||||
```
|
||||
|
||||
Install the Antigravity CLI:
|
||||
```bash
|
||||
curl -fsSL https://antigravity.google/cli/install.sh | bash
|
||||
```
|
||||
|
||||
Run `codex login` and `agy login` in your terminal to authenticate via the browser.
|
||||
|
||||
## Step 4: AI Agent Customizations
|
||||
|
||||
Because the `.agents` folder is now version-controlled inside the Orchestrator monorepo (`~/coden/Orchestrator/.agents`), your AI agents will *automatically* discover your custom skills (like `model_advisor`) and rules (like strictly using `ctx-guard` for knowledge tasks).
|
||||
|
||||
To ensure your models catalog is kept up to date, you can manually run the updater script:
|
||||
```bash
|
||||
cd ~/coden/Orchestrator
|
||||
./.agents/scripts/update_models_config.sh
|
||||
```
|
||||
|
||||
*(Note: In your primary environment, an agent has scheduled this as a daily cron job. You can instruct the agent to set up the same schedule on the new device by typing `/schedule daily run .agents/scripts/update_models_config.sh` in the chat).*
|
||||
|
||||
## Step 5: Sync Federation Repositories
|
||||
|
||||
Finally, pull down the rest of your private and shared federation repositories from the `dhive` GitLab:
|
||||
```bash
|
||||
ctx-guard fed-sync --context dhive
|
||||
```
|
||||
|
||||
Your environment is now fully identical to your original machine!
|
||||
Binary file not shown.
Generated
+1141
-1
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"start:server": "node server/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -22,6 +23,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
@@ -29,6 +32,8 @@
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"vite": "^8.0.4"
|
||||
"jsdom": "^29.1.1",
|
||||
"vite": "^8.0.4",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import crypto from 'crypto';
|
||||
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const TEST_DATA_DIR = join(__dirname, '..', 'data-test');
|
||||
const TEST_DATA_FILE = join(TEST_DATA_DIR, 'access-requests.json');
|
||||
|
||||
function createTestApp() {
|
||||
mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
writeFileSync(TEST_DATA_FILE, '[]', 'utf-8');
|
||||
|
||||
const sessions = new Map();
|
||||
const ADMIN_USER = 'admin';
|
||||
const ADMIN_PASSWORD = 'testpass';
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(cookieParser('test-secret'));
|
||||
|
||||
function createSession(user) {
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
sessions.set(token, { user, createdAt: Date.now() });
|
||||
return token;
|
||||
}
|
||||
|
||||
function getSession(token) {
|
||||
return sessions.get(token) || null;
|
||||
}
|
||||
|
||||
function requireAuth(req, res, next) {
|
||||
const token = req.cookies?.kiq_session;
|
||||
if (!token) return res.status(401).json({ error: 'Nicht authentifiziert' });
|
||||
const session = getSession(token);
|
||||
if (!session) return res.status(401).json({ error: 'Sitzung abgelaufen' });
|
||||
req.user = session.user;
|
||||
next();
|
||||
}
|
||||
|
||||
app.get('/api/health', (_req, res) => {
|
||||
res.json({ status: 'ok', time: new Date().toISOString() });
|
||||
});
|
||||
|
||||
app.post('/api/login', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
if (username === ADMIN_USER && password === ADMIN_PASSWORD) {
|
||||
const token = createSession({ username, role: 'admin' });
|
||||
res.cookie('kiq_session', token, { httpOnly: true, sameSite: 'strict' });
|
||||
return res.json({ user: { username, role: 'admin' } });
|
||||
}
|
||||
return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
|
||||
});
|
||||
|
||||
app.post('/api/logout', (req, res) => {
|
||||
const token = req.cookies?.kiq_session;
|
||||
if (token) sessions.delete(token);
|
||||
res.clearCookie('kiq_session');
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.get('/api/me', (req, res) => {
|
||||
const token = req.cookies?.kiq_session;
|
||||
if (!token) return res.json({ user: null });
|
||||
const session = getSession(token);
|
||||
if (!session) return res.json({ user: null });
|
||||
res.json({ user: session.user });
|
||||
});
|
||||
|
||||
app.post('/api/access-requests', (req, res) => {
|
||||
const { user_data } = req.body;
|
||||
if (!user_data || !user_data.name || !user_data.email || !user_data.company) {
|
||||
return res.status(400).json({ error: 'Pflichtfelder fehlen' });
|
||||
}
|
||||
const requests = JSON.parse(readFileSync(TEST_DATA_FILE, 'utf-8'));
|
||||
const newRequest = {
|
||||
id: crypto.randomUUID(),
|
||||
user_data,
|
||||
status: 'pending',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
requests.push(newRequest);
|
||||
writeFileSync(TEST_DATA_FILE, JSON.stringify(requests, null, 2), 'utf-8');
|
||||
res.status(201).json({ ok: true, id: newRequest.id });
|
||||
});
|
||||
|
||||
app.get('/api/access-requests', requireAuth, (_req, res) => {
|
||||
const requests = JSON.parse(readFileSync(TEST_DATA_FILE, 'utf-8'));
|
||||
requests.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||
res.json(requests);
|
||||
});
|
||||
|
||||
app.put('/api/access-requests/:id', requireAuth, (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { status } = req.body;
|
||||
if (!['pending', 'approved', 'rejected'].includes(status)) {
|
||||
return res.status(400).json({ error: 'Ungültiger Status' });
|
||||
}
|
||||
const requests = JSON.parse(readFileSync(TEST_DATA_FILE, 'utf-8'));
|
||||
const idx = requests.findIndex(r => r.id === id);
|
||||
if (idx === -1) return res.status(404).json({ error: 'Anfrage nicht gefunden' });
|
||||
requests[idx].status = status;
|
||||
writeFileSync(TEST_DATA_FILE, JSON.stringify(requests, null, 2), 'utf-8');
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
// --- Test Setup ---
|
||||
|
||||
let server;
|
||||
let baseUrl;
|
||||
|
||||
beforeAll(async () => {
|
||||
const app = createTestApp();
|
||||
await new Promise((resolve) => {
|
||||
server = app.listen(0, () => {
|
||||
const addr = server.address();
|
||||
baseUrl = `http://localhost:${addr.port}`;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (server) server.close();
|
||||
if (existsSync(TEST_DATA_DIR)) rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
writeFileSync(TEST_DATA_FILE, '[]', 'utf-8');
|
||||
});
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
describe('KIQ Server API', () => {
|
||||
describe('GET /api/health', () => {
|
||||
it('returns ok status', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/health`);
|
||||
expect(res.status).toBe(200);
|
||||
const data = await res.json();
|
||||
expect(data.status).toBe('ok');
|
||||
expect(data.time).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/login', () => {
|
||||
it('accepts valid credentials', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'admin', password: 'testpass' }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const data = await res.json();
|
||||
expect(data.user.username).toBe('admin');
|
||||
expect(data.user.role).toBe('admin');
|
||||
});
|
||||
|
||||
it('rejects invalid credentials', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'admin', password: 'wrong' }),
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it('sets session cookie on success', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'admin', password: 'testpass' }),
|
||||
});
|
||||
const setCookie = res.headers.get('set-cookie');
|
||||
expect(setCookie).toContain('kiq_session');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/me', () => {
|
||||
it('returns null user without session', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/me`);
|
||||
expect(res.status).toBe(200);
|
||||
const data = await res.json();
|
||||
expect(data.user).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/access-requests', () => {
|
||||
it('creates a new access request', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/access-requests`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
user_data: {
|
||||
name: 'Max Mustermann',
|
||||
email: 'max@example.com',
|
||||
company: 'Test GmbH',
|
||||
role: 'Developer',
|
||||
purpose: 'Testing',
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
const data = await res.json();
|
||||
expect(data.ok).toBe(true);
|
||||
expect(data.id).toBeTruthy();
|
||||
});
|
||||
|
||||
it('rejects when name is missing', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/access-requests`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_data: { email: 'x@x.com', company: 'X' } }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it('rejects when email is missing', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/access-requests`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_data: { name: 'Max', company: 'X' } }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it('rejects when company is missing', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/access-requests`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_data: { name: 'Max', email: 'x@x.com' } }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it('rejects empty body', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/access-requests`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/access-requests (admin)', () => {
|
||||
it('rejects without auth', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/access-requests`);
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /api/access-requests/:id (admin)', () => {
|
||||
it('rejects without auth', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/access-requests/fake-id`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: 'approved' }),
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
stages:
|
||||
- build
|
||||
- deploy
|
||||
|
||||
variables:
|
||||
DEPLOY_HOST: "217.160.174.2"
|
||||
DEPLOY_USER: "root"
|
||||
DEPLOY_PATH: "/opt/andreknie"
|
||||
|
||||
.ssh_setup: &ssh_setup
|
||||
before_script:
|
||||
- apt-get update -qq && apt-get install -y openssh-client rsync
|
||||
- install -d -m 700 ~/.ssh
|
||||
- echo "$VPS_SSH_KEY" > ~/.ssh/deploy_key
|
||||
- chmod 600 ~/.ssh/deploy_key
|
||||
- ssh-keyscan -H "${DEPLOY_HOST}" > ~/.ssh/known_hosts
|
||||
- chmod 600 ~/.ssh/known_hosts
|
||||
|
||||
build-frontend:
|
||||
stage: build
|
||||
image: node:20
|
||||
script:
|
||||
- cd privat/CV/andreknie.de
|
||||
- npm ci
|
||||
- npm run build
|
||||
artifacts:
|
||||
paths:
|
||||
- privat/CV/andreknie.de/dist/
|
||||
expire_in: 1 day
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"'
|
||||
changes:
|
||||
- "privat/CV/andreknie.de/**/*"
|
||||
|
||||
deploy-andreknie:
|
||||
stage: deploy
|
||||
image: alpine:latest
|
||||
<<: *ssh_setup
|
||||
dependencies:
|
||||
- build-frontend
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"'
|
||||
changes:
|
||||
- "privat/CV/andreknie.de/**/*"
|
||||
script:
|
||||
- ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes "${DEPLOY_USER}@${DEPLOY_HOST}" "install -d -m 755 ${DEPLOY_PATH}/site ${DEPLOY_PATH}/server ${DEPLOY_PATH}/server/data"
|
||||
- cd privat/CV/andreknie.de
|
||||
- |
|
||||
cat <<EOF > .env
|
||||
PORT=3003
|
||||
NODE_ENV=production
|
||||
BASE_URL=https://andreknie.de
|
||||
SMTP_HOST=$SMTP_HOST
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=$SMTP_USER
|
||||
SMTP_PASS=$SMTP_PASS
|
||||
STAKEHOLDER_EMAIL=kontakt@d-hive.de
|
||||
EOF
|
||||
- rsync -az --delete -e "ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" dist/ "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/site/"
|
||||
- rsync -az --delete -e "ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" server/ "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/server/"
|
||||
- rsync -az -e "ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" package.json package-lock.json Dockerfile docker-compose.yml Caddyfile .env "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/"
|
||||
- ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes "${DEPLOY_USER}@${DEPLOY_HOST}" "cd ${DEPLOY_PATH} && docker compose up -d --build"
|
||||
# Note: Caddy configuration needs to be reloaded manually if it's the first time.
|
||||
- echo "Deployment finished. Remember to include ${DEPLOY_PATH}/Caddyfile in your global /etc/caddy/Caddyfile and reload caddy if you haven't yet!"
|
||||
@@ -1,5 +1,9 @@
|
||||
andreknie.de {
|
||||
root * /opt/andreknie/site
|
||||
www.andreknie.de {
|
||||
redir https://andreknie.de{uri}
|
||||
}
|
||||
|
||||
localhost, andreknie.de {
|
||||
root * /dist
|
||||
encode zstd gzip
|
||||
|
||||
header {
|
||||
@@ -8,10 +12,11 @@ andreknie.de {
|
||||
X-Frame-Options "DENY"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
Permissions-Policy "camera=(), geolocation=(), microphone=()"
|
||||
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://stats.andreknie.de http://localhost:3000; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self' https://stats.andreknie.de http://localhost:3000"
|
||||
}
|
||||
|
||||
handle /api/* {
|
||||
reverse_proxy localhost:3003
|
||||
reverse_proxy backend:3003
|
||||
}
|
||||
|
||||
@assets path /assets/*
|
||||
@@ -26,3 +31,7 @@ andreknie.de {
|
||||
file_server
|
||||
}
|
||||
}
|
||||
|
||||
stats.andreknie.de {
|
||||
reverse_proxy umami:3000
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
items:
|
||||
- value: "50+"
|
||||
label: "Projekte"
|
||||
- value: "100+"
|
||||
label: "öffentliche Beiträge"
|
||||
- value: "13+"
|
||||
label: "Jahre KI"
|
||||
- value: "60+"
|
||||
|
||||
@@ -8,3 +8,42 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
restart: unless-stopped
|
||||
|
||||
caddy:
|
||||
image: caddy:alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./site:/dist
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
umami-db:
|
||||
image: postgres:15-alpine
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- umami_db_data:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
|
||||
umami:
|
||||
image: ghcr.io/umami-software/umami:postgresql-latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- PORT=3000
|
||||
depends_on:
|
||||
- umami-db
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
umami_db_data:
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logos/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/logos/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Dr. André Knie — KI, Transformation & Leadership</title>
|
||||
<meta name="description" content="Nüchterne KI-Expertise für Mittelstand, Verwaltung und Hochschulen. Souverän, praktisch, evidenzbasiert." />
|
||||
<!-- Umami Analytics -->
|
||||
<script async src="http://localhost:3000/script.js" data-website-id="dd098c2f-dbc7-411b-a391-47e5e60eb6db"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Generated
+179
-153
@@ -8,6 +8,7 @@
|
||||
"name": "andreknie-de",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"async-mutex": "^0.5.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.5.2",
|
||||
@@ -19,6 +20,7 @@
|
||||
"nodemailer": "^9.0.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-helmet-async": "^3.0.0",
|
||||
"react-router-dom": "^7.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -534,9 +536,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
|
||||
"integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -917,9 +919,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -937,9 +936,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -957,9 +953,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -977,9 +970,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -997,9 +987,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1017,9 +1004,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1268,9 +1252,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz",
|
||||
"integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==",
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz",
|
||||
"integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1510,6 +1494,15 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/async-mutex": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
|
||||
"integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
@@ -1521,9 +1514,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.42",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz",
|
||||
"integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.0.tgz",
|
||||
"integrity": "sha512-oCu2wfipvX3AePSgmOuKkIywOu+8n9psz7hXYmk56ghpu3+7KzNIBopaOs4c9BrtdnTtW30unG9GTfHo7EwERQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -1594,9 +1587,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz",
|
||||
"integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==",
|
||||
"version": "4.28.7",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
|
||||
"integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1614,10 +1607,10 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.42",
|
||||
"caniuse-lite": "^1.0.30001800",
|
||||
"electron-to-chromium": "^1.5.387",
|
||||
"node-releases": "^2.0.50",
|
||||
"baseline-browser-mapping": "^2.10.44",
|
||||
"caniuse-lite": "^1.0.30001806",
|
||||
"electron-to-chromium": "^1.5.393",
|
||||
"node-releases": "^2.0.51",
|
||||
"update-browserslist-db": "^1.2.3"
|
||||
},
|
||||
"bin": {
|
||||
@@ -1666,9 +1659,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001803",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz",
|
||||
"integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==",
|
||||
"version": "1.0.30001806",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
|
||||
"integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1906,9 +1899,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.389",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz",
|
||||
"integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==",
|
||||
"version": "1.5.395",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz",
|
||||
"integrity": "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -1953,9 +1946,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz",
|
||||
"integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",
|
||||
"integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2001,9 +1994,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz",
|
||||
"integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==",
|
||||
"version": "10.7.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz",
|
||||
"integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
@@ -2271,11 +2264,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
|
||||
"integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.0.tgz",
|
||||
"integrity": "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"ip-address": "^10.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2623,9 +2617,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/helmet": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.2.0.tgz",
|
||||
"integrity": "sha512-DRgTIUgnWcJ62KyarxxziuqYxKGnR6Rgg19BlbucN/dpmJbl1XOit6qvoOX0ZT+HhWe5OUVhU/a1zpGyc1xA0Q==",
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz",
|
||||
"integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -2736,6 +2730,15 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
@@ -2810,7 +2813,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
@@ -2967,9 +2969,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
|
||||
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
@@ -2983,23 +2985,23 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.32.0",
|
||||
"lightningcss-darwin-arm64": "1.32.0",
|
||||
"lightningcss-darwin-x64": "1.32.0",
|
||||
"lightningcss-freebsd-x64": "1.32.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||
"lightningcss-linux-x64-musl": "1.32.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||
"lightningcss-android-arm64": "1.33.0",
|
||||
"lightningcss-darwin-arm64": "1.33.0",
|
||||
"lightningcss-darwin-x64": "1.33.0",
|
||||
"lightningcss-freebsd-x64": "1.33.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.33.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.33.0",
|
||||
"lightningcss-linux-arm64-musl": "1.33.0",
|
||||
"lightningcss-linux-x64-gnu": "1.33.0",
|
||||
"lightningcss-linux-x64-musl": "1.33.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.33.0",
|
||||
"lightningcss-win32-x64-msvc": "1.33.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
|
||||
"integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3018,9 +3020,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
|
||||
"integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3039,9 +3041,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
|
||||
"integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3060,9 +3062,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
|
||||
"integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3081,9 +3083,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||||
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
|
||||
"integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -3102,16 +3104,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
|
||||
"integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3126,16 +3125,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
|
||||
"integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3150,16 +3146,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
|
||||
"integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3174,16 +3167,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
|
||||
"integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3198,9 +3188,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
|
||||
"integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3219,9 +3209,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
|
||||
"integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3255,6 +3245,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -3266,9 +3268,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz",
|
||||
"integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==",
|
||||
"version": "1.25.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.25.0.tgz",
|
||||
"integrity": "sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
@@ -3296,9 +3298,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "18.0.5",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz",
|
||||
"integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==",
|
||||
"version": "18.0.7",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-18.0.7.tgz",
|
||||
"integrity": "sha512-iDVQ5ldaiKXn6b2JroX5kgRfmwgqolW7NpaEzTl1k/2Zh1njIEN9yniyLV/mOvWwtsE8OGgkjsCYvijuPk1dtA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
@@ -3402,9 +3404,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.15",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
||||
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3477,9 +3479,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
|
||||
"integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
|
||||
"integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
@@ -3641,9 +3643,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.16",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
||||
"version": "8.5.22",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
|
||||
"integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3661,7 +3663,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.12",
|
||||
"nanoid": "^3.3.16",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -3719,9 +3721,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pure-rand": {
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.1.tgz",
|
||||
"integrity": "sha512-c58R2+SPFcSIPXoU834QN/KPDDOSd8sXcSrqf6e83Me6Rrp1EYkxukkjXMVrKvKaADs1SOyNkWdfvLf6zY8qLQ==",
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz",
|
||||
"integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3780,24 +3782,44 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
|
||||
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
||||
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.7"
|
||||
"react": "^19.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-helmet-async": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-3.0.0.tgz",
|
||||
"integrity": "sha512-nA3IEZfXiclgrz4KLxAhqJqIfFDuvzQwlKwpdmzZIuC1KNSghDEIXmyU0TKtbM+NafnkICcwx8CECFrZ/sL/1w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"invariant": "^2.2.4",
|
||||
"react-fast-compare": "^3.2.2",
|
||||
"shallowequal": "^1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
@@ -4038,6 +4060,12 @@
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/shallowequal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -4253,22 +4281,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.4.7",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.7.tgz",
|
||||
"integrity": "sha512-56L0/9HELHSsG1bFCzay8UoLxzRL7kpFf7Wl5q/kSYwiSJGACvro61xnKzPNM+SadxllzdtXsKDSXE7HPeqIAw==",
|
||||
"version": "7.4.9",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz",
|
||||
"integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^7.4.7"
|
||||
"tldts-core": "^7.4.9"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "7.4.7",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.7.tgz",
|
||||
"integrity": "sha512-rNlAI8fKn/JckBMUSbNL/ES2kmDiurWaE49l+ikwEc9A6lFR7gMx9AhgQMQKBK4H5w4pKLH64JzZfB99uRsGNQ==",
|
||||
"version": "7.4.9",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz",
|
||||
"integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -4311,9 +4339,7 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
@@ -4429,16 +4455,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz",
|
||||
"integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
|
||||
"version": "8.1.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
|
||||
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.5",
|
||||
"postcss": "^8.5.16",
|
||||
"rolldown": "~1.1.4",
|
||||
"postcss": "^8.5.17",
|
||||
"rolldown": "~1.1.5",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"start:server": "node server/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"async-mutex": "^0.5.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.5.2",
|
||||
@@ -24,6 +25,7 @@
|
||||
"nodemailer": "^9.0.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-helmet-async": "^3.0.0",
|
||||
"react-router-dom": "^7.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -2,11 +2,13 @@ import { Router } from 'express'
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs'
|
||||
import { join, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { Mutex } from 'async-mutex'
|
||||
import { resourceLimiter } from '../middleware/rateLimiter.js'
|
||||
import { antiSpam } from '../middleware/antiSpam.js'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const LEADS_FILE = join(__dirname, '..', 'data', 'leads.json')
|
||||
const leadsMutex = new Mutex()
|
||||
|
||||
// Only alphanumeric + hyphen resource IDs are allowed. This prevents path
|
||||
// traversal (e.g. "../../etc/passwd") when building the download URL.
|
||||
@@ -24,7 +26,7 @@ function saveLeads(leads) {
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.post('/', resourceLimiter, antiSpam, (req, res) => {
|
||||
router.post('/', resourceLimiter, antiSpam, async (req, res) => {
|
||||
const { name, email, company, resource_id } = req.body
|
||||
const errors = {}
|
||||
if (!name || name.trim().length === 0) errors.name = 'Name ist erforderlich.'
|
||||
@@ -38,6 +40,7 @@ router.post('/', resourceLimiter, antiSpam, (req, res) => {
|
||||
return res.status(400).json({ errors })
|
||||
}
|
||||
|
||||
await leadsMutex.runExclusive(async () => {
|
||||
const leads = loadLeads()
|
||||
leads.push({
|
||||
name,
|
||||
@@ -47,6 +50,7 @@ router.post('/', resourceLimiter, antiSpam, (req, res) => {
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
saveLeads(leads)
|
||||
})
|
||||
|
||||
// Return download URL (resource files are in public/resources/)
|
||||
res.json({ ok: true, download_url: `/resources/${resource_id}.pdf` })
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Router } from 'express'
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs'
|
||||
import { join, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { Mutex } from 'async-mutex'
|
||||
import { resourceLimiter } from '../middleware/rateLimiter.js'
|
||||
import { antiSpam } from '../middleware/antiSpam.js'
|
||||
import { sendSpeakerCv } from '../services/mailer.js'
|
||||
@@ -9,6 +10,7 @@ import { sendSpeakerCv } from '../services/mailer.js'
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const LEADS_FILE = join(__dirname, '..', 'data', 'speaker-cv-leads.json')
|
||||
const PDF_PATH = join(__dirname, '..', '..', 'public', 'resources', 'speaker-cv-andre-knie.pdf')
|
||||
const leadsMutex = new Mutex()
|
||||
|
||||
function loadLeads() {
|
||||
if (!existsSync(LEADS_FILE)) return []
|
||||
@@ -36,9 +38,11 @@ router.post('/', resourceLimiter, antiSpam, async (req, res) => {
|
||||
}
|
||||
|
||||
const lead = { name, email, message, timestamp: new Date().toISOString() }
|
||||
await leadsMutex.runExclusive(async () => {
|
||||
const leads = loadLeads()
|
||||
leads.push(lead)
|
||||
saveLeads(leads)
|
||||
})
|
||||
|
||||
await sendSpeakerCv(email, lead, PDF_PATH)
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
validateEmail,
|
||||
validateContact,
|
||||
validateTalkRequest,
|
||||
validateNewsletter,
|
||||
validateResourceDownload,
|
||||
} from './validation.js'
|
||||
|
||||
describe('validateEmail', () => {
|
||||
it('accepts valid emails', () => {
|
||||
expect(validateEmail('user@example.com')).toBe(true)
|
||||
expect(validateEmail('a.b@sub.domain.org')).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects invalid emails', () => {
|
||||
expect(validateEmail('')).toBe(false)
|
||||
expect(validateEmail('no-at-sign')).toBe(false)
|
||||
expect(validateEmail('@missing-local.com')).toBe(false)
|
||||
expect(validateEmail('spaces in@email.com')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateContact', () => {
|
||||
const valid = { name: 'Max', email: 'max@example.com', message: 'Hello' }
|
||||
|
||||
it('passes with valid data', () => {
|
||||
const result = validateContact(valid)
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.errors).toEqual({})
|
||||
})
|
||||
|
||||
it('fails without name', () => {
|
||||
const result = validateContact({ ...valid, name: '' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.name).toBeTruthy()
|
||||
})
|
||||
|
||||
it('fails without email', () => {
|
||||
const result = validateContact({ ...valid, email: '' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.email).toBeTruthy()
|
||||
})
|
||||
|
||||
it('fails without message', () => {
|
||||
const result = validateContact({ ...valid, message: '' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.message).toBeTruthy()
|
||||
})
|
||||
|
||||
it('fails with name over 100 chars', () => {
|
||||
const result = validateContact({ ...valid, name: 'x'.repeat(101) })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.name).toBeTruthy()
|
||||
})
|
||||
|
||||
it('fails with message over 2000 chars', () => {
|
||||
const result = validateContact({ ...valid, message: 'x'.repeat(2001) })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.message).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateTalkRequest', () => {
|
||||
const valid = {
|
||||
name: 'Max',
|
||||
email: 'max@example.com',
|
||||
event_name: 'Conference 2026',
|
||||
topic: 'KI im Mittelstand',
|
||||
message: 'Interested in a keynote',
|
||||
}
|
||||
|
||||
it('passes with valid data', () => {
|
||||
const result = validateTalkRequest(valid)
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
|
||||
it('fails without event_name', () => {
|
||||
const result = validateTalkRequest({ ...valid, event_name: '' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.event_name).toBeTruthy()
|
||||
})
|
||||
|
||||
it('fails without topic', () => {
|
||||
const result = validateTalkRequest({ ...valid, topic: '' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.topic).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateNewsletter', () => {
|
||||
it('passes with valid email', () => {
|
||||
const result = validateNewsletter({ email: 'user@test.com' })
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
|
||||
it('fails with invalid email', () => {
|
||||
const result = validateNewsletter({ email: 'bad' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.email).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateResourceDownload', () => {
|
||||
const valid = {
|
||||
name: 'Max',
|
||||
email: 'max@example.com',
|
||||
company: 'Test GmbH',
|
||||
resource_id: 'speaker-cv',
|
||||
}
|
||||
|
||||
it('passes with valid data', () => {
|
||||
const result = validateResourceDownload(valid)
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
|
||||
it('fails without company', () => {
|
||||
const result = validateResourceDownload({ ...valid, company: '' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.company).toBeTruthy()
|
||||
})
|
||||
|
||||
it('fails without resource_id', () => {
|
||||
const result = validateResourceDownload({ ...valid, resource_id: '' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.resource_id).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -23,7 +23,7 @@ class TestSanitizeWorkspaceKey:
|
||||
assert sanitize_workspace_key("v1.0_release") == "v1.0_release"
|
||||
|
||||
def test_unicode_replaced(self):
|
||||
assert sanitize_workspace_key("café-123") == "caf__123"
|
||||
assert sanitize_workspace_key("café-123") == "caf_-123"
|
||||
|
||||
|
||||
class TestGetWorkspacePath:
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
name: Backup Dismissed Tasks
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */6 * * *' # Every 6 hours
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: repo-file-writes
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
backup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch dismissed task IDs from OrgMyLife
|
||||
run: |
|
||||
# Get all dismissed and completed tasks (their source_ids)
|
||||
RESPONSE=$(curl -s -u "${{ secrets.ORGMYLIFE_USER }}:${{ secrets.ORGMYLIFE_PASS }}" \
|
||||
-H "Authorization: Bearer ${{ secrets.ORGMYLIFE_API_SECRET }}" \
|
||||
"https://api.andreknie.de/api/tasks/dismissed")
|
||||
echo "$RESPONSE" > dismissed_tasks.json
|
||||
cat dismissed_tasks.json | head -c 200
|
||||
|
||||
- name: Commit if changed
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add dismissed_tasks.json
|
||||
git diff --cached --quiet && echo "No changes" && exit 0
|
||||
git commit -m "chore: backup dismissed task IDs [skip ci]"
|
||||
git pull --rebase origin main || true
|
||||
git push
|
||||
@@ -1,81 +0,0 @@
|
||||
name: Mark Issues for Review
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'DONE_ISSUES.md'
|
||||
|
||||
concurrency:
|
||||
group: repo-file-writes
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
mark-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Mark issues as ready for review
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('DONE_ISSUES.md', 'utf8');
|
||||
const issueNumbers = [...content.matchAll(/#(\d+)/g)].map(m => parseInt(m[1]));
|
||||
|
||||
if (issueNumbers.length === 0) {
|
||||
console.log('No issue numbers found in DONE_ISSUES.md');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const num of issueNumbers) {
|
||||
try {
|
||||
const issue = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: num,
|
||||
});
|
||||
|
||||
if (issue.data.state === 'open') {
|
||||
// Add review label
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: num,
|
||||
labels: ['ready-for-review'],
|
||||
});
|
||||
|
||||
// Add a comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: num,
|
||||
body: '✅ Implementation complete and deployed. Ready for your review.\n\nClose this issue when you\'re satisfied with the result.',
|
||||
});
|
||||
|
||||
console.log(`Marked #${num} as ready-for-review`);
|
||||
} else {
|
||||
console.log(`Issue #${num} already closed.`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Error processing #${num}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the file after processing
|
||||
fs.writeFileSync('DONE_ISSUES.md', '# Done Issues\n\n_Empty — no issues to process._\n');
|
||||
|
||||
- name: Commit cleared file
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add DONE_ISSUES.md
|
||||
git diff --cached --quiet && exit 0
|
||||
git commit -m "chore: clear DONE_ISSUES.md after processing [skip ci]"
|
||||
git pull --rebase origin main || true
|
||||
git push
|
||||
@@ -1,80 +0,0 @@
|
||||
name: Collect AI Tasks
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/30 * * * *'
|
||||
workflow_dispatch: {}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
sync-tasks:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch agent-ready tasks from OrgMyLife
|
||||
id: fetch
|
||||
run: |
|
||||
curl -s -u "${{ secrets.ORGMYLIFE_USER }}:${{ secrets.ORGMYLIFE_PASS }}" \
|
||||
-H "Authorization: Bearer ${{ secrets.ORGMYLIFE_API_SECRET }}" \
|
||||
"https://api.andreknie.de/api/orchestrator/tasks" > /tmp/tasks.json
|
||||
cat /tmp/tasks.json | head -c 500
|
||||
|
||||
- name: Create GitHub Issues for new tasks
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const data = JSON.parse(fs.readFileSync('/tmp/tasks.json', 'utf8'));
|
||||
const tasks = data.tasks || [];
|
||||
|
||||
if (tasks.length === 0) {
|
||||
console.log('No agent-ready tasks found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const existingIssues = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
labels: 'ai-task',
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const existingTitles = new Set(
|
||||
existingIssues.data.map(i => i.title)
|
||||
);
|
||||
|
||||
for (const task of tasks) {
|
||||
const issueTitle = `[AI] ${task.identifier}: ${task.title}`;
|
||||
|
||||
if (existingTitles.has(issueTitle)) {
|
||||
console.log(`Skipping (already exists): ${issueTitle}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const body = [
|
||||
`**Task ID:** ${task.identifier}`,
|
||||
`**Priority:** ${task.priority || 'unset'}`,
|
||||
`**State:** ${task.state}`,
|
||||
task.due_date ? `**Due:** ${task.due_date}` : '',
|
||||
'',
|
||||
'## Description',
|
||||
task.description || '_No description provided._',
|
||||
'',
|
||||
'---',
|
||||
'_Auto-created from OrgMyLife task board._',
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: issueTitle,
|
||||
body: body,
|
||||
labels: ['ai-task'],
|
||||
});
|
||||
|
||||
console.log(`Created issue: ${issueTitle}`);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
name: Update Task State on Issue Events
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [closed, labeled]
|
||||
|
||||
jobs:
|
||||
mark-review:
|
||||
if: |
|
||||
github.event.action == 'labeled' &&
|
||||
github.event.label.name == 'ready-for-review' &&
|
||||
contains(github.event.issue.labels.*.name, 'ai-task')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mark task as review in OrgMyLife
|
||||
run: |
|
||||
TITLE="${{ github.event.issue.title }}"
|
||||
TASK_ID=$(echo "$TITLE" | grep -oP 'OML-\K[0-9]+')
|
||||
|
||||
if [ -z "$TASK_ID" ]; then
|
||||
echo "Could not extract task ID from: $TITLE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Marking task $TASK_ID as review..."
|
||||
|
||||
curl -s -X POST \
|
||||
-u "${{ secrets.ORGMYLIFE_USER }}:${{ secrets.ORGMYLIFE_PASS }}" \
|
||||
-H "Authorization: Bearer ${{ secrets.ORGMYLIFE_API_SECRET }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"status": "review"}' \
|
||||
"https://api.andreknie.de/api/orchestrator/tasks/${TASK_ID}/state"
|
||||
|
||||
mark-done:
|
||||
if: |
|
||||
github.event.action == 'closed' &&
|
||||
contains(github.event.issue.labels.*.name, 'ai-task')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mark task as completed in OrgMyLife
|
||||
run: |
|
||||
TITLE="${{ github.event.issue.title }}"
|
||||
TASK_ID=$(echo "$TITLE" | grep -oP 'OML-\K[0-9]+')
|
||||
|
||||
if [ -z "$TASK_ID" ]; then
|
||||
echo "Could not extract task ID from: $TITLE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Marking task $TASK_ID as completed..."
|
||||
|
||||
curl -s -X POST \
|
||||
-u "${{ secrets.ORGMYLIFE_USER }}:${{ secrets.ORGMYLIFE_PASS }}" \
|
||||
-H "Authorization: Bearer ${{ secrets.ORGMYLIFE_API_SECRET }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"status": "completed"}' \
|
||||
"https://api.andreknie.de/api/orchestrator/tasks/${TASK_ID}/state"
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
name: Deploy to VPS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Write .env file to VPS
|
||||
uses: appleboy/ssh-action@v1
|
||||
env:
|
||||
ENV_CONTENT: |
|
||||
POSTGRES_PASSWORD=${{ secrets.ENV_POSTGRES_PASSWORD }}
|
||||
API_SECRET=${{ secrets.ENV_API_SECRET }}
|
||||
APP_USERNAME=${{ secrets.ENV_APP_USERNAME }}
|
||||
APP_PASSWORD=${{ secrets.ENV_APP_PASSWORD }}
|
||||
NEXTCLOUD_URL=${{ secrets.ENV_NEXTCLOUD_URL }}
|
||||
NEXTCLOUD_USERNAME=${{ secrets.ENV_NEXTCLOUD_USERNAME }}
|
||||
NEXTCLOUD_PASSWORD=${{ secrets.ENV_NEXTCLOUD_PASSWORD }}
|
||||
IMAP_SERVER=${{ secrets.ENV_IMAP_SERVER }}
|
||||
IMAP_PORT=993
|
||||
EMAIL_USERNAME=${{ secrets.ENV_EMAIL_USERNAME }}
|
||||
EMAIL_PASSWORD=${{ secrets.ENV_EMAIL_PASSWORD }}
|
||||
EMAIL_WEBMAIL_URL=${{ secrets.ENV_EMAIL_WEBMAIL_URL }}
|
||||
GMAIL_USERNAME=${{ secrets.ENV_GMAIL_USERNAME }}
|
||||
GMAIL_PASSWORD=${{ secrets.ENV_GMAIL_PASSWORD }}
|
||||
with:
|
||||
host: 217.160.174.2
|
||||
username: root
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
envs: ENV_CONTENT
|
||||
script: |
|
||||
# Ensure directory exists
|
||||
if [ ! -d /opt/OrgMyLife ]; then
|
||||
cd /opt
|
||||
git clone https://github.com/DoctoDre/OrgMyLife.git || true
|
||||
fi
|
||||
|
||||
cd /opt/OrgMyLife
|
||||
git pull origin main || true
|
||||
|
||||
# Write .env from environment variable (safe for special chars)
|
||||
printf '%s\n' "$ENV_CONTENT" > .env
|
||||
|
||||
docker compose up -d --build
|
||||
echo "Deploy complete"
|
||||
@@ -1,68 +0,0 @@
|
||||
name: Sync Issues to File
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/30 * * * *'
|
||||
workflow_dispatch: {}
|
||||
issues:
|
||||
types: [opened, closed, reopened, edited, labeled, unlabeled]
|
||||
|
||||
concurrency:
|
||||
group: repo-file-writes
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Write open AI issues to file
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
const issues = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
labels: 'ai-task',
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
let content = '# AI Tasks (auto-generated)\n\n';
|
||||
content += `_Last updated: ${new Date().toISOString()}_\n\n`;
|
||||
|
||||
if (issues.data.length === 0) {
|
||||
content += 'No open AI tasks.\n';
|
||||
} else {
|
||||
content += `${issues.data.length} open task(s):\n\n`;
|
||||
for (const issue of issues.data) {
|
||||
content += `---\n\n`;
|
||||
content += `## #${issue.number}: ${issue.title}\n\n`;
|
||||
content += `- **State:** open\n`;
|
||||
content += `- **Created:** ${issue.created_at}\n`;
|
||||
content += `- **URL:** ${issue.html_url}\n\n`;
|
||||
content += `### Description\n\n`;
|
||||
content += `${issue.body || '_No description._'}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync('AI_TASKS.md', content);
|
||||
console.log(`Wrote ${issues.data.length} issues to AI_TASKS.md`);
|
||||
|
||||
- name: Commit and push if changed
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add AI_TASKS.md
|
||||
git diff --cached --quiet && echo "No changes" && exit 0
|
||||
git commit -m "chore: update AI_TASKS.md [skip ci]"
|
||||
git pull --rebase origin main || true
|
||||
git push
|
||||
@@ -1,35 +0,0 @@
|
||||
name: Sync OrgMyLife Plan to Projects Board
|
||||
|
||||
on:
|
||||
push:
|
||||
paths: ['PLAN.md']
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Parse open tasks and sync to Projects board
|
||||
run: |
|
||||
REPO_NAME="OrgMyLife"
|
||||
|
||||
TASKS="[]"
|
||||
if [ -f "PLAN.md" ]; then
|
||||
ITEMS=$(grep -E '^\s*- \[ \]' PLAN.md | sed 's/.*- \[ \] //' | head -50)
|
||||
if [ -n "$ITEMS" ]; then
|
||||
TASKS=$(echo "$ITEMS" | jq -R -s 'split("\n") | map(select(length > 0)) | map({"title": ., "status": "backlog"})')
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Found tasks: $TASKS"
|
||||
|
||||
PAYLOAD=$(jq -n --arg repo "$REPO_NAME" --argjson tasks "$TASKS" '{"repo": $repo, "tasks": $tasks}')
|
||||
|
||||
curl -s -X POST \
|
||||
-u "${{ secrets.ORGMYLIFE_USER }}:${{ secrets.ORGMYLIFE_PASS }}" \
|
||||
-H "X-API-Key: ${{ secrets.ORGMYLIFE_API_SECRET }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" \
|
||||
"https://api.andreknie.de/api/projects/sync"
|
||||
@@ -0,0 +1,77 @@
|
||||
stages:
|
||||
- sync
|
||||
- deploy
|
||||
|
||||
variables:
|
||||
DEPLOY_HOST: "217.160.174.2"
|
||||
DEPLOY_USER: "root"
|
||||
DEPLOY_PATH: "/opt/OrgMyLife"
|
||||
|
||||
.ssh_setup: &ssh_setup
|
||||
before_script:
|
||||
- apt-get update -qq && apt-get install -y openssh-client rsync curl
|
||||
- install -d -m 700 ~/.ssh
|
||||
- echo "$VPS_SSH_KEY" > ~/.ssh/deploy_key
|
||||
- chmod 600 ~/.ssh/deploy_key
|
||||
- ssh-keyscan -H "${DEPLOY_HOST}" > ~/.ssh/known_hosts
|
||||
- chmod 600 ~/.ssh/known_hosts
|
||||
|
||||
deploy-vps:
|
||||
stage: deploy
|
||||
image: node:20
|
||||
<<: *ssh_setup
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"'
|
||||
changes:
|
||||
- "shared/OrgMyLife/**/*"
|
||||
script:
|
||||
- ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes "${DEPLOY_USER}@${DEPLOY_HOST}" "install -d -m 755 ${DEPLOY_PATH}"
|
||||
- |
|
||||
cat <<EOF > .env
|
||||
POSTGRES_PASSWORD=$ENV_POSTGRES_PASSWORD
|
||||
API_SECRET=$ENV_API_SECRET
|
||||
APP_USERNAME=$ENV_APP_USERNAME
|
||||
APP_PASSWORD=$ENV_APP_PASSWORD
|
||||
NEXTCLOUD_URL=$ENV_NEXTCLOUD_URL
|
||||
NEXTCLOUD_USERNAME=$ENV_NEXTCLOUD_USERNAME
|
||||
NEXTCLOUD_PASSWORD=$ENV_NEXTCLOUD_PASSWORD
|
||||
IMAP_SERVER=$ENV_IMAP_SERVER
|
||||
IMAP_PORT=993
|
||||
EMAIL_USERNAME=$ENV_EMAIL_USERNAME
|
||||
EMAIL_PASSWORD=$ENV_EMAIL_PASSWORD
|
||||
EMAIL_WEBMAIL_URL=$ENV_EMAIL_WEBMAIL_URL
|
||||
GMAIL_USERNAME=$ENV_GMAIL_USERNAME
|
||||
GMAIL_PASSWORD=$ENV_GMAIL_PASSWORD
|
||||
EOF
|
||||
- rsync -az --delete -e "ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" shared/OrgMyLife/ "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/"
|
||||
- rsync -az -e "ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" .env "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/.env"
|
||||
- ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes "${DEPLOY_USER}@${DEPLOY_HOST}" "cd ${DEPLOY_PATH} && docker compose up -d --build"
|
||||
|
||||
collect-tasks:
|
||||
stage: sync
|
||||
image: alpine:latest
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TARGET == "collect-tasks"'
|
||||
script:
|
||||
- apk add --no-cache curl jq
|
||||
- |
|
||||
curl -s -u "${ORGMYLIFE_USER}:${ORGMYLIFE_PASS}" \
|
||||
-H "Authorization: Bearer ${ORGMYLIFE_API_SECRET}" \
|
||||
"https://api.andreknie.de/api/orchestrator/tasks" > /tmp/tasks.json
|
||||
- echo "Tasks fetched. Since GitLab handles issues differently, issue creation logic needs to be adapted for GitLab API."
|
||||
|
||||
backup-dismissed:
|
||||
stage: sync
|
||||
image: alpine:latest
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TARGET == "backup-dismissed"'
|
||||
script:
|
||||
- apk add --no-cache curl git
|
||||
- |
|
||||
curl -s -u "${ORGMYLIFE_USER}:${ORGMYLIFE_PASS}" \
|
||||
-H "Authorization: Bearer ${ORGMYLIFE_API_SECRET}" \
|
||||
"https://api.andreknie.de/api/tasks/dismissed" > shared/OrgMyLife/dismissed_tasks.json
|
||||
- git config user.name "gitlab-ci[bot]"
|
||||
- git config user.email "gitlab-ci[bot]@gitlab.dhive.io"
|
||||
- git add shared/OrgMyLife/dismissed_tasks.json
|
||||
- git diff --cached --quiet || (git commit -m "chore: backup dismissed task IDs [skip ci]" && git push "https://oauth2:${PROJECT_ACCESS_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" HEAD:$CI_COMMIT_BRANCH)
|
||||
@@ -0,0 +1,31 @@
|
||||
# GitLab Migration Plan & Status
|
||||
|
||||
## What has been done
|
||||
- The scheduled GitHub workflows have been disabled (`Collect AI Tasks`, `Sync Issues to File`, `Backup Dismissed Tasks`) because of GitHub billing limits that were causing pipeline failures and costing money.
|
||||
- This repository is being prepared for migration to GitLab.
|
||||
|
||||
## What needs to be done
|
||||
In order to get the CI/CD and automation processes running again on GitLab, the following tasks must be completed:
|
||||
|
||||
1. **Import the repository to GitLab**:
|
||||
- Migrate the source code.
|
||||
- Migrate GitHub Issues, labels, and Projects.
|
||||
|
||||
2. **Translate GitHub Actions to GitLab CI/CD (`.gitlab-ci.yml`)**:
|
||||
- `Backup Dismissed Tasks`: Implement as a GitLab Scheduled Pipeline running every 6 hours.
|
||||
- `Mark Issues for Review`: Migrate to a standard GitLab CI pipeline triggered by pushes changing `DONE_ISSUES.md`.
|
||||
- `Collect AI Tasks`: Implement as a GitLab Scheduled Pipeline running every 30 minutes.
|
||||
- `Update Task State on Issue Events`: Create a GitLab webhook responding to issue events, or use GitLab CI pipelines with issue event triggers.
|
||||
- `Deploy to VPS`: Migrate to a GitLab CI pipeline triggered on branch pushes to `main`.
|
||||
- `Sync Issues to File`: Migrate to a GitLab Scheduled Pipeline (every 30 minutes) and/or use GitLab Webhooks for issue event triggers.
|
||||
- `Sync OrgMyLife Plan to Projects Board`: Migrate to a GitLab CI pipeline triggered by pushes to `PLAN.md`.
|
||||
|
||||
3. **Configure CI/CD Variables**:
|
||||
- Transfer any repository secrets or environment variables (such as deployment keys, SSH credentials, API tokens) into GitLab CI/CD Settings under Variables.
|
||||
|
||||
4. **Runner Configuration**:
|
||||
- Ensure the GitLab Runners have the necessary capabilities (e.g., Node.js environments) by utilizing standard Docker images in the `.gitlab-ci.yml` (e.g., `image: node:20`).
|
||||
|
||||
5. **Optimize Pipeline Compute Efficiency**:
|
||||
- Analyze and optimize the pipeline execution to drastically reduce compute time.
|
||||
- **Context:** The previous runner consumed ~2000 minutes of compute time in just 10 days, which is highly inefficient for a small application. Strategies like reducing cron frequencies, utilizing caching, and conditionally running jobs should be implemented.
|
||||
@@ -5,30 +5,20 @@ federation:
|
||||
conflict_strategy: "team-wins"
|
||||
|
||||
team_repos:
|
||||
- context: privat
|
||||
url: "https://github.com/andreknie/privat-team.git"
|
||||
branch: main
|
||||
sync_direction: bidirectional
|
||||
sync_frequency: "on-push"
|
||||
shared_mirror:
|
||||
enabled: true
|
||||
paths:
|
||||
- "shared/tools/common-scripts/"
|
||||
- "shared/config/base-config.yaml"
|
||||
mode: read-only
|
||||
|
||||
- context: dhive
|
||||
url: "https://gitlab.dhive.io/team/dhive-mono.git"
|
||||
url: "https://git.d-hive.de/team/dhive-mono.git"
|
||||
branch: main
|
||||
sync_direction: bidirectional
|
||||
sync_frequency: "on-push"
|
||||
shared_mirror:
|
||||
enabled: true
|
||||
paths:
|
||||
- "privat/"
|
||||
- "shared/AI-Orchestrator/"
|
||||
- "shared/OrgMyLife/"
|
||||
- "shared/config/"
|
||||
- "shared/tools/"
|
||||
- "shared/powers/db-dxp-platform/"
|
||||
- "shared/mcp-servers/"
|
||||
mode: read-only
|
||||
mode: bidirectional
|
||||
|
||||
- context: bahn
|
||||
url: "https://gitlab.2700.2db.it/team/bahn-workspace.git"
|
||||
|
||||
@@ -218,7 +218,7 @@ class SharedMirrorConfig:
|
||||
|
||||
enabled: bool
|
||||
paths: list[str] = field(default_factory=list)
|
||||
mode: Literal["read-only"] = "read-only"
|
||||
mode: Literal["read-only", "bidirectional"] = "read-only"
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
Reference in New Issue
Block a user