This commit is contained in:
2025-11-28 18:27:15 +09:00
parent 2481d44eb8
commit c0d3312bca
52 changed files with 13363 additions and 1444 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

65
app.py
View File

@@ -8,6 +8,7 @@ from flask_login import LoginManager
from flask_migrate import Migrate
from flask_socketio import SocketIO
from flask_wtf import CSRFProtect
from dotenv import load_dotenv
from config import Config
from backend.models.user import db, load_user
@@ -15,10 +16,19 @@ from backend.routes import register_routes
from backend.services.logger import setup_logging
from backend.services import watchdog_handler
# 텔레그램 서비스 (별도 모듈)에서 가져옴
from telegram_bot_service import run_polling as telegram_run_polling
# ─────────────────────────────────────────────────────────────
# .env 파일 로드 (환경변수 우선순위 보장)
# ─────────────────────────────────────────────────────────────
load_dotenv()
# ─────────────────────────────────────────────────────────────
# 템플릿/정적 경로를 파일 위치 기준으로 안전하게 설정
# structure: <project_root>/backend/templates, <project_root>/backend/static
# ─────────────────────────────────────────────────────────────
BASE_DIR = Path(__file__).resolve().parent
TEMPLATE_DIR = (BASE_DIR / "backend" / "templates").resolve()
STATIC_DIR = (BASE_DIR / "backend" / "static").resolve()
@@ -32,9 +42,11 @@ setup_logging(app)
# ─────────────────────────────────────────────────────────────
# CSRF 보호 + 템플릿에서 {{ csrf_token() }} 사용 가능하게 주입
# ─────────────────────────────────────────────────────────────
csrf = CSRFProtect()
csrf.init_app(app)
@app.context_processor
def inject_csrf():
try:
@@ -44,9 +56,11 @@ def inject_csrf():
# Flask-WTF 미설치/에러 시에도 앱이 뜨도록 방어
return dict(csrf_token=lambda: "")
# ─────────────────────────────────────────────────────────────
# SocketIO: Windows 기본 threading, Linux는 eventlet 설치 시 eventlet 사용
# 환경변수 SOCKETIO_ASYNC_MODE 로 강제 지정 가능 ("threading"/"eventlet"/"gevent"/"auto")
# ─────────────────────────────────────────────────────────────
async_mode = (app.config.get("SOCKETIO_ASYNC_MODE") or "threading").lower()
if async_mode == "auto":
async_mode = "threading"
@@ -67,39 +81,86 @@ socketio = SocketIO(app, cors_allowed_origins="*", async_mode=async_mode)
# watchdog에서 socketio 사용
watchdog_handler.socketio = socketio
# ─────────────────────────────────────────────────────────────
# DB / 마이그레이션
# ─────────────────────────────────────────────────────────────
app.logger.info("DB URI = %s", app.config.get("SQLALCHEMY_DATABASE_URI"))
db.init_app(app)
Migrate(app, db)
# (선택) 개발 편의용: 테이블 자동 부트스트랩
# 환경변수 AUTO_BOOTSTRAP_DB=true 일 때만 동작 (운영에서는 flask db upgrade 사용 권장)
if (os.getenv("AUTO_BOOTSTRAP_DB", "false").lower() == "true"):
if os.getenv("AUTO_BOOTSTRAP_DB", "false").lower() == "true":
from sqlalchemy import inspect
with app.app_context():
insp = inspect(db.engine)
if "user" not in insp.get_table_names():
db.create_all()
app.logger.info("DB bootstrap: created tables via create_all()")
# ─────────────────────────────────────────────────────────────
# Login
# ─────────────────────────────────────────────────────────────
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = "auth.login"
@login_manager.user_loader
def _load_user(user_id: str):
return load_user(user_id)
# 라우트 등록 (Blueprints 등)
register_routes(app, socketio)
# ─────────────────────────────────────────────────────────────
# 텔레그램 봇 폴링 서비스 (중복 실행 방지 포함)
# ─────────────────────────────────────────────────────────────
_bot_polling_started = False # 전역 플래그로 중복 방지
def start_telegram_bot_polling() -> None:
"""텔레그램 봇 폴링을 백그라운드 스레드로 시작 (한 번만 실행)"""
import threading
global _bot_polling_started
if _bot_polling_started:
app.logger.warning("🤖 텔레그램 봇 폴링은 이미 시작됨 - 중복 요청 무시")
return
_bot_polling_started = True
def _runner():
try:
# telegram_bot_service.run_polling(app) 호출
telegram_run_polling(app)
except Exception as e:
app.logger.error("텔레그램 봇 폴링 서비스 오류: %s", e)
polling_thread = threading.Thread(target=_runner, daemon=True)
polling_thread.start()
app.logger.info("🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)")
# ─────────────────────────────────────────────────────────────
# 텔레그램 봇 폴링 자동 시작
# Flask 앱이 초기화되면 자동으로 봇 폴링 시작
# ─────────────────────────────────────────────────────────────
start_telegram_bot_polling()
# ─────────────────────────────────────────────────────────────
# 엔트리포인트
# ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
host = os.getenv("FLASK_HOST", "0.0.0.0")
port = int(os.getenv("FLASK_PORT", 5000))
debug = os.getenv("FLASK_DEBUG", "true").lower() == "true"
socketio.run(app, host=host, port=port, debug=debug, allow_unsafe_werkzeug=True)
socketio.run(app, host=host, port=port, debug=debug, allow_unsafe_werkzeug=True, use_reloader=False)

Binary file not shown.

View File

@@ -0,0 +1,24 @@
from backend.models.user import db
class TelegramBot(db.Model):
__tablename__ = 'telegram_bots'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False) # 봇 식별 이름 (예: 알림용, 경고용)
token = db.Column(db.String(200), nullable=False)
chat_id = db.Column(db.String(100), nullable=False)
is_active = db.Column(db.Boolean, default=True)
description = db.Column(db.String(255), nullable=True)
# 알림 유형: 콤마로 구분 (예: "auth,activity,system")
notification_types = db.Column(db.String(255), default="auth,activity,system")
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'token': self.token,
'chat_id': self.chat_id,
'is_active': self.is_active,
'description': self.description,
'notification_types': self.notification_types
}

View File

@@ -38,6 +38,10 @@ class User(db.Model, UserMixin):
password: Mapped[str] = mapped_column(String(255), nullable=False)
is_admin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
# 가입 승인 관련 필드
is_approved: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
approval_token: Mapped[Optional[str]] = mapped_column(String(100), unique=True, nullable=True)
# ── 유틸 메서드
def __repr__(self) -> str: # pragma: no cover

View File

@@ -10,6 +10,7 @@ from .file_view import register_file_view
from .jobs import register_jobs_routes
from .idrac_routes import register_idrac_routes
from .catalog_sync import catalog_bp
from .scp_routes import scp_bp
def register_routes(app: Flask, socketio=None) -> None:
"""블루프린트 일괄 등록. socketio는 main 라우트에서만 사용."""
@@ -23,3 +24,4 @@ def register_routes(app: Flask, socketio=None) -> None:
register_jobs_routes(app)
register_idrac_routes(app)
app.register_blueprint(catalog_bp)
app.register_blueprint(scp_bp)

Binary file not shown.

View File

@@ -18,6 +18,11 @@ from flask import (
from flask_login import login_required, current_user
from backend.models.user import User, db
from backend.models.telegram_bot import TelegramBot
try:
from telegram import Bot
except ImportError:
Bot = None
admin_bp = Blueprint("admin", __name__)
@@ -124,3 +129,111 @@ def reset_password(user_id: int):
flash("비밀번호 변경 중 오류가 발생했습니다.", "danger")
return redirect(url_for("admin.admin_panel"))
# ▼▼▼ 시스템 설정 (텔레그램 봇 관리) ▼▼▼
@admin_bp.route("/admin/settings", methods=["GET"])
@login_required
@admin_required
def settings():
# 테이블 생성 확인 (임시)
try:
bots = TelegramBot.query.all()
except Exception:
db.create_all()
bots = []
return render_template("admin_settings.html", bots=bots)
@admin_bp.route("/admin/settings/bot/add", methods=["POST"])
@login_required
@admin_required
def add_bot():
name = request.form.get("name")
token = request.form.get("token")
chat_id = request.form.get("chat_id")
desc = request.form.get("description")
# 알림 유형 (체크박스 다중 선택)
notify_types = request.form.getlist("notify_types")
notify_str = ",".join(notify_types) if notify_types else ""
if not (name and token and chat_id):
flash("필수 항목(이름, 토큰, Chat ID)을 입력하세요.", "warning")
return redirect(url_for("admin.settings"))
bot = TelegramBot(
name=name,
token=token,
chat_id=chat_id,
description=desc,
is_active=True,
notification_types=notify_str
)
db.session.add(bot)
db.session.commit()
flash("텔레그램 봇이 추가되었습니다.", "success")
return redirect(url_for("admin.settings"))
@admin_bp.route("/admin/settings/bot/edit/<int:bot_id>", methods=["POST"])
@login_required
@admin_required
def edit_bot(bot_id):
bot = db.session.get(TelegramBot, bot_id)
if not bot:
abort(404)
bot.name = request.form.get("name")
bot.token = request.form.get("token")
bot.chat_id = request.form.get("chat_id")
bot.description = request.form.get("description")
bot.is_active = request.form.get("is_active") == "on"
# 알림 유형 업데이트
notify_types = request.form.getlist("notify_types")
bot.notification_types = ",".join(notify_types) if notify_types else ""
db.session.commit()
flash("봇 설정이 수정되었습니다.", "success")
return redirect(url_for("admin.settings"))
@admin_bp.route("/admin/settings/bot/delete/<int:bot_id>", methods=["POST"])
@login_required
@admin_required
def delete_bot(bot_id):
bot = db.session.get(TelegramBot, bot_id)
if bot:
db.session.delete(bot)
db.session.commit()
flash("봇이 삭제되었습니다.", "success")
return redirect(url_for("admin.settings"))
@admin_bp.route("/admin/settings/bot/test/<int:bot_id>", methods=["POST"])
@login_required
@admin_required
def test_bot(bot_id):
if not Bot:
flash("python-telegram-bot 라이브러리가 설치되지 않았습니다.", "danger")
return redirect(url_for("admin.settings"))
bot_obj = db.session.get(TelegramBot, bot_id)
if not bot_obj:
abort(404)
import asyncio
async def _send():
bot = Bot(token=bot_obj.token)
await bot.send_message(chat_id=bot_obj.chat_id, text="🔔 <b>테스트 메시지</b>\n이 메시지가 보이면 설정이 정상입니다.", parse_mode="HTML")
try:
asyncio.run(_send())
flash(f"'{bot_obj.name}' 봇으로 테스트 메시지를 보냈습니다.", "success")
except Exception as e:
flash(f"테스트 실패: {e}", "danger")
return redirect(url_for("admin.settings"))

View File

@@ -3,8 +3,11 @@ from __future__ import annotations
import logging
import threading
import asyncio
import secrets
from typing import Optional
from urllib.parse import urlparse, urljoin
from datetime import datetime
from flask import (
Blueprint,
@@ -23,11 +26,13 @@ from backend.models.user import User, db
# ── (선택) Telegram: 미설정이면 조용히 패스
try:
from telegram import Bot
from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.constants import ParseMode
except Exception: # 라이브러리 미설치/미사용 환경
Bot = None
ParseMode = None
InlineKeyboardButton = None
InlineKeyboardMarkup = None
auth_bp = Blueprint("auth", __name__)
@@ -42,21 +47,162 @@ def _is_safe_url(target: str) -> bool:
return (test.scheme in ("http", "https")) and (ref.netloc == test.netloc)
def _notify(text: str) -> None:
"""텔레그램 알림 (설정 없으면 바로 return)."""
token = (current_app.config.get("TELEGRAM_BOT_TOKEN") or "").strip()
chat_id = (current_app.config.get("TELEGRAM_CHAT_ID") or "").strip()
if not (token and chat_id and Bot and ParseMode):
return
def _send():
def _notify(text: str, category: str = "system") -> None:
"""
텔레그램 알림 전송
- DB(TelegramBot)에 등록된 활성 봇들에게 전송
- category: 'auth', 'activity', 'system'
"""
try:
from backend.models.telegram_bot import TelegramBot
# 앱 컨텍스트 안에서 실행되므로 바로 DB 접근 가능
try:
bot = Bot(token=token)
bot.send_message(chat_id=chat_id, text=text, parse_mode=ParseMode.HTML)
except Exception as e:
current_app.logger.warning("Telegram send failed: %s", e)
bots = TelegramBot.query.filter_by(is_active=True).all()
except Exception:
db.create_all()
bots = []
threading.Thread(target=_send, daemon=True).start()
# 1. DB에 봇이 없고, Config에 설정이 있는 경우 -> 자동 마이그레이션
if not bots:
cfg_token = (current_app.config.get("TELEGRAM_BOT_TOKEN") or "").strip()
cfg_chat = (current_app.config.get("TELEGRAM_CHAT_ID") or "").strip()
if cfg_token and cfg_chat:
new_bot = TelegramBot(
name="기본 봇 (Config)",
token=cfg_token,
chat_id=cfg_chat,
is_active=True,
description="config.py 설정에서 자동 가져옴",
notification_types="auth,activity,system"
)
db.session.add(new_bot)
db.session.commit()
bots = [new_bot]
current_app.logger.info("Telegram: Config settings migrated to DB.")
if not bots:
return
# 카테고리 필터링
target_bots = []
for b in bots:
allowed = (b.notification_types or "auth,activity,system").split(",")
if category in allowed:
target_bots.append(b)
if not target_bots:
return
if not (Bot and ParseMode):
current_app.logger.warning("Telegram: Library not installed.")
return
app = current_app._get_current_object()
async def _send_to_bot(bot_obj, msg):
try:
t_bot = Bot(token=bot_obj.token)
await t_bot.send_message(chat_id=bot_obj.chat_id, text=msg, parse_mode=ParseMode.HTML)
except Exception as e:
app.logger.error(f"Telegram fail ({bot_obj.name}): {e}")
async def _send_all():
tasks = [_send_to_bot(b, text) for b in target_bots]
await asyncio.gather(*tasks)
def _run_thread():
try:
asyncio.run(_send_all())
except Exception as e:
app.logger.error(f"Telegram async loop error: {e}")
threading.Thread(target=_run_thread, daemon=True).start()
except Exception as e:
current_app.logger.error(f"Telegram notification error: {e}")
def _notify_with_buttons(text: str, buttons: list, category: str = "auth") -> None:
"""
텔레그램 알림 전송 (인라인 버튼 포함)
- buttons: [(text, callback_data), ...] 형식의 리스트
"""
try:
from backend.models.telegram_bot import TelegramBot
try:
bots = TelegramBot.query.filter_by(is_active=True).all()
except Exception:
db.create_all()
bots = []
if not bots:
cfg_token = (current_app.config.get("TELEGRAM_BOT_TOKEN") or "").strip()
cfg_chat = (current_app.config.get("TELEGRAM_CHAT_ID") or "").strip()
if cfg_token and cfg_chat:
new_bot = TelegramBot(
name="기본 봇 (Config)",
token=cfg_token,
chat_id=cfg_chat,
is_active=True,
description="config.py 설정에서 자동 가져옴",
notification_types="auth,activity,system"
)
db.session.add(new_bot)
db.session.commit()
bots = [new_bot]
if not bots:
return
target_bots = []
for b in bots:
allowed = (b.notification_types or "auth,activity,system").split(",")
if category in allowed:
target_bots.append(b)
if not target_bots:
return
if not (Bot and ParseMode and InlineKeyboardButton and InlineKeyboardMarkup):
current_app.logger.warning("Telegram: Library not installed.")
return
# 인라인 키보드 생성
keyboard = [[InlineKeyboardButton(btn[0], callback_data=btn[1]) for btn in buttons]]
reply_markup = InlineKeyboardMarkup(keyboard)
app = current_app._get_current_object()
async def _send_to_bot(bot_obj, msg, markup):
try:
t_bot = Bot(token=bot_obj.token)
await t_bot.send_message(
chat_id=bot_obj.chat_id,
text=msg,
parse_mode=ParseMode.HTML,
reply_markup=markup
)
except Exception as e:
app.logger.error(f"Telegram fail ({bot_obj.name}): {e}")
async def _send_all():
tasks = [_send_to_bot(b, text, reply_markup) for b in target_bots]
await asyncio.gather(*tasks)
def _run_thread():
try:
asyncio.run(_send_all())
except Exception as e:
app.logger.error(f"Telegram async loop error: {e}")
threading.Thread(target=_run_thread, daemon=True).start()
except Exception as e:
current_app.logger.error(f"Telegram notification with buttons error: {e}")
# ─────────────────────────────────────────────────────────────
@@ -67,12 +213,28 @@ def register_auth_routes(app):
app.register_blueprint(auth_bp)
@app.before_request
def _touch_session():
# 요청마다 세션 갱신(만료 슬라이딩) + 로그아웃 플래그 정리
def _global_hooks():
# 1. 세션 갱신 (요청마다 세션 타임아웃 연장)
session.modified = True
if current_user.is_authenticated and session.get("just_logged_out"):
session.pop("just_logged_out", None)
flash("세션이 만료되어 자동 로그아웃 되었습니다.", "info")
# 2. 활동 알림 (로그인된 사용자)
if current_user.is_authenticated:
# 정적 리소스 및 불필요한 경로 제외
if request.endpoint == 'static':
return
# 제외할 확장자/경로 (필요 시 추가)
ignored_exts = ('.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2')
if request.path.lower().endswith(ignored_exts):
return
# 활동 내용 구성
msg = (
f"👣 <b>활동 감지</b>\n"
f"👤 <code>{current_user.username}</code>\n"
f"📍 <code>{request.method} {request.path}</code>"
)
_notify(msg, category="activity")
# ─────────────────────────────────────────────────────────────
@@ -97,17 +259,36 @@ def register():
current_app.logger.info("REGISTER: dup username %s", form.username.data)
return render_template("register.html", form=form)
user = User(username=form.username.data, email=form.email.data, is_active=False)
user.set_password(form.password.data) # passlib: 기본 Argon2id
# 승인 토큰 생성
approval_token = secrets.token_urlsafe(32)
user = User(
username=form.username.data,
email=form.email.data,
is_active=False,
is_approved=False,
approval_token=approval_token
)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
_notify(
f"🆕 <b>신규 가입 요청</b>\n"
f"📛 사용자: <code>{user.username}</code>\n"
f"📧 이메일: <code>{user.email}</code>"
# 텔레그램 알림 (인라인 버튼 포함)
message = (
f"🆕 <b>신규 가입 요청</b>\n\n"
f"<EFBFBD> 사용자: <code>{user.username}</code>\n"
f"📧 이메일: <code>{user.email}</code>\n"
f"🕐 신청시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
current_app.logger.info("REGISTER: created id=%s email=%s", user.id, user.email)
buttons = [
("✅ 승인", f"approve_{approval_token}"),
("❌ 거부", f"reject_{approval_token}")
]
_notify_with_buttons(message, buttons, category="auth")
current_app.logger.info("REGISTER: created id=%s email=%s token=%s", user.id, user.email, approval_token[:10])
flash("회원가입이 완료되었습니다. 관리자의 승인을 기다려주세요.", "success")
return redirect(url_for("auth.login"))
else:
@@ -136,24 +317,28 @@ def login():
current_app.logger.info("LOGIN: user not found")
return render_template("login.html", form=form)
pass_ok = user.check_password(form.password.data) # passlib verify(+자동 재해시)
pass_ok = user.check_password(form.password.data)
current_app.logger.info(
"LOGIN: found id=%s active=%s pass_ok=%s",
user.id, user.is_active, pass_ok
"LOGIN: found id=%s active=%s approved=%s pass_ok=%s",
user.id, user.is_active, user.is_approved, pass_ok
)
if not pass_ok:
flash("이메일 또는 비밀번호가 올바르지 않습니다.", "danger")
return render_template("login.html", form=form)
if not user.is_approved:
flash("계정이 아직 승인되지 않았습니다. 관리자의 승인을 기다려주세요.", "warning")
return render_template("login.html", form=form)
if not user.is_active:
flash("계정이 아직 승인되지 않았습니다.", "warning")
flash("계정이 비활성화되었습니다. 관리자에게 문의하세요.", "warning")
return render_template("login.html", form=form)
# 성공
login_user(user, remember=form.remember.data)
session.permanent = True
_notify(f"🔐 <b>로그인 성공</b>\n👤 <code>{user.username}</code>")
_notify(f"🔐 <b>로그인 성공</b>\n👤 <code>{user.username}</code>", category="auth")
current_app.logger.info("LOGIN: SUCCESS → redirect")
nxt = request.args.get("next")
@@ -175,6 +360,7 @@ def login():
def logout():
if current_user.is_authenticated:
current_app.logger.info("LOGOUT: user=%s", current_user.username)
_notify(f"🚪 <b>로그아웃</b>\n👤 <code>{current_user.username}</code>", category="auth")
logout_user()
session["just_logged_out"] = True
flash("정상적으로 로그아웃 되었습니다.", "success")
return redirect(url_for("auth.login"))

View File

@@ -0,0 +1,144 @@
from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for
from flask_login import login_required, current_user
import logging
import difflib
from pathlib import Path
from config import Config
from backend.services.redfish_client import RedfishClient
from backend.routes.xml import sanitize_preserve_unicode
scp_bp = Blueprint("scp", __name__)
logger = logging.getLogger(__name__)
@scp_bp.route("/scp/diff", methods=["GET"])
@login_required
def diff_scp():
"""
두 XML 파일의 차이점을 비교하여 보여줍니다.
"""
file1_name = request.args.get("file1")
file2_name = request.args.get("file2")
if not file1_name or not file2_name:
flash("비교할 두 파일을 선택해주세요.", "warning")
return redirect(url_for("xml.xml_management"))
try:
file1_path = Path(Config.XML_FOLDER) / sanitize_preserve_unicode(file1_name)
file2_path = Path(Config.XML_FOLDER) / sanitize_preserve_unicode(file2_name)
if not file1_path.exists() or not file2_path.exists():
flash("파일을 찾을 수 없습니다.", "danger")
return redirect(url_for("xml.xml_management"))
# 파일 내용 읽기 (LF로 통일)
content1 = file1_path.read_text(encoding="utf-8").replace("\r\n", "\n").splitlines()
content2 = file2_path.read_text(encoding="utf-8").replace("\r\n", "\n").splitlines()
# Diff 생성
diff = difflib.unified_diff(
content1, content2,
fromfile=file1_name,
tofile=file2_name,
lineterm=""
)
diff_content = "\n".join(diff)
return render_template("scp_diff.html", file1=file1_name, file2=file2_name, diff_content=diff_content)
except Exception as e:
logger.error(f"Diff error: {e}")
flash(f"비교 중 오류가 발생했습니다: {str(e)}", "danger")
return redirect(url_for("xml.xml_management"))
@scp_bp.route("/scp/export", methods=["POST"])
@login_required
def export_scp():
"""
iDRAC에서 설정을 내보내기 (Export)
네트워크 공유 설정이 필요합니다.
"""
data = request.form
target_ip = data.get("target_ip")
username = data.get("username")
password = data.get("password")
# Share Parameters
share_ip = data.get("share_ip")
share_name = data.get("share_name")
share_user = data.get("share_user")
share_pwd = data.get("share_pwd")
filename = data.get("filename")
if not all([target_ip, username, password, share_ip, share_name, filename]):
flash("필수 정보가 누락되었습니다.", "warning")
return redirect(url_for("xml.xml_management"))
share_params = {
"IPAddress": share_ip,
"ShareName": share_name,
"FileName": filename,
"ShareType": "CIFS", # 기본값 CIFS
"UserName": share_user,
"Password": share_pwd
}
try:
with RedfishClient(target_ip, username, password) as client:
job_id = client.export_system_configuration(share_params)
if job_id:
flash(f"내보내기 작업이 시작되었습니다. Job ID: {job_id}", "success")
else:
flash("작업을 시작했으나 Job ID를 받지 못했습니다.", "warning")
except Exception as e:
logger.error(f"Export failed: {e}")
flash(f"내보내기 실패: {str(e)}", "danger")
return redirect(url_for("xml.xml_management"))
@scp_bp.route("/scp/import", methods=["POST"])
@login_required
def import_scp():
"""
iDRAC로 설정 가져오기 (Import/Deploy)
"""
data = request.form
target_ip = data.get("target_ip")
username = data.get("username")
password = data.get("password")
# Share Parameters
share_ip = data.get("share_ip")
share_name = data.get("share_name")
share_user = data.get("share_user")
share_pwd = data.get("share_pwd")
filename = data.get("filename")
import_mode = data.get("import_mode", "Replace")
if not all([target_ip, username, password, share_ip, share_name, filename]):
flash("필수 정보가 누락되었습니다.", "warning")
return redirect(url_for("xml.xml_management"))
share_params = {
"IPAddress": share_ip,
"ShareName": share_name,
"FileName": filename,
"ShareType": "CIFS",
"UserName": share_user,
"Password": share_pwd
}
try:
with RedfishClient(target_ip, username, password) as client:
job_id = client.import_system_configuration(share_params, import_mode=import_mode)
if job_id:
flash(f"설정 적용(Import) 작업이 시작되었습니다. Job ID: {job_id}", "success")
else:
flash("작업을 시작했으나 Job ID를 받지 못했습니다.", "warning")
except Exception as e:
logger.error(f"Import failed: {e}")
flash(f"설정 적용 실패: {str(e)}", "danger")
return redirect(url_for("xml.xml_management"))

View File

@@ -45,18 +45,31 @@ class RedfishClient:
ip: str,
username: str,
password: str,
timeout: int = 15,
verify_ssl: bool = False
timeout: Optional[int] = None,
verify_ssl: Optional[bool] = None
):
self.ip = ip
self.base_url = f"https://{ip}/redfish/v1"
self.host_url = f"https://{ip}" # ← 추가: 호스트 URL
self.timeout = timeout
self.verify_ssl = verify_ssl
self.host_url = f"https://{ip}"
# Config defaults
default_timeout = 15
default_verify = False
try:
from flask import current_app
if current_app:
default_timeout = current_app.config.get("REDFISH_TIMEOUT", 15)
default_verify = current_app.config.get("REDFISH_VERIFY_SSL", False)
except ImportError:
pass
self.timeout = timeout if timeout is not None else default_timeout
self.verify_ssl = verify_ssl if verify_ssl is not None else default_verify
self.session = requests.Session()
self.session.auth = (username, password)
self.session.verify = verify_ssl
self.session.verify = self.verify_ssl
self.session.headers.update({
"Content-Type": "application/json",
"Accept": "application/json"
@@ -214,11 +227,77 @@ class RedfishClient:
"PercentComplete": str(percent),
"Message": message_text,
"ScheduledStartTime": job_data.get("ScheduledStartTime", ""),
"StartTime": job_data.get("StartTime", ""),
"EndTime": job_data.get("EndTime", ""),
"LastUpdateTime": job_data.get("EndTime") or job_data.get("StartTime", ""),
}
def export_system_configuration(self, share_parameters: Dict[str, Any], target: str = "ALL") -> str:
"""
시스템 설정 내보내기 (SCP Export)
:param share_parameters: 네트워크 공유 설정 (IP, ShareName, FileName, UserName, Password 등)
:param target: 내보낼 대상 (ALL, IDRAC, BIOS, NIC, RAID)
:return: Job ID
"""
url = f"{self.host_url}/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/DellManager.ExportSystemConfiguration"
payload = {
"ExportFormat": "XML",
"ShareParameters": share_parameters,
"Target": target
}
logger.info(f"{self.ip}: Exporting system configuration to {share_parameters.get('FileName')}")
response = self.session.post(url, json=payload, verify=False)
response.raise_for_status()
# Job ID 추출 (Location 헤더 또는 응답 본문)
job_id = ""
if response.status_code == 202:
location = response.headers.get("Location")
if location:
job_id = location.split("/")[-1]
if not job_id:
# 응답 본문에서 시도 (일부 펌웨어 버전 대응)
try:
data = response.json()
# 일반적인 JID 포맷 확인 필요
# 여기서는 간단히 헤더 우선으로 처리하고 없으면 에러 처리하거나 데이터 파싱
pass
except:
pass
return job_id
def import_system_configuration(self, share_parameters: Dict[str, Any], import_mode: str = "Replace", target: str = "ALL") -> str:
"""
시스템 설정 가져오기 (SCP Import)
:param share_parameters: 네트워크 공유 설정
:param import_mode: Replace, Append 등
:param target: 가져올 대상
:return: Job ID
"""
url = f"{self.host_url}/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/DellManager.ImportSystemConfiguration"
payload = {
"ImportSystemConfigurationXMLFile": share_parameters.get("FileName"),
"ShareParameters": share_parameters,
"ImportMode": import_mode,
"Target": target,
"ShutdownType": "Graceful" # 적용 후 재부팅 방식
}
logger.info(f"{self.ip}: Importing system configuration from {share_parameters.get('FileName')}")
response = self.session.post(url, json=payload, verify=False)
response.raise_for_status()
job_id = ""
if response.status_code == 202:
location = response.headers.get("Location")
if location:
job_id = location.split("/")[-1]
return job_id
def close(self):
"""세션 종료"""
self.session.close()

View File

@@ -0,0 +1,18 @@
/* Hover and Transition Effects */
.hover-shadow:hover {
transform: translateY(-3px);
box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15) !important;
}
.transition-all {
transition: all 0.3s ease;
}
/* Text Truncation */
.text-truncate-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}

View File

@@ -0,0 +1,55 @@
/* Scrollbar Styles */
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background-color: #888;
border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
background-color: #555;
}
html {
scrollbar-width: thin;
scrollbar-color: #888 #f1f1f1;
}
/* Textarea Styles */
textarea {
width: 100%;
height: 600px;
padding: 10px;
font-size: 14px;
line-height: 1.5;
background-color: #f9f9f9;
border: 1px solid #ccc;
transition: background-color 0.3s ease;
}
textarea:hover {
background-color: #f0f0f0;
}
/* XML List Item Styles */
.xml-list-item {
padding: 10px;
background-color: #ffffff;
transition: background-color 0.3s ease;
}
.xml-list-item:hover {
background-color: #e9ecef;
cursor: pointer;
}
/* Button Styles */
.btn {
margin-top: 20px;
}

View File

@@ -0,0 +1,120 @@
/* ===== 공통 파일 카드 컴팩트 스타일 ===== */
.file-card-compact {
transition: all 0.2s ease;
background: #fff;
min-width: 120px;
max-width: 200px;
}
.file-card-compact:hover {
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.file-card-compact a {
font-size: 0.9rem;
overflow: hidden;
text-overflow: ellipsis;
max-width: 180px;
}
/* ===== 목록별 버튼 분리 규칙 ===== */
/* 처리된 파일 목록 전용 컨테이너(보기/삭제 2열) */
.processed-list .file-card-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: .5rem;
}
/* 보기(처리된) */
.processed-list .btn-view-processed {
border-color: #3b82f6;
color: #1d4ed8;
padding: .425rem .6rem;
font-size: .8125rem;
font-weight: 600;
}
.processed-list .btn-view-processed:hover {
background: rgba(59, 130, 246, .08);
}
/* 삭제(처리된) — 더 작게 */
.processed-list .btn-delete-processed {
border-color: #ef4444;
color: #b91c1c;
padding: .3rem .5rem;
font-size: .75rem;
font-weight: 600;
}
.processed-list .btn-delete-processed:hover {
background: rgba(239, 68, 68, .08);
}
/* 백업 파일 목록 전용 컨테이너(단일 버튼) */
.backup-list .file-card-single-button {
display: flex;
margin-top: .25rem;
}
/* 보기(백업) — 강조 색상 */
.backup-list .btn-view-backup {
width: 100%;
border-color: #10b981;
color: #047857;
padding: .45rem .75rem;
font-size: .8125rem;
font-weight: 700;
}
.backup-list .btn-view-backup:hover {
background: rgba(16, 185, 129, .08);
}
/* ===== 백업 파일 날짜 헤더 ===== */
.list-group-item .bg-light {
transition: background-color 0.2s ease;
}
.list-group-item:hover .bg-light {
background-color: #e9ecef !important;
}
/* ===== 진행바 애니메이션 ===== */
.progress {
border-radius: 10px;
overflow: hidden;
}
.progress-bar {
transition: width 0.6s ease;
}
/* ===== 반응형 텍스트 ===== */
@media (max-width: 768px) {
.card-body {
padding: 1.5rem !important;
}
}
/* ===== 스크롤바 스타일링(모달) ===== */
.modal-body pre::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.modal-body pre::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.modal-body pre::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
.modal-body pre::-webkit-scrollbar-thumb:hover {
background: #555;
}

View File

@@ -0,0 +1,86 @@
/* Status Dot Styles */
.status-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #6c757d;
}
.status-dot.active {
background-color: #198754;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* Table Text Handling */
#jobs-table {
table-layout: fixed;
width: 100%;
}
#jobs-table td {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 300px;
}
/* Column Width Fixed */
#jobs-table td:nth-child(1) {
max-width: 110px;
}
/* IP */
#jobs-table td:nth-child(2) {
max-width: 160px;
font-size: 0.85rem;
}
/* JID */
#jobs-table td:nth-child(3) {
max-width: 200px;
}
/* 작업명 */
#jobs-table td:nth-child(4) {
max-width: 180px;
}
/* 상태 */
#jobs-table td:nth-child(5) {
max-width: 120px;
}
/* 진행률 */
#jobs-table td:nth-child(6) {
max-width: 300px;
}
/* 메시지 */
#jobs-table td:nth-child(7) {
max-width: 150px;
}
/* 시간 */
/* Hover to Show Full Text */
#jobs-table td:hover {
white-space: normal;
overflow: visible;
position: relative;
z-index: 1000;
background-color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}

