From 501f8985ecb8d6c704d1d803aae477c5dc46c3f7 Mon Sep 17 00:00:00 2001 From: ln0422 Date: Tue, 7 Apr 2026 22:07:34 +0800 Subject: [PATCH] Add /careerbot base path for www.ityb.me/careerbot deployment - Add BASE_PATH config, include all routers with prefix - Inject {{ base }} Jinja2 global for all template URLs - Add window.BASE_PATH for static JS files - Update Nginx to proxy /careerbot/ path - Add OPS_MANUAL.md Co-Authored-By: Claude Opus 4.6 --- OPS_MANUAL.md | 396 ++++++++++++++++++++++++++++++++ app/config.py | 1 + app/main.py | 29 ++- app/routers/admin.py | 5 +- app/routers/auth.py | 2 +- app/routers/public.py | 7 +- app/static/js/admin.js | 23 +- app/static/js/chat.js | 9 +- app/static/js/main.js | 5 +- templates/admin/dashboard.html | 23 +- templates/admin/education.html | 23 +- templates/admin/experience.html | 23 +- templates/admin/llm-config.html | 23 +- templates/admin/login.html | 7 +- templates/admin/messages.html | 23 +- templates/admin/profile.html | 23 +- templates/admin/resume.html | 30 +-- templates/admin/skills.html | 23 +- templates/admin/tokens.html | 23 +- templates/index.html | 7 +- templates/login.html | 11 +- 21 files changed, 574 insertions(+), 142 deletions(-) create mode 100644 OPS_MANUAL.md diff --git a/OPS_MANUAL.md b/OPS_MANUAL.md new file mode 100644 index 0000000..6d76a3c --- /dev/null +++ b/OPS_MANUAL.md @@ -0,0 +1,396 @@ +# CareerBot 线上运维手册 + +## 1. 服务器信息 + +| 项目 | 详情 | +|------|------| +| 云服务商 | 阿里云 ECS | +| 公网 IP | `39.106.14.107` | +| 操作系统 | Alibaba Cloud Linux 3 (OpenAnolis Edition) | +| 配置 | 2 核 CPU / 2GB 内存 / 40GB 磁盘 | +| Python 版本 | 3.11.13 | +| Nginx 版本 | 1.20.1 | +| Gitea 版本 | 1.22.6 | + +--- + +## 2. 账号信息 + +### 2.1 服务器账号 + +| 账号 | 用途 | 登录方式 | +|------|------|---------| +| `root` | 系统管理 | SSH 密钥 (`~/.ssh/id_ed25519`) | +| `deploy` | 应用部署和运行 | SSH 密钥(同上),sudo 免密 | +| `gitea` | Gitea 服务运行 | 系统用户,不可直接登录 | +| `nginx` | Nginx 服务运行 | 系统用户,不可直接登录 | + +**本地 SSH 快捷连接**(已配置在 `~/.ssh/config`): +```bash +ssh ecs # 以 deploy 用户连接 +ssh root@ecs # 以 root 用户连接(需将 config 中 User 临时改为 root,或直接用 IP) +ssh root@39.106.14.107 # root 直连 +``` + +### 2.2 应用账号 + +| 系统 | 地址 | 账号 | 密码 | +|------|------|------|------| +| CareerBot 管理后台 | `http://39.106.14.107/admin/login` | `ln0422@gmail.com` | `qshs123456` | +| Gitea 代码管理 | `http://39.106.14.107:3000` | `ln0422` | `Qshs123456_` | + +### 2.3 ECS 用户密码 + +| 用户 | 密码 | +|------|------| +| `deploy` | `CareerBot2026!` | + +> **注意**:日常运维通过 SSH 密钥登录,密码仅在密钥不可用时作为备用。 + +--- + +## 3. 服务架构 + +``` +外部访问 + │ + ▼ +┌─────────────────────────────────────────┐ +│ Nginx (:80) │ +│ 配置: /etc/nginx/sites-enabled/ │ +│ 日志: /var/log/nginx/ │ +├─────────────────────────────────────────┤ +│ http://39.106.14.107/ │ +│ → proxy_pass http://127.0.0.1:8000 │ +│ → CareerBot (uvicorn) │ +├─────────────────────────────────────────┤ +│ http://39.106.14.107:3000 │ +│ → Gitea (直接监听,未经 Nginx 代理) │ +└─────────────────────────────────────────┘ +``` + +### 端口使用 + +| 端口 | 服务 | 监听地址 | 防火墙 | 安全组 | +|------|------|---------|--------|--------| +| 22 | SSH | 0.0.0.0 | 已放行 | 已放行 | +| 80 | Nginx → CareerBot | 0.0.0.0 | 已放行 | 已放行 | +| 443 | HTTPS (预留) | - | 已放行 | 已放行 | +| 3000 | Gitea | 0.0.0.0 | 已放行 | 已放行 | +| 8000 | CareerBot (uvicorn) | 127.0.0.1 | 内部 | 无需 | + +--- + +## 4. 文件目录结构 + +``` +/home/deploy/apps/CareerBot/ ← 项目根目录 +├── venv/ ← Python 虚拟环境 +├── app/ ← 应用代码 +├── templates/ ← 页面模板 +├── careerbot.db ← SQLite 数据库(重要数据!) +├── uploads/ ← 用户上传文件 +├── requirements.txt +├── init_data.py +├── start.bat / stop.bat ← Windows 本地启动脚本(线上不用) +├── DESIGN.md ← 设计文档 +└── OPS_MANUAL.md ← 本文档 + +/etc/systemd/system/ +├── careerbot.service ← CareerBot systemd 服务 +└── gitea.service ← Gitea systemd 服务 + +/etc/nginx/ +├── nginx.conf ← Nginx 主配置 +├── sites-available/ +│ └── careerbot.conf ← CareerBot 站点配置 +└── sites-enabled/ + └── careerbot.conf → ../sites-available/careerbot.conf + +/usr/local/bin/gitea ← Gitea 二进制文件 +/etc/gitea/app.ini ← Gitea 配置文件 +/var/lib/gitea/ ← Gitea 数据目录(仓库、数据库) +``` + +--- + +## 5. 常用运维命令 + +### 5.1 服务管理 + +```bash +# ── CareerBot ── +sudo systemctl status careerbot # 查看状态 +sudo systemctl restart careerbot # 重启 +sudo systemctl stop careerbot # 停止 +sudo systemctl start careerbot # 启动 +sudo journalctl -u careerbot -f # 实时查看日志 +sudo journalctl -u careerbot --since "1 hour ago" # 查看最近1小时日志 + +# ── Nginx ── +sudo systemctl status nginx +sudo systemctl reload nginx # 重载配置(不中断连接) +sudo systemctl restart nginx # 重启 +sudo nginx -t # 测试配置文件语法 + +# ── Gitea ── +sudo systemctl status gitea +sudo systemctl restart gitea +sudo journalctl -u gitea -f +``` + +### 5.2 代码更新部署 + +**从本地推送代码并部署到线上(标准流程):** + +```bash +# 1. 本地:提交并推送到 Gitea +cd D:/Files/Projects/VibeCoding/CareerBot +git add . +git commit -m "描述改动" +git push origin main + +# 2. 线上:拉取并重启 +ssh ecs "cd ~/apps/CareerBot && git pull && sudo systemctl restart careerbot" +``` + +**一键部署脚本(在本地执行):** +```bash +ssh ecs "cd ~/apps/CareerBot && git pull origin main && sudo systemctl restart careerbot && sleep 2 && sudo systemctl is-active careerbot" +``` + +### 5.3 数据库操作 + +```bash +# 连接到 SQLite 数据库 +ssh ecs "cd ~/apps/CareerBot && sqlite3 careerbot.db" + +# 常用 SQL +.tables # 查看所有表 +SELECT * FROM admin_user; # 查看管理员 +SELECT token, note, max_questions, used_questions, is_active FROM access_token; # 查看令牌 +SELECT * FROM recruiter_message ORDER BY created_at DESC LIMIT 10; # 最近招聘消息 +SELECT COUNT(*) FROM chat_history; # 对话记录总数 +.quit # 退出 +``` + +### 5.4 系统监控 + +```bash +# 查看内存使用 +ssh ecs "free -h" + +# 查看磁盘使用 +ssh ecs "df -h /" + +# 查看各服务内存占用 +ssh ecs "ps aux --sort=-rss | head -10" + +# 查看端口监听 +ssh ecs "ss -tlnp" +``` + +--- + +## 6. 配置文件详情 + +### 6.1 CareerBot systemd 服务 + +文件:`/etc/systemd/system/careerbot.service` +```ini +[Unit] +Description=CareerBot Web Application +After=network.target + +[Service] +Type=simple +User=deploy +WorkingDirectory=/home/deploy/apps/CareerBot +ExecStart=/home/deploy/apps/CareerBot/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 +Restart=always +RestartSec=5 +Environment=PATH=/home/deploy/apps/CareerBot/venv/bin:/usr/bin + +[Install] +WantedBy=multi-user.target +``` + +关键参数: +- `--host 127.0.0.1`:只监听本地,由 Nginx 代理外部访问 +- `Restart=always`:崩溃后自动重启 +- `RestartSec=5`:重启间隔 5 秒 + +### 6.2 Nginx 站点配置 + +文件:`/etc/nginx/sites-available/careerbot.conf` +```nginx +server { + listen 80; + server_name 39.106.14.107; + + client_max_body_size 10M; + + location / { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # SSE 流式响应支持(关键!关闭缓冲) + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 300s; + } + + location /uploads/ { + alias /home/deploy/apps/CareerBot/uploads/; + } +} +``` + +关键配置说明: +- `proxy_buffering off` + `proxy_cache off`:**必须关闭**,否则 SSE 流式对话无法实时返回 +- `proxy_read_timeout 300s`:LLM 长回复可能需要较长时间 +- `client_max_body_size 10M`:允许上传最大 10MB 文件 + +### 6.3 防火墙规则 + +```bash +# 查看当前规则 +sudo firewall-cmd --list-ports +# 输出: 22/tcp 80/tcp 443/tcp 3000/tcp + +# 添加新端口(如未来部署新项目) +sudo firewall-cmd --permanent --add-port=PORT/tcp +sudo firewall-cmd --reload +``` + +> **注意**:开放端口还需要在阿里云控制台的**安全组**中同步添加入方向规则,否则外部仍无法访问。 + +--- + +## 7. 备份策略 + +### 7.1 数据库备份 + +CareerBot 的所有业务数据存储在 SQLite 文件中: + +```bash +# 手动备份 +ssh ecs "cp ~/apps/CareerBot/careerbot.db ~/apps/CareerBot/careerbot.db.bak.$(date +%Y%m%d)" + +# 下载到本地 +scp ecs:~/apps/CareerBot/careerbot.db ./careerbot_backup_$(date +%Y%m%d).db +``` + +### 7.2 Gitea 数据备份 + +```bash +# Gitea 数据和仓库 +ssh ecs "sudo tar czf /tmp/gitea-backup.tar.gz /var/lib/gitea /etc/gitea" +scp ecs:/tmp/gitea-backup.tar.gz ./gitea-backup.tar.gz +``` + +### 7.3 需要备份的关键文件 + +| 文件/目录 | 说明 | 重要程度 | +|-----------|------|---------| +| `~/apps/CareerBot/careerbot.db` | CareerBot 全部业务数据 | **极高** | +| `~/apps/CareerBot/uploads/` | 用户上传的文件 | 高 | +| `/var/lib/gitea/` | Gitea 所有仓库和数据 | **极高** | +| `/etc/gitea/app.ini` | Gitea 配置 | 中 | +| `/etc/nginx/sites-available/` | Nginx 站点配置 | 中 | +| `/etc/systemd/system/careerbot.service` | 服务配置 | 低(可重建) | + +--- + +## 8. 故障排查 + +### 8.1 CareerBot 无法访问 + +```bash +# 1. 检查服务状态 +sudo systemctl status careerbot + +# 2. 查看错误日志 +sudo journalctl -u careerbot --since "10 min ago" + +# 3. 检查端口 +ss -tlnp | grep 8000 + +# 4. 检查 Nginx +sudo nginx -t +sudo systemctl status nginx + +# 5. 手动启动测试 +cd ~/apps/CareerBot +source venv/bin/activate +uvicorn app.main:app --host 127.0.0.1 --port 8000 +``` + +### 8.2 AI 对话不工作 + +- 检查管理后台 LLM Config 是否已配置(API URL、API Key、Model Name) +- 点击 "Test Connection" 测试连通性 +- 确认 ECS 能访问外网:`curl https://api.deepseek.com` + +### 8.3 Gitea 无法访问 + +```bash +sudo systemctl status gitea +sudo journalctl -u gitea --since "10 min ago" +# 检查 3000 端口和安全组 +``` + +### 8.4 磁盘/内存不足 + +```bash +# 查看磁盘 +df -h / + +# 清理日志 +sudo journalctl --vacuum-time=7d + +# 查看内存 +free -h + +# 查看占用最多的进程 +ps aux --sort=-rss | head -10 +``` + +--- + +## 9. 新项目部署指南 + +后续在同一台 ECS 上部署新项目的标准流程: + +1. **Gitea 上创建仓库**:`http://39.106.14.107:3000` → New Repository + +2. **本地推送代码** +3. **ECS 上克隆并配置**: + ```bash + cd ~/apps + git clone http://39.106.14.107:3000/ln0422/新项目.git + # 安装依赖、初始化等 + ``` + +4. **创建 systemd 服务**(参考 careerbot.service,注意更换端口号) + +5. **添加 Nginx 站点配置**: + ```bash + sudo vim /etc/nginx/sites-available/新项目.conf + sudo ln -s /etc/nginx/sites-available/新项目.conf /etc/nginx/sites-enabled/ + sudo nginx -t && sudo systemctl reload nginx + ``` + +6. **如需新端口**:防火墙 + 阿里云安全组同步放行 + +--- + +## 10. 安全注意事项 + +1. **SSH 密钥**:本地私钥 `~/.ssh/id_ed25519` 请妥善保管,丢失需重新生成并更新服务器 +2. **Gitea 端口**:3000 端口对外开放,建议设置强密码,或后续通过 Nginx 代理并限制访问 +3. **数据库文件**:`careerbot.db` 不应对外暴露,uvicorn 仅监听 127.0.0.1 已确保安全 +4. **定期更新**:定期执行 `sudo dnf update -y` 更新系统安全补丁 +5. **HTTPS**:后续绑定域名后建议配置 Let's Encrypt SSL 证书 diff --git a/app/config.py b/app/config.py index cc51c4e..9db3634 100644 --- a/app/config.py +++ b/app/config.py @@ -8,6 +8,7 @@ class Settings(BaseSettings): ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 480 UPLOAD_DIR: str = "./uploads" + BASE_PATH: str = "/careerbot" model_config = {"env_file": ".env", "env_file_encoding": "utf-8"} diff --git a/app/main.py b/app/main.py index c59fade..c27084b 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,7 @@ import os from fastapi import FastAPI +from fastapi.responses import RedirectResponse from fastapi.staticfiles import StaticFiles from app.config import settings @@ -9,18 +10,30 @@ from app.models import AdminUser, Profile from app.routers import auth, public, admin, chat from app.routers.auth import hash_password +BASE = settings.BASE_PATH + app = FastAPI(title="CareerBot", description="Personal Career Showcase with AI Assistant") -# Mount static files and uploads -app.mount("/static", StaticFiles(directory="app/static"), name="static") +# Mount static files and uploads under BASE_PATH +app.mount(f"{BASE}/static", StaticFiles(directory="app/static"), name="static") os.makedirs(settings.UPLOAD_DIR, exist_ok=True) -app.mount("/uploads", StaticFiles(directory=settings.UPLOAD_DIR), name="uploads") +app.mount(f"{BASE}/uploads", StaticFiles(directory=settings.UPLOAD_DIR), name="uploads") -# Include routers -app.include_router(auth.router) -app.include_router(public.router) -app.include_router(admin.router) -app.include_router(chat.router) +# Include routers with BASE_PATH prefix +app.include_router(auth.router, prefix=BASE) +app.include_router(public.router, prefix=BASE) +app.include_router(admin.router, prefix=BASE) +app.include_router(chat.router, prefix=BASE) + +# Set Jinja2 template globals for BASE_PATH +admin.templates.env.globals["base"] = BASE +public.templates.env.globals["base"] = BASE + + +# Root redirect to CareerBot +@app.get("/") +def root_redirect(): + return RedirectResponse(url=f"{BASE}/", status_code=302) @app.on_event("startup") diff --git a/app/routers/admin.py b/app/routers/admin.py index 333a900..bc2a61c 100644 --- a/app/routers/admin.py +++ b/app/routers/admin.py @@ -9,6 +9,9 @@ from pydantic import BaseModel from sqlalchemy.orm import Session from app.config import settings + +BASE = settings.BASE_PATH + from app.database import get_db from app.models import ( Profile, Skill, Education, WorkExperience, @@ -95,7 +98,7 @@ async def upload_photo( if not p: p = Profile() db.add(p) - p.photo_url = f"/uploads/{filename}" + p.photo_url = f"{BASE}/uploads/{filename}" db.commit() return {"photo_url": p.photo_url} diff --git a/app/routers/auth.py b/app/routers/auth.py index 0124daf..4fba663 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -12,7 +12,7 @@ from app.database import get_db from app.models import AdminUser, AccessToken router = APIRouter() -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/admin/login", auto_error=False) +oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.BASE_PATH}/api/admin/login", auto_error=False) def hash_password(password: str) -> str: diff --git a/app/routers/public.py b/app/routers/public.py index 2f9f2b3..a0900f7 100644 --- a/app/routers/public.py +++ b/app/routers/public.py @@ -3,9 +3,12 @@ from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from sqlalchemy.orm import Session +from app.config import settings from app.database import get_db from app.models import Profile, Skill, Education, WorkExperience, AccessToken +BASE = settings.BASE_PATH + router = APIRouter() templates = Jinja2Templates(directory="templates") @@ -14,14 +17,14 @@ templates = Jinja2Templates(directory="templates") def index_page(request: Request, db: Session = Depends(get_db)): token = request.cookies.get("visitor_token") if not token: - return RedirectResponse(url="/login", status_code=302) + return RedirectResponse(url=f"{BASE}/login", status_code=302) # Allow both valid tokens and anonymous if token != "__anonymous__": record = db.query(AccessToken).filter( AccessToken.token == token, AccessToken.is_active == True ).first() if not record: - return RedirectResponse(url="/login", status_code=302) + return RedirectResponse(url=f"{BASE}/login", status_code=302) return templates.TemplateResponse(request, "index.html") diff --git a/app/static/js/admin.js b/app/static/js/admin.js index 70b961c..de95dad 100644 --- a/app/static/js/admin.js +++ b/app/static/js/admin.js @@ -1,36 +1,37 @@ // ── Admin Panel JavaScript ── +const BASE_PATH = window.BASE_PATH || ''; function authHeaders() { return { 'Authorization': 'Bearer ' + localStorage.getItem('admin_token') }; } async function apiGet(url) { - const resp = await fetch(url, { headers: authHeaders() }); - if (resp.status === 401) { window.location.href = '/admin/login'; return null; } + const resp = await fetch(BASE_PATH + url, { headers: authHeaders() }); + if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; } return resp.json(); } async function apiPost(url, data) { - const resp = await fetch(url, { + const resp = await fetch(BASE_PATH + url, { method: 'POST', headers: { ...authHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); - if (resp.status === 401) { window.location.href = '/admin/login'; return null; } + if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; } return resp.json(); } async function apiPut(url, data) { - const resp = await fetch(url, { + const resp = await fetch(BASE_PATH + url, { method: 'PUT', headers: { ...authHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); - if (resp.status === 401) { window.location.href = '/admin/login'; return null; } + if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; } return resp.json(); } async function apiDelete(url) { - const resp = await fetch(url, { method: 'DELETE', headers: authHeaders() }); - if (resp.status === 401) { window.location.href = '/admin/login'; return null; } + const resp = await fetch(BASE_PATH + url, { method: 'DELETE', headers: authHeaders() }); + if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; } return resp.json(); } @@ -68,7 +69,7 @@ document.addEventListener('DOMContentLoaded', () => { function adminLogout() { localStorage.removeItem('admin_token'); - window.location.href = '/admin/login'; + window.location.href = BASE_PATH + '/admin/login'; } // ── Dashboard ── @@ -113,10 +114,10 @@ async function uploadPhoto() { if (!input.files[0]) return; const formData = new FormData(); formData.append('file', input.files[0]); - const resp = await fetch('/api/admin/profile/photo', { + const resp = await fetch(BASE_PATH + '/api/admin/profile/photo', { method: 'POST', headers: authHeaders(), body: formData, }); - if (resp.status === 401) { window.location.href = '/admin/login'; return; } + if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return; } const data = await resp.json(); if (data.photo_url) { document.getElementById('photo-preview').innerHTML = ``; diff --git a/app/static/js/chat.js b/app/static/js/chat.js index 54cf569..2ba829f 100644 --- a/app/static/js/chat.js +++ b/app/static/js/chat.js @@ -1,4 +1,5 @@ // ── Chat Widget JavaScript ── +const BASE_PATH = window.BASE_PATH || ''; let chatSessionId = sessionStorage.getItem('chat_session'); if (!chatSessionId) { @@ -66,7 +67,7 @@ document.addEventListener('DOMContentLoaded', () => { async function loadChatConfig() { try { - const resp = await fetch('/api/chat/config'); + const resp = await fetch(BASE_PATH + '/api/chat/config'); if (resp.ok) { const data = await resp.json(); maxQuestions = data.max_questions || 10; @@ -96,7 +97,7 @@ async function loadChatConfig() { async function loadChatHistory() { try { - const resp = await fetch(`/api/chat/history/${chatSessionId}`); + const resp = await fetch(BASE_PATH + `/api/chat/history/${chatSessionId}`); if (!resp.ok) return; const history = await resp.json(); for (const msg of history) { @@ -147,7 +148,7 @@ async function sendMessage() { formData.append('message', message); if (file) formData.append('file', file); - const response = await fetch('/api/chat', { + const response = await fetch(BASE_PATH + '/api/chat', { method: 'POST', body: formData, }); @@ -243,7 +244,7 @@ async function enterToken() { const token = prompt('请输入访问令牌 (Access Token):'); if (!token || !token.trim()) return; try { - const resp = await fetch('/api/verify-token', { + const resp = await fetch(BASE_PATH + '/api/verify-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: token.trim() }), diff --git a/app/static/js/main.js b/app/static/js/main.js index 0e947b1..aa5b708 100644 --- a/app/static/js/main.js +++ b/app/static/js/main.js @@ -1,4 +1,5 @@ // ── Main Page JavaScript ── +const BASE_PATH = window.BASE_PATH || ''; document.addEventListener('DOMContentLoaded', () => { loadProfile(); @@ -8,9 +9,9 @@ document.addEventListener('DOMContentLoaded', () => { }); async function apiFetch(url) { - const resp = await fetch(url); + const resp = await fetch(BASE_PATH + url); if (resp.status === 401) { - window.location.href = '/login'; + window.location.href = BASE_PATH + '/login'; return null; } return resp.json(); diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html index 2a32316..187e269 100644 --- a/templates/admin/dashboard.html +++ b/templates/admin/dashboard.html @@ -4,22 +4,22 @@ CareerBot Admin - Dashboard - + - + + diff --git a/templates/admin/education.html b/templates/admin/education.html index 11184cd..6831488 100644 --- a/templates/admin/education.html +++ b/templates/admin/education.html @@ -4,22 +4,22 @@ CareerBot Admin - Education - + - + + diff --git a/templates/admin/experience.html b/templates/admin/experience.html index 29caefa..6392d9e 100644 --- a/templates/admin/experience.html +++ b/templates/admin/experience.html @@ -4,22 +4,22 @@ CareerBot Admin - Experience - + - + + diff --git a/templates/admin/llm-config.html b/templates/admin/llm-config.html index 0e191ea..0d35611 100644 --- a/templates/admin/llm-config.html +++ b/templates/admin/llm-config.html @@ -4,22 +4,22 @@ CareerBot Admin - LLM Configuration - + - + + diff --git a/templates/admin/login.html b/templates/admin/login.html index 6448010..83f4c25 100644 --- a/templates/admin/login.html +++ b/templates/admin/login.html @@ -4,7 +4,7 @@ CareerBot Admin - Login - + + + diff --git a/templates/admin/profile.html b/templates/admin/profile.html index 99ea5e0..8cfc8b5 100644 --- a/templates/admin/profile.html +++ b/templates/admin/profile.html @@ -4,22 +4,22 @@ CareerBot Admin - Profile - + - + + diff --git a/templates/admin/resume.html b/templates/admin/resume.html index 13970cc..e2bdcb3 100644 --- a/templates/admin/resume.html +++ b/templates/admin/resume.html @@ -4,7 +4,7 @@ CareerBot Admin - Resume Generator - +