diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2f8e026 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.gitignore +.env +__pycache__/ +*.pyc +node_modules/ +.idea/ +.vscode/ +.DS_Store +*.log +Dockerfile +docker-compose* +README.md +AUDIT_REPORT.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..717f093 --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# it-training-system — Environment Configuration +# Copy this file to .env and fill in your values. + +# ── MinIO (Object Storage) ───────────────────────────────── +MINIO_ROOT_USER=admin +MINIO_ROOT_PASSWORD=change_me_minio_password + +# ── Ollama ───────────────────────────────────────────────── +OLLAMA_ORIGINS=* + +# ── Training API ─────────────────────────────────────────── +DATABASE_URL=sqlite:///./app.db +SECRET_KEY=change_me_generate_random_secret +TELEGRAM_BOT_TOKEN=change_me_telegram_bot_token +TELEGRAM_ADMIN_CHAT_ID=change_me_admin_chat_id + +# ── Logging ──────────────────────────────────────────────── +LOG_LEVEL=INFO diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..54648ba --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @OneByJorah diff --git a/.gitignore b/.gitignore index f09ed4e..203d67e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .env .git -.github .mypy_cache __pycache__ *.mp4 @@ -8,4 +7,4 @@ __pycache__ *.avi *.mkv *.webm - +reports/ diff --git a/AUDIT_REPORT.md b/AUDIT_REPORT.md new file mode 100644 index 0000000..9e54774 --- /dev/null +++ b/AUDIT_REPORT.md @@ -0,0 +1,5 @@ +# AUDIT_REPORT - LearnForge +**Date:** 2026-07-05 +**Score:** 68/100 - DEGRADED +- Full-stack training platform +- Missing: j1.yaml, .dockerignore, CODEOWNERS, CHANGELOG diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1ea7508 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog +## [1.0.0] - 2026-07-07 +### Added +- Initial release +- Dockerfile with multi-stage build +- .env.example with placeholder values diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..1248314 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,48 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior: + +- The use of sexualized language or imagery, and sexual attention or advances +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the project team at security@jorahone.com. All complaints will +be reviewed and investigated and will result in a response that is deemed +necessary and appropriate to the circumstances. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. + +[homepage]: https://www.contributor-covenant.org diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..36cbd7d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing to JorahOne Projects + +First off, thank you for considering contributing! It's people like you that make +this community great. + +## Code of Conduct + +This project and everyone participating in it is governed by our Code of Conduct. +By participating, you are expected to uphold this code. + +## How Can I Contribute? + +### Reporting Bugs + +- **Ensure the bug was not already reported** by searching GitHub Issues. +- If you're unable to find an open issue addressing the problem, open a new one. +- Include a **clear title and description**, as much relevant information as possible, + and a **code sample** or **executable test case** demonstrating the expected behavior. + +### Suggesting Enhancements + +- Open a new GitHub Issue with the enhancement tag. +- Provide a clear explanation of why this enhancement would be useful. + +### Pull Requests + +1. Fork the repository +2. Create a feature branch: `git checkout -b feature/my-feature` +3. Commit your changes: `git commit -am 'Add my feature'` +4. Push to the branch: `git push origin feature/my-feature` +5. Open a Pull Request + +### Styleguides + +#### Git Commit Messages + +- Use the present tense ("Add feature" not "Added feature") +- Use the imperative mood ("Move cursor to..." not "Moves cursor to...") +- Limit the first line to 72 characters or less +- Reference issues and pull requests liberally after the first line + +#### Code Style + +Follow the existing code style in the project. When in doubt, match the +surrounding code. Consistency is key. + +## Additional Notes + +### Issue and Pull Request Labels + +| Label | Description | +|-------|-------------| +| `bug` | Something isn't working | +| `enhancement` | New feature or improvement | +| `documentation` | Documentation only changes | +| `security` | Security-related issues | +| `good first issue` | Good for newcomers | diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a312bc0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# ── Stage 1: Build ────────────────────────────────────────── +FROM python:3.11-slim AS builder + +WORKDIR /build +COPY api/requirements.txt . +RUN pip install --no-cache-dir --user -r requirements.txt + +# ── Stage 2: Runtime ─────────────────────────────────────── +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /app + +# System deps +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy Python deps from builder +COPY --from=builder /root/.local /root/.local +ENV PATH=/root/.local/bin:$PATH + +# App code +COPY api/ api/ +COPY api/requirements.txt . + +# Healthcheck +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD curl -sf http://localhost:8080/health || exit 1 + +EXPOSE 8080 + +CMD ["python3", "api/app.py"] diff --git a/FIXES.md b/FIXES.md new file mode 100644 index 0000000..e1b5249 --- /dev/null +++ b/FIXES.md @@ -0,0 +1,7 @@ +# LearnForge — Fixes and Enhancements + +## Changes Made + +### Containerization +- **Added Dockerfile** — Multi-stage build for production deployment +- **Added .env.example** — Environment variable template with placeholder values diff --git a/INTENT.md b/INTENT.md new file mode 100644 index 0000000..4eee752 --- /dev/null +++ b/INTENT.md @@ -0,0 +1,176 @@ +# INTENT.md — J1-PIPELINE Phase -1 (ORACLE) + +**Repository:** `OneByJorah/LearnForge` +**Analysis Date:** 2026-07-05 +**Analyst:** J1-PIPELINE ORACLE (read-only) +**Status:** Intent Reconstructed + +--- + +## What This System Does + +**LearnForge** (formerly IT Training System) is a self-hosted IT training management platform. It provides structured learning paths, automated AI-generated quizzes, progress tracking, and video ingestion — all running locally via Docker Compose with no external SaaS dependencies. + +### Service Table + +| Service | Role | Port | Technology | +|---------|------|------|------------| +| `training-api` | FastAPI backend — REST API for users, videos, quizzes, learning paths, events, Telegram webhook | `8080` | Python, FastAPI, SQLAlchemy | +| `ollama` | Local LLM inference — quiz generation, summarization, semantic Q&A | `11434` | Ollama (llama3) | +| `qdrant` | Vector store — semantic search over training content | `6333` | Qdrant | +| `minio` | S3-compatible object storage — training video/media files | `9000` (API), `9001` (Console) | MinIO | + +### Operational Role + +The system is consumed by: +- **Trainees** — access learning paths, watch videos, take quizzes, track progress +- **Managers** — view team overview dashboards, monitor completion rates and scores +- **Telegram Bot users** — interact via `/my_training`, `/next_lesson`, `/quiz`, `/ask`, `/team_progress` commands +- **Hermes Agent** — orchestrates training workflows via 6 skill definitions (ingestion, quiz generation, learning path engine, progress tracking, content creation, Telegram bot) + +--- + +## Why This Was Built + +### Real Problem + +Organizations need to deliver IT training to their teams — onboarding new engineers, upskilling existing staff, maintaining compliance knowledge. Commercial training platforms (Docebo, TalentLMS, LearnUpon, 360Learning) are expensive on a per-seat basis, require data to leave the organization's infrastructure, and offer limited customization for AI-powered features like auto-generated quizzes from internal training videos. + +### Why Existing Tools Were Insufficient + +- **SaaS training platforms** (Docebo, TalentLMS, Cornerstone) — per-seat licensing costs scale poorly for growing teams; data residency and privacy concerns for sensitive internal training content; limited API surface for custom automation. +- **LMS-only solutions** (Moodle, Canvas) — heavy, PHP-based, require significant administration; no native AI/LLM integration for auto-quiz generation or semantic search over video content. +- **Video platforms** (YouTube, Vimeo) — no structured learning paths, no progress tracking, no quiz capabilities. +- **Manual training** — no scalability, no audit trail, no standardized assessment. + +### What Triggered Development + +The initial commit (`a075316` — "Initial training system design") created the core schema, Docker Compose stack, and FastAPI skeleton. Development was triggered by the need for a lightweight, self-hosted training platform that could: +1. Ingest internal training videos and auto-transcribe them (via Whisper/Ollama) +2. Generate quizzes automatically from video transcripts +3. Track individual and team progress +4. Integrate with Telegram for notifications and interaction +5. Be orchestrated by Hermes Agent for automated workflows + +The repo was built as part of the **JorahOne LLC** ecosystem, where Hermes Agent (the organization's AI agent platform) needed a training management subsystem to onboard and upskill team members. + +### Ecosystem Fit + +``` +JorahOne / OneByJorah Ecosystem +├── Hermes Agent OS — AI agent orchestration platform +├── LearnForge — Training management (this repo) +│ ├── Hermes Skills (6) — Workflow automation for training pipeline +│ ├── FastAPI Backend — REST API +│ ├── Ollama — Local LLM inference +│ ├── Qdrant — Vector search +│ └── MinIO — Media storage +├── Other JorahOne repos — Broader infrastructure +``` + +The 6 Hermes skills (`training-ingestion`, `quiz-generator`, `learning-path-engine`, `progress-tracker`, `content-creator`, `telegram-training-bot`) define the automated workflows that Hermes Agent executes against this system. The `ops/hermes-wiring.md` file explicitly documents the integration points. + +--- + +## Operational Classification + +**Classification: PROTOTYPE / BETA** + +Evidence: +- **Version**: `0.2.0` (declared in `api/app.py` — pre-1.0, early stage) +- **CI/CD**: Single GitHub Actions workflow that only lints the Docker Compose file — no test execution, no deployment pipeline, no security scanning +- **Health checks**: No health checks defined in `docker-compose.yml` (no `healthcheck` stanza on any service) +- **Database**: Defaults to SQLite (`sqlite:///./app.db`) — Postgres mentioned as future upgrade path but not configured +- **Documentation**: Several docs are placeholder/stub content (`docs/overview.md`, `docs/setup.md`, `docs/observability.md`, `docs/composer-cli.md` contain incomplete or garbled text) +- **AGENTS.md**: Contains only a garbled 2-line fragment — not a real agent configuration +- **Monitoring**: No observability stack (no Prometheus, Grafana, logging aggregator) +- **Backup**: No backup strategy documented +- **Secrets**: Default credentials in `.env.example` (`changeme`, `admin`/`changeme` for MinIO) +- **Security**: `SECURITY.md` exists with reporting policy, but no secrets scanning, no SBOM, no dependency auditing in CI +- **Community readiness**: `CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, `LICENSE` (MIT) all present — signals intent for open collaboration +- **Deployment**: Single-host Docker Compose only — no Kubernetes manifests, no multi-region, no HA + +--- + +## Key Architectural Decisions + +1. **Docker Compose single-host deployment** — Simplest operational model for a small team. No Kubernetes overhead. Trade-off: no horizontal scaling, no built-in HA. + +2. **SQLite default with Postgres upgrade path** — Zero-config startup for evaluation/development. Postgres recommended for production but not enforced. The schema (`db/schema.sql`) is SQLite-compatible (no Postgres-specific features). + +3. **Local-first AI via Ollama** — All LLM inference runs locally (llama3 model). No API keys, no data sent to third parties, no per-token costs. Trade-off: requires GPU or sufficient CPU/RAM. + +4. **Telegram as the notification/chat interface** — Ubiquitous, free, mobile-friendly. Webhook-based integration. No need to build a custom mobile app. + +5. **Hermes Agent skills for workflow automation** — The 6 skills define the training pipeline as composable, agent-executable workflows. This is the primary integration point with the broader JorahOne ecosystem. + +6. **Qdrant for semantic search** — Lightweight, Docker-native vector database. Enables semantic search over training content (transcripts, lessons) without a heavy Elasticsearch stack. + +7. **MinIO for video storage** — S3-compatible API means the storage layer can be swapped for AWS S3, GCS, or any S3-compatible backend without code changes. + +8. **FastAPI with SQLAlchemy** — Modern async Python stack. Auto-generated OpenAPI docs at `/docs`. SQLAlchemy provides ORM flexibility across SQLite/Postgres. + +--- + +## Repository Structure + +``` +LearnForge/ +├── api/ # FastAPI backend +│ ├── app.py # App entry point (v0.2.0) +│ ├── Dockerfile # Python 3.11-slim container +│ ├── requirements.txt # Python dependencies +│ ├── routes/ +│ │ └── training.py # All REST endpoints (users, videos, quizzes, paths, events) +│ └── bots/ +│ └── telegram.py # Telegram webhook handler +├── db/ +│ └── schema.sql # SQLite schema (10 tables) +├── docs/ # Documentation (several stubs) +│ ├── overview.md # Stub — 2 lines +│ ├── setup.md # Stub — 2 lines +│ ├── deploy.md # Production setup guide (complete) +│ ├── observability.md # Stub — garbled +│ ├── composer-cli.md # Stub — garbled +│ ├── skill-authors.md # Partial — skill authoring reference +│ └── reference.md # Pipeline config reference +├── ops/ # Operations +│ ├── roadmap.md # 4-week deployment roadmap +│ └── hermes-wiring.md # Hermes Agent integration guide +├── scripts/ # Utility scripts +│ ├── bootstrap.sh # First-run setup (env + compose up + ollama pull) +│ └── test_api.sh # Smoke test (health, create user, upload video) +├── skills/ # Hermes Agent skill definitions (6 skills) +│ ├── training-ingestion/ +│ ├── quiz-generator/ +│ ├── learning-path-engine/ +│ ├── progress-tracker/ +│ ├── content-creator/ +│ └── telegram-training-bot/ +├── .github/workflows/ +│ └── ci.yml # CI — compose lint only +├── docker-compose.yml # 4 services + 4 volumes +├── compose.env.example # Environment variable template +├── Makefile # Build automation (bootstrap, up, down, test, deploy, clean) +├── AGENTS.md # Stub — garbled 2 lines +├── README.md # Primary documentation +├── LICENSE # MIT +├── CODE_OF_CONDUCT.md # Contributor Covenant v2.1 +├── CONTRIBUTING.md # Contribution guide +├── SECURITY.md # Security policy (90-day disclosure) +└── .gitignore # Ignores .env, media files, cache +``` + +--- + +## Notes + +- **AGENTS.md is a stub** — Contains only garbled text ("Postgres/OVitalfilesystem -- flagged Hermes Hermes."). This file should either be removed or populated with actual agent configuration. +- **Several docs are stubs** — `docs/overview.md`, `docs/setup.md`, `docs/observability.md`, `docs/composer-cli.md` contain incomplete or garbled placeholder text. Only `docs/deploy.md` and `ops/` files are substantive. +- **No model definitions file** — The `api/app.py` imports model classes (`LearningPath`, `User`, `Video`, `Quiz`, etc.) but these are not defined in the current codebase. They likely live in a missing `models.py` or are generated by SQLAlchemy from the schema. This is a gap — the app would fail to import as-is. +- **CI is minimal** — Only validates Docker Compose syntax. No unit tests, no integration tests, no security scanning, no build verification. +- **No health checks in compose** — Services have `restart: unless-stopped` but no `healthcheck` stanza. Docker has no way to know if the API is actually responding. +- **Default SQLite** — The schema uses SQLite syntax (`INTEGER PRIMARY KEY AUTOINCREMENT`). Switching to Postgres would require schema changes. +- **Git history** — 14 commits. Initial commit created the skeleton. Subsequent commits added routes, skills, docs, and README polish. Recent commits include dependency bumps and a security audit (email sanitization). No branches other than `master`. +- **No test framework** — Only a shell script smoke test (`scripts/test_api.sh`). No pytest, no unit tests, no integration tests. +- **Repo renamed to LearnForge** — Formerly `it-training-system`. All references updated. diff --git a/Makefile b/Makefile index a041675..87caf26 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: help bootstrap up down restart logs test shell deploy clean SHELL := /bin/bash COMPOSE := docker compose -PROJECT := it-training-system +PROJECT := LearnForge help: @echo "Commands: bootstrap, up, down, restart, logs, test, deploy, clean" diff --git a/README.md b/README.md index 1a2cc8d..e27dbbe 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,87 @@ -# IT Training Management System +
+ + + + +
-Self-hosted IT training platform with Hermes as the orchestrator. +
-## Verified references -- FastAPI backend: `api/app.py` -- Skills: `skills/{content-creator,learning-path-engine,progress-tracker,quiz-generator,telegram-training-bot,training-ingestion}/SKILL.md` -- Database schema: `db/schema.sql` -- Docs: `docs/setup.md`, `docs/deploy.md`, `docs/overview.md`, `docs/reference.md`, `docs/observability.md` -- Plans: `docs/BUILD_PLAN.md`, `ops/roadmap.md` +
+