296
backend/static/css/scp.css Normal file
View File

@@ -0,0 +1,296 @@
/* Custom styles for XML Management (manage_xml.html) */
.main-title {
font-size: 1.8rem;
font-weight: 600;
color: #333;
margin-bottom: 10px;
}
.subtitle {
color: #666;
font-size: 0.95rem;
margin-bottom: 30px;
}
.card {
border: none;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
background: white;
}
.card-header-custom {
background-color: #007bff;
color: white;
padding: 12px 20px;
font-weight: 600;
border-radius: 8px 8px 0 0;
font-size: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-body {
padding: 20px;
}
.form-label {
font-weight: 500;
color: #555;
margin-bottom: 8px;
font-size: 0.9rem;
}
.form-control {
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px 12px;
font-size: 0.9rem;
}
.upload-section {
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
}
/* File Input Styling */
.custom-file {
position: relative;
display: inline-block;
width: 100%;
margin-bottom: 0;
}
.custom-file-input {
position: relative;
z-index: 2;
width: 100%;
height: calc(1.5em + .75rem + 2px);
margin: 0;
opacity: 0;
}
.custom-file-label {
position: absolute;
top: 0;
right: 0;
left: 0;
z-index: 1;
height: calc(1.5em + .75rem + 2px);
padding: .375rem .75rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
border: 1px solid #ced4da;
border-radius: .25rem;
}
.custom-file-label::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
z-index: 3;
display: block;
height: calc(1.5em + .75rem);
padding: .375rem .75rem;
line-height: 1.5;
color: #495057;
content: "Browse";
background-color: #e9ecef;
border-left: inherit;
border-radius: 0 .25rem .25rem 0;
}
/* 아이콘 + 뱃지 스타일 */
.file-list {
max-height: 600px;
overflow-y: auto;
padding-right: 5px;
/* 스크롤바 공간 확보 */
}
.file-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.file-list::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
}
.file-list::-webkit-scrollbar-thumb:hover {
background: #555;
}
.icon-badge-item {
border: 1px solid #e9ecef;
border-radius: 8px;
/* 둥글게 */
padding: 12px 16px;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.2s ease-in-out;
background: white;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
}
.icon-badge-item:hover {
background-color: #f1f8ff;
/* 아주 연한 파랑 */
border-color: #cce5ff;
transform: translateY(-2px);
/* 살짝 위로 */
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.icon-badge-left {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
min-width: 0;
/* 텍스트 말줄임 필수 */
margin-right: 15px;
/* 버튼과 간격 확보 */
}
.select-checkbox {
width: 18px;
height: 18px;
margin-right: 10px;
cursor: pointer;
}
.file-icon-small {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #007bff, #0056b3);
/* 그라데이션 */
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 18px;
flex-shrink: 0;
box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2);
}
.file-name-section {
display: flex;
flex-direction: column;
/* 이름과 뱃지를 위아래로 */
justify-content: center;
min-width: 0;
flex: 1;
}
.file-name-badge {
font-weight: 600;
color: #333;
font-size: 0.95rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 2px;
}
.badge-custom {
background-color: #e7f3ff;
color: #007bff;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.7rem;
font-weight: 600;
display: inline-block;
width: fit-content;
}
.action-buttons {
display: flex;
gap: 8px;
flex-shrink: 0;
/* 절대 줄어들지 않음 */
align-items: center;
}
/* 버튼 스타일 개선 */
.action-buttons .btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 12px;
font-size: 0.85rem;
font-weight: 500;
border-radius: 6px;
transition: all 0.2s;
}
.action-buttons .btn i {
margin-right: 4px;
/* 아이콘과 텍스트 사이 간격 */
}
/* 모바일 대응: 화면이 좁을 땐 텍스트 숨기기 */
@media (max-width: 768px) {
.action-buttons .btn span {
display: none;
}
.action-buttons .btn i {
margin-right: 0;
}
.action-buttons .btn {
padding: 6px 10px;
}
}
.empty-message {
text-align: center;
color: #999;
padding: 40px;
font-size: 1rem;
background: #f8f9fa;
border-radius: 8px;
border: 1px dashed #ddd;
}
/* Diff View Styles (scp_diff.html) */
.diff-container {
background-color: #f8f9fa;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
overflow-x: auto;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.9rem;
white-space: pre;
}
.diff-line {
display: block;
}
.diff-add {
background-color: #e6ffec;
color: #24292e;
}
.diff-del {
background-color: #ffebe9;
color: #24292e;
}
.diff-header {
color: #6f42c1;
font-weight: bold;
}

View File

@@ -0,0 +1,51 @@
(function () {
// Bootstrap 5을 사용한다고 가정. data-bs-* 이벤트로 처리.
const changePasswordModal = document.getElementById('changePasswordModal');
const modalUserInfo = document.getElementById('modalUserInfo');
const changePasswordForm = document.getElementById('changePasswordForm');
const newPasswordInput = document.getElementById('newPasswordInput');
const confirmPasswordInput = document.getElementById('confirmPasswordInput');
const pwMismatch = document.getElementById('pwMismatch');
if (!changePasswordModal) return;
changePasswordModal.addEventListener('show.bs.modal', function (event) {
const button = event.relatedTarget; // 버튼 that triggered the modal
const userId = button.getAttribute('data-user-id');
const username = button.getAttribute('data-username') || ('ID ' + userId);
// 표시 텍스트 세팅
modalUserInfo.textContent = username + ' (ID: ' + userId + ')';
// 폼 action 동적 설정: admin.reset_password 라우트 기대
// 예: /admin/users/123/reset_password
// Note: This assumes the URL pattern exists. Adjust if needed.
const baseUrl = changePasswordForm.getAttribute('data-base-url') || '/admin/users/0/reset_password';
changePasswordForm.action = baseUrl.replace('/0/', '/' + userId + '/');
// 폼 내부 비밀번호 필드 초기화
newPasswordInput.value = '';
confirmPasswordInput.value = '';
confirmPasswordInput.classList.remove('is-invalid');
pwMismatch.style.display = 'none';
});
// 폼 제출 전 클라이언트에서 비밀번호 일치 검사
changePasswordForm.addEventListener('submit', function (e) {
const a = newPasswordInput.value || '';
const b = confirmPasswordInput.value || '';
if (a.length < 8) {
newPasswordInput.focus();
e.preventDefault();
return;
}
if (a !== b) {
e.preventDefault();
confirmPasswordInput.classList.add('is-invalid');
pwMismatch.style.display = 'block';
confirmPasswordInput.focus();
return;
}
// 제출 허용 (서버측에서도 반드시 검증)
});
})();

163
backend/static/js/index.js Normal file
View File

@@ -0,0 +1,163 @@
document.addEventListener('DOMContentLoaded', () => {
// ─────────────────────────────────────────────────────────────
// 스크립트 선택 시 XML 드롭다운 토글
// ─────────────────────────────────────────────────────────────
const TARGET_SCRIPT = "02-set_config.py";
const scriptSelect = document.getElementById('script');
const xmlGroup = document.getElementById('xmlFileGroup');
function toggleXml() {
if (!scriptSelect || !xmlGroup) return;
if (scriptSelect.value === TARGET_SCRIPT) {
xmlGroup.style.display = 'block';
xmlGroup.classList.add('fade-in');
} else {
xmlGroup.style.display = 'none';
}
}
if (scriptSelect) {
toggleXml();
scriptSelect.addEventListener('change', toggleXml);
}
// ─────────────────────────────────────────────────────────────
// 파일 보기 모달
// ─────────────────────────────────────────────────────────────
const modalEl = document.getElementById('fileViewModal');
const titleEl = document.getElementById('fileViewModalLabel');
const contentEl = document.getElementById('fileViewContent');
if (modalEl) {
modalEl.addEventListener('show.bs.modal', async (ev) => {
const btn = ev.relatedTarget;
const folder = btn?.getAttribute('data-folder') || '';
const date = btn?.getAttribute('data-date') || '';
const filename = btn?.getAttribute('data-filename') || '';
titleEl.innerHTML = `<i class="bi bi-file-text me-2"></i>${filename || '파일'}`;
contentEl.textContent = '불러오는 중...';
const params = new URLSearchParams();
if (folder) params.set('folder', folder);
if (date) params.set('date', date);
if (filename) params.set('filename', filename);
try {
const res = await fetch(`/view_file?${params.toString()}`, { cache: 'no-store' });
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
contentEl.textContent = data?.content ?? '(빈 파일)';
} catch (e) {
contentEl.textContent = '파일을 불러오지 못했습니다: ' + (e?.message || e);
}
});
}
// ─────────────────────────────────────────────────────────────
// 진행바 업데이트
// ─────────────────────────────────────────────────────────────
window.updateProgress = function (val) {
const bar = document.getElementById('progressBar');
if (!bar) return;
const v = Math.max(0, Math.min(100, Number(val) || 0));
bar.style.width = v + '%';
bar.setAttribute('aria-valuenow', v);
bar.innerHTML = `<span class="fw-semibold">${v}%</span>`;
};
// ─────────────────────────────────────────────────────────────
// CSRF 토큰
// ─────────────────────────────────────────────────────────────
const csrfToken = document.querySelector('input[name="csrf_token"]')?.value || '';
// ─────────────────────────────────────────────────────────────
// 공통 POST 함수
// ─────────────────────────────────────────────────────────────
async function postFormAndHandle(url) {
const res = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'X-CSRFToken': csrfToken,
'Accept': 'application/json, text/html;q=0.9,*/*;q=0.8',
},
});
const ct = (res.headers.get('content-type') || '').toLowerCase();
if (ct.includes('application/json')) {
const data = await res.json();
if (data.success === false) {
throw new Error(data.error || ('HTTP ' + res.status));
}
return data;
}
return { success: true, html: true };
}
// ─────────────────────────────────────────────────────────────
// MAC 파일 이동
// ─────────────────────────────────────────────────────────────
const macForm = document.getElementById('macMoveForm');
if (macForm) {
macForm.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = macForm.querySelector('button');
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>처리 중...';
try {
await postFormAndHandle(macForm.action);
location.reload();
} catch (err) {
alert('MAC 이동 중 오류: ' + (err?.message || err));
btn.disabled = false;
btn.innerHTML = originalHtml;
}
});
}
// ─────────────────────────────────────────────────────────────
// GUID 파일 이동
// ─────────────────────────────────────────────────────────────
const guidForm = document.getElementById('guidMoveForm');
if (guidForm) {
guidForm.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = guidForm.querySelector('button');
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>처리 중...';
try {
await postFormAndHandle(guidForm.action);
location.reload();
} catch (err) {
alert('GUID 이동 중 오류: ' + (err?.message || err));
btn.disabled = false;
btn.innerHTML = originalHtml;
}
});
}
// ─────────────────────────────────────────────────────────────
// 알림 자동 닫기
// ─────────────────────────────────────────────────────────────
setTimeout(() => {
document.querySelectorAll('.alert').forEach(alert => {
const bsAlert = new bootstrap.Alert(alert);
bsAlert.close();
});
}, 5000);
});

419
backend/static/js/jobs.js Normal file
View File

@@ -0,0 +1,419 @@
// Note: This script expects csrfToken to be available globally
// It should be set in the HTML template before this script loads
// ========== 전역 변수 ==========
let CONFIG = {
grace_minutes: 60,
recency_hours: 24,
poll_interval_ms: 10000
};
let monitoringOn = false;
let pollTimer = null;
let lastRenderHash = "";
// ========== Elements ==========
const $ = id => document.getElementById(id);
const $body = $('jobs-body');
const $last = $('last-updated');
const $loading = $('loading-indicator');
const $btn = $('btn-refresh');
const $auto = $('autoRefreshSwitch');
const $ipInput = $('ipInput');
const $ipCount = $('ip-count');
const $btnLoad = $('btn-load-file');
const $btnApply = $('btn-apply');
const $monSw = $('monitorSwitch');
const $statusDot = $('status-dot');
const $statusText = $('status-text');
const $showCompleted = $('showCompletedSwitch');
const $pollInterval = $('poll-interval');
// Stats
const $statTotal = $('stat-total');
const $statRunning = $('stat-running');
const $statCompleted = $('stat-completed');
const $statError = $('stat-error');
// ========== LocalStorage Keys ==========
const LS_IPS = 'idrac_job_ips';
const LS_MON = 'idrac_monitor_on';
const LS_AUTO = 'idrac_monitor_auto';
const LS_SHOW_COMPLETED = 'idrac_show_completed';
// ========== 유틸리티 ==========
function parseIps(text) {
if (!text) return [];
const raw = text.replace(/[,;]+/g, '\n');
const out = [], seen = new Set();
raw.split('\n').forEach(line => {
const parts = line.trim().split(/\s+/);
parts.forEach(p => {
if (!p || p.startsWith('#')) return;
if (!seen.has(p)) {
seen.add(p);
out.push(p);
}
});
});
return out;
}
function getIpsFromUI() { return parseIps($ipInput.value); }
function setIpsToUI(ips) {
$ipInput.value = (ips || []).join('\n');
updateIpCount();
}
function updateIpCount() {
const ips = getIpsFromUI();
$ipCount.textContent = ips.length;
}
function saveIps() {
localStorage.setItem(LS_IPS, JSON.stringify(getIpsFromUI()));
updateIpCount();
}
function loadIps() {
try {
const v = JSON.parse(localStorage.getItem(LS_IPS) || "[]");
return Array.isArray(v) ? v : [];
} catch {
return [];
}
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, m => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;',
'"': '&quot;', "'": '&#39;'
}[m]));
}
function progressBar(pc) {
const n = parseInt(String(pc ?? "").toString().replace('%', '').trim(), 10);
if (isNaN(n)) return `<span class="text-muted small">${escapeHtml(pc ?? "")}</span>`;
let bgClass = 'bg-info';
if (n === 100) bgClass = 'bg-success';
else if (n < 30) bgClass = 'bg-warning';
return `<div class="progress" style="height:8px;">
<div class="progress-bar ${bgClass}" role="progressbar"
style="width:${n}%;" aria-valuenow="${n}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<small class="text-muted">${n}%</small>`;
}
function badgeStatus(status, pc, recently = false) {
const raw = String(status || "");
const s = raw.toLowerCase();
let cls = "bg-secondary";
let icon = "info-circle";
if (recently) {
cls = "bg-success";
icon = "check-circle";
} else if (s.includes("completed")) {
cls = "bg-success";
icon = "check-circle";
} else if (s.includes("running") || s.includes("progress")) {
cls = "bg-info";
icon = "arrow-repeat";
} else if (s.includes("scheduled") || s.includes("pending")) {
cls = "bg-warning text-dark";
icon = "clock";
} else if (s.includes("failed") || s.includes("error")) {
cls = "bg-danger";
icon = "x-circle";
}
const pct = parseInt(String(pc ?? "").toString().replace('%', '').trim(), 10);
const pctText = isNaN(pct) ? "" : ` (${pct}%)`;
const text = recently ? `${raw || "Completed"} (최근${pctText})` : `${raw || "-"}${pctText}`;
return `<span class="badge ${cls}">
<i class="bi bi-${icon}"></i> ${escapeHtml(text)}
</span>`;
}
function updateStats(items) {
let total = 0, running = 0, completed = 0, error = 0;
items.forEach(it => {
if (!it.ok) {
error++;
return;
}
if (it.jobs && it.jobs.length) {
total++;
it.jobs.forEach(j => {
const s = (j.Status || "").toLowerCase();
if (s.includes("running") || s.includes("progress") || s.includes("starting")) {
running++;
} else if (s.includes("completed") || s.includes("success")) {
completed++;
} else if (s.includes("failed") || s.includes("error")) {
error++;
}
});
}
});
$statTotal.textContent = items.length;
$statRunning.textContent = running;
$statCompleted.textContent = completed;
$statError.textContent = error;
}
// ========== 렌더링 ==========
function renderTable(items) {
if (!items) return;
const hash = JSON.stringify(items);
if (hash === lastRenderHash) return;
lastRenderHash = hash;
if (!items.length) {
$body.innerHTML = `<tr><td colspan="7" class="text-center text-muted py-4">
<i class="bi bi-inbox fs-2 d-block mb-2"></i>
현재 모니터링 중인 Job이 없습니다.
</td></tr>`;
updateStats([]);
return;
}
const rows = [];
for (const it of items) {
if (!it.ok) {
rows.push(`<tr class="table-danger">
<td><code>${escapeHtml(it.ip)}</code></td>
<td colspan="6">
<i class="bi bi-exclamation-triangle"></i>
오류: ${escapeHtml(it.error || "Unknown")}
</td>
</tr>`);
continue;
}
if (!it.jobs || !it.jobs.length) continue;
for (const j of it.jobs) {
const recent = !!j.RecentlyCompleted;
const timeText = j.CompletedAt
? `완료: ${escapeHtml(j.CompletedAt.split('T')[1]?.split('.')[0] || j.CompletedAt)}`
: escapeHtml(j.LastUpdateTime || "");
rows.push(`<tr ${recent ? 'class="table-success"' : ''}>
<td><code>${escapeHtml(it.ip)}</code></td>
<td><small class="font-monospace">${escapeHtml(j.JID || "")}</small></td>
<td><strong>${escapeHtml(j.Name || "")}</strong></td>
<td>${badgeStatus(j.Status || "", j.PercentComplete || "", recent)}</td>
<td>${progressBar(j.PercentComplete || "0")}</td>
<td><small>${escapeHtml(j.Message || "")}</small></td>
<td><small class="text-muted">${timeText}</small></td>
</tr>`);
}
}
$body.innerHTML = rows.length
? rows.join("")
: `<tr><td colspan="7" class="text-center text-success py-4">
<i class="bi bi-check-circle fs-2 d-block mb-2"></i>
현재 진행 중인 Job이 없습니다. ✅
</td></tr>`;
updateStats(items);
}
// ========== 서버 요청 ==========
async function fetchJobs(auto = false) {
if (!monitoringOn) {
$body.innerHTML = `<tr><td colspan="7" class="text-center text-muted py-4">
<i class="bi bi-power fs-2 d-block mb-2"></i>
모니터링이 꺼져 있습니다. 상단 스위치를 켜면 조회가 시작됩니다.
</td></tr>`;
$last.textContent = "";
updateStats([]);
return;
}
const ips = getIpsFromUI();
if (!ips.length) {
$body.innerHTML = `<tr><td colspan="7" class="text-center text-warning py-4">
<i class="bi bi-exclamation-triangle fs-2 d-block mb-2"></i>
IP 목록이 비어 있습니다.
</td></tr>`;
$last.textContent = "";
updateStats([]);
return;
}
try {
$loading.classList.remove('d-none');
$last.textContent = "조회 중… " + new Date().toLocaleTimeString();
const res = await fetch("/jobs/scan", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": csrfToken
},
body: JSON.stringify({
ips,
method: "redfish", // Redfish 사용
recency_hours: CONFIG.recency_hours,
grace_minutes: CONFIG.grace_minutes,
include_tracked_done: $showCompleted.checked
})
});
const data = await res.json();
if (!data.ok) throw new Error(data.error || "Scan failed");
renderTable(data.items);
$last.textContent = "업데이트: " + new Date().toLocaleString();
} catch (e) {
$body.innerHTML = `<tr><td colspan="7" class="text-danger text-center py-4">
<i class="bi bi-exclamation-circle fs-2 d-block mb-2"></i>
로드 실패: ${escapeHtml(e.message)}
<br><button class="btn btn-sm btn-outline-primary mt-2" onclick="fetchJobs(false)">
<i class="bi bi-arrow-clockwise"></i> 재시도
</button>
</td></tr>`;
$last.textContent = "에러: " + new Date().toLocaleString();
console.error(e);
} finally {
$loading.classList.add('d-none');
}
}
// ========== 모니터링 제어 ==========
function startAuto() {
stopAuto();
pollTimer = setInterval(() => fetchJobs(true), CONFIG.poll_interval_ms);
}
function stopAuto() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
function updateMonitorUI() {
$btn.disabled = !monitoringOn;
$auto.disabled = !monitoringOn;
if (monitoringOn) {
$statusDot.classList.add('active');
$statusText.textContent = '모니터링 중';
} else {
$statusDot.classList.remove('active');
$statusText.textContent = '모니터링 꺼짐';
}
}
async function setMonitoring(on) {
monitoringOn = !!on;
localStorage.setItem(LS_MON, monitoringOn ? "1" : "0");
updateMonitorUI();
if (!monitoringOn) {
stopAuto();
$last.textContent = "";
$body.innerHTML = `<tr><td colspan="7" class="text-center text-muted py-4">
<i class="bi bi-power fs-2 d-block mb-2"></i>
모니터링이 꺼져 있습니다.
</td></tr>`;
lastRenderHash = "";
updateStats([]);
return;
}
await fetchJobs(false);
if ($auto.checked) startAuto();
}
// ========== 초기화 ==========
document.addEventListener('DOMContentLoaded', async () => {
// 설정 로드
try {
const res = await fetch('/jobs/config');
const data = await res.json();
if (data.ok) {
CONFIG = data.config;
$pollInterval.textContent = Math.round(CONFIG.poll_interval_ms / 1000);
}
} catch (e) {
console.error("Failed to load config:", e);
}
// IP 복원
const savedIps = loadIps();
if (savedIps.length) {
setIpsToUI(savedIps);
} else {
try {
const res = await fetch('/jobs/iplist', {
headers: { 'X-CSRFToken': csrfToken }
});
const data = await res.json();
if (data.ok && data.ips) {
setIpsToUI(data.ips);
saveIps();
}
} catch (e) {
console.error(e);
}
}
// 설정 복원
const savedMon = localStorage.getItem(LS_MON);
const savedAuto = localStorage.getItem(LS_AUTO);
const savedShowCompleted = localStorage.getItem(LS_SHOW_COMPLETED);
$monSw.checked = savedMon === "1";
$auto.checked = savedAuto === "1";
$showCompleted.checked = savedShowCompleted !== "0";
// 이벤트
$ipInput.addEventListener('input', updateIpCount);
$btn.addEventListener('click', () => { if (monitoringOn) fetchJobs(false); });
$auto.addEventListener('change', e => {
localStorage.setItem(LS_AUTO, e.target.checked ? "1" : "0");
if (!monitoringOn) return;
if (e.target.checked) startAuto();
else stopAuto();
});
$monSw.addEventListener('click', e => setMonitoring(e.target.checked));
$showCompleted.addEventListener('change', e => {
localStorage.setItem(LS_SHOW_COMPLETED, e.target.checked ? "1" : "0");
if (monitoringOn) fetchJobs(false);
});
$btnLoad.addEventListener('click', async () => {
try {
const res = await fetch('/jobs/iplist', {
headers: { 'X-CSRFToken': csrfToken }
});
const data = await res.json();
if (data.ok) {
setIpsToUI(data.ips || []);
saveIps();
if (monitoringOn) await fetchJobs(false);
}
} catch (e) {
alert('IP 목록 불러오기 실패: ' + e.message);
}
});
$btnApply.addEventListener('click', () => {
saveIps();
if (monitoringOn) fetchJobs(false);
});
// 초기 상태
updateMonitorUI();
updateIpCount();
if ($monSw.checked) {
setMonitoring(true);
}
});

30
backend/static/js/scp.js Normal file
View File

@@ -0,0 +1,30 @@
function updateFileName(input) {
const fileName = input.files[0]?.name || '파일 선택';
document.getElementById('fileLabel').textContent = fileName;
}
function openDeployModal(filename) {
document.getElementById('deployFilename').value = filename;
var myModal = new bootstrap.Modal(document.getElementById('deployModal'));
myModal.show();
}
function compareSelected() {
const checkboxes = document.querySelectorAll('.file-selector:checked');
if (checkboxes.length !== 2) {
alert('비교할 파일을 정확히 2개 선택해주세요.');
return;
}
const file1 = checkboxes[0].value;
const file2 = checkboxes[1].value;
// HTML 버튼의 data-url 속성에서 base URL을 가져옴
const btn = document.getElementById('compareBtn');
if (!btn) {
console.error('compareBtn not found');
return;
}
const baseUrl = btn.dataset.url;
window.location.href = `${baseUrl}?file1=${encodeURIComponent(file1)}&file2=${encodeURIComponent(file2)}`;
}

View File

@@ -5,19 +5,24 @@
<div class="container mt-4">
<div class="card">
<div class="card-body">
<h3 class="card-title">Admin Page</h3>
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="card-title mb-0">Admin Page</h3>
<a href="{{ url_for('admin.settings') }}" class="btn btn-outline-primary">
<i class="bi bi-gear-fill me-1"></i>시스템 설정
</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="mt-2">
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }} alert-dismissible fade show" role="alert">
{{ msg }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% if messages %}
<div class="mt-2">
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }} alert-dismissible fade show" role="alert">
{{ msg }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div class="table-responsive">
@@ -39,28 +44,24 @@
<td>{{ user.email }}</td>
<td>
{% if user.is_active %}
<span class="badge bg-success">Yes</span>
<span class="badge bg-success">Yes</span>
{% else %}
<span class="badge bg-secondary">No</span>
<span class="badge bg-secondary">No</span>
{% endif %}
</td>
<td>
{% if not user.is_active %}
<a href="{{ url_for('admin.approve_user', user_id=user.id) }}" class="btn btn-success btn-sm me-1">Approve</a>
<a href="{{ url_for('admin.approve_user', user_id=user.id) }}"
class="btn btn-success btn-sm me-1">Approve</a>
{% endif %}
<a href="{{ url_for('admin.delete_user', user_id=user.id) }}"
class="btn btn-danger btn-sm me-1"
onclick="return confirm('사용자 {{ user.username }} (ID={{ user.id }}) 를 삭제하시겠습니까?');">
Delete
<a href="{{ url_for('admin.delete_user', user_id=user.id) }}" class="btn btn-danger btn-sm me-1"
onclick="return confirm('사용자 {{ user.username }} (ID={{ user.id }}) 를 삭제하시겠습니까?');">
Delete
</a>
<!-- Change Password 버튼: 모달 오픈 -->
<button type="button"
class="btn btn-primary btn-sm"
data-user-id="{{ user.id }}"
data-username="{{ user.username | e }}"
data-bs-toggle="modal"
data-bs-target="#changePasswordModal">
<button type="button" class="btn btn-primary btn-sm" data-user-id="{{ user.id }}"
data-username="{{ user.username | e }}" data-bs-toggle="modal" data-bs-target="#changePasswordModal">
Change Password
</button>
</td>
@@ -74,7 +75,8 @@
</div>
{# ========== Change Password Modal ========== #}
<div class="modal fade" id="changePasswordModal" tabindex="-1" aria-labelledby="changePasswordModalLabel" aria-hidden="true">
<div class="modal fade" id="changePasswordModal" tabindex="-1" aria-labelledby="changePasswordModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form id="changePasswordForm" method="post" action="">
@@ -92,13 +94,15 @@
<div class="mb-3">
<label for="newPasswordInput" class="form-label">New password</label>
<input id="newPasswordInput" name="new_password" type="password" class="form-control" required minlength="8" placeholder="Enter new password">
<input id="newPasswordInput" name="new_password" type="password" class="form-control" required minlength="8"
placeholder="Enter new password">
<div class="form-text">최소 8자 이상을 권장합니다.</div>
</div>
<div class="mb-3">
<label for="confirmPasswordInput" class="form-label">Confirm password</label>
<input id="confirmPasswordInput" name="confirm_password" type="password" class="form-control" required minlength="8" placeholder="Confirm new password">
<input id="confirmPasswordInput" name="confirm_password" type="password" class="form-control" required
minlength="8" placeholder="Confirm new password">
<div id="pwMismatch" class="invalid-feedback">비밀번호가 일치하지 않습니다.</div>
</div>
</div>
@@ -112,58 +116,8 @@
</div>
</div>
{# ========== 스크립트: 모달에 사용자 정보 채우기 + 클라이언트 확인 ========== #}
{% block scripts %}
{{ super() }}
<script>
(function () {
// Bootstrap 5을 사용한다고 가정. data-bs-* 이벤트로 처리.
const changePasswordModal = document.getElementById('changePasswordModal');
const modalUserInfo = document.getElementById('modalUserInfo');
const changePasswordForm = document.getElementById('changePasswordForm');
const newPasswordInput = document.getElementById('newPasswordInput');
const confirmPasswordInput = document.getElementById('confirmPasswordInput');
const pwMismatch = document.getElementById('pwMismatch');
if (!changePasswordModal) return;
changePasswordModal.addEventListener('show.bs.modal', function (event) {
const button = event.relatedTarget; // 버튼 that triggered the modal
const userId = button.getAttribute('data-user-id');
const username = button.getAttribute('data-username') || ('ID ' + userId);
// 표시 텍스트 세팅
modalUserInfo.textContent = username + ' (ID: ' + userId + ')';
// 폼 action 동적 설정: admin.reset_password 라우트 기대
// 예: /admin/users/123/reset_password
changePasswordForm.action = "{{ url_for('admin.reset_password', user_id=0) }}".replace('/0/', '/' + userId + '/');
// 폼 내부 비밀번호 필드 초기화
newPasswordInput.value = '';
confirmPasswordInput.value = '';
confirmPasswordInput.classList.remove('is-invalid');
pwMismatch.style.display = 'none';
});
// 폼 제출 전 클라이언트에서 비밀번호 일치 검사
changePasswordForm.addEventListener('submit', function (e) {
const a = newPasswordInput.value || '';
const b = confirmPasswordInput.value || '';
if (a.length < 8) {
newPasswordInput.focus();
e.preventDefault();
return;
}
if (a !== b) {
e.preventDefault();
confirmPasswordInput.classList.add('is-invalid');
pwMismatch.style.display = 'block';
confirmPasswordInput.focus();
return;
}
// 제출 허용 (서버측에서도 반드시 검증)
});
})();
</script>
{% endblock %}
{{ super() }}
<script src="{{ url_for('static', filename='js/admin.js') }}"></script>
{% endblock %}
{% endblock %}

View File

@@ -0,0 +1,320 @@
{% extends "base.html" %}
{% block title %}시스템 설정 - Dell Server Info{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/admin_settings.css') }}">
{% endblock %}
{% block content %}
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">
<i class="bi bi-gear-fill text-primary me-2"></i>시스템 설정
</h2>
<a href="{{ url_for('admin.admin_panel') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>관리자 패널로 돌아가기
</a>
</div>
<!-- 텔레그램 봇 설정 섹션 -->
<div class="card border shadow-sm mb-4">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-bold">
<i class="bi bi-telegram text-info me-2"></i>텔레그램 봇 관리
</h5>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addBotModal">
<i class="bi bi-plus-lg me-1"></i>봇 추가
</button>
</div>
<div class="card-body">
<div class="alert alert-info d-flex align-items-center" role="alert">
<i class="bi bi-info-circle-fill me-2 flex-shrink-0 fs-5"></i>
<div>
등록된 모든 <strong>활성(Active)</strong> 봇에게 알림이 동시에 전송됩니다.<br>
<small class="text-muted">알림 종류: 로그인, 회원가입, 로그아웃, 서버 작업 등</small>
</div>
</div>
{% if bots %}
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-4">
{% for bot in bots %}
<div class="col">
<div class="card h-100 border-0 shadow-sm hover-shadow transition-all">
<div
class="card-header bg-white border-0 pt-3 pb-0 d-flex justify-content-between align-items-start">
<div class="d-flex align-items-center">
<div class="avatar-circle bg-primary bg-opacity-10 text-primary me-2 rounded-circle d-flex align-items-center justify-content-center"
style="width: 40px; height: 40px;">
<i class="bi bi-robot fs-5"></i>
</div>
<div>
<h5 class="card-title fw-bold mb-0 text-truncate" style="max-width: 150px;"
title="{{ bot.name }}">
{{ bot.name }}
</h5>
<small class="text-muted" style="font-size: 0.75rem;">ID: {{ bot.id }}</small>
</div>
</div>
{% if bot.is_active %}
<span
class="badge bg-success-subtle text-success border border-success-subtle rounded-pill px-2">
<i class="bi bi-check-circle-fill me-1"></i>Active
</span>
{% else %}
<span
class="badge bg-secondary-subtle text-secondary border border-secondary-subtle rounded-pill px-2">
<i class="bi bi-dash-circle me-1"></i>Inactive
</span>
{% endif %}
</div>
<div class="card-body">
<p class="card-text text-muted small mb-3 text-truncate-2" style="min-height: 2.5em;">
{{ bot.description or "설명이 없습니다." }}
</p>
<div class="bg-light rounded p-2 mb-2">
<div class="d-flex align-items-center text-secondary small mb-1">
<i class="bi bi-key me-2 text-primary"></i>
<span class="fw-semibold me-1">Token:</span>
<span class="font-monospace text-truncate">{{ bot.token[:10] }}...</span>
</div>
<div class="d-flex align-items-center text-secondary small">
<i class="bi bi-chat-dots me-2 text-success"></i>
<span class="fw-semibold me-1">Chat ID:</span>
<span class="font-monospace">{{ bot.chat_id }}</span>
</div>
</div>
<div class="mb-3">
{% set types = (bot.notification_types or "").split(",") %}
{% if 'auth' in types %}
<span
class="badge bg-info-subtle text-info border border-info-subtle rounded-pill me-1"><i
class="bi bi-shield-lock-fill me-1"></i>인증</span>
{% endif %}
{% if 'activity' in types %}
<span
class="badge bg-warning-subtle text-warning border border-warning-subtle rounded-pill me-1"><i
class="bi bi-activity me-1"></i>활동</span>
{% endif %}
{% if 'system' in types %}
<span
class="badge bg-danger-subtle text-danger border border-danger-subtle rounded-pill me-1"><i
class="bi bi-gear-fill me-1"></i>시스템</span>
{% endif %}
</div>
</div>
<div class="card-footer bg-white border-0 pt-0 pb-3">
<div class="btn-group w-100 shadow-sm" role="group">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal"
data-bs-target="#editBotModal{{ bot.id }}">
<i class="bi bi-pencil me-1"></i>수정
</button>
<button type="submit" form="testForm{{ bot.id }}"
class="btn btn-outline-primary btn-sm">
<i class="bi bi-send me-1"></i>테스트
</button>
<button type="submit" form="deleteForm{{ bot.id }}"
class="btn btn-outline-danger btn-sm" onclick="return confirm('정말 삭제하시겠습니까?');">
<i class="bi bi-trash me-1"></i>삭제
</button>
</div>
<!-- Hidden Forms -->
<form id="testForm{{ bot.id }}" action="{{ url_for('admin.test_bot', bot_id=bot.id) }}"
method="post" class="d-none">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
</form>
<form id="deleteForm{{ bot.id }}" action="{{ url_for('admin.delete_bot', bot_id=bot.id) }}"
method="post" class="d-none">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
</form>
</div>
</div>
</div>
<!-- 수정 모달 -->
<div class="modal fade" id="editBotModal{{ bot.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<form action="{{ url_for('admin.edit_bot', bot_id=bot.id) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="modal-header bg-light">
<h5 class="modal-title fw-bold">
<i class="bi bi-pencil-square me-2"></i>봇 설정 수정
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-semibold">이름 (식별용)</label>
<input type="text" class="form-control" name="name" value="{{ bot.name }}"
required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Bot Token</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-key"></i></span>
<input type="text" class="form-control font-monospace" name="token"
value="{{ bot.token }}" required>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Chat ID</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-chat-dots"></i></span>
<input type="text" class="form-control font-monospace" name="chat_id"
value="{{ bot.chat_id }}" required>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">알림 유형</label>
<div class="d-flex gap-3">
{% set types = (bot.notification_types or "").split(",") %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="notify_types"
value="auth" id="edit_auth_{{ bot.id }}" {{ 'checked' if 'auth' in
types else '' }}>
<label class="form-check-label" for="edit_auth_{{ bot.id }}">
인증
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="notify_types"
value="activity" id="edit_activity_{{ bot.id }}" {{ 'checked'
if 'activity' in types else '' }}>
<label class="form-check-label" for="edit_activity_{{ bot.id }}">
활동
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="notify_types"
value="system" id="edit_system_{{ bot.id }}" {{ 'checked'
if 'system' in types else '' }}>
<label class="form-check-label" for="edit_system_{{ bot.id }}">
시스템
</label>
</div>
</div>
<small class="text-muted">선택한 알림 유형만 전송됩니다</small>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">설명</label>
<textarea class="form-control" name="description"
rows="2">{{ bot.description or '' }}</textarea>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active"
id="activeCheck{{ bot.id }}" {{ 'checked' if bot.is_active else '' }}>
<label class="form-check-label" for="activeCheck{{ bot.id }}">
활성화
</label>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
<button type="submit" class="btn btn-primary px-4">저장</button>
</div>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5">
<div class="mb-3">
<div class="d-inline-flex align-items-center justify-content-center bg-light rounded-circle"
style="width: 80px; height: 80px;">
<i class="bi bi-robot fs-1 text-secondary"></i>
</div>
</div>
<h5 class="text-muted fw-normal">등록된 텔레그램 봇이 없습니다.</h5>
<p class="text-muted small mb-4">우측 상단의 '봇 추가' 버튼을 눌러 알림을 설정하세요.</p>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addBotModal">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<form action="{{ url_for('admin.add_bot') }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="modal-header bg-light">
<h5 class="modal-title fw-bold">
<i class="bi bi-plus-circle me-2"></i>새 텔레그램 봇 추가
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-semibold">이름 (식별용)</label>
<input type="text" class="form-control" name="name" placeholder="예: 알림용 봇"
required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Bot Token</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-key"></i></span>
<input type="text" class="form-control font-monospace" name="token"
placeholder="123456:ABC..." required>
</div>
<div class="form-text">BotFather에게 받은 API Token</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Chat ID</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-chat-dots"></i></span>
<input type="text" class="form-control font-monospace" name="chat_id"
placeholder="-100..." required>
</div>
<div class="form-text">메시지를 받을 채팅방 ID (그룹은 음수)</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">알림 유형</label>
<div class="d-flex gap-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="notify_types"
value="auth" id="add_auth" checked>
<label class="form-check-label" for="add_auth">
인증
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="notify_types"
value="activity" id="add_activity" checked>
<label class="form-check-label" for="add_activity">
활동
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="notify_types"
value="system" id="add_system" checked>
<label class="form-check-label" for="add_system">
시스템
</label>
</div>
</div>
<small class="text-muted">선택한 알림 유형만 전송됩니다</small>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">설명</label>
<textarea class="form-control" name="description" rows="2"
placeholder="선택 사항"></textarea>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
<button type="submit" class="btn btn-primary px-4">추가</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,35 +1,49 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="description" content="Dell Server 정보 및 MAC 주소 처리 시스템">
<title>{% block title %}Dell Server Info, MAC 정보 처리{% endblock %}</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<!-- Bootstrap 5.3.3 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
integrity="sha384-tViUnnbYAV00FLIhhi3v/dWt3Jxw4gZQcNoSCxCIFNJVCx7/D55/wXsrNIRANwdD"
crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
integrity="sha384-tViUnnbYAV00FLIhhi3v/dWt3Jxw4gZQcNoSCxCIFNJVCx7/D55/wXsrNIRANwdD" crossorigin="anonymous">
<!-- Custom CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- Skip to main content (접근성) -->
<a href="#main-content" class="visually-hidden-focusable">본문으로 건너뛰기</a>
{# 플래시 메시지 (전역) #}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="position-fixed end-0 p-3" style="z-index: 2000; top: 70px;">
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }} alert-dismissible fade show shadow-lg" role="alert">
<i class="bi bi-{{ 'check-circle' if cat == 'success' else 'exclamation-triangle' }} me-2"></i>
{{ msg }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<div class="container-fluid">
@@ -37,12 +51,8 @@
<i class="bi bi-server me-2"></i>
Dell Server Info
</a>
<button class="navbar-toggler" type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="네비게이션 토글">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="네비게이션 토글">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
@@ -53,44 +63,44 @@
</a>
</li>
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.index') }}">
<i class="bi bi-hdd-network me-1"></i>ServerInfo
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('xml.xml_management') }}">
<i class="bi bi-file-code me-1"></i>XML Management
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('jobs.jobs_page') }}">
<i class="bi bi-list-task me-1"></i>Job Monitor
</a>
</li>
{% if current_user.is_admin %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin.admin_panel') }}">
<i class="bi bi-shield-lock me-1"></i>Admin
</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.logout') }}">
<i class="bi bi-box-arrow-right me-1"></i>Logout
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.index') }}">
<i class="bi bi-hdd-network me-1"></i>ServerInfo
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('xml.xml_management') }}">
<i class="bi bi-file-code me-1"></i>XML Management
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('jobs.jobs_page') }}">
<i class="bi bi-list-task me-1"></i>Job Monitor
</a>
</li>
{% if current_user.is_admin %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin.admin_panel') }}">
<i class="bi bi-shield-lock me-1"></i>Admin
</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.logout') }}">
<i class="bi bi-box-arrow-right me-1"></i>Logout
</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">
<i class="bi bi-box-arrow-in-right me-1"></i>Login
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.register') }}">
<i class="bi bi-person-plus me-1"></i>Register
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">
<i class="bi bi-box-arrow-in-right me-1"></i>Login
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.register') }}">
<i class="bi bi-person-plus me-1"></i>Register
</a>
</li>
{% endif %}
</ul>
</div>
@@ -98,7 +108,8 @@
</nav>
<!-- Main Content -->
<main id="main-content" class="{% if request.endpoint in ['auth.login', 'auth.register', 'auth.reset_password'] %}container mt-5{% else %}container mt-4 container-card{% endif %}">
<main id="main-content"
class="{% if request.endpoint in ['auth.login', 'auth.register', 'auth.reset_password'] %}container mt-5{% else %}container mt-4 container-card{% endif %}">
{% block content %}{% endblock %}
</main>
@@ -110,17 +121,18 @@
</footer>
<!-- Bootstrap 5.3.3 JS Bundle (Popper.js 포함) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<!-- Socket.IO (필요한 경우만) -->
{% if config.USE_SOCKETIO %}
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"
integrity="sha384-Gr6Lu2Ajx28mzwyVR8CFkULdCU7kMlZ9UthllibdOSo6qAiN+yXNHqtgdTvFXMT4"
crossorigin="anonymous"></script>
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"
integrity="sha384-Gr6Lu2Ajx28mzwyVR8CFkULdCU7kMlZ9UthllibdOSo6qAiN+yXNHqtgdTvFXMT4"
crossorigin="anonymous"></script>
{% endif %}
{% block scripts %}{% endblock %}
</body>
</html>
</html>

View File

@@ -1,51 +1,26 @@
{% extends "base.html" %}
{% block title %}Edit XML File - Dell Server Info{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/edit_xml.css') }}">
{% endblock %}
{% block content %}
<head>
<meta charset="UTF-8">
<title>Edit XML File</title>
<style>
::-webkit-scrollbar { width: 12px; }
::-webkit-scrollbar-track { background: #f1f1f1; }
::-webkit-scrollbar-thumb { background-color: #888; border-radius: 6px; }
::-webkit-scrollbar-thumb:hover { background-color: #555; }
html { scrollbar-width: thin; scrollbar-color: #888 #f1f1f1; }
textarea {
width: 100%;
height: 600px;
padding: 10px;
font-size: 14px;
line-height: 1.5;
background-color: #f9f9f9;
border: 1px solid #ccc;
transition: background-color 0.3s ease;
}
textarea:hover { background-color: #f0f0f0; }
.xml-list-item {
padding: 10px;
background-color: #ffffff;
transition: background-color 0.3s ease;
}
.xml-list-item:hover { background-color: #e9ecef; cursor: pointer; }
.btn { margin-top: 20px; }
</style>
</head>
<body>
<div class="card shadow-lg">
<div class="card-header bg-primary text-white">
<h3>Edit XML File: <strong>{{ filename }}</strong></h3>
</div>
<div class="card-body">
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="form-group">
<label for="xmlContent">XML Content</label>
<textarea id="xmlContent" name="content" class="form-control" rows="20">{{ content }}</textarea>
</div>
<button type="submit" class="btn btn-success mt-3">Save Changes</button>
<a href="{{ url_for('xml.xml_management') }}" class="btn btn-secondary mt-3">Cancel</a>
</form>
</div>
<div class="card shadow-lg">
<div class="card-header bg-primary text-white">
<h3>Edit XML File: <strong>{{ filename }}</strong></h3>
</div>
</body>
<div class="card-body">
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="form-group">
<label for="xmlContent">XML Content</label>
<textarea id="xmlContent" name="content" class="form-control" rows="20">{{ content }}</textarea>
</div>
<button type="submit" class="btn btn-success mt-3">Save Changes</button>
<a href="{{ url_for('xml.xml_management') }}" class="btn btn-secondary mt-3">Cancel</a>
</form>
</div>
</div>
{% endblock %}

View File

@@ -3,20 +3,7 @@
{% block content %}
<div class="container-fluid py-4">
{# 플래시 메시지 #}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="position-fixed top-0 end-0 p-3" style="z-index: 1050">
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }} alert-dismissible fade show shadow-lg" role="alert">
<i class="bi bi-{{ 'check-circle' if cat == 'success' else 'exclamation-triangle' }} me-2"></i>
{{ msg }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{# 헤더 섹션 #}
<div class="row mb-4">
@@ -50,7 +37,7 @@
<select id="script" name="script" class="form-select" required>
<option value="">스크립트를 선택하세요</option>
{% for script in scripts %}
<option value="{{ script }}">{{ script }}</option>
<option value="{{ script }}">{{ script }}</option>
{% endfor %}
</select>
</div>
@@ -61,7 +48,7 @@
<select id="xmlFile" name="xmlFile" class="form-select">
<option value="">XML 파일 선택</option>
{% for xml_file in xml_files %}
<option value="{{ xml_file }}">{{ xml_file }}</option>
<option value="{{ xml_file }}">{{ xml_file }}</option>
{% endfor %}
</select>
</div>
@@ -72,8 +59,8 @@
IP 주소 (각 줄에 하나)
<span class="badge bg-secondary ms-2" id="ipLineCount">0 대설정</span>
</label>
<textarea id="ips" name="ips" rows="4" class="form-control font-monospace"
placeholder="예:&#10;192.168.1.1&#10;192.168.1.2&#10;192.168.1.3" required></textarea>
<textarea id="ips" name="ips" rows="4" class="form-control font-monospace"
placeholder="예:&#10;192.168.1.1&#10;192.168.1.2&#10;192.168.1.3" required></textarea>
</div>
<button type="submit" class="btn btn-primary w-100">
@@ -102,22 +89,18 @@
서버 리스트 (덮어쓰기)
<span class="badge bg-secondary ms-2" id="serverLineCount">0 대설정</span>
</label>
<textarea id="server_list_content" name="server_list_content" rows="8"
class="form-control font-monospace" style="font-size: 0.95rem;"
placeholder="서버 리스트를 입력하세요..."></textarea>
<textarea id="server_list_content" name="server_list_content" rows="8" class="form-control font-monospace"
style="font-size: 0.95rem;" placeholder="서버 리스트를 입력하세요..."></textarea>
</div>
<div class="d-flex gap-2">
<button type="submit" formaction="{{ url_for('utils.update_server_list') }}"
class="btn btn-secondary">
<button type="submit" formaction="{{ url_for('utils.update_server_list') }}" class="btn btn-secondary">
MAC to Excel
</button>
<button type="submit" formaction="{{ url_for('utils.update_guid_list') }}"
class="btn btn-success">
<button type="submit" formaction="{{ url_for('utils.update_guid_list') }}" class="btn btn-success">
GUID to Excel
</button>
<button type="submit" formaction="{{ url_for('utils.update_gpu_list') }}"
class="btn btn-warning">
<button type="submit" formaction="{{ url_for('utils.update_gpu_list') }}" class="btn btn-warning">
GPU to Excel
</button>
</div>
@@ -137,8 +120,8 @@
<span class="fw-semibold">처리 진행률</span>
</div>
<div class="progress" style="height: 25px;">
<div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-success"
role="progressbar" style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-success"
role="progressbar" style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<span class="fw-semibold">0%</span>
</div>
</div>
@@ -218,45 +201,44 @@
</div>
</div>
{# 처리된 파일 목록 #}
<div class="row mb-4 processed-list">
<div class="col">
<div class="card border shadow-sm">
<div class="card-header bg-light border-0 py-2 d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="bi bi-files me-2"></i>
처리된 파일 목록
</h6>
</div>
<div class="card-body p-4">
{% if files_to_display and files_to_display|length > 0 %}
{# 처리된 파일 목록 #}
<div class="row mb-4 processed-list">
<div class="col">
<div class="card border shadow-sm">
<div class="card-header bg-light border-0 py-2 d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="bi bi-files me-2"></i>
처리된 파일 목록
</h6>
</div>
<div class="card-body p-4">
{% if files_to_display and files_to_display|length > 0 %}
<div class="row g-3">
{% for file_info in files_to_display %}
<div class="col-auto">
<div class="file-card-compact border rounded p-2 text-center">
<a href="{{ url_for('main.download_file', filename=file_info.file) }}"
class="text-decoration-none text-dark fw-semibold d-block mb-2 text-nowrap px-2"
download title="{{ file_info.name or file_info.file }}">
{{ file_info.name or file_info.file }}
</a>
<div class="file-card-buttons d-flex gap-2 justify-content-center">
<button type="button" class="btn btn-sm btn-outline btn-view-processed flex-fill"
data-bs-toggle="modal" data-bs-target="#fileViewModal"
data-folder="idrac_info"
data-filename="{{ file_info.file }}">
보기
<div class="col-auto">
<div class="file-card-compact border rounded p-2 text-center">
<a href="{{ url_for('main.download_file', filename=file_info.file) }}"
class="text-decoration-none text-dark fw-semibold d-block mb-2 text-nowrap px-2" download
title="{{ file_info.name or file_info.file }}">
{{ file_info.name or file_info.file }}
</a>
<div class="file-card-buttons d-flex gap-2 justify-content-center">
<button type="button" class="btn btn-sm btn-outline btn-view-processed flex-fill"
data-bs-toggle="modal" data-bs-target="#fileViewModal" data-folder="idrac_info"
data-filename="{{ file_info.file }}">
보기
</button>
<form action="{{ url_for('main.delete_file', filename=file_info.file) }}" method="post"
class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-outline btn-delete-processed flex-fill"
onclick="return confirm('삭제하시겠습니까?');">
삭제
</button>
<form action="{{ url_for('main.delete_file', filename=file_info.file) }}"
method="post" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-outline btn-delete-processed flex-fill"
onclick="return confirm('삭제하시겠습니까?');">
삭제
</button>
</form>
</div>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
@@ -267,54 +249,53 @@
<!-- 이전 페이지 -->
{% if page > 1 %}
<li class="page-item">
<a class="page-link" href="{{ url_for('main.index', page=page-1) }}">
<i class="bi bi-chevron-left"></i> 이전
</a>
</li>
<li class="page-item">
<a class="page-link" href="{{ url_for('main.index', page=page-1) }}">
<i class="bi bi-chevron-left"></i> 이전
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link"><i class="bi bi-chevron-left"></i> 이전</span>
</li>
<li class="page-item disabled">
<span class="page-link"><i class="bi bi-chevron-left"></i> 이전</span>
</li>
{% endif %}
<!-- 페이지 번호 (최대 10개 표시) -->
{% set start_page = ((page - 1) // 10) * 10 + 1 %}
{% set end_page = [start_page + 9, total_pages]|min %}
{% for p in range(start_page, end_page + 1) %}
<li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link" href="{{ url_for('main.index', page=p) }}">{{ p }}</a>
</li>
<li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link" href="{{ url_for('main.index', page=p) }}">{{ p }}</a>
</li>
{% endfor %}
<!-- 다음 페이지 -->
{% if page < total_pages %}
<li class="page-item">
<a class="page-link" href="{{ url_for('main.index', page=page+1) }}">
다음 <i class="bi bi-chevron-right"></i>
</a>
{% if page < total_pages %} <li class="page-item">
<a class="page-link" href="{{ url_for('main.index', page=page+1) }}">
다음 <i class="bi bi-chevron-right"></i>
</a>
</li>
{% else %}
{% else %}
<li class="page-item disabled">
<span class="page-link">다음 <i class="bi bi-chevron-right"></i></span>
</li>
{% endif %}
{% endif %}
</ul>
</nav>
{% endif %}
<!-- /페이지네이션 -->
{% else %}
{% else %}
<div class="text-center py-5">
<i class="bi bi-inbox fs-1 text-muted mb-3"></i>
<p class="text-muted mb-0">표시할 파일이 없습니다.</p>
</div>
{% endif %}
{% endif %}
</div>
</div>
</div>
</div>
</div>
{# 백업된 파일 목록 #}
<div class="row backup-list">
@@ -328,55 +309,52 @@
</div>
<div class="card-body p-4">
{% if backup_files and backup_files|length > 0 %}
<div class="list-group">
{% for date, info in backup_files.items() %}
<div class="list-group-item border rounded mb-2 p-0 overflow-hidden">
<div class="d-flex justify-content-between align-items-center p-3 bg-light">
<div class="d-flex align-items-center">
<i class="bi bi-calendar3 text-primary me-2"></i>
<strong>{{ date }}</strong>
<span class="badge bg-primary ms-3">{{ info.count }} 파일</span>
</div>
<button class="btn btn-sm btn-outline-secondary" type="button"
data-bs-toggle="collapse" data-bs-target="#collapse-{{ loop.index }}"
aria-expanded="false">
<i class="bi bi-chevron-down"></i>
</button>
</div>
<div id="collapse-{{ loop.index }}" class="collapse">
<div class="p-3">
<div class="row g-3">
{% for file in info.files %}
<div class="col-auto">
<div class="file-card-compact border rounded p-2 text-center">
<a href="{{ url_for('main.download_backup_file', date=date, filename=file) }}"
class="text-decoration-none text-dark fw-semibold d-block mb-2 text-nowrap px-2"
download title="{{ file }}">
{{ file.rsplit('.', 1)[0] }}
</a>
<div class="file-card-single-button">
<button type="button" class="btn btn-sm btn-outline btn-view-backup w-100"
data-bs-toggle="modal" data-bs-target="#fileViewModal"
data-folder="backup"
data-date="{{ date }}"
data-filename="{{ file }}">
보기
</button>
</div>
</div>
</div>
{% endfor %}
<div class="list-group">
{% for date, info in backup_files.items() %}
<div class="list-group-item border rounded mb-2 p-0 overflow-hidden">
<div class="d-flex justify-content-between align-items-center p-3 bg-light">
<div class="d-flex align-items-center">
<i class="bi bi-calendar3 text-primary me-2"></i>
<strong>{{ date }}</strong>
<span class="badge bg-primary ms-3">{{ info.count }} 파일</span>
</div>
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse"
data-bs-target="#collapse-{{ loop.index }}" aria-expanded="false">
<i class="bi bi-chevron-down"></i>
</button>
</div>
<div id="collapse-{{ loop.index }}" class="collapse">
<div class="p-3">
<div class="row g-3">
{% for file in info.files %}
<div class="col-auto">
<div class="file-card-compact border rounded p-2 text-center">
<a href="{{ url_for('main.download_backup_file', date=date, filename=file) }}"
class="text-decoration-none text-dark fw-semibold d-block mb-2 text-nowrap px-2" download
title="{{ file }}">
{{ file.rsplit('.', 1)[0] }}
</a>
<div class="file-card-single-button">
<button type="button" class="btn btn-sm btn-outline btn-view-backup w-100"
data-bs-toggle="modal" data-bs-target="#fileViewModal" data-folder="backup"
data-date="{{ date }}" data-filename="{{ file }}">
보기
</button>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-inbox fs-1 text-muted mb-3"></i>
<p class="text-muted mb-0">백업된 파일이 없습니다.</p>
</div>
<div class="text-center py-5">
<i class="bi bi-inbox fs-1 text-muted mb-3"></i>
<p class="text-muted mb-0">백업된 파일이 없습니다.</p>
</div>
{% endif %}
</div>
</div>
@@ -397,8 +375,8 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
</div>
<div class="modal-body bg-light">
<pre id="fileViewContent" class="mb-0 p-3 bg-white border rounded font-monospace"
style="white-space:pre-wrap;word-break:break-word;max-height:70vh;">불러오는 중...</pre>
<pre id="fileViewContent" class="mb-0 p-3 bg-white border rounded font-monospace"
style="white-space:pre-wrap;word-break:break-word;max-height:70vh;">불러오는 중...</pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
@@ -410,277 +388,15 @@
</div>
{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
{% endblock %}
{% block scripts %}
<style>
/* ===== 공통 파일 카드 컴팩트 스타일 ===== */
.file-card-compact {
transition: all 0.2s ease;
background: #fff;
min-width: 120px;
max-width: 200px;
}
{{ super() }}
.file-card-compact:hover {
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.file-card-compact a {
font-size: 0.9rem;
overflow: hidden;
text-overflow: ellipsis;
max-width: 180px;
}
/* ===== 목록별 버튼 분리 규칙 ===== */
/* 처리된 파일 목록 전용 컨테이너(보기/삭제 2열) */
.processed-list .file-card-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: .5rem;
}
/* 보기(처리된) */
.processed-list .btn-view-processed {
border-color: #3b82f6;
color: #1d4ed8;
padding: .425rem .6rem;
font-size: .8125rem;
font-weight: 600;
}
.processed-list .btn-view-processed:hover {
background: rgba(59,130,246,.08);
}
/* 삭제(처리된) — 더 작게 */
.processed-list .btn-delete-processed {
border-color: #ef4444;
color: #b91c1c;
padding: .3rem .5rem;
font-size: .75rem;
font-weight: 600;
}
.processed-list .btn-delete-processed:hover {
background: rgba(239,68,68,.08);
}
/* 백업 파일 목록 전용 컨테이너(단일 버튼) */
.backup-list .file-card-single-button {
display: flex;
margin-top: .25rem;
}
/* 보기(백업) — 강조 색상 */
.backup-list .btn-view-backup {
width: 100%;
border-color: #10b981;
color: #047857;
padding: .45rem .75rem;
font-size: .8125rem;
font-weight: 700;
}
.backup-list .btn-view-backup:hover {
background: rgba(16,185,129,.08);
}
/* ===== 백업 파일 날짜 헤더 ===== */
.list-group-item .bg-light {
transition: background-color 0.2s ease;
}
.list-group-item:hover .bg-light {
background-color: #e9ecef !important;
}
/* ===== 진행바 애니메이션 ===== */
.progress {
border-radius: 10px;
overflow: hidden;
}
.progress-bar {
transition: width 0.6s ease;
}
/* ===== 반응형 텍스트 ===== */
@media (max-width: 768px) {
.card-body {
padding: 1.5rem !important;
}
}
/* ===== 스크롤바 스타일링(모달) ===== */
.modal-body pre::-webkit-scrollbar { width: 8px; height: 8px; }
.modal-body pre::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; }
.modal-body pre::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; }
.modal-body pre::-webkit-scrollbar-thumb:hover { background: #555; }
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
// ─────────────────────────────────────────────────────────────
// 스크립트 선택 시 XML 드롭다운 토글
// ─────────────────────────────────────────────────────────────
const TARGET_SCRIPT = "02-set_config.py";
const scriptSelect = document.getElementById('script');
const xmlGroup = document.getElementById('xmlFileGroup');
function toggleXml() {
if (!scriptSelect || !xmlGroup) return;
if (scriptSelect.value === TARGET_SCRIPT) {
xmlGroup.style.display = 'block';
xmlGroup.classList.add('fade-in');
} else {
xmlGroup.style.display = 'none';
}
}
if (scriptSelect) {
toggleXml();
scriptSelect.addEventListener('change', toggleXml);
}
// ─────────────────────────────────────────────────────────────
// 파일 보기 모달
// ─────────────────────────────────────────────────────────────
const modalEl = document.getElementById('fileViewModal');
const titleEl = document.getElementById('fileViewModalLabel');
const contentEl = document.getElementById('fileViewContent');
if (modalEl) {
modalEl.addEventListener('show.bs.modal', async (ev) => {
const btn = ev.relatedTarget;
const folder = btn?.getAttribute('data-folder') || '';
const date = btn?.getAttribute('data-date') || '';
const filename = btn?.getAttribute('data-filename') || '';
titleEl.innerHTML = `<i class="bi bi-file-text me-2"></i>${filename || '파일'}`;
contentEl.textContent = '불러오는 중...';
const params = new URLSearchParams();
if (folder) params.set('folder', folder);
if (date) params.set('date', date);
if (filename) params.set('filename', filename);
try {
const res = await fetch(`/view_file?${params.toString()}`, { cache: 'no-store' });
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
contentEl.textContent = data?.content ?? '(빈 파일)';
} catch (e) {
contentEl.textContent = '파일을 불러오지 못했습니다: ' + (e?.message || e);
}
});
}
// ─────────────────────────────────────────────────────────────
// 진행바 업데이트
// ─────────────────────────────────────────────────────────────
window.updateProgress = function (val) {
const bar = document.getElementById('progressBar');
if (!bar) return;
const v = Math.max(0, Math.min(100, Number(val) || 0));
bar.style.width = v + '%';
bar.setAttribute('aria-valuenow', v);
bar.innerHTML = `<span class="fw-semibold">${v}%</span>`;
};
// ─────────────────────────────────────────────────────────────
// CSRF 토큰
// ─────────────────────────────────────────────────────────────
const csrfToken = document.querySelector('input[name="csrf_token"]')?.value || '';
// ─────────────────────────────────────────────────────────────
// 공통 POST 함수
// ─────────────────────────────────────────────────────────────
async function postFormAndHandle(url) {
const res = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'X-CSRFToken': csrfToken,
'Accept': 'application/json, text/html;q=0.9,*/*;q=0.8',
},
});
const ct = (res.headers.get('content-type') || '').toLowerCase();
if (ct.includes('application/json')) {
const data = await res.json();
if (data.success === false) {
throw new Error(data.error || ('HTTP ' + res.status));
}
return data;
}
return { success: true, html: true };
}
// ─────────────────────────────────────────────────────────────
// MAC 파일 이동
// ─────────────────────────────────────────────────────────────
const macForm = document.getElementById('macMoveForm');
if (macForm) {
macForm.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = macForm.querySelector('button');
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>처리 중...';
try {
await postFormAndHandle(macForm.action);
location.reload();
} catch (err) {
alert('MAC 이동 중 오류: ' + (err?.message || err));
btn.disabled = false;
btn.innerHTML = originalHtml;
}
});
}
// ─────────────────────────────────────────────────────────────
// GUID 파일 이동
// ─────────────────────────────────────────────────────────────
const guidForm = document.getElementById('guidMoveForm');
if (guidForm) {
guidForm.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = guidForm.querySelector('button');
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>처리 중...';
try {
await postFormAndHandle(guidForm.action);
location.reload();
} catch (err) {
alert('GUID 이동 중 오류: ' + (err?.message || err));
btn.disabled = false;
btn.innerHTML = originalHtml;
}
});
}
// ─────────────────────────────────────────────────────────────
// 알림 자동 닫기
// ─────────────────────────────────────────────────────────────
setTimeout(() => {
document.querySelectorAll('.alert').forEach(alert => {
const bsAlert = new bootstrap.Alert(alert);
bsAlert.close();
});
}, 5000);
});
</script>
<script src="{{ url_for('static', filename='js/index.js') }}"></script>
<!-- 외부 script.js 파일 (IP 폼 처리 로직 포함) -->
<script src="{{ url_for('static', filename='script.js') }}"></script>
{% endblock %}
{% endblock %}

View File

@@ -1,6 +1,18 @@
{% extends "base.html" %}
{% block title %}iDRAC Job Queue 모니터링 (Redfish){% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/jobs.css') }}">
{% endblock %}
{% block scripts %}
<!-- CSRF Token for JavaScript -->
<script>
const csrfToken = "{{ csrf_token() }}";
</script>
<script src="{{ url_for('static', filename='js/jobs.js') }}"></script>
{% endblock %}
{% block content %}
<div class="container-fluid py-3">
<!-- 헤더 -->
@@ -40,8 +52,8 @@
<small class="text-muted d-block mb-2">
한 줄에 하나씩 입력 (쉼표/세미콜론/공백 구분 가능, # 주석 지원)
</small>
<textarea id="ipInput" class="form-control font-monospace" rows="4"
placeholder="10.10.0.11&#10;10.10.0.12&#10;# 주석 가능"></textarea>
<textarea id="ipInput" class="form-control font-monospace" rows="4"
placeholder="10.10.0.11&#10;10.10.0.12&#10;# 주석 가능"></textarea>
<div class="mt-2">
<small class="text-muted"><strong id="ip-count">0</strong>개 IP</small>
</div>
@@ -60,13 +72,13 @@
</label>
</div>
</div>
<div class="col-auto">
<button id="btn-refresh" class="btn btn-primary btn-sm" disabled>
<i class="bi bi-arrow-clockwise"></i> 지금 새로고침
</button>
</div>
<div class="col-auto">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch">
@@ -75,7 +87,7 @@
</label>
</div>
</div>
<div class="col-auto">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="showCompletedSwitch" checked>
@@ -150,494 +162,4 @@
</div>
</div>
<style>
.status-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #6c757d;
}
.status-dot.active {
background-color: #198754;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* ↓↓↓ 여기부터 추가 ↓↓↓ */
/* 테이블 텍스트 한 줄 처리 */
#jobs-table {
table-layout: fixed;
width: 100%;
}
#jobs-table td {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 300px;
}
/* 열별 너비 고정 */
#jobs-table td:nth-child(1) { max-width: 110px; } /* IP */
#jobs-table td:nth-child(2) { max-width: 160px; font-size: 0.85rem; } /* JID */
#jobs-table td:nth-child(3) { max-width: 200px; } /* 작업명 */
#jobs-table td:nth-child(4) { max-width: 180px; } /* 상태 */
#jobs-table td:nth-child(5) { max-width: 120px; } /* 진행률 */
#jobs-table td:nth-child(6) { max-width: 300px; } /* 메시지 */
#jobs-table td:nth-child(7) { max-width: 150px; } /* 시간 */
/* 마우스 올리면 전체 텍스트 보기 */
#jobs-table td:hover {
white-space: normal;
overflow: visible;
position: relative;
z-index: 1000;
background-color: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
/* ↑↑↑ 여기까지 추가 ↑↑↑ */
.status-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #6c757d;
}
.status-dot.active {
background-color: #198754;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
<script>
const csrfToken = "{{ csrf_token() }}";
// ========== 전역 변수 ==========
let CONFIG = {
grace_minutes: 60,
recency_hours: 24,
poll_interval_ms: 10000
};
let monitoringOn = false;
let pollTimer = null;
let lastRenderHash = "";
// ========== Elements ==========
const $ = id => document.getElementById(id);
const $body = $('jobs-body');
const $last = $('last-updated');
const $loading = $('loading-indicator');
const $btn = $('btn-refresh');
const $auto = $('autoRefreshSwitch');
const $ipInput = $('ipInput');
const $ipCount = $('ip-count');
const $btnLoad = $('btn-load-file');
const $btnApply = $('btn-apply');
const $monSw = $('monitorSwitch');
const $statusDot = $('status-dot');
const $statusText = $('status-text');
const $showCompleted = $('showCompletedSwitch');
const $pollInterval = $('poll-interval');
// Stats
const $statTotal = $('stat-total');
const $statRunning = $('stat-running');
const $statCompleted = $('stat-completed');
const $statError = $('stat-error');
// ========== LocalStorage Keys ==========
const LS_IPS = 'idrac_job_ips';
const LS_MON = 'idrac_monitor_on';
const LS_AUTO = 'idrac_monitor_auto';
const LS_SHOW_COMPLETED = 'idrac_show_completed';
// ========== 유틸리티 ==========
function parseIps(text) {
if (!text) return [];
const raw = text.replace(/[,;]+/g, '\n');
const out = [], seen = new Set();
raw.split('\n').forEach(line => {
const parts = line.trim().split(/\s+/);
parts.forEach(p => {
if (!p || p.startsWith('#')) return;
if (!seen.has(p)) {
seen.add(p);
out.push(p);
}
});
});
return out;
}
function getIpsFromUI() { return parseIps($ipInput.value); }
function setIpsToUI(ips) {
$ipInput.value = (ips || []).join('\n');
updateIpCount();
}
function updateIpCount() {
const ips = getIpsFromUI();
$ipCount.textContent = ips.length;
}
function saveIps() {
localStorage.setItem(LS_IPS, JSON.stringify(getIpsFromUI()));
updateIpCount();
}
function loadIps() {
try {
const v = JSON.parse(localStorage.getItem(LS_IPS) || "[]");
return Array.isArray(v) ? v : [];
} catch {
return [];
}
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, m => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;',
'"': '&quot;', "'": '&#39;'
}[m]));
}
function progressBar(pc) {
const n = parseInt(String(pc ?? "").toString().replace('%', '').trim(), 10);
if (isNaN(n)) return `<span class="text-muted small">${escapeHtml(pc ?? "")}</span>`;
let bgClass = 'bg-info';
if (n === 100) bgClass = 'bg-success';
else if (n < 30) bgClass = 'bg-warning';
return `<div class="progress" style="height:8px;">
<div class="progress-bar ${bgClass}" role="progressbar"
style="width:${n}%;" aria-valuenow="${n}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<small class="text-muted">${n}%</small>`;
}
function badgeStatus(status, pc, recently = false) {
const raw = String(status || "");
const s = raw.toLowerCase();
let cls = "bg-secondary";
let icon = "info-circle";
if (recently) {
cls = "bg-success";
icon = "check-circle";
} else if (s.includes("completed")) {
cls = "bg-success";
icon = "check-circle";
} else if (s.includes("running") || s.includes("progress")) {
cls = "bg-info";
icon = "arrow-repeat";
} else if (s.includes("scheduled") || s.includes("pending")) {
cls = "bg-warning text-dark";
icon = "clock";
} else if (s.includes("failed") || s.includes("error")) {
cls = "bg-danger";
icon = "x-circle";
}
const pct = parseInt(String(pc ?? "").toString().replace('%', '').trim(), 10);
const pctText = isNaN(pct) ? "" : ` (${pct}%)`;
const text = recently ? `${raw || "Completed"} (최근${pctText})` : `${raw || "-"}${pctText}`;
return `<span class="badge ${cls}">
<i class="bi bi-${icon}"></i> ${escapeHtml(text)}
</span>`;
}
function updateStats(items) {
let total = 0, running = 0, completed = 0, error = 0;
items.forEach(it => {
if (!it.ok) {
error++;
return;
}
if (it.jobs && it.jobs.length) {
total++;
it.jobs.forEach(j => {
const s = (j.Status || "").toLowerCase();
if (s.includes("running") || s.includes("progress") || s.includes("starting")) {
running++;
} else if (s.includes("completed") || s.includes("success")) {
completed++;
} else if (s.includes("failed") || s.includes("error")) {
error++;
}
});
}
});
$statTotal.textContent = items.length;
$statRunning.textContent = running;
$statCompleted.textContent = completed;
$statError.textContent = error;
}
// ========== 렌더링 ==========
function renderTable(items) {
if (!items) return;
const hash = JSON.stringify(items);
if (hash === lastRenderHash) return;
lastRenderHash = hash;
if (!items.length) {
$body.innerHTML = `<tr><td colspan="7" class="text-center text-muted py-4">
<i class="bi bi-inbox fs-2 d-block mb-2"></i>
현재 모니터링 중인 Job이 없습니다.
</td></tr>`;
updateStats([]);
return;
}
const rows = [];
for (const it of items) {
if (!it.ok) {
rows.push(`<tr class="table-danger">
<td><code>${escapeHtml(it.ip)}</code></td>
<td colspan="6">
<i class="bi bi-exclamation-triangle"></i>
오류: ${escapeHtml(it.error || "Unknown")}
</td>
</tr>`);
continue;
}
if (!it.jobs || !it.jobs.length) continue;
for (const j of it.jobs) {
const recent = !!j.RecentlyCompleted;
const timeText = j.CompletedAt
? `완료: ${escapeHtml(j.CompletedAt.split('T')[1]?.split('.')[0] || j.CompletedAt)}`
: escapeHtml(j.LastUpdateTime || "");
rows.push(`<tr ${recent ? 'class="table-success"' : ''}>
<td><code>${escapeHtml(it.ip)}</code></td>
<td><small class="font-monospace">${escapeHtml(j.JID || "")}</small></td>
<td><strong>${escapeHtml(j.Name || "")}</strong></td>
<td>${badgeStatus(j.Status || "", j.PercentComplete || "", recent)}</td>
<td>${progressBar(j.PercentComplete || "0")}</td>
<td><small>${escapeHtml(j.Message || "")}</small></td>
<td><small class="text-muted">${timeText}</small></td>
</tr>`);
}
}
$body.innerHTML = rows.length
? rows.join("")
: `<tr><td colspan="7" class="text-center text-success py-4">
<i class="bi bi-check-circle fs-2 d-block mb-2"></i>
현재 진행 중인 Job이 없습니다. ✅
</td></tr>`;
updateStats(items);
}
// ========== 서버 요청 ==========
async function fetchJobs(auto = false) {
if (!monitoringOn) {
$body.innerHTML = `<tr><td colspan="7" class="text-center text-muted py-4">
<i class="bi bi-power fs-2 d-block mb-2"></i>
모니터링이 꺼져 있습니다. 상단 스위치를 켜면 조회가 시작됩니다.
</td></tr>`;
$last.textContent = "";
updateStats([]);
return;
}
const ips = getIpsFromUI();
if (!ips.length) {
$body.innerHTML = `<tr><td colspan="7" class="text-center text-warning py-4">
<i class="bi bi-exclamation-triangle fs-2 d-block mb-2"></i>
IP 목록이 비어 있습니다.
</td></tr>`;
$last.textContent = "";
updateStats([]);
return;
}
try {
$loading.classList.remove('d-none');
$last.textContent = "조회 중… " + new Date().toLocaleTimeString();
const res = await fetch("/jobs/scan", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": csrfToken
},
body: JSON.stringify({
ips,
method: "redfish", // Redfish 사용
recency_hours: CONFIG.recency_hours,
grace_minutes: CONFIG.grace_minutes,
include_tracked_done: $showCompleted.checked
})
});
const data = await res.json();
if (!data.ok) throw new Error(data.error || "Scan failed");
renderTable(data.items);
$last.textContent = "업데이트: " + new Date().toLocaleString();
} catch (e) {
$body.innerHTML = `<tr><td colspan="7" class="text-danger text-center py-4">
<i class="bi bi-exclamation-circle fs-2 d-block mb-2"></i>
로드 실패: ${escapeHtml(e.message)}
<br><button class="btn btn-sm btn-outline-primary mt-2" onclick="fetchJobs(false)">
<i class="bi bi-arrow-clockwise"></i> 재시도
</button>
</td></tr>`;
$last.textContent = "에러: " + new Date().toLocaleString();
console.error(e);
} finally {
$loading.classList.add('d-none');
}
}
// ========== 모니터링 제어 ==========
function startAuto() {
stopAuto();
pollTimer = setInterval(() => fetchJobs(true), CONFIG.poll_interval_ms);
}
function stopAuto() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
function updateMonitorUI() {
$btn.disabled = !monitoringOn;
$auto.disabled = !monitoringOn;
if (monitoringOn) {
$statusDot.classList.add('active');
$statusText.textContent = '모니터링 중';
} else {
$statusDot.classList.remove('active');
$statusText.textContent = '모니터링 꺼짐';
}
}
async function setMonitoring(on) {
monitoringOn = !!on;
localStorage.setItem(LS_MON, monitoringOn ? "1" : "0");
updateMonitorUI();
if (!monitoringOn) {
stopAuto();
$last.textContent = "";
$body.innerHTML = `<tr><td colspan="7" class="text-center text-muted py-4">
<i class="bi bi-power fs-2 d-block mb-2"></i>
모니터링이 꺼져 있습니다.
</td></tr>`;
lastRenderHash = "";
updateStats([]);
return;
}
await fetchJobs(false);
if ($auto.checked) startAuto();
}
// ========== 초기화 ==========
document.addEventListener('DOMContentLoaded', async () => {
// 설정 로드
try {
const res = await fetch('/jobs/config');
const data = await res.json();
if (data.ok) {
CONFIG = data.config;
$pollInterval.textContent = Math.round(CONFIG.poll_interval_ms / 1000);
}
} catch (e) {
console.error("Failed to load config:", e);
}
// IP 복원
const savedIps = loadIps();
if (savedIps.length) {
setIpsToUI(savedIps);
} else {
try {
const res = await fetch('/jobs/iplist', {
headers: { 'X-CSRFToken': csrfToken }
});
const data = await res.json();
if (data.ok && data.ips) {
setIpsToUI(data.ips);
saveIps();
}
} catch (e) {
console.error(e);
}
}
// 설정 복원
const savedMon = localStorage.getItem(LS_MON);
const savedAuto = localStorage.getItem(LS_AUTO);
const savedShowCompleted = localStorage.getItem(LS_SHOW_COMPLETED);
$monSw.checked = savedMon === "1";
$auto.checked = savedAuto === "1";
$showCompleted.checked = savedShowCompleted !== "0";
// 이벤트
$ipInput.addEventListener('input', updateIpCount);
$btn.addEventListener('click', () => { if (monitoringOn) fetchJobs(false); });
$auto.addEventListener('change', e => {
localStorage.setItem(LS_AUTO, e.target.checked ? "1" : "0");
if (!monitoringOn) return;
if (e.target.checked) startAuto();
else stopAuto();
});
$monSw.addEventListener('click', e => setMonitoring(e.target.checked));
$showCompleted.addEventListener('change', e => {
localStorage.setItem(LS_SHOW_COMPLETED, e.target.checked ? "1" : "0");
if (monitoringOn) fetchJobs(false);
});
$btnLoad.addEventListener('click', async () => {
try {
const res = await fetch('/jobs/iplist', {
headers: { 'X-CSRFToken': csrfToken }
});
const data = await res.json();
if (data.ok) {
setIpsToUI(data.ips || []);
saveIps();
if (monitoringOn) await fetchJobs(false);
}
} catch (e) {
alert('IP 목록 불러오기 실패: ' + e.message);
}
});
$btnApply.addEventListener('click', () => {
saveIps();
if (monitoringOn) fetchJobs(false);
});
// 초기 상태
updateMonitorUI();
updateIpCount();
if ($monSw.checked) {
setMonitoring(true);
}
});
</script>
{% endblock %}
{% endblock %}

View File

@@ -1,305 +1,223 @@
{% extends "base.html" %}
{% block title %}XML 파일 관리 & 배포 - Dell Server Info{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/scp.css') }}">
{% endblock %}
{% block content %}
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XML 파일 관리</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
body {
background-color: #f5f5f5;
padding: 20px 0;
}
<h1 class="main-title">설정 파일 관리 (SCP)</h1>
<p class="subtitle">iDRAC 서버 설정(XML)을 내보내거나 가져오고, 버전을 비교할 수 있습니다.</p>
.main-title {
font-size: 1.8rem;
font-weight: 600;
color: #333;
margin-bottom: 10px;
}
.subtitle {
color: #666;
font-size: 0.95rem;
margin-bottom: 30px;
}
.card {
border: none;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
background: white;
}
.card-header-custom {
background-color: #007bff;
color: white;
padding: 12px 20px;
font-weight: 600;
border-radius: 8px 8px 0 0;
font-size: 1rem;
}
.card-body {
padding: 20px;
}
.form-label {
font-weight: 500;
color: #555;
margin-bottom: 8px;
font-size: 0.9rem;
}
.form-control {
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px 12px;
font-size: 0.9rem;
}
.btn-primary {
background-color: #007bff;
border: none;
padding: 8px 24px;
font-weight: 500;
border-radius: 4px;
font-size: 0.9rem;
}
.btn-success {
background-color: #28a745;
border: none;
padding: 6px 16px;
font-weight: 500;
border-radius: 4px;
font-size: 0.85rem;
}
.btn-danger {
background-color: #dc3545;
border: none;
padding: 6px 16px;
font-weight: 500;
border-radius: 4px;
font-size: 0.85rem;
}
.upload-section {
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
}
.custom-file-label {
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9rem;
}
.custom-file-label::after {
background-color: #007bff;
color: white;
border-radius: 0 3px 3px 0;
font-size: 0.85rem;
}
/* 아이콘 + 뱃지 스타일 */
.file-list {
max-height: 500px;
overflow-y: auto;
}
.icon-badge-item {
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 10px 15px;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.3s;
background: white;
}
.icon-badge-item:hover {
background-color: #f8f9fa;
border-color: #007bff;
transform: translateX(3px);
}
.icon-badge-left {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
min-width: 0;
}
.file-icon-small {
width: 36px;
height: 36px;
background: #007bff;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
flex-shrink: 0;
}
.file-name-section {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
flex: 1;
}
.file-name-badge {
font-weight: 500;
color: #333;
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.badge-custom {
background-color: #e7f3ff;
color: #007bff;
padding: 3px 10px;
border-radius: 10px;
font-size: 0.75rem;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
}
.action-buttons {
display: flex;
gap: 6px;
flex-shrink: 0;
}
.empty-message {
text-align: center;
color: #999;
padding: 30px;
font-size: 0.9rem;
}
.container {
max-width: 1200px;
}
/* 스크롤바 스타일 */
.file-list::-webkit-scrollbar {
width: 6px;
}
.file-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.file-list::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
}
.file-list::-webkit-scrollbar-thumb:hover {
background: #555;
}
</style>
</head>
<body>
<div class="container">
<h1 class="main-title">XML 파일 관리</h1>
<p class="subtitle">XML 파일을 업로드하고 관리할 수 있습니다</p>
<!-- XML 파일 업로드 폼 -->
<div class="row">
<!-- 왼쪽: 파일 업로드 및 내보내기 -->
<div class="col-md-4">
<div class="card">
<div class="card-header-custom">
<i class="fas fa-cloud-upload-alt mr-2"></i>파일 업로드
<span><i class="fas fa-cloud-upload-alt me-2"></i>파일 등록</span>
</div>
<div class="card-body">
<form action="{{ url_for('xml.upload_xml') }}" method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<!-- 1. PC에서 업로드 -->
<h6 class="mb-3"><i class="fas fa-laptop me-2"></i>PC에서 업로드</h6>
<form action="{{ url_for('xml.upload_xml') }}" method="POST" enctype="multipart/form-data" class="mb-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="upload-section">
<div class="form-group mb-2">
<label for="xmlFile" class="form-label">XML 파일 선택</label>
<div class="mb-2">
<div class="custom-file">
<input type="file" class="custom-file-input" id="xmlFile" name="xmlFile" accept=".xml" onchange="updateFileName(this)">
<label class="custom-file-label" for="xmlFile" id="fileLabel">파일을 선택하세요</label>
<input type="file" class="custom-file-input" id="xmlFile" name="xmlFile" accept=".xml"
onchange="updateFileName(this)">
<label class="custom-file-label" for="xmlFile" id="fileLabel">파일 선택</label>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-upload mr-1"></i>업로드
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-upload me-1"></i>업로드
</button>
</div>
</form>
</div>
</div>
<!-- XML 파일 목록 -->
<div class="card">
<div class="card-header-custom">
<i class="fas fa-list mr-2"></i>파일 목록
</div>
<div class="card-body">
{% if xml_files %}
<div class="file-list">
{% for xml_file in xml_files %}
<div class="icon-badge-item">
<div class="icon-badge-left">
<div class="file-icon-small">
<i class="fas fa-file-code"></i>
</div>
<div class="file-name-section">
<span class="file-name-badge" title="{{ xml_file }}">{{ xml_file }}</span>
<span class="badge-custom">XML</span>
</div>
</div>
<div class="action-buttons">
<!-- 파일 편집 버튼 -->
<a href="{{ url_for('xml.edit_xml', filename=xml_file) }}" class="btn btn-success btn-sm">
<i class="fas fa-edit"></i> 편집
</a>
<!-- 파일 삭제 버튼 -->
<form action="{{ url_for('xml.delete_xml', filename=xml_file) }}" method="POST" style="display:inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('정말 삭제하시겠습니까?')">
<i class="fas fa-trash"></i> 삭제
</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-message">
<i class="fas fa-folder-open" style="font-size: 2rem; color: #ddd;"></i>
<p class="mt-2 mb-0">파일이 없습니다.</p>
</div>
{% endif %}
<hr>
<!-- 2. iDRAC에서 내보내기 -->
<h6 class="mb-3"><i class="fas fa-server me-2"></i>iDRAC에서 추출 (Export)</h6>
<button type="button" class="btn btn-outline-primary w-100" data-bs-toggle="modal"
data-bs-target="#exportModal">
<i class="fas fa-download me-1"></i>설정 추출하기
</button>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script>
function updateFileName(input) {
const fileName = input.files[0]?.name || '파일을 선택하세요';
document.getElementById('fileLabel').textContent = fileName;
}
</script>
</body>
</html>
<!-- 오른쪽: 파일 목록 및 작업 -->
<div class="col-md-8">
<div class="card">
<div class="card-header-custom">
<span><i class="fas fa-list me-2"></i>파일 목록</span>
<button class="btn btn-light btn-sm text-primary" id="compareBtn"
data-url="{{ url_for('scp.diff_scp') }}" onclick="compareSelected()">
<i class="fas fa-exchange-alt me-1"></i>선택 비교
</button>
</div>
<div class="card-body">
{% if xml_files %}
<div class="file-list">
{% for xml_file in xml_files %}
<div class="icon-badge-item">
<div class="icon-badge-left">
<input type="checkbox" class="select-checkbox file-selector" value="{{ xml_file }}">
<div class="file-icon-small">
<i class="fas fa-file-code"></i>
</div>
<div class="file-name-section">
<span class="file-name-badge" title="{{ xml_file }}">{{ xml_file }}</span>
<span class="badge-custom">XML</span>
</div>
</div>
<div class="action-buttons">
<!-- 배포 버튼 -->
<button type="button" class="btn btn-info btn-sm text-white"
onclick="openDeployModal('{{ xml_file }}')">
<i class="fas fa-plane-departure"></i> <span>배포</span>
</button>
<!-- 편집 버튼 -->
<a href="{{ url_for('xml.edit_xml', filename=xml_file) }}" class="btn btn-success btn-sm">
<i class="fas fa-edit"></i> <span>편집</span>
</a>
<!-- 삭제 버튼 -->
<form action="{{ url_for('xml.delete_xml', filename=xml_file) }}" method="POST"
style="display:inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button type="submit" class="btn btn-danger btn-sm"
onclick="return confirm('정말 삭제하시겠습니까?')">
<i class="fas fa-trash"></i> <span>삭제</span>
</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-message">
<i class="fas fa-folder-open" style="font-size: 2rem; color: #ddd;"></i>
<p class="mt-2 mb-0">파일이 없습니다.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Export Modal -->
<div class="modal fade" id="exportModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ url_for('scp.export_scp') }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="modal-header">
<h5 class="modal-title">iDRAC 설정 내보내기</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-info py-2" style="font-size: 0.9rem;">
<i class="fas fa-info-circle me-1"></i> 네트워크 공유 폴더(CIFS)가 필요합니다.
</div>
<h6>대상 iDRAC</h6>
<div class="mb-2"><input type="text" class="form-control" name="target_ip" placeholder="iDRAC IP"
required></div>
<div class="row mb-3">
<div class="col"><input type="text" class="form-control" name="username" placeholder="User"
required></div>
<div class="col"><input type="password" class="form-control" name="password"
placeholder="Password" required></div>
</div>
<hr>
<h6>네트워크 공유 (저장소)</h6>
<div class="mb-2"><input type="text" class="form-control" name="share_ip"
placeholder="Share Server IP" required></div>
<div class="mb-2"><input type="text" class="form-control" name="share_name"
placeholder="Share Name (e.g. public)" required></div>
<div class="mb-2"><input type="text" class="form-control" name="filename"
placeholder="Save Filename (e.g. backup.xml)" required></div>
<div class="row">
<div class="col"><input type="text" class="form-control" name="share_user"
placeholder="Share User"></div>
<div class="col"><input type="password" class="form-control" name="share_pwd"
placeholder="Share Password"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
<button type="submit" class="btn btn-primary">내보내기 시작</button>
</div>
</form>
</div>
</div>
</div>
<!-- Deploy (Import) Modal -->
<div class="modal fade" id="deployModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ url_for('scp.import_scp') }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="modal-header">
<h5 class="modal-title">설정 배포 (Import)</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning py-2" style="font-size: 0.9rem;">
<i class="fas fa-exclamation-triangle me-1"></i> 적용 후 서버가 재부팅될 수 있습니다.
</div>
<div class="mb-3">
<label class="form-label">배포할 파일</label>
<input type="text" class="form-control" id="deployFilename" name="filename" readonly>
</div>
<h6>대상 iDRAC</h6>
<div class="mb-2"><input type="text" class="form-control" name="target_ip" placeholder="iDRAC IP"
required></div>
<div class="row mb-3">
<div class="col"><input type="text" class="form-control" name="username" placeholder="User"
required></div>
<div class="col"><input type="password" class="form-control" name="password"
placeholder="Password" required></div>
</div>
<hr>
<h6>네트워크 공유 (소스 위치)</h6>
<div class="mb-2"><input type="text" class="form-control" name="share_ip"
placeholder="Share Server IP" required></div>
<div class="mb-2"><input type="text" class="form-control" name="share_name" placeholder="Share Name"
required></div>
<div class="row mb-3">
<div class="col"><input type="text" class="form-control" name="share_user"
placeholder="Share User"></div>
<div class="col"><input type="password" class="form-control" name="share_pwd"
placeholder="Share Password"></div>
</div>
<div class="mb-3">
<label class="form-label">적용 모드</label>
<select class="form-select" name="import_mode">
<option value="Replace">전체 교체 (Replace)</option>
<option value="Append">변경분만 적용 (Append)</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
<button type="submit" class="btn btn-danger">배포 시작</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/scp.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,58 @@
{% extends "base.html" %}
{% block title %}설정 파일 비교 - Dell Server Info{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/scp.css') }}">
{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>설정 파일 비교</h2>
<a href="{{ url_for('xml.xml_management') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i> 목록으로
</a>
</div>
<div class="card mb-4">
<div class="card-header bg-info text-white">
<i class="fas fa-exchange-alt me-2"></i>비교 대상
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-md-5">
<h5>{{ file1 }}</h5>
</div>
<div class="col-md-2">
<i class="fas fa-arrow-right text-muted"></i>
</div>
<div class="col-md-5">
<h5>{{ file2 }}</h5>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<i class="fas fa-code me-2"></i>Diff 결과
</div>
<div class="card-body p-0">
<div class="diff-container">
{% for line in diff_content.splitlines() %}
{% if line.startswith('+++') or line.startswith('---') %}
<span class="diff-line diff-header">{{ line }}</span>
{% elif line.startswith('+') %}
<span class="diff-line diff-add">{{ line }}</span>
{% elif line.startswith('-') %}
<span class="diff-line diff-del">{{ line }}</span>
{% else %}
<span class="diff-line">{{ line }}</span>
{% endif %}
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,21 +1,19 @@
{% extends "base.html" %}
{% block content %}
<body>
<h1>Uploaded XML Files</h1>
<ul>
{% for file in files %}
<li>
{{ file }}
<a href="{{ url_for('download_xml', filename=file) }}">Download</a>
<a href="{{ url_for('edit_xml', filename=file) }}">Edit</a>
<form action="{{ url_for('delete_xml', filename=file) }}" method="post" style="display:inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit">Delete</button>
</form>
</li>
{% endfor %}
</ul>
<a href="{{ url_for('upload_xml') }}">Upload new file</a>
</body>
<h1>Uploaded XML Files</h1>
<ul>
{% for file in files %}
<li>
{{ file }}
<a href="{{ url_for('download_xml', filename=file) }}">Download</a>
<a href="{{ url_for('edit_xml', filename=file) }}">Edit</a>
<form action="{{ url_for('delete_xml', filename=file) }}" method="post" style="display:inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button type="submit">Delete</button>
</form>
</li>
{% endfor %}
</ul>
<a href="{{ url_for('upload_xml') }}">Upload new file</a>
{% endblock %}

10
check_telegram.py Normal file
View File

@@ -0,0 +1,10 @@
import sys
try:
import telegram
print(f"Telegram library found. Version: {telegram.__version__}")
from telegram import Bot
print("Bot class imported successfully.")
except ImportError as e:
print(f"ImportError: {e}")
except Exception as e:
print(f"Error during import: {e}")

View File

@@ -40,6 +40,10 @@ class Config:
# ── 보안
SECRET_KEY = os.environ.get("SECRET_KEY", "change-me") # 반드시 환경변수로 고정 권장
# ── Redfish
REDFISH_TIMEOUT = int(os.environ.get("REDFISH_TIMEOUT", 15))
REDFISH_VERIFY_SSL = os.environ.get("REDFISH_VERIFY_SSL", "false").lower() == "true"
# ── DB (환경변수 DATABASE_URL 있으면 그 값을 우선 사용)
sqlite_path = (INSTANCE_DIR / "site.db").as_posix()
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", f"sqlite:///{sqlite_path}")

684
data/logs/2025-11-24.log Normal file
View File

@@ -0,0 +1,684 @@
2025-11-24 16:23:44,003 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 16:23:44,030 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:23:44,030 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:23:44,055 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 16:23:44,090 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-24 16:23:44,090 [INFO] werkzeug: Press CTRL+C to quit
2025-11-24 16:23:44,090 [INFO] werkzeug: * Restarting with watchdog (windowsapi)
2025-11-24 16:23:44,690 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 16:23:44,710 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:23:44,710 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:23:44,726 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 16:23:44,739 [WARNING] werkzeug: * Debugger is active!
2025-11-24 16:23:44,741 [INFO] werkzeug: * Debugger PIN: 778-054-746
2025-11-24 16:23:52,286 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:23:52] "GET / HTTP/1.1" 302 -
2025-11-24 16:23:52,316 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:23:52] "GET /login?next=/ HTTP/1.1" 200 -
2025-11-24 16:23:52,359 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:23:52] "GET /static/style.css HTTP/1.1" 200 -
2025-11-24 16:23:52,414 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:23:52] "GET /static/favicon.ico HTTP/1.1" 200 -
2025-11-24 16:23:58,612 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 16:23:58,612 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 16:23:58,676 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 16:23:58,676 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 16:23:58,676 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 16:23:58,676 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 16:23:58,677 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:23:58] "POST /login HTTP/1.1" 302 -
2025-11-24 16:23:58,717 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:23:58] "GET /index HTTP/1.1" 200 -
2025-11-24 16:23:58,735 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:23:58] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:23:58,745 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:23:58] "GET /static/script.js HTTP/1.1" 200 -
2025-11-24 16:24:08,700 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:08] "GET /home/ HTTP/1.1" 200 -
2025-11-24 16:24:08,711 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:08] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:24:10,139 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:10] "GET /home/ HTTP/1.1" 200 -
2025-11-24 16:24:10,154 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:10] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:24:10,632 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:10] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 16:24:10,645 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:10] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:24:14,477 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:14] "GET /edit_xml/T8A_R6625_RAID_A.xml HTTP/1.1" 200 -
2025-11-24 16:24:14,492 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:14] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:24:16,178 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:16] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 16:24:16,191 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:16] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:24:22,156 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:22] "GET /index HTTP/1.1" 200 -
2025-11-24 16:24:22,173 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:22] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:24:22,174 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:22] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:24:22,770 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:22] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 16:24:22,783 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:22] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:24:23,371 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:23] "GET /index HTTP/1.1" 200 -
2025-11-24 16:24:23,387 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:23] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:24:23,389 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:23] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:24:23,851 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:23] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 16:24:23,863 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:23] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:24:24,257 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:24] "GET /index HTTP/1.1" 200 -
2025-11-24 16:24:24,271 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:24] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:24:24,274 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:24] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:24:30,856 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:30] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 16:24:30,871 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:24:30] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:25:28,267 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:25:28] "GET /index HTTP/1.1" 200 -
2025-11-24 16:25:28,283 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:25:28] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:25:28,286 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:25:28] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:26:46,574 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:26:46] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 16:26:46,595 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:26:46] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:26:47,173 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:26:47] "GET /index HTTP/1.1" 200 -
2025-11-24 16:26:47,188 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:26:47] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:26:47,190 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:26:47] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:26:48,441 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:26:48] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 16:26:48,453 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:26:48] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:26:48,952 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:26:48] "GET /index HTTP/1.1" 200 -
2025-11-24 16:26:48,964 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:26:48] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:26:48,967 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:26:48] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:27:17,588 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:17] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 16:27:17,601 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:17] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:27:19,596 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:19] "GET /index HTTP/1.1" 200 -
2025-11-24 16:27:19,613 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:19] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:27:19,614 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:19] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:27:20,075 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:20] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 16:27:20,087 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:20] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:27:21,231 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:21] "GET /index HTTP/1.1" 200 -
2025-11-24 16:27:21,243 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:21] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:27:21,247 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:21] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:27:21,587 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:21] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 16:27:21,603 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:21] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:27:28,942 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:28] "GET /jobs HTTP/1.1" 200 -
2025-11-24 16:27:28,957 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:28] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:27:28,965 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:28] "GET /jobs/config HTTP/1.1" 200 -
2025-11-24 16:27:28,985 [WARNING] backend.services.idrac_jobs: IP list file not found: data/server_list/idrac_ip_list.txt
2025-11-24 16:27:28,986 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:28] "GET /jobs/iplist HTTP/1.1" 200 -
2025-11-24 16:27:33,846 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:33] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 16:27:33,860 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:33] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:27:34,536 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:34] "GET /index HTTP/1.1" 200 -
2025-11-24 16:27:34,548 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:34] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:27:34,552 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:34] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:27:35,135 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:35] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 16:27:35,147 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:35] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:27:35,691 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:35] "GET /index HTTP/1.1" 200 -
2025-11-24 16:27:35,705 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:35] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:27:35,706 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:35] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:27:54,433 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:54] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 16:27:54,447 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:54] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:27:56,102 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:56] "GET /edit_xml/R6615_raid.xml HTTP/1.1" 200 -
2025-11-24 16:27:56,116 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:27:56] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:43:03,673 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 16:43:03,693 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:43:03,693 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:43:03,710 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 16:43:03,730 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-24 16:43:03,730 [INFO] werkzeug: Press CTRL+C to quit
2025-11-24 16:43:03,730 [INFO] werkzeug: * Restarting with watchdog (windowsapi)
2025-11-24 16:43:04,322 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 16:43:04,342 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:43:04,342 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:43:04,358 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 16:43:04,372 [WARNING] werkzeug: * Debugger is active!
2025-11-24 16:43:04,373 [INFO] werkzeug: * Debugger PIN: 778-054-746
2025-11-24 16:43:06,033 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:43:06] "GET /home/ HTTP/1.1" 200 -
2025-11-24 16:43:06,081 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:43:06] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:43:06,124 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:43:06] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-24 16:43:07,831 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:43:07] "GET /home/ HTTP/1.1" 200 -
2025-11-24 16:43:07,846 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:43:07] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:43:08,675 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:43:08] "GET /login HTTP/1.1" 200 -
2025-11-24 16:43:08,686 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:43:08] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:43:14,285 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 16:43:14,285 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 16:43:14,334 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 16:43:14,334 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 16:43:14,334 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 16:43:14,334 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 16:43:14,335 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:43:14] "POST /login HTTP/1.1" 302 -
2025-11-24 16:43:14,352 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:43:14] "GET /index HTTP/1.1" 200 -
2025-11-24 16:43:14,370 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:43:14] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:43:14,371 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:43:14] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:46:00,350 [INFO] app: LOGOUT: user=김강희
2025-11-24 16:46:00,350 [INFO] app: LOGOUT: user=김강희
2025-11-24 16:46:00,351 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:46:00] "GET /logout HTTP/1.1" 302 -
2025-11-24 16:46:00,354 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:46:00] "GET /login HTTP/1.1" 200 -
2025-11-24 16:46:00,370 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:46:00] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:47:44,807 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 16:47:44,827 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:47:44,827 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:47:44,841 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 16:47:44,862 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-24 16:47:44,862 [INFO] werkzeug: Press CTRL+C to quit
2025-11-24 16:47:44,862 [INFO] werkzeug: * Restarting with watchdog (windowsapi)
2025-11-24 16:47:45,441 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 16:47:45,460 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:47:45,460 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:47:45,479 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 16:47:45,494 [WARNING] werkzeug: * Debugger is active!
2025-11-24 16:47:45,496 [INFO] werkzeug: * Debugger PIN: 778-054-746
2025-11-24 16:47:47,414 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:47:47] "GET /login HTTP/1.1" 200 -
2025-11-24 16:47:47,454 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:47:47] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:47:47,464 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:47:47] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-24 16:47:51,936 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 16:47:51,936 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 16:47:51,984 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 16:47:51,984 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 16:47:51,985 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 16:47:51,985 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 16:47:51,985 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:47:51] "POST /login HTTP/1.1" 302 -
2025-11-24 16:47:52,003 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:47:52] "GET /index HTTP/1.1" 200 -
2025-11-24 16:47:52,022 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:47:52] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:47:52,022 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:47:52] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:49:41,046 [INFO] app: LOGOUT: user=김강희
2025-11-24 16:49:41,046 [INFO] app: LOGOUT: user=김강희
2025-11-24 16:49:41,048 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:49:41] "GET /logout HTTP/1.1" 302 -
2025-11-24 16:49:41,050 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:49:41] "GET /login HTTP/1.1" 200 -
2025-11-24 16:49:41,066 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:49:41] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:49:43,916 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 16:49:43,936 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:49:43,936 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:49:43,953 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 16:49:43,974 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-24 16:49:43,974 [INFO] werkzeug: Press CTRL+C to quit
2025-11-24 16:49:43,975 [INFO] werkzeug: * Restarting with watchdog (windowsapi)
2025-11-24 16:49:44,586 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 16:49:44,606 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:49:44,606 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:49:44,622 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 16:49:44,636 [WARNING] werkzeug: * Debugger is active!
2025-11-24 16:49:44,638 [INFO] werkzeug: * Debugger PIN: 778-054-746
2025-11-24 16:49:45,767 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:49:45] "GET /login HTTP/1.1" 200 -
2025-11-24 16:49:45,801 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:49:45] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:49:45,817 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:49:45] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-24 16:49:50,363 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 16:49:50,363 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 16:49:50,411 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 16:49:50,411 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 16:49:50,412 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 16:49:50,412 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 16:49:50,412 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:49:50] "POST /login HTTP/1.1" 302 -
2025-11-24 16:49:50,429 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:49:50] "GET /index HTTP/1.1" 200 -
2025-11-24 16:49:50,446 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:49:50] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:49:50,447 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:49:50] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:57:24,910 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 16:57:24,930 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:57:24,930 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:57:24,946 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 16:57:24,967 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-24 16:57:24,968 [INFO] werkzeug: Press CTRL+C to quit
2025-11-24 16:57:24,968 [INFO] werkzeug: * Restarting with watchdog (windowsapi)
2025-11-24 16:57:25,580 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 16:57:25,600 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:57:25,600 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:57:25,616 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 16:57:25,629 [WARNING] werkzeug: * Debugger is active!
2025-11-24 16:57:25,632 [INFO] werkzeug: * Debugger PIN: 778-054-746
2025-11-24 16:57:26,867 [INFO] app: LOGIN: already auth → /index
2025-11-24 16:57:26,867 [INFO] app: LOGIN: already auth → /index
2025-11-24 16:57:26,868 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:57:26] "GET /login HTTP/1.1" 302 -
2025-11-24 16:57:26,893 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:57:26] "GET /index HTTP/1.1" 200 -
2025-11-24 16:57:26,973 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:57:26] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:57:26,975 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:57:26] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:57:27,029 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:57:27] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-24 16:57:28,419 [INFO] app: LOGOUT: user=김강희
2025-11-24 16:57:28,419 [INFO] app: LOGOUT: user=김강희
2025-11-24 16:57:28,420 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:57:28] "GET /logout HTTP/1.1" 302 -
2025-11-24 16:57:28,425 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:57:28] "GET /login HTTP/1.1" 200 -
2025-11-24 16:57:28,441 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:57:28] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:57:34,916 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 16:57:34,916 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 16:57:34,968 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 16:57:34,968 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 16:57:34,968 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=MISSING
2025-11-24 16:57:34,968 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=MISSING
2025-11-24 16:57:34,968 [WARNING] app: Telegram notification skipped: Missing configuration or library.
2025-11-24 16:57:34,968 [WARNING] app: Telegram notification skipped: Missing configuration or library.
2025-11-24 16:57:34,970 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 16:57:34,970 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 16:57:34,970 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:57:34] "POST /login HTTP/1.1" 302 -
2025-11-24 16:57:34,975 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:57:34] "GET /index HTTP/1.1" 200 -
2025-11-24 16:57:34,993 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:57:34] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:57:34,994 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:57:34] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:58:17,994 [INFO] werkzeug: * Detected change in 'D:\\Code\\iDRAC_Info\\idrac_info\\check_telegram.py', reloading
2025-11-24 16:58:18,038 [INFO] werkzeug: * Detected change in 'D:\\Code\\iDRAC_Info\\idrac_info\\check_telegram.py', reloading
2025-11-24 16:58:18,039 [INFO] werkzeug: * Detected change in 'D:\\Code\\iDRAC_Info\\idrac_info\\check_telegram.py', reloading
2025-11-24 16:58:18,379 [INFO] werkzeug: * Restarting with watchdog (windowsapi)
2025-11-24 16:58:19,034 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 16:58:19,055 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:58:19,055 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:58:19,072 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 16:58:19,086 [WARNING] werkzeug: * Debugger is active!
2025-11-24 16:58:19,089 [INFO] werkzeug: * Debugger PIN: 778-054-746
2025-11-24 16:58:42,574 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:58:42] "GET /index HTTP/1.1" 200 -
2025-11-24 16:58:42,632 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:58:42] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:58:42,633 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:58:42] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:58:44,677 [INFO] app: LOGOUT: user=김강희
2025-11-24 16:58:44,677 [INFO] app: LOGOUT: user=김강희
2025-11-24 16:58:44,678 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:58:44] "GET /logout HTTP/1.1" 302 -
2025-11-24 16:58:44,684 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:58:44] "GET /login HTTP/1.1" 200 -
2025-11-24 16:58:44,702 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:58:44] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:58:49,916 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 16:58:49,916 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 16:58:49,966 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 16:58:49,966 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 16:58:49,966 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=MISSING
2025-11-24 16:58:49,966 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=MISSING
2025-11-24 16:58:49,966 [WARNING] app: Telegram notification skipped: Missing configuration or library.
2025-11-24 16:58:49,966 [WARNING] app: Telegram notification skipped: Missing configuration or library.
2025-11-24 16:58:49,966 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 16:58:49,966 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 16:58:49,967 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:58:49] "POST /login HTTP/1.1" 302 -
2025-11-24 16:58:49,972 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:58:49] "GET /index HTTP/1.1" 200 -
2025-11-24 16:58:49,993 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:58:49] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:58:49,993 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:58:49] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:59:34,417 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 16:59:34,438 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:59:34,438 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:59:34,453 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 16:59:34,473 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-24 16:59:34,473 [INFO] werkzeug: Press CTRL+C to quit
2025-11-24 16:59:34,474 [INFO] werkzeug: * Restarting with watchdog (windowsapi)
2025-11-24 16:59:35,084 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 16:59:35,104 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:59:35,104 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 16:59:35,121 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 16:59:35,136 [WARNING] werkzeug: * Debugger is active!
2025-11-24 16:59:35,138 [INFO] werkzeug: * Debugger PIN: 778-054-746
2025-11-24 16:59:36,736 [INFO] app: LOGOUT: user=김강희
2025-11-24 16:59:36,736 [INFO] app: LOGOUT: user=김강희
2025-11-24 16:59:36,737 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:59:36] "GET /logout HTTP/1.1" 302 -
2025-11-24 16:59:36,747 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:59:36] "GET /login HTTP/1.1" 200 -
2025-11-24 16:59:36,783 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:59:36] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:59:42,340 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 16:59:42,340 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 16:59:42,391 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 16:59:42,391 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 16:59:42,391 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=MISSING
2025-11-24 16:59:42,391 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=MISSING
2025-11-24 16:59:42,392 [WARNING] app: Telegram notification skipped: Missing configuration or library.
2025-11-24 16:59:42,392 [WARNING] app: Telegram notification skipped: Missing configuration or library.
2025-11-24 16:59:42,392 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 16:59:42,392 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 16:59:42,392 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:59:42] "POST /login HTTP/1.1" 302 -
2025-11-24 16:59:42,407 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:59:42] "GET /index HTTP/1.1" 200 -
2025-11-24 16:59:42,428 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:59:42] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 16:59:42,429 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:59:42] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 16:59:50,145 [INFO] app: LOGOUT: user=김강희
2025-11-24 16:59:50,145 [INFO] app: LOGOUT: user=김강희
2025-11-24 16:59:50,146 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:59:50] "GET /logout HTTP/1.1" 302 -
2025-11-24 16:59:50,150 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:59:50] "GET /login HTTP/1.1" 200 -
2025-11-24 16:59:50,164 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 16:59:50] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:06:17,663 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 17:06:17,684 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:06:17,684 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:06:17,700 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 17:06:17,722 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-24 17:06:17,722 [INFO] werkzeug: Press CTRL+C to quit
2025-11-24 17:06:17,722 [INFO] werkzeug: * Restarting with watchdog (windowsapi)
2025-11-24 17:06:18,428 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 17:06:18,448 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:06:18,448 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:06:18,463 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 17:06:18,477 [WARNING] werkzeug: * Debugger is active!
2025-11-24 17:06:18,479 [INFO] werkzeug: * Debugger PIN: 778-054-746
2025-11-24 17:06:18,548 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:06:18] "GET /index HTTP/1.1" 302 -
2025-11-24 17:06:18,558 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:06:18] "GET /login?next=/index HTTP/1.1" 200 -
2025-11-24 17:06:18,602 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:06:18] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:06:18,643 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:06:18] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-24 17:06:24,340 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 17:06:24,340 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 17:06:24,396 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 17:06:24,396 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 17:06:24,396 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:06:24,396 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:06:24,397 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 17:06:24,397 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 17:06:24,398 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:06:24] "POST /login HTTP/1.1" 302 -
2025-11-24 17:06:24,418 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:06:24] "GET /index HTTP/1.1" 200 -
2025-11-24 17:06:24,437 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:06:24] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:06:24,438 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:06:24] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 17:06:26,931 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:09:29,579 [INFO] app: LOGOUT: user=김강희
2025-11-24 17:09:29,579 [INFO] app: LOGOUT: user=김강희
2025-11-24 17:09:29,580 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:09:29] "GET /logout HTTP/1.1" 302 -
2025-11-24 17:09:29,583 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:09:29] "GET /login HTTP/1.1" 200 -
2025-11-24 17:09:29,602 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:09:29] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:09:33,150 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 17:09:33,173 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:09:33,173 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:09:33,190 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 17:09:33,210 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-24 17:09:33,211 [INFO] werkzeug: Press CTRL+C to quit
2025-11-24 17:09:33,211 [INFO] werkzeug: * Restarting with watchdog (windowsapi)
2025-11-24 17:09:33,914 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 17:09:33,934 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:09:33,934 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:09:33,949 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 17:09:33,964 [WARNING] werkzeug: * Debugger is active!
2025-11-24 17:09:33,966 [INFO] werkzeug: * Debugger PIN: 778-054-746
2025-11-24 17:09:34,397 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:09:34] "GET /login HTTP/1.1" 200 -
2025-11-24 17:09:34,435 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:09:34] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:09:34,444 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:09:34] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-24 17:09:40,004 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 17:09:40,004 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 17:09:40,052 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 17:09:40,052 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 17:09:40,053 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:09:40,053 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:09:40,053 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 17:09:40,053 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 17:09:40,054 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:09:40] "POST /login HTTP/1.1" 302 -
2025-11-24 17:09:40,071 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:09:40] "GET /index HTTP/1.1" 200 -
2025-11-24 17:09:40,090 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:09:40] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:09:40,090 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:09:40] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 17:09:41,447 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:11:04,806 [INFO] werkzeug: * Detected change in 'D:\\Code\\iDRAC_Info\\idrac_info\\backend\\routes\\auth.py', reloading
2025-11-24 17:11:04,806 [INFO] werkzeug: * Detected change in 'D:\\Code\\iDRAC_Info\\idrac_info\\backend\\routes\\auth.py', reloading
2025-11-24 17:11:04,807 [INFO] werkzeug: * Detected change in 'D:\\Code\\iDRAC_Info\\idrac_info\\backend\\routes\\auth.py', reloading
2025-11-24 17:11:05,574 [INFO] werkzeug: * Restarting with watchdog (windowsapi)
2025-11-24 17:17:15,522 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 17:17:15,543 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:17:15,543 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:17:15,560 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 17:17:15,582 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-24 17:17:15,582 [INFO] werkzeug: Press CTRL+C to quit
2025-11-24 17:17:15,583 [INFO] werkzeug: * Restarting with watchdog (windowsapi)
2025-11-24 17:17:16,314 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 17:17:16,336 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:17:16,336 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:17:16,353 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 17:17:16,367 [WARNING] werkzeug: * Debugger is active!
2025-11-24 17:17:16,369 [INFO] werkzeug: * Debugger PIN: 778-054-746
2025-11-24 17:17:16,953 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:16,953 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:16,973 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:16] "GET /index HTTP/1.1" 200 -
2025-11-24 17:17:17,034 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:17] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:17:17,035 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:17] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 17:17:17,095 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:17] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-24 17:17:18,155 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:18,155 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:18,158 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:18] "GET /home/ HTTP/1.1" 200 -
2025-11-24 17:17:18,176 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:18] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:17:18,440 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:17:19,064 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:19,064 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:19,064 [INFO] app: LOGOUT: user=김강희
2025-11-24 17:17:19,064 [INFO] app: LOGOUT: user=김강희
2025-11-24 17:17:19,064 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:19,064 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:19,065 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:19] "GET /logout HTTP/1.1" 302 -
2025-11-24 17:17:19,070 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:19] "GET /login HTTP/1.1" 200 -
2025-11-24 17:17:19,085 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:19] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:17:19,454 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:17:20,491 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:17:20,517 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:17:25,147 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 17:17:25,167 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:17:25,167 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:17:25,183 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 17:17:25,203 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-24 17:17:25,204 [INFO] werkzeug: Press CTRL+C to quit
2025-11-24 17:17:25,204 [INFO] werkzeug: * Restarting with watchdog (windowsapi)
2025-11-24 17:17:25,903 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 17:17:25,924 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:17:25,924 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:17:25,940 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 17:17:25,953 [WARNING] werkzeug: * Debugger is active!
2025-11-24 17:17:25,955 [INFO] werkzeug: * Debugger PIN: 778-054-746
2025-11-24 17:17:36,901 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 17:17:36,901 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 17:17:36,954 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 17:17:36,954 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 17:17:36,954 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:36,954 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:36,956 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 17:17:36,956 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 17:17:36,957 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:36] "POST /login HTTP/1.1" 302 -
2025-11-24 17:17:36,961 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:36,961 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:36,982 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:36] "GET /index HTTP/1.1" 200 -
2025-11-24 17:17:37,034 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:37] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:17:37,035 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:37] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 17:17:38,456 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:17:38,490 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:17:48,010 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:48,010 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:48,013 [INFO] root: [AJAX] 작업 시작: 1763972268.0111287, script: 01-settings.py
2025-11-24 17:17:48,014 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:48] "POST /process_ips HTTP/1.1" 200 -
2025-11-24 17:17:48,025 [INFO] root: 🔧 실행 명령: D:\Code\imges_ocr\venv312\Scripts\python.exe D:\Code\iDRAC_Info\idrac_info\data\scripts\01-settings.py D:\Code\iDRAC_Info\idrac_info\data\temp_ip\ip_0.txt
2025-11-24 17:17:48,182 [INFO] root: [10.10.0.1] ✅ stdout:
오류 발생: IP 10.10.0.1 에 대한 SVC Tag 가져오기 실패
정보 수집 완료.
수집 완료 시간: 0 시간, 0 분, 0 초.
2025-11-24 17:17:48,519 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:48,519 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:48,519 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:48] "GET /progress_status/1763972268.0111287 HTTP/1.1" 200 -
2025-11-24 17:17:49,327 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:17:49,853 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:17:50,032 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:50,032 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:50,035 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:50] "GET /index HTTP/1.1" 200 -
2025-11-24 17:17:50,051 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:50] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:17:50,054 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:50] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 17:17:50,077 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:50] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-24 17:17:51,340 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:17:59,126 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:59,126 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:17:59,132 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:59] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 17:17:59,146 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:17:59] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:18:00,432 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:00,582 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:00,582 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:00,587 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:00] "GET /jobs HTTP/1.1" 200 -
2025-11-24 17:18:00,603 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:00] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:18:00,614 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:00,614 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:00,614 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:00] "GET /jobs/config HTTP/1.1" 200 -
2025-11-24 17:18:00,634 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:00,634 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:00,634 [WARNING] backend.services.idrac_jobs: IP list file not found: data/server_list/idrac_ip_list.txt
2025-11-24 17:18:00,634 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:00] "GET /jobs/iplist HTTP/1.1" 200 -
2025-11-24 17:18:02,070 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:02,106 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:02,162 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:03,986 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:03,986 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:03,989 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:03] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 17:18:04,001 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:04] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:18:04,638 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:04,638 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:04,647 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:04] "GET /index HTTP/1.1" 200 -
2025-11-24 17:18:04,661 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:04] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:18:04,663 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:04] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 17:18:05,231 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:05,231 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:05,234 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:05] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 17:18:05,248 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:05] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:18:05,301 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:05,614 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:05,614 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:05,615 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:05] "GET /jobs HTTP/1.1" 200 -
2025-11-24 17:18:05,630 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:05] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:18:05,642 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:05,642 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:05,644 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:05] "GET /jobs/config HTTP/1.1" 200 -
2025-11-24 17:18:05,655 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:05,655 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:05,656 [WARNING] backend.services.idrac_jobs: IP list file not found: data/server_list/idrac_ip_list.txt
2025-11-24 17:18:05,657 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:05] "GET /jobs/iplist HTTP/1.1" 200 -
2025-11-24 17:18:05,963 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:06,138 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:06,138 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:06,146 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:06] "GET /admin HTTP/1.1" 200 -
2025-11-24 17:18:06,159 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:06] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:18:06,547 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:07,125 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:07,155 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:07,180 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:07,474 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:09,491 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:09,491 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:09,495 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:09] "GET /index HTTP/1.1" 200 -
2025-11-24 17:18:09,508 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:09] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:18:09,512 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:09] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 17:18:11,282 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:17,413 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:17,413 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:17,414 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:17] "GET /moniter HTTP/1.1" 404 -
2025-11-24 17:18:17,452 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:17] "GET /favicon.ico HTTP/1.1" 404 -
2025-11-24 17:18:18,720 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:24,648 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:24,648 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:24,652 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:24] "GET /home/ HTTP/1.1" 200 -
2025-11-24 17:18:24,664 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:24] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:18:25,978 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:26,133 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:26,133 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:26,137 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:26] "GET /admin HTTP/1.1" 200 -
2025-11-24 17:18:26,149 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:26] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:18:26,463 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:26,463 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:26,464 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:26] "GET /jobs HTTP/1.1" 200 -
2025-11-24 17:18:26,479 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:26] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:18:26,490 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:26,490 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:26,491 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:26] "GET /jobs/config HTTP/1.1" 200 -
2025-11-24 17:18:26,502 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:26,502 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:26,502 [WARNING] backend.services.idrac_jobs: IP list file not found: data/server_list/idrac_ip_list.txt
2025-11-24 17:18:26,503 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:26] "GET /jobs/iplist HTTP/1.1" 200 -
2025-11-24 17:18:27,440 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:27,941 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:27,971 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:28,010 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:29,254 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:29,254 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:29,257 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:29] "GET /xml_management HTTP/1.1" 200 -
2025-11-24 17:18:29,270 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:29] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:18:30,162 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:30,162 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:30,164 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:30] "GET /home/ HTTP/1.1" 200 -
2025-11-24 17:18:30,180 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:30] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:18:30,530 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:31,470 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:18:58,945 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:58,945 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:58,946 [INFO] app: LOGOUT: user=김강희
2025-11-24 17:18:58,946 [INFO] app: LOGOUT: user=김강희
2025-11-24 17:18:58,946 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:58,946 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:18:58,947 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:58] "GET /logout HTTP/1.1" 302 -
2025-11-24 17:18:58,953 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:58] "GET /login HTTP/1.1" 200 -
2025-11-24 17:18:58,965 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:18:58] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:19:02,424 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 17:19:02,444 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:19:02,444 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:19:02,459 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 17:19:02,482 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-24 17:19:02,482 [INFO] werkzeug: Press CTRL+C to quit
2025-11-24 17:19:02,482 [INFO] werkzeug: * Restarting with watchdog (windowsapi)
2025-11-24 17:19:03,191 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 17:19:03,210 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:19:03,210 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:19:03,226 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 17:19:03,240 [WARNING] werkzeug: * Debugger is active!
2025-11-24 17:19:03,242 [INFO] werkzeug: * Debugger PIN: 778-054-746
2025-11-24 17:36:28,877 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 17:36:28,899 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:36:28,899 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:36:28,919 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 17:36:28,943 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-24 17:36:28,943 [INFO] werkzeug: Press CTRL+C to quit
2025-11-24 17:36:28,943 [INFO] werkzeug: * Restarting with watchdog (windowsapi)
2025-11-24 17:36:29,684 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 17:36:29,704 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:36:29,704 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 17:36:29,722 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 17:36:29,736 [WARNING] werkzeug: * Debugger is active!
2025-11-24 17:36:29,739 [INFO] werkzeug: * Debugger PIN: 778-054-746
2025-11-24 17:36:31,166 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:36:31] "GET /login HTTP/1.1" 200 -
2025-11-24 17:36:31,209 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:36:31] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:36:31,230 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:36:31] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-24 17:36:37,567 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 17:36:37,567 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-24 17:36:37,621 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 17:36:37,621 [INFO] app: LOGIN: found id=1 active=True pass_ok=True
2025-11-24 17:36:37,621 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:36:37,621 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:36:37,621 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 17:36:37,621 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-24 17:36:37,623 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:36:37] "POST /login HTTP/1.1" 302 -
2025-11-24 17:36:37,626 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:36:37,626 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 17:36:37,643 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:36:37] "GET /index HTTP/1.1" 200 -
2025-11-24 17:36:37,662 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:36:37] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 17:36:37,665 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 17:36:37] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 17:36:39,139 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 17:36:39,167 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 18:05:50,478 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 18:05:50,499 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 18:05:50,499 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 18:05:50,515 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 18:05:50,535 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-24 18:05:50,535 [INFO] werkzeug: Press CTRL+C to quit
2025-11-24 18:05:50,536 [INFO] werkzeug: * Restarting with watchdog (windowsapi)
2025-11-24 18:05:51,256 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-24 18:05:51,276 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 18:05:51,276 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-24 18:05:51,291 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-24 18:05:51,305 [WARNING] werkzeug: * Debugger is active!
2025-11-24 18:05:51,308 [INFO] werkzeug: * Debugger PIN: 778-054-746
2025-11-24 18:05:51,329 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 18:05:51,329 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 18:05:51,337 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 18:05:51] "GET /home/ HTTP/1.1" 200 -
2025-11-24 18:05:51,377 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 18:05:51] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 18:05:51,409 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 18:05:51,409 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 18:05:51,411 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 18:05:51] "GET /home/ HTTP/1.1" 200 -
2025-11-24 18:05:51,427 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 18:05:51] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 18:05:51,446 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 18:05:51] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-24 18:05:52,762 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 18:05:52,797 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 18:05:52,797 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 18:05:52,801 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 18:05:52,816 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 18:05:52] "GET /index HTTP/1.1" 200 -
2025-11-24 18:05:52,832 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 18:05:52] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 18:05:52,833 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 18:05:52] "GET /static/script.js HTTP/1.1" 304 -
2025-11-24 18:05:54,028 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 18:05:54,426 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 18:05:54,426 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 18:05:54,427 [INFO] app: LOGOUT: user=김강희
2025-11-24 18:05:54,427 [INFO] app: LOGOUT: user=김강희
2025-11-24 18:05:54,427 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 18:05:54,427 [INFO] app: Telegram Debug: Token=OK, ChatID=OK, Lib=OK
2025-11-24 18:05:54,429 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 18:05:54] "GET /logout HTTP/1.1" 302 -
2025-11-24 18:05:54,434 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 18:05:54] "GET /login HTTP/1.1" 200 -
2025-11-24 18:05:54,451 [INFO] werkzeug: 127.0.0.1 - - [24/Nov/2025 18:05:54] "GET /static/style.css HTTP/1.1" 304 -
2025-11-24 18:05:55,766 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-24 18:05:55,801 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"