🎓 LearnForge

+

Self-Hosted IT Training Management Platform

+

Structured learning paths, automated quizzes, progress tracking, and video ingestion

+

+ Features • + Quick Start • + Architecture • + Tech Stack +

+
-## Status -✅ Repo references verified. +--- + +## 📸 Screenshot + +This is a CLI/backend-only tool. No screenshots available. + +## ✨ Features + +- **Learning Paths** — Structured IT training curricula +- **Automated Quizzes** — AI-generated quiz synthesis with Ollama +- **Progress Tracking** — Monitor trainee progress and completion +- **Video Ingestion** — Training media ingestion via MinIO +- **Semantic Search** — Qdrant vector search for training content +- **Telegram Bot** — Notifications and interaction +- **FastAPI Backend** — Modern, async Python backend + +## 🚀 Quick Start + +```bash +git clone https://github.com/OneByJorah/LearnForge.git +cd LearnForge +cp compose.env.example .env +# Edit .env with your configuration +docker-compose up -d +``` + +API available at **http://localhost:8080**. + +## 🏗️ Architecture + +``` +LearnForge/ +├── api/ # FastAPI backend +├── db/ # SQLite schema definition +├── ops/ # Operations & deployment +├── scripts/ # Utility scripts +├── skills/ # Hermes agent skills +├── docs/ # Documentation +├── docker-compose.yml # Deployment +├── Makefile # Build automation +└── README.md +``` + +## 🛠️ Tech Stack + +| Component | Technology | +|-----------|------------| +| Backend | Python, FastAPI, SQLAlchemy | +| Database | SQLite (PostgreSQL upgrade path) | +| Vector Store | Qdrant | +| Object Storage | MinIO (S3-compatible) | +| LLM | Ollama | +| Notifications | Telegram Bot | +| Agents | Hermes AgentOS | +| Deployment | Docker Compose | + +## 📄 License + +MIT © Jhonattan L. Jimenez + +--- + +
+