8939
data/logs/2025-11-26.log Normal file

File diff suppressed because it is too large Load Diff

557
data/logs/2025-11-27.log Normal file
View File

@@ -0,0 +1,557 @@
2025-11-27 18:53:56,188 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-27 18:53:56,211 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 18:53:56,211 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 18:53:56,236 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-27 18:53:56,270 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-27 18:53:56,270 [INFO] werkzeug: Press CTRL+C to quit
2025-11-27 18:54:04,239 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:04] "GET / HTTP/1.1" 302 -
2025-11-27 18:54:04,265 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:04] "GET /login?next=/ HTTP/1.1" 200 -
2025-11-27 18:54:04,330 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:04] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 18:54:16,632 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-27 18:54:16,632 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-27 18:54:16,702 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-27 18:54:16,702 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-27 18:54:16,704 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-27 18:54:16,704 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-27 18:54:16,704 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:16] "POST /login HTTP/1.1" 302 -
2025-11-27 18:54:16,720 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:16] "GET /index HTTP/1.1" 200 -
2025-11-27 18:54:16,741 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:16] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 18:54:16,753 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:16] "GET /static/script.js HTTP/1.1" 304 -
2025-11-27 18:54:18,104 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-27 18:54:24,104 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:24] "GET /admin HTTP/1.1" 200 -
2025-11-27 18:54:24,119 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:24] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 18:54:25,237 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:25] "GET /admin/settings HTTP/1.1" 200 -
2025-11-27 18:54:25,251 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:25] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 18:54:37,033 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:37] "GET /home/ HTTP/1.1" 200 -
2025-11-27 18:54:37,047 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:37] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 18:54:38,345 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:38] "GET /admin HTTP/1.1" 200 -
2025-11-27 18:54:38,357 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:38] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 18:54:45,869 [INFO] app: LOGOUT: user=김강희
2025-11-27 18:54:45,869 [INFO] app: LOGOUT: user=김강희
2025-11-27 18:54:45,872 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:45] "GET /logout HTTP/1.1" 302 -
2025-11-27 18:54:45,877 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:45] "GET /login HTTP/1.1" 200 -
2025-11-27 18:54:45,899 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 18:54:45] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 18:54:47,218 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-27 19:49:49,532 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-27 19:49:49,554 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 19:49:49,554 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 19:49:49,571 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-27 19:49:49,593 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-27 19:49:49,593 [INFO] werkzeug: Press CTRL+C to quit
2025-11-27 19:49:52,485 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:49:52] "GET / HTTP/1.1" 302 -
2025-11-27 19:49:52,496 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:49:52] "GET /login?next=/ HTTP/1.1" 200 -
2025-11-27 19:49:52,544 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:49:52] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 19:50:00,494 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-27 19:50:00,494 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-27 19:50:00,549 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-27 19:50:00,549 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-27 19:50:00,550 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-27 19:50:00,550 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-27 19:50:00,551 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:00] "POST /login HTTP/1.1" 302 -
2025-11-27 19:50:00,571 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:00] "GET /index HTTP/1.1" 200 -
2025-11-27 19:50:00,590 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:00] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 19:50:00,591 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:00] "GET /static/script.js HTTP/1.1" 304 -
2025-11-27 19:50:01,888 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-27 19:50:03,009 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:03] "GET /admin HTTP/1.1" 200 -
2025-11-27 19:50:03,022 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:03] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 19:50:04,715 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:04] "GET /admin/settings HTTP/1.1" 200 -
2025-11-27 19:50:04,727 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:04] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 19:50:06,249 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:06] "GET /jobs HTTP/1.1" 200 -
2025-11-27 19:50:06,263 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:06] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 19:50:06,276 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:06] "GET /jobs/config HTTP/1.1" 200 -
2025-11-27 19:50:06,292 [WARNING] backend.services.idrac_jobs: IP list file not found: data/server_list/idrac_ip_list.txt
2025-11-27 19:50:06,292 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:06] "GET /jobs/iplist HTTP/1.1" 200 -
2025-11-27 19:50:06,939 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:06] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 19:50:06,952 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:06] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 19:50:06,956 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:06] "GET /static/css/scp.css HTTP/1.1" 200 -
2025-11-27 19:50:06,956 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:06] "GET /static/js/scp.js HTTP/1.1" 200 -
2025-11-27 19:50:22,779 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:22] "GET /scp/diff?file1=LinePlus_T1.xml&file2=PO-20250826-0158%20_가산3_XE9680_384EA.xml HTTP/1.1" 200 -
2025-11-27 19:50:22,795 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:22] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 19:50:22,796 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:22] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-27 19:50:37,550 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:37] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 19:50:37,576 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:37] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 19:50:37,577 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:37] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-27 19:50:37,578 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:37] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-27 19:50:46,123 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:46] "GET /edit_xml/LinePlus_T1.xml HTTP/1.1" 200 -
2025-11-27 19:50:46,138 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:46] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 19:50:49,706 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:49] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 19:50:49,723 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:49] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 19:50:49,724 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:49] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-27 19:50:49,724 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:50:49] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-27 19:55:01,647 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:55:01] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 19:55:01,665 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:55:01] "GET /static/style.css HTTP/1.1" 200 -
2025-11-27 19:55:01,669 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:55:01] "GET /static/css/scp.css HTTP/1.1" 200 -
2025-11-27 19:55:01,669 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:55:01] "GET /static/js/scp.js HTTP/1.1" 200 -
2025-11-27 19:55:01,718 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:55:01] "GET /static/favicon.ico HTTP/1.1" 200 -
2025-11-27 19:59:23,834 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:59:23] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 19:59:23,858 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:59:23] "GET /static/style.css HTTP/1.1" 200 -
2025-11-27 19:59:23,859 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:59:23] "GET /static/css/scp.css HTTP/1.1" 200 -
2025-11-27 19:59:23,859 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:59:23] "GET /static/js/scp.js HTTP/1.1" 200 -
2025-11-27 19:59:23,891 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 19:59:23] "GET /static/favicon.ico HTTP/1.1" 200 -
2025-11-27 20:03:11,961 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:03:11] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:03:11,984 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:03:11] "GET /static/style.css HTTP/1.1" 200 -
2025-11-27 20:03:11,986 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:03:11] "GET /static/css/scp.css HTTP/1.1" 200 -
2025-11-27 20:03:11,987 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:03:11] "GET /static/js/scp.js HTTP/1.1" 200 -
2025-11-27 20:03:12,018 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:03:12] "GET /static/favicon.ico HTTP/1.1" 200 -
2025-11-27 20:05:25,592 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:05:25] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:05:25,617 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:05:25] "GET /static/style.css HTTP/1.1" 200 -
2025-11-27 20:05:25,618 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:05:25] "GET /static/css/scp.css HTTP/1.1" 200 -
2025-11-27 20:05:25,618 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:05:25] "GET /static/js/scp.js HTTP/1.1" 200 -
2025-11-27 20:05:25,650 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:05:25] "GET /static/favicon.ico HTTP/1.1" 200 -
2025-11-27 20:06:41,550 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:06:41] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:06:41,572 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:06:41] "GET /static/style.css HTTP/1.1" 200 -
2025-11-27 20:06:41,573 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:06:41] "GET /static/css/scp.css HTTP/1.1" 200 -
2025-11-27 20:06:41,574 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:06:41] "GET /static/js/scp.js HTTP/1.1" 200 -
2025-11-27 20:06:41,660 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:06:41] "GET /static/favicon.ico HTTP/1.1" 200 -
2025-11-27 20:07:01,417 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:07:01] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:07:01,437 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:07:01] "GET /static/css/scp.css HTTP/1.1" 200 -
2025-11-27 20:07:01,437 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:07:01] "GET /static/style.css HTTP/1.1" 200 -
2025-11-27 20:07:01,438 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:07:01] "GET /static/js/scp.js HTTP/1.1" 200 -
2025-11-27 20:07:01,466 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:07:01] "GET /static/favicon.ico HTTP/1.1" 200 -
2025-11-27 20:07:42,817 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:07:42] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:07:42,837 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:07:42] "GET /static/style.css HTTP/1.1" 200 -
2025-11-27 20:07:42,838 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:07:42] "GET /static/css/scp.css HTTP/1.1" 200 -
2025-11-27 20:07:42,838 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:07:42] "GET /static/js/scp.js HTTP/1.1" 200 -
2025-11-27 20:07:42,872 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:07:42] "GET /static/favicon.ico HTTP/1.1" 200 -
2025-11-27 20:07:55,087 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-27 20:07:55,108 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:07:55,108 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:07:55,128 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-27 20:07:55,151 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-27 20:07:55,151 [INFO] werkzeug: Press CTRL+C to quit
2025-11-27 20:07:56,391 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:07:56] "GET /xml_management HTTP/1.1" 404 -
2025-11-27 20:07:56,422 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:07:56] "GET /favicon.ico HTTP/1.1" 404 -
2025-11-27 20:07:57,096 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:07:57] "GET /xml_management HTTP/1.1" 404 -
2025-11-27 20:07:58,679 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:07:58] "GET /xml_management HTTP/1.1" 404 -
2025-11-27 20:07:59,693 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:07:59] "GET /xml_management HTTP/1.1" 404 -
2025-11-27 20:08:03,545 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:03] "GET / HTTP/1.1" 500 -
2025-11-27 20:08:03,594 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:03] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 304 -
2025-11-27 20:08:03,608 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:03] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 304 -
2025-11-27 20:08:03,631 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:03] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=KPmjd5URLBkm9fZIHKLS HTTP/1.1" 200 -
2025-11-27 20:08:03,638 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:03] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 304 -
2025-11-27 20:08:05,565 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:05] "GET / HTTP/1.1" 500 -
2025-11-27 20:08:05,576 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:05] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 304 -
2025-11-27 20:08:05,580 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:05] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 304 -
2025-11-27 20:08:05,584 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:05] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=KPmjd5URLBkm9fZIHKLS HTTP/1.1" 304 -
2025-11-27 20:08:05,591 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:05] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 304 -
2025-11-27 20:08:05,752 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:05] "GET / HTTP/1.1" 500 -
2025-11-27 20:08:05,765 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:05] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 304 -
2025-11-27 20:08:05,768 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:05] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 304 -
2025-11-27 20:08:05,775 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:05] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=KPmjd5URLBkm9fZIHKLS HTTP/1.1" 304 -
2025-11-27 20:08:05,792 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:05] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 304 -
2025-11-27 20:08:06,661 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:06] "GET / HTTP/1.1" 500 -
2025-11-27 20:08:06,674 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:06] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 -
2025-11-27 20:08:06,678 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:06] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 -
2025-11-27 20:08:06,685 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:06] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=KPmjd5URLBkm9fZIHKLS HTTP/1.1" 200 -
2025-11-27 20:08:06,704 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:06] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 -
2025-11-27 20:08:07,063 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:07] "GET / HTTP/1.1" 500 -
2025-11-27 20:08:07,074 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:07] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 -
2025-11-27 20:08:07,075 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:07] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 -
2025-11-27 20:08:07,081 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:07] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=KPmjd5URLBkm9fZIHKLS HTTP/1.1" 200 -
2025-11-27 20:08:07,100 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:07] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 -
2025-11-27 20:08:07,213 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:07] "GET / HTTP/1.1" 500 -
2025-11-27 20:08:07,227 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:07] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 -
2025-11-27 20:08:07,228 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:07] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 -
2025-11-27 20:08:07,246 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:07] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=KPmjd5URLBkm9fZIHKLS HTTP/1.1" 200 -
2025-11-27 20:08:07,255 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:08:07] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 -
2025-11-27 20:08:37,699 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-27 20:08:37,721 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:08:37,721 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:08:37,738 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-27 20:08:37,760 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-27 20:08:37,760 [INFO] werkzeug: Press CTRL+C to quit
2025-11-27 20:09:08,168 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-27 20:09:08,188 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:09:08,188 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:09:08,209 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-27 20:09:08,232 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-27 20:09:08,232 [INFO] werkzeug: Press CTRL+C to quit
2025-11-27 20:09:10,319 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:10] "GET / HTTP/1.1" 500 -
2025-11-27 20:09:10,362 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:10] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 304 -
2025-11-27 20:09:10,365 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:10] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 304 -
2025-11-27 20:09:10,415 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:10] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=toWbFrLNStmsnxQRxd1N HTTP/1.1" 200 -
2025-11-27 20:09:10,421 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:10] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 304 -
2025-11-27 20:09:11,297 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:11] "GET / HTTP/1.1" 500 -
2025-11-27 20:09:11,308 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:11] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 304 -
2025-11-27 20:09:11,311 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:11] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 304 -
2025-11-27 20:09:11,318 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:11] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=toWbFrLNStmsnxQRxd1N HTTP/1.1" 304 -
2025-11-27 20:09:11,337 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:11] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 304 -
2025-11-27 20:09:11,859 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:11] "GET / HTTP/1.1" 500 -
2025-11-27 20:09:11,870 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:11] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 304 -
2025-11-27 20:09:11,874 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:11] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 304 -
2025-11-27 20:09:11,880 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:11] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=toWbFrLNStmsnxQRxd1N HTTP/1.1" 304 -
2025-11-27 20:09:11,895 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:11] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 304 -
2025-11-27 20:09:12,026 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:12] "GET / HTTP/1.1" 500 -
2025-11-27 20:09:12,038 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:12] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 304 -
2025-11-27 20:09:12,042 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:12] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 304 -
2025-11-27 20:09:12,048 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:12] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=toWbFrLNStmsnxQRxd1N HTTP/1.1" 304 -
2025-11-27 20:09:12,065 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:12] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 304 -
2025-11-27 20:09:13,701 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:13] "GET / HTTP/1.1" 500 -
2025-11-27 20:09:13,713 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:13] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 304 -
2025-11-27 20:09:13,713 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:13] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 304 -
2025-11-27 20:09:13,720 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:13] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=toWbFrLNStmsnxQRxd1N HTTP/1.1" 304 -
2025-11-27 20:09:19,987 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:19] "GET / HTTP/1.1" 500 -
2025-11-27 20:09:20,588 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:20] "GET / HTTP/1.1" 500 -
2025-11-27 20:09:20,605 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:20] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 304 -
2025-11-27 20:09:20,607 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:20] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 304 -
2025-11-27 20:09:20,632 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:20] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=toWbFrLNStmsnxQRxd1N HTTP/1.1" 304 -
2025-11-27 20:09:33,685 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-27 20:09:33,707 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:09:33,707 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:09:33,729 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-27 20:09:33,751 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-27 20:09:33,751 [INFO] werkzeug: Press CTRL+C to quit
2025-11-27 20:09:35,916 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:35] "GET / HTTP/1.1" 500 -
2025-11-27 20:09:35,929 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:35] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 -
2025-11-27 20:09:35,932 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:35] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 -
2025-11-27 20:09:35,941 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:35] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=toWbFrLNStmsnxQRxd1N HTTP/1.1" 200 -
2025-11-27 20:09:35,960 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:35] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 -
2025-11-27 20:09:36,395 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET / HTTP/1.1" 500 -
2025-11-27 20:09:36,406 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 -
2025-11-27 20:09:36,407 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 -
2025-11-27 20:09:36,419 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=toWbFrLNStmsnxQRxd1N HTTP/1.1" 200 -
2025-11-27 20:09:36,435 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 -
2025-11-27 20:09:36,568 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET / HTTP/1.1" 500 -
2025-11-27 20:09:36,584 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 -
2025-11-27 20:09:36,584 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 -
2025-11-27 20:09:36,592 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=toWbFrLNStmsnxQRxd1N HTTP/1.1" 200 -
2025-11-27 20:09:36,608 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 -
2025-11-27 20:09:36,721 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET / HTTP/1.1" 500 -
2025-11-27 20:09:36,734 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 -
2025-11-27 20:09:36,736 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 -
2025-11-27 20:09:36,743 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=toWbFrLNStmsnxQRxd1N HTTP/1.1" 200 -
2025-11-27 20:09:36,763 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 -
2025-11-27 20:09:36,863 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET / HTTP/1.1" 500 -
2025-11-27 20:09:36,876 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 -
2025-11-27 20:09:36,877 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 -
2025-11-27 20:09:36,882 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=toWbFrLNStmsnxQRxd1N HTTP/1.1" 200 -
2025-11-27 20:09:36,901 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:36] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 -
2025-11-27 20:09:52,809 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:52] "GET / HTTP/1.1" 302 -
2025-11-27 20:09:52,820 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:52] "GET /login?next=/ HTTP/1.1" 200 -
2025-11-27 20:09:52,862 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:52] "GET /static/style.css HTTP/1.1" 200 -
2025-11-27 20:09:52,966 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:52] "GET /static/favicon.ico HTTP/1.1" 200 -
2025-11-27 20:09:57,841 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-27 20:09:57,841 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-27 20:09:57,895 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-27 20:09:57,895 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-27 20:09:57,897 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-27 20:09:57,897 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-27 20:09:57,897 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:57] "POST /login HTTP/1.1" 302 -
2025-11-27 20:09:57,917 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:57] "GET /index HTTP/1.1" 500 -
2025-11-27 20:09:57,934 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:57] "GET /index?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 -
2025-11-27 20:09:57,938 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:57] "GET /index?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 -
2025-11-27 20:09:57,953 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:57] "GET /index?__debugger__=yes&cmd=resource&f=console.png&s=toWbFrLNStmsnxQRxd1N HTTP/1.1" 200 -
2025-11-27 20:09:57,982 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:09:57] "GET /index?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 -
2025-11-27 20:09:59,216 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-27 20:10:02,357 [INFO] app: LOGIN: already auth → /index
2025-11-27 20:10:02,357 [INFO] app: LOGIN: already auth → /index
2025-11-27 20:10:02,359 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:10:02] "GET /login?next=/ HTTP/1.1" 302 -
2025-11-27 20:10:19,124 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-27 20:10:19,143 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:10:19,143 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:10:19,163 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-27 20:10:19,184 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-27 20:10:19,184 [INFO] werkzeug: Press CTRL+C to quit
2025-11-27 20:10:21,639 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:10:21] "GET / HTTP/1.1" 500 -
2025-11-27 20:10:21,657 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:10:21] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 -
2025-11-27 20:10:21,664 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:10:21] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 -
2025-11-27 20:10:21,670 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:10:21] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=toWbFrLNStmsnxQRxd1N HTTP/1.1" 200 -
2025-11-27 20:10:21,720 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:10:21] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 -
2025-11-27 20:11:34,671 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-27 20:11:34,689 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:11:34,689 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:11:34,708 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-27 20:11:34,731 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-27 20:11:34,731 [INFO] werkzeug: Press CTRL+C to quit
2025-11-27 20:11:42,219 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-27 20:11:42,238 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:11:42,238 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:11:42,254 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-27 20:11:42,278 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-27 20:11:42,278 [INFO] werkzeug: Press CTRL+C to quit
2025-11-27 20:11:44,565 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:11:44] "GET / HTTP/1.1" 200 -
2025-11-27 20:11:44,635 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:11:44] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:11:44,637 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:11:44] "GET /static/script.js HTTP/1.1" 200 -
2025-11-27 20:11:46,595 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:11:46] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:11:46,611 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:11:46] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:11:46,618 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:11:46] "GET /static/css/scp.css HTTP/1.1" 200 -
2025-11-27 20:11:46,619 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:11:46] "GET /static/js/scp.js HTTP/1.1" 200 -
2025-11-27 20:11:54,440 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:11:54] "GET /edit_xml/LinePlus_T1.xml HTTP/1.1" 200 -
2025-11-27 20:11:54,458 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:11:54] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:11:56,650 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:11:56] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:11:56,668 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:11:56] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:11:56,671 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:11:56] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-27 20:11:56,672 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:11:56] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-27 20:12:12,744 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:12:12] "GET /home/ HTTP/1.1" 200 -
2025-11-27 20:12:12,762 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:12:12] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:12:14,222 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:12:14] "GET /index HTTP/1.1" 200 -
2025-11-27 20:12:14,239 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:12:14] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:12:14,244 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:12:14] "GET /static/script.js HTTP/1.1" 304 -
2025-11-27 20:17:16,167 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:16] "GET / HTTP/1.1" 200 -
2025-11-27 20:17:16,176 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:16] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:17:16,187 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:16] "GET /static/script.js HTTP/1.1" 304 -
2025-11-27 20:17:16,646 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:16] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-27 20:17:18,728 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:18] "GET /admin HTTP/1.1" 200 -
2025-11-27 20:17:18,744 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:18] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:17:20,726 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:20] "GET /home/ HTTP/1.1" 200 -
2025-11-27 20:17:20,739 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:20] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:17:21,701 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:21] "GET /index HTTP/1.1" 200 -
2025-11-27 20:17:21,714 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:21] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:17:21,718 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:21] "GET /static/script.js HTTP/1.1" 304 -
2025-11-27 20:17:29,307 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:29] "GET /index HTTP/1.1" 200 -
2025-11-27 20:17:29,322 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:29] "GET /static/script.js HTTP/1.1" 304 -
2025-11-27 20:17:29,322 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:29] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:17:29,884 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:29] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:17:29,897 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:29] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:17:29,900 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:29] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-27 20:17:29,901 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:17:29] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-27 20:20:30,931 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:30] "GET / HTTP/1.1" 200 -
2025-11-27 20:20:30,941 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:30] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:20:30,952 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:30] "GET /static/script.js HTTP/1.1" 304 -
2025-11-27 20:20:33,122 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:33] "GET /admin HTTP/1.1" 200 -
2025-11-27 20:20:33,138 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:33] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:20:34,023 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:34] "GET /jobs HTTP/1.1" 200 -
2025-11-27 20:20:34,035 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:34] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:20:34,063 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:34] "GET /jobs/config HTTP/1.1" 200 -
2025-11-27 20:20:34,068 [WARNING] backend.services.idrac_jobs: IP list file not found: data/server_list/idrac_ip_list.txt
2025-11-27 20:20:34,069 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:34] "GET /jobs/iplist HTTP/1.1" 200 -
2025-11-27 20:20:35,634 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:35] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:20:35,646 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:35] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:20:35,651 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:35] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-27 20:20:35,651 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:35] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-27 20:20:39,035 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:39] "GET /scp/diff?file1=LinePlus_T1.xml&file2=PO-20250826-0158%20_가산3_XE9680_384EA.xml HTTP/1.1" 200 -
2025-11-27 20:20:39,051 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:39] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:20:39,052 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:39] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-27 20:20:52,531 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:52] "GET /home/ HTTP/1.1" 200 -
2025-11-27 20:20:52,546 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:20:52] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:21:32,928 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:32] "GET /jobs HTTP/1.1" 200 -
2025-11-27 20:21:32,945 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:32] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:21:32,987 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:32] "GET /jobs/config HTTP/1.1" 200 -
2025-11-27 20:21:32,993 [WARNING] backend.services.idrac_jobs: IP list file not found: data/server_list/idrac_ip_list.txt
2025-11-27 20:21:32,993 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:32] "GET /jobs/iplist HTTP/1.1" 200 -
2025-11-27 20:21:34,137 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:34] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:21:34,156 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:34] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:21:34,164 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:34] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-27 20:21:34,164 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:34] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-27 20:21:34,600 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:34] "GET /home/ HTTP/1.1" 200 -
2025-11-27 20:21:34,617 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:34] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:21:35,135 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:35] "GET /index HTTP/1.1" 200 -
2025-11-27 20:21:35,151 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:35] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:21:35,154 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:35] "GET /static/script.js HTTP/1.1" 304 -
2025-11-27 20:21:35,599 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:35] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:21:35,614 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:35] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:21:35,622 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:35] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-27 20:21:35,622 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:35] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-27 20:21:36,236 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:36] "GET /jobs HTTP/1.1" 200 -
2025-11-27 20:21:36,251 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:36] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:21:36,265 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:36] "GET /jobs/config HTTP/1.1" 200 -
2025-11-27 20:21:36,279 [WARNING] backend.services.idrac_jobs: IP list file not found: data/server_list/idrac_ip_list.txt
2025-11-27 20:21:36,279 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:36] "GET /jobs/iplist HTTP/1.1" 200 -
2025-11-27 20:21:36,700 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:36] "GET /admin HTTP/1.1" 200 -
2025-11-27 20:21:36,715 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:36] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:21:38,082 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:38] "GET /jobs HTTP/1.1" 200 -
2025-11-27 20:21:38,101 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:38] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:21:38,109 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:38] "GET /jobs/config HTTP/1.1" 200 -
2025-11-27 20:21:38,127 [WARNING] backend.services.idrac_jobs: IP list file not found: data/server_list/idrac_ip_list.txt
2025-11-27 20:21:38,128 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:38] "GET /jobs/iplist HTTP/1.1" 200 -
2025-11-27 20:21:38,372 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:38] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:21:38,388 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:38] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:21:38,395 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:38] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-27 20:21:38,395 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:38] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-27 20:21:38,719 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:38] "GET /index HTTP/1.1" 200 -
2025-11-27 20:21:38,735 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:38] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:21:38,738 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:38] "GET /static/script.js HTTP/1.1" 304 -
2025-11-27 20:21:39,219 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:39] "GET /home/ HTTP/1.1" 200 -
2025-11-27 20:21:39,237 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:39] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:21:39,902 [INFO] app: LOGOUT: user=김강희
2025-11-27 20:21:39,902 [INFO] app: LOGOUT: user=김강희
2025-11-27 20:21:39,904 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:39] "GET /logout HTTP/1.1" 302 -
2025-11-27 20:21:39,910 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:39] "GET /login HTTP/1.1" 200 -
2025-11-27 20:21:39,926 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:39] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:21:41,234 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-27 20:21:42,246 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:42] "GET /register HTTP/1.1" 200 -
2025-11-27 20:21:42,262 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:21:42] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:22:04,167 [INFO] app: REGISTER: created id=3 email=rnfjrl@test.com token=jvoddlCPFQ
2025-11-27 20:22:04,167 [INFO] app: REGISTER: created id=3 email=rnfjrl@test.com token=jvoddlCPFQ
2025-11-27 20:22:04,168 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:22:04] "POST /register HTTP/1.1" 302 -
2025-11-27 20:22:04,172 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:22:04] "GET /login HTTP/1.1" 200 -
2025-11-27 20:22:04,190 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:22:04] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:22:05,431 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-27 20:25:48,870 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-27 20:25:48,891 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:25:48,891 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:25:48,908 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-27 20:25:48,931 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-27 20:25:48,931 [INFO] werkzeug: Press CTRL+C to quit
2025-11-27 20:25:53,316 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:25:53] "GET /login HTTP/1.1" 200 -
2025-11-27 20:25:53,377 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:25:53] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:25:53,444 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:25:53] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-27 20:25:53,880 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:25:53] "GET /login HTTP/1.1" 200 -
2025-11-27 20:25:53,893 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:25:53] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:25:53,913 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:25:53] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-27 20:28:38,443 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-27 20:28:38,463 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:28:38,463 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-27 20:28:38,481 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-27 20:28:38,494 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-27 20:28:38,494 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-27 20:28:38,494 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-27 20:28:38,494 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-27 20:28:38,525 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-27 20:28:38,525 [INFO] werkzeug: Press CTRL+C to quit
2025-11-27 20:28:39,629 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getMe "HTTP/1.1 200 OK"
2025-11-27 20:28:39,861 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/deleteWebhook "HTTP/1.1 200 OK"
2025-11-27 20:28:39,861 [INFO] telegram.ext.Application: Application started
2025-11-27 20:28:43,484 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:28:43] "GET /login HTTP/1.1" 200 -
2025-11-27 20:28:43,527 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:28:43] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:28:43,542 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:28:43] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-27 20:28:50,542 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:28:50,849 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:28:51,707 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/answerCallbackQuery "HTTP/1.1 200 OK"
2025-11-27 20:28:51,707 [INFO] telegram_bot_service: Received callback: approve_jvoddlCPFQRfhbW_yZUPj3lzoaX9ByxZq6qJcrPtlHU
2025-11-27 20:28:52,115 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/editMessageText "HTTP/1.1 200 OK"
2025-11-27 20:28:52,115 [INFO] telegram_bot_service: User 꾸러기 approved
2025-11-27 20:29:01,076 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:29:11,305 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:29:21,550 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:29:31,778 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:29:42,004 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:29:46,006 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:29:46] "GET /login HTTP/1.1" 200 -
2025-11-27 20:29:46,027 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:29:46] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:29:46,051 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:29:46] "GET /static/favicon.ico HTTP/1.1" 304 -
2025-11-27 20:29:51,337 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-27 20:29:51,337 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-27 20:29:51,385 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-27 20:29:51,385 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-27 20:29:51,385 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-27 20:29:51,385 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-27 20:29:51,387 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:29:51] "POST /login HTTP/1.1" 302 -
2025-11-27 20:29:51,403 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:29:51] "GET /index HTTP/1.1" 200 -
2025-11-27 20:29:51,424 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:29:51] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:29:51,426 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:29:51] "GET /static/script.js HTTP/1.1" 304 -
2025-11-27 20:29:52,238 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:29:52,602 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-27 20:29:52,831 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:29:52] "GET /admin HTTP/1.1" 200 -
2025-11-27 20:29:52,848 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:29:52] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:29:56,303 [INFO] root: 🗑 삭제된 사용자: 꾸러기 (id=3)
2025-11-27 20:29:56,304 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:29:56] "GET /admin/delete/3 HTTP/1.1" 302 -
2025-11-27 20:29:56,311 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:29:56] "GET /admin HTTP/1.1" 200 -
2025-11-27 20:29:56,327 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:29:56] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:29:59,804 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:29:59] "GET /home/ HTTP/1.1" 200 -
2025-11-27 20:29:59,821 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:29:59] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:30:02,465 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:30:12,699 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:30:22,951 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:30:33,178 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:30:40,127 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:30:40] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:30:40,150 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:30:40] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:30:40,157 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:30:40] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-27 20:30:40,158 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:30:40] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-27 20:30:43,407 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:30:53,647 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:31:03,882 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:31:14,109 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:31:24,340 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:31:34,589 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:31:44,816 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:31:55,041 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:32:05,272 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:32:15,514 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:32:25,741 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:32:35,967 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:32:46,197 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:32:56,422 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:32:58,550 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:32:58] "GET /home/ HTTP/1.1" 200 -
2025-11-27 20:32:58,570 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:32:58] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:33:06,650 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:33:16,876 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:33:27,104 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:33:29,679 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:29] "GET /home/ HTTP/1.1" 200 -
2025-11-27 20:33:29,699 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:29] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:33:37,329 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:33:47,556 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:33:48,168 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:48] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:33:48,184 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:48] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:33:48,192 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:48] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-27 20:33:48,192 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:48] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-27 20:33:49,273 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:49] "GET /index HTTP/1.1" 200 -
2025-11-27 20:33:49,291 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:49] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:33:49,295 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:49] "GET /static/script.js HTTP/1.1" 304 -
2025-11-27 20:33:50,150 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:50] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:33:50,167 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:50] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:33:50,172 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:50] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-27 20:33:50,172 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:50] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-27 20:33:50,728 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:50] "GET /jobs HTTP/1.1" 200 -
2025-11-27 20:33:50,747 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:50] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:33:50,761 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:50] "GET /jobs/config HTTP/1.1" 200 -
2025-11-27 20:33:50,778 [WARNING] backend.services.idrac_jobs: IP list file not found: data/server_list/idrac_ip_list.txt
2025-11-27 20:33:50,779 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:50] "GET /jobs/iplist HTTP/1.1" 200 -
2025-11-27 20:33:51,185 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:51] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:33:51,205 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:51] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:33:51,210 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:51] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-27 20:33:51,211 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:51] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-27 20:33:52,127 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:52] "GET /xml_management HTTP/1.1" 200 -
2025-11-27 20:33:52,144 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:52] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:33:52,148 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:52] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-27 20:33:52,149 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:52] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-27 20:33:52,556 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:52] "GET /index HTTP/1.1" 200 -
2025-11-27 20:33:52,572 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:52] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:33:52,575 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:52] "GET /static/script.js HTTP/1.1" 304 -
2025-11-27 20:33:54,040 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:54] "GET /index HTTP/1.1" 200 -
2025-11-27 20:33:54,055 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:54] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:33:54,059 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:33:54] "GET /static/script.js HTTP/1.1" 304 -
2025-11-27 20:33:57,792 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:34:05,559 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:05] "GET /idrac HTTP/1.1" 308 -
2025-11-27 20:34:05,566 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:05] "GET /idrac/ HTTP/1.1" 200 -
2025-11-27 20:34:05,585 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:05] "GET /static/css/idrac_style.css HTTP/1.1" 200 -
2025-11-27 20:34:05,588 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:05] "GET /static/js/idrac_main.js HTTP/1.1" 200 -
2025-11-27 20:34:05,654 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:05] "GET /socket.io/?EIO=4&transport=polling&t=Ph55ufK HTTP/1.1" 200 -
2025-11-27 20:34:05,661 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:05] "GET /idrac/api/servers HTTP/1.1" 200 -
2025-11-27 20:34:05,662 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:05] "GET /idrac/api/groups HTTP/1.1" 200 -
2025-11-27 20:34:05,663 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:05] "POST /socket.io/?EIO=4&transport=polling&t=Ph55ufT&sid=Kd7MSTztfydrG7iVAAAA HTTP/1.1" 200 -
2025-11-27 20:34:05,665 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:05] "GET /favicon.ico HTTP/1.1" 404 -
2025-11-27 20:34:05,666 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:05] "GET /socket.io/?EIO=4&transport=polling&t=Ph55ufT.0&sid=Kd7MSTztfydrG7iVAAAA HTTP/1.1" 200 -
2025-11-27 20:34:08,020 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:34:18,248 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:34:24,288 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:24] "GET /socket.io/?EIO=4&transport=websocket&sid=Kd7MSTztfydrG7iVAAAA HTTP/1.1" 200 -
2025-11-27 20:34:26,800 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:26] "GET /jobs HTTP/1.1" 200 -
2025-11-27 20:34:26,818 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:26] "GET /static/style.css HTTP/1.1" 304 -
2025-11-27 20:34:26,835 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:26] "GET /jobs/config HTTP/1.1" 200 -
2025-11-27 20:34:26,849 [WARNING] backend.services.idrac_jobs: IP list file not found: data/server_list/idrac_ip_list.txt
2025-11-27 20:34:26,849 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:26] "GET /jobs/iplist HTTP/1.1" 200 -
2025-11-27 20:34:28,478 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-27 20:34:31,570 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:31] "GET /admin HTTP/1.1" 200 -
2025-11-27 20:34:31,586 [INFO] werkzeug: 127.0.0.1 - - [27/Nov/2025 20:34:31] "GET /static/style.css HTTP/1.1" 304 -

95
data/logs/app.log Normal file
View File

@@ -0,0 +1,95 @@
2025-11-28 15:15:26,731 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-28 15:15:26,756 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-28 15:15:26,756 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-28 15:15:26,778 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-28 15:15:26,792 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-28 15:15:26,792 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-28 15:15:26,793 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-28 15:15:26,793 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-28 15:15:26,835 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-28 15:15:26,835 [INFO] werkzeug: Press CTRL+C to quit
2025-11-28 15:15:27,893 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getMe "HTTP/1.1 200 OK"
2025-11-28 15:15:28,121 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/deleteWebhook "HTTP/1.1 200 OK"
2025-11-28 15:15:28,122 [INFO] telegram.ext.Application: Application started
2025-11-28 15:15:38,822 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-28 15:15:49,057 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-28 15:15:59,291 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-28 15:16:09,530 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-28 15:16:19,765 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-28 15:16:30,000 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-28 15:16:40,234 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-28 15:21:41,955 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-28 15:21:41,978 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-28 15:21:41,978 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-28 15:21:41,997 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-28 15:21:42,011 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-28 15:21:42,011 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-28 15:21:42,012 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-28 15:21:42,012 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-28 15:21:42,028 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-28 15:21:42,028 [INFO] werkzeug: Press CTRL+C to quit
2025-11-28 15:21:43,126 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getMe "HTTP/1.1 200 OK"
2025-11-28 15:21:43,358 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/deleteWebhook "HTTP/1.1 200 OK"
2025-11-28 15:21:43,360 [INFO] telegram.ext.Application: Application started
2025-11-28 15:21:49,495 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:21:49] "GET / HTTP/1.1" 302 -
2025-11-28 15:21:49,520 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:21:49] "GET /login?next=/ HTTP/1.1" 200 -
2025-11-28 15:21:49,602 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:21:49] "GET /static/style.css HTTP/1.1" 304 -
2025-11-28 15:21:54,039 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-28 15:21:58,823 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-28 15:21:58,823 [INFO] app: LOGIN: form ok email=ganghee@zespro.co.kr
2025-11-28 15:21:58,900 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-28 15:21:58,900 [INFO] app: LOGIN: found id=1 active=True approved=True pass_ok=True
2025-11-28 15:21:58,901 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-28 15:21:58,901 [INFO] app: LOGIN: SUCCESS → redirect
2025-11-28 15:21:58,901 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:21:58] "POST /login HTTP/1.1" 302 -
2025-11-28 15:21:58,918 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:21:58] "GET /index HTTP/1.1" 200 -
2025-11-28 15:21:58,935 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:21:58] "GET /static/style.css HTTP/1.1" 304 -
2025-11-28 15:21:58,946 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:21:58] "GET /static/script.js HTTP/1.1" 304 -
2025-11-28 15:22:00,150 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/sendMessage "HTTP/1.1 200 OK"
2025-11-28 15:22:01,767 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:22:01] "GET /admin HTTP/1.1" 200 -
2025-11-28 15:22:01,779 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:22:01] "GET /static/style.css HTTP/1.1" 304 -
2025-11-28 15:22:02,852 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:22:02] "GET /jobs HTTP/1.1" 200 -
2025-11-28 15:22:02,865 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:22:02] "GET /static/style.css HTTP/1.1" 304 -
2025-11-28 15:22:02,893 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:22:02] "GET /jobs/config HTTP/1.1" 200 -
2025-11-28 15:22:02,900 [WARNING] backend.services.idrac_jobs: IP list file not found: data/server_list/idrac_ip_list.txt
2025-11-28 15:22:02,900 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:22:02] "GET /jobs/iplist HTTP/1.1" 200 -
2025-11-28 15:22:04,264 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-28 15:22:06,057 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:22:06] "GET /xml_management HTTP/1.1" 200 -
2025-11-28 15:22:06,070 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:22:06] "GET /static/style.css HTTP/1.1" 304 -
2025-11-28 15:22:06,076 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:22:06] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-28 15:22:06,076 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:22:06] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-28 15:22:14,492 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-28 15:22:24,718 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-28 15:22:25,589 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:22:25] "GET /edit_xml/LinePlus_T1.xml HTTP/1.1" 200 -
2025-11-28 15:22:25,607 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:22:25] "GET /static/style.css HTTP/1.1" 304 -
2025-11-28 15:22:29,679 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:22:29] "GET /xml_management HTTP/1.1" 200 -
2025-11-28 15:22:29,692 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:22:29] "GET /static/style.css HTTP/1.1" 304 -
2025-11-28 15:22:29,696 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:22:29] "GET /static/css/scp.css HTTP/1.1" 304 -
2025-11-28 15:22:29,696 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 15:22:29] "GET /static/js/scp.js HTTP/1.1" 304 -
2025-11-28 15:22:34,945 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-28 15:22:45,172 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getUpdates "HTTP/1.1 200 OK"
2025-11-28 18:18:06,930 [INFO] root: Logger initialized | level=INFO | file=D:\Code\iDRAC_Info\idrac_info\data\logs\app.log
2025-11-28 18:18:06,953 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-28 18:18:06,953 [INFO] app: DB URI = sqlite:///D:/Code/iDRAC_Info/idrac_info/backend/instance/site.db
2025-11-28 18:18:06,972 [INFO] backend.routes.jobs: Jobs routes registered at /jobs
2025-11-28 18:18:06,985 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-28 18:18:06,985 [INFO] app: 🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)
2025-11-28 18:18:06,985 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-28 18:18:06,985 [INFO] app: Starting polling for bot: admin_bot (ID: 1)
2025-11-28 18:18:07,024 [INFO] werkzeug: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.0.122:5000
2025-11-28 18:18:07,024 [INFO] werkzeug: Press CTRL+C to quit
2025-11-28 18:18:08,082 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/getMe "HTTP/1.1 200 OK"
2025-11-28 18:18:08,314 [INFO] httpx: HTTP Request: POST https://api.telegram.org/bot6719918880:AAHC1on-KlzH0G3ylJP57p-q5qMyorFUGZo/deleteWebhook "HTTP/1.1 200 OK"
2025-11-28 18:18:08,315 [INFO] telegram.ext.Application: Application started
2025-11-28 18:18:11,284 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 18:18:11] "GET / HTTP/1.1" 302 -
2025-11-28 18:18:11,304 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 18:18:11] "GET /login?next=/ HTTP/1.1" 200 -
2025-11-28 18:18:11,334 [INFO] werkzeug: 127.0.0.1 - - [28/Nov/2025 18:18:11] "GET /static/style.css HTTP/1.1" 304 -