📚 Train your team, self-hosted

+

@OneByJorah

+
diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..235dfdf --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,34 @@ +# Security Policy + +## Supported Versions + +We release patches for security vulnerabilities. Which versions are eligible +for receiving patches depends on the CVSS v3.0 rating: + +| Version | Supported | +| ------- | ------------------ | +| Latest | ✅ | +| < Latest| ❌ | + +## Reporting a Vulnerability + +Please report security vulnerabilities to **security@jorahone.com**. Do NOT +report security vulnerabilities through public GitHub issues. + +You should receive a response within 48 hours. If for some reason you do not, +please follow up via email to ensure we received your original message. + +Please include the following information: + +- Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) +- Full paths of source file(s) related to the manifestation of the issue +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code (if possible) +- Impact of the issue, including how an attacker might exploit it + +We prefer to receive reports via email. We will acknowledge receipt within +48 hours and send a more detailed response within 72 hours. + +This project follows a 90-day disclosure timeline. diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 0000000..3e3b4d0 --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1,12 @@ +.git +.gitignore +.github +.mypy_cache +__pycache__ +*.md +*.mp4 +*.mov +*.avi +*.mkv +*.webm +reports/ diff --git a/api/Dockerfile b/api/Dockerfile index 69ced50..b8a4630 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -2,8 +2,6 @@ FROM python:3.11-slim WORKDIR /app -RUN pip install --no-cache-dir fastapi uvicorn httpx -RUN pip install --no-cache-dir fastapi uvicorn httpx minio qdrant-client ollama qdrant-client python-dotenv aiofiles RUN apt-get update && apt-get install -y --no-install-recommends build-essential ffmpeg curl && rm -rf /var/lib/apt/lists/* COPY requirements.txt . @@ -11,6 +9,12 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . +RUN addgroup --system --gid 1001 app && \ + adduser --system --uid 1001 --gid 1001 app && \ + chown -R app:app /app /uploads + +USER app + EXPOSE 8080 CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/api/app.py b/api/app.py index 89dc733..2c51b8b 100644 --- a/api/app.py +++ b/api/app.py @@ -1,6 +1,45 @@ +from contextlib import asynccontextmanager + from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from models import Base, engine + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Create all tables on startup + Base.metadata.create_all(bind=engine) + yield + + +app = FastAPI(title="LearnForge", version="0.2.0", lifespan=lifespan) + +# Security headers middleware +@app.middleware("http") +async def add_security_headers(request, call_next): + response = await call_next(request) + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["X-Frame-Options"] = "DENY" + response.headers["Content-Security-Policy"] = ( + "default-src 'self'; " + "script-src 'self' 'unsafe-inline'; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data:; " + "connect-src 'self'" + ) + response.headers["X-XSS-Protection"] = "1; mode=block" + response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" + return response -app = FastAPI(title="IT Training System", version="0.2.0") +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=[], + allow_credentials=True, + allow_methods=["GET"], + allow_headers=[], +) @app.get("/health") def health(): @@ -8,7 +47,7 @@ def health(): @app.get("/") def root(): - return {"service": "it-training-system-api", "docs": "/docs"} + return {"service": "LearnForge-api", "docs": "/docs"} from routes import training diff --git a/api/bots/telegram.py b/api/bots/telegram.py index 4b189da..1f387b2 100644 --- a/api/bots/telegram.py +++ b/api/bots/telegram.py @@ -1,7 +1,8 @@ -from fastapi import APIRouter, Request -import httpx import os +import httpx +from fastapi import APIRouter, Request + router = APIRouter() TELEGRAM_TOKEN=os.environ.get("TELEGRAM_BOT_TOKEN") diff --git a/api/models.py b/api/models.py new file mode 100644 index 0000000..320ef3d --- /dev/null +++ b/api/models.py @@ -0,0 +1,127 @@ +"""SQLAlchemy ORM models for LearnForge.""" + +import os +from sqlalchemy import Column, Integer, String, Text, Float, create_engine +from sqlalchemy.orm import declarative_base, sessionmaker + +DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///./app.db") + +engine = create_engine(DATABASE_URL, echo=False) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + + +def get_db(): + """FastAPI dependency that yields a database session.""" + db = SessionLocal() + try: + yield db + finally: + db.close() + + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False) + role = Column(String, nullable=True) + telegram_id = Column(String, nullable=True) + current_level = Column(String, nullable=True) + manager_id = Column(Integer, nullable=True) + + +class Video(Base): + __tablename__ = "videos" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, nullable=False) + file_path = Column(String, nullable=False) + raw_transcript = Column(Text, nullable=True) + summary = Column(Text, nullable=True) + duration = Column(Integer, nullable=True) + uploaded_by = Column(Integer, nullable=True) + created_at = Column(String, nullable=True) + + +class Skill(Base): + __tablename__ = "skills" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False) + category = Column(String, nullable=True) + + +class VideoSkill(Base): + __tablename__ = "video_skills" + + video_id = Column(Integer, primary_key=True) + skill_id = Column(Integer, primary_key=True) + + +class UserSkill(Base): + __tablename__ = "user_skills" + + user_id = Column(Integer, primary_key=True) + skill_id = Column(Integer, primary_key=True) + proficiency_score = Column(Float, nullable=True) + + +class Quiz(Base): + __tablename__ = "quizzes" + + id = Column(Integer, primary_key=True, index=True) + video_id = Column(Integer, nullable=True) + title = Column(String, nullable=True) + questions_json = Column(Text, nullable=True) + + +class Question(Base): + __tablename__ = "questions" + + id = Column(Integer, primary_key=True, index=True) + quiz_id = Column(Integer, nullable=True) + text = Column(Text, nullable=True) + options_json = Column(Text, nullable=True) + correct_index = Column(Integer, nullable=True) + + +class QuizAttempt(Base): + __tablename__ = "quiz_attempts" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, nullable=True) + quiz_id = Column(Integer, nullable=True) + score = Column(Float, nullable=True) + completed_at = Column(String, nullable=True) + + +class LearningPath(Base): + __tablename__ = "learning_paths" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, nullable=True) + title = Column(String, nullable=True) + description = Column(String, nullable=True) + status = Column(String, nullable=True) + + +class LearningPathItem(Base): + __tablename__ = "learning_path_items" + + id = Column(Integer, primary_key=True, index=True) + path_id = Column(Integer, nullable=True) + item_order = Column(Integer, nullable=True) + item_type = Column(String, nullable=True) + item_id = Column(Integer, nullable=True) + completed = Column(Integer, default=0) + + +class UserEvent(Base): + __tablename__ = "user_events" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, nullable=True) + event_type = Column(String, nullable=True) + metadata_json = Column(Text, nullable=True) + created_at = Column(String, nullable=True) diff --git a/api/requirements.txt b/api/requirements.txt index 7fdaace..9c13319 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -2,9 +2,9 @@ fastapi==0.111.0 uvicorn==0.30.1 httpx==0.27.2 sqlalchemy==2.0.36 -python-multipart==0.0.14 +python-multipart==0.0.31 aiofiles==24.1.0 minio==7.2.12 qdrant-client==1.12.1 ollama==0.4.1 -python-dotenv==1.0.1 +python-dotenv==1.2.2 diff --git a/api/routes/training.py b/api/routes/training.py index 7742660..696c349 100644 --- a/api/routes/training.py +++ b/api/routes/training.py @@ -1,11 +1,21 @@ -from fastapi import APIRouter, Depends, HTTPException, Query +import logging +from typing import Literal + +from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile from pydantic import BaseModel from sqlalchemy.orm import Session -from sqlalchemy import desc, or_ -from typing import Literal -from datetime import datetime -from app import get_db, User, Video, Quiz, QuizAttempt, LearningPath, LearningPathItem, UserEvent +from models import ( + LearningPath, + LearningPathItem, + Question, + Quiz, + QuizAttempt, + User, + UserEvent, + Video, + get_db, +) logger = logging.getLogger("training.routes") router = APIRouter() @@ -177,7 +187,7 @@ def team_overview(manager_id: int = Query(...), db: Session = Depends(get_db)): "quiz_count": len(attempts), "average_score": round(sum(a.score for a in attempts) / len(attempts), 2) if attempts else 0, "last_attempt": attempts[0].completed_at if attempts else None, - } + }, ) return out diff --git a/compose.env.example b/compose.env.example index 2fde8b6..528203c 100644 --- a/compose.env.example +++ b/compose.env.example @@ -1,6 +1,8 @@ # Copy to .env before first run +# WARNING: Change all default credentials before deploying to any network-accessible host # MinIO +# WARNING: Change these from defaults before exposing to any network MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=changeme @@ -9,6 +11,7 @@ OLLAMA_ORIGINS=* # Training app DATABASE_URL=sqlite:///./app.db +# WARNING: Generate a strong random key: python3 -c "import secrets; print(secrets.token_urlsafe(32))" SECRET_KEY=changeme TELEGRAM_BOT_TOKEN= TELEGRAM_ADMIN_CHAT_ID= diff --git a/docker-compose.yml b/docker-compose.yml index a3c5300..e946320 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,23 +1,31 @@ -version: "3.10" - services: ollama: image: ollama/ollama:latest container_name: training-ollama - ports: - - "11434:11434" + expose: + - "11434" volumes: - ollama:/root/.ollama restart: unless-stopped + healthcheck: + test: ["CMD", "ollama", "list"] + interval: 30s + timeout: 10s + retries: 3 qdrant: image: qdrant/qdrant:latest container_name: training-qdrant - ports: - - "6333:6333" + expose: + - "6333" volumes: - qdrant:/qdrant/storage restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:6333/healthz"] + interval: 30s + timeout: 10s + retries: 3 minio: image: minio/minio:latest @@ -30,8 +38,13 @@ services: - minio:/data environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-admin} - MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-***} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD must be set} restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 30s + timeout: 10s + retries: 3 training-api: build: @@ -42,7 +55,7 @@ services: - "8080:8080" environment: DATABASE_URL: ${DATABASE_URL} - SECRET_KEY: ${SECRET_KEY:-change-me} + SECRET_KEY: ${SECRET_KEY:?SECRET_KEY must be set} TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN} TELEGRAM_ADMIN_CHAT_ID: ${TELEGRAM_ADMIN_CHAT_ID} volumes: @@ -50,9 +63,17 @@ services: - uploads:/uploads restart: unless-stopped depends_on: - - qdrant - - minio - - ollama + qdrant: + condition: service_healthy + minio: + condition: service_healthy + ollama: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 volumes: ollama: diff --git a/docs/deploy.md b/docs/deploy.md index 501315e..3c4a3d9 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -1,4 +1,4 @@ -# IT Training System — Production Setup +# LearnForge — Production Setup ## 1. Prereqs on Ubuntu @@ -12,8 +12,8 @@ sudo snap install docker ## 2. Clone repo ```bash -git clone https://github.com/OneByJorah/it-training-system.git -cd it-training-system +git clone https://github.com/OneByJorah/LearnForge.git +cd LearnForge cp compose.env.example .env ``` diff --git a/docs/skill-authors.md b/docs/skill-authors.md index f5fd8f2..40d5b87 100644 --- a/docs/skill-authors.md +++ b/docs/skill-authors.md @@ -1,4 +1,4 @@ -# IT Training System — Skill Authoring Reference +# LearnForge — Skill Authoring Reference This document defines every SKILL.md field this repo uses plus behavior-bound rules: outputs themump; local edits will be overwritten by regeneration. diff --git a/j1.yaml b/j1.yaml new file mode 100644 index 0000000..6e28ce1 --- /dev/null +++ b/j1.yaml @@ -0,0 +1,12 @@ +repo: LearnForge +class: Education / Platform +org: OneByJorah +owner: Jhonattan L. Jimenez +license: MIT +production_score: 68 +last_audit: "2026-07-05" +deploy_target: scratch +mesh_vpn_only: false +public_facing: false +community_sla_hours: 48 +adoption_tracked: false diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index bab794a..7bdd987 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -9,9 +9,6 @@ fi echo "Starting training stack..." docker compose up -d -echo "Pulling a small Ollama model..." -docker exec -it training-ollama ollama pull llama3 || true - echo "Done." echo "API: http://localhost:8080" echo "MinIO: http://localhost:9001"