1
data/temp_ip/ip_0.txt Normal file
View File

@@ -0,0 +1 @@
10.10.0.1

View File

@@ -21,21 +21,6 @@ def main() -> int:
app.config.from_object(Config)
db.init_app(app)
with app.app_context():
users = User.query.all()
updated_count = 0
for user in users:
if user.password and not is_hashed(user.password):
print(f"🔄 변환 대상: {user.username}")
user.password = generate_password_hash(user.password)
updated_count += 1
if updated_count:
db.session.commit()
print(f"✅ 완료: {updated_count}명 해시 처리")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -28,10 +28,15 @@ openpyxl==3.1.5
pandas==2.3.3
passlib==1.7.4
pycparser==2.23
pytest==8.0.0
pytest-mock==3.12.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python-engineio==4.12.3
python-socketio==5.14.1
python-telegram-bot==22.5
pytz==2025.2
requests==2.32.3
simple-websocket==1.1.0
six==1.17.0
SQLAlchemy==2.0.43

157
telegram_bot_service.py Normal file
View File

@@ -0,0 +1,157 @@
"""
텔레그램 봇 폴링 서비스
- 백그라운드에서 텔레그램 봇의 업데이트를 폴링
- 인라인 버튼 클릭 처리 (가입 승인/거부)
"""
import logging
from typing import Optional
from telegram import Update
from telegram.ext import Application, CallbackQueryHandler, ContextTypes
from flask import Flask
from backend.models.telegram_bot import TelegramBot
from backend.models.user import User, db
logger = logging.getLogger(__name__)
async def handle_approval_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
텔레그램 인라인 버튼 클릭 처리
callback_data 형식: "approve_{token}" 또는 "reject_{token}"
"""
query = update.callback_query
await query.answer()
data = query.data or ""
logger.info("Received callback: %s", data)
# Flask app 객체는 bot_data에 저장해둔 것을 사용
flask_app: Optional[Flask] = context.application.bot_data.get("flask_app")
if flask_app is None:
logger.error("Flask app context is missing in bot_data")
await query.edit_message_text(
text="❌ 내부 설정 오류로 요청을 처리할 수 없습니다. 관리자에게 문의해 주세요."
)
return
# callback_data 형식 검증
if "_" not in data:
logger.warning("Invalid callback data format: %s", data)
await query.edit_message_text(
text="❌ 유효하지 않은 요청입니다."
)
return
try:
action, token = data.split("_", 1)
except ValueError:
logger.warning("Failed to split callback data: %s", data)
await query.edit_message_text(
text="❌ 유효하지 않은 요청입니다."
)
return
try:
with flask_app.app_context():
# 토큰으로 사용자 찾기
user = User.query.filter_by(approval_token=token).first()
if not user:
await query.edit_message_text(
text="❌ 유효하지 않은 승인 요청입니다.\n(이미 처리되었거나 만료된 요청)"
)
return
if action == "approve":
# 승인 처리
user.is_approved = True
user.is_active = True
user.approval_token = None # 토큰 무효화
db.session.commit()
await query.edit_message_text(
text=(
"✅ 승인 완료!\n\n"
f"👤 사용자: {user.username}\n"
f"📧 이메일: {user.email}\n\n"
"사용자가 이제 로그인할 수 있습니다."
)
)
logger.info("User %s approved", user.username)
elif action == "reject":
# 거부 처리 - 사용자 삭제
username = user.username
email = user.email
db.session.delete(user)
db.session.commit()
await query.edit_message_text(
text=(
"❌ 가입 거부됨\n\n"
f"👤 사용자: {username}\n"
f"📧 이메일: {email}\n\n"
"계정이 삭제되었습니다."
)
)
logger.info("User %s rejected and deleted", username)
else:
logger.warning("Unknown action in callback: %s", action)
await query.edit_message_text(
text="❌ 유효하지 않은 요청입니다."
)
except Exception as e:
logger.exception("Error handling callback: %s", e)
# 예외 내용은 사용자에게 직접 노출하지 않음
try:
db.session.rollback()
except Exception:
logger.exception("DB rollback failed")
await query.edit_message_text(
text="❌ 요청 처리 중 오류가 발생했습니다. 잠시 후 다시 시도하거나 관리자에게 문의해 주세요."
)
def run_polling(flask_app: Flask) -> None:
"""
동기 함수: 백그라운드 스레드에서 직접 호출됨
Application.run_polling() 이 내부에서 asyncio 이벤트 루프를 관리하므로
여기서는 asyncio.run 을 사용하지 않는다.
"""
if flask_app is None:
raise ValueError("flask_app is required for run_polling")
# DB에서 활성 봇 조회
with flask_app.app_context():
bots = TelegramBot.query.filter_by(is_active=True).all()
if not bots:
logger.warning("No active bots found")
return
# 첫 번째 활성 봇만 사용 (여러 봇이 동시에 폴링하면 충돌 가능)
bot = bots[0]
flask_app.logger.info("Starting polling for bot: %s (ID: %s)", bot.name, bot.id)
# Application 생성
application = Application.builder().token(bot.token).build()
# Flask app을 bot_data에 넣어서 핸들러에서 사용할 수 있게 함
application.bot_data["flask_app"] = flask_app
# 콜백 쿼리 핸들러 등록
application.add_handler(CallbackQueryHandler(handle_approval_callback))
try:
# v20 스타일: run_polling 은 동기 함수이고, 내부에서 이벤트 루프를 직접 관리함
application.run_polling(drop_pending_updates=True)
except Exception as e:
flask_app.logger.exception("Error in bot polling: %s", e)

View File

@@ -0,0 +1,64 @@
import pytest
from unittest.mock import MagicMock, patch
from flask import Flask
from backend.services.redfish_client import RedfishClient
@pytest.fixture
def app():
app = Flask(__name__)
app.config["REDFISH_TIMEOUT"] = 10
app.config["REDFISH_VERIFY_SSL"] = True
return app
@patch("backend.services.redfish_client.requests.Session")
def test_redfish_client_init_defaults(mock_session):
"""Test initialization with default values when no app context."""
client = RedfishClient("1.2.3.4", "user", "pass")
assert client.timeout == 15
assert client.verify_ssl == False
assert client.base_url == "https://1.2.3.4/redfish/v1"
@patch("backend.services.redfish_client.requests.Session")
def test_redfish_client_init_with_app_config(mock_session, app):
"""Test initialization picking up Flask app config."""
with app.app_context():
client = RedfishClient("1.2.3.4", "user", "pass")
assert client.timeout == 10
assert client.verify_ssl == True
@patch("backend.services.redfish_client.requests.Session")
def test_redfish_client_init_override(mock_session, app):
"""Test explicit arguments override config."""
with app.app_context():
client = RedfishClient("1.2.3.4", "user", "pass", timeout=30, verify_ssl=False)
assert client.timeout == 30
assert client.verify_ssl == False
@patch("backend.services.redfish_client.requests.Session")
def test_get_success(mock_session):
"""Test successful GET request."""
mock_response = MagicMock()
mock_response.json.return_value = {"key": "value"}
mock_response.status_code = 200
session_instance = mock_session.return_value
session_instance.get.return_value = mock_response
client = RedfishClient("1.2.3.4", "user", "pass")
result = client.get("/Some/Endpoint")
assert result == {"key": "value"}
session_instance.get.assert_called_with("https://1.2.3.4/redfish/v1/Some/Endpoint", timeout=15)
@patch("backend.services.redfish_client.requests.Session")
def test_get_absolute_path(mock_session):
"""Test GET request with absolute path."""
mock_response = MagicMock()
mock_response.json.return_value = {}
session_instance = mock_session.return_value
session_instance.get.return_value = mock_response
client = RedfishClient("1.2.3.4", "user", "pass")
client.get("/redfish/v1/Managers/1")
session_instance.get.assert_called_with("https://1.2.3.4/redfish/v1/Managers/1", timeout=15)

27
update_db.py Normal file
View File

@@ -0,0 +1,27 @@
import sqlite3
import os
db_path = r"D:\Code\iDRAC_Info\idrac_info\backend\instance\site.db"
if not os.path.exists(db_path):
print(f"Database not found at {db_path}")
else:
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Check if column exists
cursor.execute("PRAGMA table_info(telegram_bots)")
columns = [info[1] for info in cursor.fetchall()]
if 'notification_types' not in columns:
print("Adding notification_types column...")
cursor.execute("ALTER TABLE telegram_bots ADD COLUMN notification_types VARCHAR(255) DEFAULT 'auth,activity,system'")
conn.commit()
print("Column added successfully.")
else:
print("Column already exists.")
conn.close()
except Exception as e:
print(f"Error: {e}")

67
update_user_approval.py Normal file
View File

@@ -0,0 +1,67 @@
"""
DB 업데이트 스크립트: User 테이블에 승인 관련 필드 추가
- is_approved: 가입 승인 여부
- approval_token: 승인 토큰 (텔레그램 버튼 콜백용)
"""
import sys
import os
# 프로젝트 루트를 sys.path에 추가
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from app import app
from backend.models.user import db
def update_user_table():
"""User 테이블에 승인 관련 컬럼 추가"""
with app.app_context():
try:
# 컬럼 존재 여부 확인
inspector = db.inspect(db.engine)
columns = [col['name'] for col in inspector.get_columns('user')]
if 'is_approved' not in columns:
print("✅ is_approved 컬럼 추가 중...")
db.session.execute(db.text(
"ALTER TABLE user ADD COLUMN is_approved BOOLEAN DEFAULT 0 NOT NULL"
))
print("✅ is_approved 컬럼 추가 완료")
else:
print(" is_approved 컬럼이 이미 존재합니다.")
if 'approval_token' not in columns:
print("✅ approval_token 컬럼 추가 중...")
# SQLite는 UNIQUE 제약조건을 ALTER TABLE에서 직접 추가할 수 없음
# 먼저 컬럼만 추가하고, 나중에 인덱스로 UNIQUE 처리
db.session.execute(db.text(
"ALTER TABLE user ADD COLUMN approval_token VARCHAR(100)"
))
print("✅ approval_token 컬럼 추가 완료")
# UNIQUE 인덱스 생성
try:
db.session.execute(db.text(
"CREATE UNIQUE INDEX idx_user_approval_token ON user(approval_token)"
))
print("✅ approval_token UNIQUE 인덱스 생성 완료")
except Exception as e:
print(f" UNIQUE 인덱스 생성 스킵 (이미 존재하거나 NULL 값 때문): {e}")
else:
print(" approval_token 컬럼이 이미 존재합니다.")
# 기존 사용자들은 자동 승인 처리
print("✅ 기존 사용자 자동 승인 처리 중...")
db.session.execute(db.text(
"UPDATE user SET is_approved = 1 WHERE is_approved = 0"
))
db.session.commit()
print("✅ 데이터베이스 업데이트 완료!")
except Exception as e:
print(f"❌ 오류 발생: {e}")
db.session.rollback()
raise
if __name__ == "__main__":
update_user_table()