User can now replace manifest with new one and the age is nott refreshed untill he chages happan on remote
This commit is contained in:
parent
12a85c2fc3
commit
952ab3a906
BIN
__pycache__/config.cpython-312.pyc
Normal file
BIN
__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/drawing_checker.cpython-312.pyc
Normal file
BIN
__pycache__/drawing_checker.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/manifest_reader.cpython-312.pyc
Normal file
BIN
__pycache__/manifest_reader.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/progress_calculator.cpython-312.pyc
Normal file
BIN
__pycache__/progress_calculator.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/scada_checker.cpython-312.pyc
Normal file
BIN
__pycache__/scada_checker.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/utils.cpython-312.pyc
Normal file
BIN
__pycache__/utils.cpython-312.pyc
Normal file
Binary file not shown.
373
app.py
373
app.py
@ -7,6 +7,7 @@ import re # Import re for project name validation
|
||||
from flask import Flask, render_template, jsonify, Response, request # Add request
|
||||
from werkzeug.utils import secure_filename # For securing filenames
|
||||
from concurrent.futures import ThreadPoolExecutor # Import ThreadPoolExecutor
|
||||
import shutil # Import shutil for directory cleanup on errors
|
||||
|
||||
# Import configurations and new modules
|
||||
import config
|
||||
@ -172,7 +173,6 @@ def check_and_update_repo(project_name):
|
||||
initial_hash = current_local_commit # Update local var too
|
||||
|
||||
print(f"[{project_name}] Fetching updates from remote...")
|
||||
set_status(project_name, "Checking for updates...")
|
||||
origin = repo.remotes.origin
|
||||
|
||||
# --- Fetch happens OUTSIDE lock ---
|
||||
@ -212,7 +212,10 @@ def check_and_update_repo(project_name):
|
||||
project_last_commit[project_name] = new_commit_hash
|
||||
print(f"[{project_name}] Pull successful. New commit: {new_commit_hash}")
|
||||
did_update = True
|
||||
# Set status AFTER successful pull (set_status handles lock & event)
|
||||
set_status(project_name, f"Pull successful. New commit: {new_commit_hash[:7]}")
|
||||
except git.GitCommandError as pull_err:
|
||||
# Set status on pull error (set_status handles lock & event)
|
||||
set_status(project_name, f"Error pulling repository: {pull_err}")
|
||||
print(f"[{project_name}] Git pull error: {pull_err}")
|
||||
# Revert shared state hash if pull failed? Safest is to keep the pre-pull local commit.
|
||||
@ -223,9 +226,15 @@ def check_and_update_repo(project_name):
|
||||
else:
|
||||
print(f"[{project_name}] No new commits detected.")
|
||||
# Update status only if it wasn't an error before (set_status handles lock)
|
||||
current_status = project_status.get(project_name, "")
|
||||
if not current_status.startswith("Error"):
|
||||
set_status(project_name, f"Checked repo at {time.strftime('%Y-%m-%d %H:%M:%S')}. No changes.")
|
||||
current_status = ""
|
||||
with repo_lock: # Need lock to safely read current status
|
||||
current_status = project_status.get(project_name, "")
|
||||
|
||||
# Set status to indicate no changes were found
|
||||
no_change_msg = f"Checked repo at {time.strftime('%Y-%m-%d %H:%M:%S')}. No changes."
|
||||
# Call set_status even if no changes, it handles event logic internally
|
||||
# This ensures the timestamp updates in the UI status bar via SSE if the message differs
|
||||
set_status(project_name, no_change_msg)
|
||||
# --- End Fetch/Pull Logic ---
|
||||
|
||||
# --- Run analysis IF repo was updated (outside lock) ---
|
||||
@ -383,7 +392,193 @@ def stream():
|
||||
|
||||
return Response(event_stream(), mimetype="text/event-stream")
|
||||
|
||||
# --- NEW: Add Project Endpoint ---
|
||||
# --- NEW: File Management Endpoints ---
|
||||
|
||||
@app.route('/list_pdfs/<project_name>', methods=['GET'])
|
||||
def list_project_pdfs(project_name):
|
||||
"""Lists PDF files for a given project."""
|
||||
safe_project_name = secure_filename(project_name) # Sanitize input
|
||||
if project_name != safe_project_name or not ALLOWED_PROJECT_NAME_REGEX.match(project_name):
|
||||
return jsonify(success=False, message="Invalid project name format."), 400
|
||||
|
||||
project_base_path = utils.get_project_base_path(safe_project_name)
|
||||
pdf_dir_path = os.path.join(project_base_path, 'pdfs')
|
||||
|
||||
if not os.path.isdir(pdf_dir_path):
|
||||
# If the project exists but pdfs dir doesn't, return empty list
|
||||
if os.path.isdir(project_base_path):
|
||||
return jsonify(success=True, files=[])
|
||||
else:
|
||||
return jsonify(success=False, message=f"Project '{safe_project_name}' not found."), 404
|
||||
|
||||
try:
|
||||
pdf_files = [f for f in os.listdir(pdf_dir_path)
|
||||
if os.path.isfile(os.path.join(pdf_dir_path, f)) and f.lower().endswith('.pdf')]
|
||||
return jsonify(success=True, files=sorted(pdf_files))
|
||||
except OSError as e:
|
||||
print(f"Error listing PDFs for {safe_project_name}: {e}")
|
||||
return jsonify(success=False, message=f"Server error listing PDF files: {e}"), 500
|
||||
|
||||
|
||||
@app.route('/delete_pdf/<project_name>', methods=['POST'])
|
||||
def delete_project_pdf(project_name):
|
||||
"""Deletes a specific PDF file and its corresponding TXT file from a project."""
|
||||
safe_project_name = secure_filename(project_name) # Sanitize input
|
||||
if project_name != safe_project_name or not ALLOWED_PROJECT_NAME_REGEX.match(project_name):
|
||||
return jsonify(success=False, message="Invalid project name format."), 400
|
||||
|
||||
data = request.get_json()
|
||||
if not data or 'filename' not in data:
|
||||
return jsonify(success=False, message="Missing filename in request."), 400
|
||||
|
||||
pdf_filename_raw = data['filename']
|
||||
# Use secure_filename on the filename as well, although less critical if only deleting
|
||||
safe_pdf_filename = secure_filename(pdf_filename_raw)
|
||||
if pdf_filename_raw != safe_pdf_filename:
|
||||
print(f"Warning: PDF filename sanitized from '{pdf_filename_raw}' to '{safe_pdf_filename}' during delete request.")
|
||||
# You might want to reject if sanitation changed the name significantly
|
||||
|
||||
project_base_path = utils.get_project_base_path(safe_project_name)
|
||||
pdf_file_path = os.path.join(project_base_path, 'pdfs', safe_pdf_filename)
|
||||
|
||||
if not os.path.isfile(pdf_file_path):
|
||||
return jsonify(success=False, message=f"PDF file '{safe_pdf_filename}' not found in project '{safe_project_name}'."), 404
|
||||
|
||||
try:
|
||||
print(f"Deleting PDF file: {pdf_file_path}")
|
||||
|
||||
# Delete the PDF file first
|
||||
os.remove(pdf_file_path)
|
||||
|
||||
# --- Attempt to delete corresponding TXT file ---
|
||||
deleted_txt = False
|
||||
try:
|
||||
base_name, _ = os.path.splitext(safe_pdf_filename)
|
||||
# *** Update: Using 'extracted_texts' subdirectory based on user feedback ***
|
||||
txt_output_dir = os.path.join(project_base_path, 'extracted_texts')
|
||||
txt_file_path = os.path.join(txt_output_dir, base_name + '.txt')
|
||||
|
||||
if os.path.isfile(txt_file_path):
|
||||
print(f"Deleting corresponding TXT file: {txt_file_path}")
|
||||
os.remove(txt_file_path)
|
||||
deleted_txt = True
|
||||
else:
|
||||
print(f"Corresponding TXT file not found or already deleted: {txt_file_path}")
|
||||
|
||||
except OSError as txt_err:
|
||||
print(f"Warning: Could not delete corresponding TXT file {txt_file_path}: {txt_err}")
|
||||
# Don't fail the request, just report that TXT wasn't deleted
|
||||
# --- End TXT deletion attempt ---
|
||||
|
||||
# Update status message based on TXT deletion result
|
||||
success_message = f"PDF file '{safe_pdf_filename}' deleted successfully."
|
||||
if deleted_txt:
|
||||
success_message += " Corresponding TXT file also deleted."
|
||||
else:
|
||||
success_message += " Corresponding TXT file was not found or could not be deleted."
|
||||
|
||||
# Set status and return success
|
||||
set_status(safe_project_name, f"Deleted PDF '{safe_pdf_filename}'. Re-analysis pending.")
|
||||
return jsonify(success=True, message=success_message)
|
||||
|
||||
except OSError as e:
|
||||
print(f"Error deleting PDF {safe_pdf_filename} for {safe_project_name}: {e}")
|
||||
set_status(safe_project_name, f"Error deleting PDF '{safe_pdf_filename}'.")
|
||||
return jsonify(success=False, message=f"Server error deleting PDF file: {e}"), 500
|
||||
|
||||
|
||||
@app.route('/upload_pdfs/<project_name>', methods=['POST'])
|
||||
def upload_project_pdfs(project_name):
|
||||
"""Uploads one or more PDF files to a project."""
|
||||
safe_project_name = secure_filename(project_name) # Sanitize input
|
||||
if project_name != safe_project_name or not ALLOWED_PROJECT_NAME_REGEX.match(project_name):
|
||||
return jsonify(success=False, message="Invalid project name format."), 400
|
||||
|
||||
if 'pdfFiles' not in request.files:
|
||||
return jsonify(success=False, message="No files part in the request."), 400
|
||||
|
||||
uploaded_files = request.files.getlist('pdfFiles')
|
||||
if not uploaded_files or all(not f.filename for f in uploaded_files):
|
||||
return jsonify(success=False, message="No files selected for upload."), 400
|
||||
|
||||
project_base_path = utils.get_project_base_path(safe_project_name)
|
||||
pdf_dir_path = os.path.join(project_base_path, 'pdfs')
|
||||
|
||||
if not os.path.isdir(project_base_path):
|
||||
return jsonify(success=False, message=f"Project '{safe_project_name}' not found."), 404
|
||||
# Ensure pdfs directory exists within the project
|
||||
os.makedirs(pdf_dir_path, exist_ok=True)
|
||||
|
||||
saved_files_count = 0
|
||||
errors = []
|
||||
for pdf_file in uploaded_files:
|
||||
if pdf_file and pdf_file.filename and pdf_file.filename.lower().endswith('.pdf'):
|
||||
pdf_filename = secure_filename(pdf_file.filename)
|
||||
pdf_save_path = os.path.join(pdf_dir_path, pdf_filename)
|
||||
try:
|
||||
print(f"Saving uploaded PDF file to: {pdf_save_path}")
|
||||
pdf_file.save(pdf_save_path)
|
||||
saved_files_count += 1
|
||||
except Exception as e:
|
||||
print(f"Error saving uploaded PDF {pdf_filename} for {safe_project_name}: {e}")
|
||||
errors.append(f"Error saving {pdf_filename}: {e}")
|
||||
elif pdf_file and pdf_file.filename:
|
||||
errors.append(f"Skipped invalid file type: {pdf_file.filename}. Only PDFs allowed.")
|
||||
|
||||
|
||||
message = f"Successfully uploaded {saved_files_count} PDF(s)."
|
||||
if errors:
|
||||
message += " Errors occurred: " + "; ".join(errors)
|
||||
|
||||
status_code = 200 if saved_files_count > 0 and not errors else (400 if errors else 200) # Use 400 if only errors
|
||||
|
||||
if saved_files_count > 0:
|
||||
set_status(safe_project_name, f"Uploaded {saved_files_count} new PDF(s). Re-analysis pending.")
|
||||
|
||||
return jsonify(success=(saved_files_count > 0), message=message, errors=errors), status_code
|
||||
|
||||
|
||||
@app.route('/trigger_analysis/<project_name>', methods=['POST'])
|
||||
def trigger_project_analysis(project_name):
|
||||
"""Triggers a background analysis for the specified project."""
|
||||
safe_project_name = secure_filename(project_name) # Sanitize input
|
||||
if project_name != safe_project_name or not ALLOWED_PROJECT_NAME_REGEX.match(project_name):
|
||||
return jsonify(success=False, message="Invalid project name format."), 400
|
||||
|
||||
# Check if project actually exists in our tracked state
|
||||
with repo_lock:
|
||||
if safe_project_name not in all_projects:
|
||||
# Maybe it was just added? Check filesystem.
|
||||
project_base_path = utils.get_project_base_path(safe_project_name)
|
||||
if not os.path.isdir(project_base_path):
|
||||
return jsonify(success=False, message=f"Project '{safe_project_name}' not found."), 404
|
||||
# If it exists on filesystem but not in memory, maybe warn or just proceed?
|
||||
# For now, let's assume if it exists on disk, we can try analyzing.
|
||||
|
||||
print(f"Received request to trigger analysis for project: {safe_project_name}")
|
||||
set_status(safe_project_name, "Manual analysis triggered...")
|
||||
|
||||
# Run the analysis in a background thread so the request returns quickly.
|
||||
# Use a simple thread for now, or submit to an existing executor if available/appropriate.
|
||||
analysis_thread = threading.Thread(target=run_analysis_and_log_errors, args=(safe_project_name,), name=f"ManualAnalysis_{safe_project_name}")
|
||||
analysis_thread.start()
|
||||
|
||||
return jsonify(success=True, message=f"Analysis triggered for project '{safe_project_name}'. Check status updates.")
|
||||
|
||||
def run_analysis_and_log_errors(project_name):
|
||||
"""Wrapper to run update_progress_data and log any exceptions."""
|
||||
try:
|
||||
print(f"--- [Manual Analysis] Running analysis for project: {project_name} ---")
|
||||
update_progress_data(project_name) # Call the main analysis function
|
||||
print(f"--- [Manual Analysis] Finished analysis for project: {project_name} ---")
|
||||
except Exception as e:
|
||||
err_msg = f"Critical error during manual analysis for {project_name}: {e}"
|
||||
print(err_msg)
|
||||
# Use set_status which handles locking and event signaling
|
||||
set_status(project_name, f"Error during manual analysis: {e}")
|
||||
|
||||
|
||||
# --- Add Project Endpoint ---
|
||||
ALLOWED_PROJECT_NAME_REGEX = re.compile(r'^[a-zA-Z0-9_-]+$')
|
||||
|
||||
@app.route('/add_project', methods=['POST'])
|
||||
@ -440,7 +635,8 @@ def add_project():
|
||||
|
||||
# --- Save Manifest File ---
|
||||
try:
|
||||
manifest_filename = secure_filename(manifest_file.filename)
|
||||
# manifest_filename = secure_filename(manifest_file.filename) # Use standard name
|
||||
manifest_filename = "manifest.csv"
|
||||
manifest_save_path = os.path.join(project_base_path, manifest_filename)
|
||||
print(f"Saving manifest file to: {manifest_save_path}")
|
||||
manifest_file.save(manifest_save_path)
|
||||
@ -454,16 +650,26 @@ def add_project():
|
||||
saved_pdfs = []
|
||||
try:
|
||||
for pdf_file in pdf_files:
|
||||
if pdf_file and pdf_file.filename: # Check again if file is valid
|
||||
pdf_filename = secure_filename(pdf_file.filename)
|
||||
pdf_save_path = os.path.join(pdf_dir_path, pdf_filename)
|
||||
print(f"Saving PDF file to: {pdf_save_path}")
|
||||
pdf_file.save(pdf_save_path)
|
||||
saved_pdfs.append(pdf_filename)
|
||||
# Check if file is valid *before* calling secure_filename
|
||||
if pdf_file and pdf_file.filename and pdf_file.filename.lower().endswith('.pdf'):
|
||||
pdf_filename = secure_filename(pdf_file.filename)
|
||||
pdf_save_path = os.path.join(pdf_dir_path, pdf_filename)
|
||||
print(f"Saving PDF file to: {pdf_save_path}")
|
||||
pdf_file.save(pdf_save_path)
|
||||
saved_pdfs.append(pdf_filename)
|
||||
elif pdf_file and pdf_file.filename:
|
||||
print(f"Warning: Skipped non-PDF file during initial project add: {pdf_file.filename}")
|
||||
# Optionally add to an errors list to return to the user
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error saving PDF files for {safe_project_name}: {e}")
|
||||
# Clean up potentially partially saved files and directories?
|
||||
# shutil.rmtree(project_base_path, ignore_errors=True)
|
||||
# Use shutil.rmtree for cleanup
|
||||
try:
|
||||
shutil.rmtree(project_base_path)
|
||||
print(f"Cleaned up directory {project_base_path} due to PDF save error.")
|
||||
except OSError as cleanup_error:
|
||||
print(f"Error during cleanup of {project_base_path}: {cleanup_error}")
|
||||
return jsonify(success=False, message=f"Error saving PDF files: {e}"), 500
|
||||
|
||||
# --- Store Repo URL (optional, e.g., in a simple info file) ---
|
||||
@ -480,7 +686,146 @@ def add_project():
|
||||
|
||||
print(f"Successfully added project '{safe_project_name}' with {len(saved_pdfs)} PDF(s).")
|
||||
# NOTE: Server needs restart for this new project to be discovered and processed.
|
||||
return jsonify(success=True, message=f"Project '{safe_project_name}' created successfully.")
|
||||
# --> Update: Let's add it dynamically instead of requiring restart!
|
||||
# Add the new project to the global state
|
||||
with repo_lock:
|
||||
if safe_project_name not in all_projects:
|
||||
all_projects.append(safe_project_name)
|
||||
# Initialize state for the new project
|
||||
project_last_commit[safe_project_name] = "Newly Added (No Clone Yet)" # Or None? Let check_repo handle it.
|
||||
project_last_commit[safe_project_name] = None
|
||||
project_progress_data[safe_project_name] = get_default_progress()
|
||||
project_status[safe_project_name] = "Project added. Initial check pending."
|
||||
data_updated_event.set() # Signal the state change (new project added)
|
||||
data_updated_event.clear()
|
||||
print(f"Added '{safe_project_name}' to active projects list.")
|
||||
|
||||
# Schedule an initial check/analysis for the new project immediately
|
||||
# Use a simple thread or submit to the initial check pool if it's still running (tricky)
|
||||
# For simplicity, let's use a new thread here.
|
||||
print(f"--- Submitting initial setup for newly added project: {safe_project_name} ---")
|
||||
initial_setup_thread = threading.Thread(target=initial_project_setup_and_analysis, args=(safe_project_name,), name=f"InitialSetup_{safe_project_name}")
|
||||
initial_setup_thread.start()
|
||||
|
||||
return jsonify(success=True, message=f"Project '{safe_project_name}' added and initial processing started.")
|
||||
# return jsonify(success=True, message=f"Project '{safe_project_name}' created successfully. Restart server to activate.") # Old message
|
||||
|
||||
|
||||
# --- NEW: Upload/Overwrite Manifest Endpoint ---
|
||||
@app.route('/upload_manifest/<project_name>', methods=['POST'])
|
||||
def upload_manifest(project_name):
|
||||
"""Uploads and overwrites the manifest.csv file for a given project."""
|
||||
safe_project_name = secure_filename(project_name)
|
||||
if project_name != safe_project_name or not ALLOWED_PROJECT_NAME_REGEX.match(project_name):
|
||||
return jsonify(success=False, message="Invalid project name format."), 400
|
||||
|
||||
# Check project existence (filesystem check is sufficient here)
|
||||
project_base_path = utils.get_project_base_path(safe_project_name)
|
||||
if not os.path.isdir(project_base_path):
|
||||
return jsonify(success=False, message=f"Project '{safe_project_name}' not found."), 404
|
||||
|
||||
# --- File Handling ---
|
||||
if 'manifestFile' not in request.files:
|
||||
return jsonify(success=False, message="Missing manifest file in request."), 400
|
||||
|
||||
manifest_file = request.files['manifestFile']
|
||||
|
||||
# Validate file
|
||||
if not manifest_file or not manifest_file.filename:
|
||||
return jsonify(success=False, message="No manifest file selected."), 400
|
||||
if not manifest_file.filename.lower().endswith('.csv'):
|
||||
return jsonify(success=False, message="Invalid file type. Manifest must be a .csv file."), 400
|
||||
|
||||
# --- Save and Overwrite ---
|
||||
try:
|
||||
manifest_filename = "manifest.csv" # Standard name
|
||||
manifest_save_path = os.path.join(project_base_path, manifest_filename)
|
||||
print(f"Saving/Overwriting manifest file at: {manifest_save_path}")
|
||||
manifest_file.save(manifest_save_path)
|
||||
|
||||
# Update status and signal for UI update
|
||||
set_status(safe_project_name, f"Manifest file updated. Re-analysis pending.")
|
||||
|
||||
return jsonify(success=True, message="Manifest file updated successfully. Please trigger analysis.")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error saving updated manifest file for {safe_project_name}: {e}"
|
||||
print(error_msg)
|
||||
# Update status to reflect the error
|
||||
set_status(safe_project_name, f"Error updating manifest: {e}")
|
||||
return jsonify(success=False, message=error_msg), 500
|
||||
|
||||
|
||||
# --- NEW: Delete Project Endpoint ---
|
||||
@app.route('/delete_project/<project_name>', methods=['POST']) # Using POST for simplicity from JS fetch
|
||||
def delete_project(project_name):
|
||||
"""Deletes an entire project, including its directory and removes it from tracking."""
|
||||
safe_project_name = secure_filename(project_name)
|
||||
if project_name != safe_project_name or not ALLOWED_PROJECT_NAME_REGEX.match(project_name):
|
||||
return jsonify(success=False, message="Invalid project name format."), 400
|
||||
|
||||
# --- Critical Validations ---
|
||||
if not project_name or project_name == '.' or project_name == '..':
|
||||
print(f"Attempted deletion of invalid project name: '{project_name}'")
|
||||
return jsonify(success=False, message="Invalid project name specified."), 400
|
||||
|
||||
# Check if project exists in memory first (requires lock)
|
||||
with repo_lock:
|
||||
if safe_project_name not in all_projects:
|
||||
# Double-check filesystem in case it exists but wasn't loaded?
|
||||
# Or just rely on the in-memory state? Relying on memory state is safer.
|
||||
print(f"Attempted deletion of non-tracked project: '{safe_project_name}'")
|
||||
return jsonify(success=False, message=f"Project '{safe_project_name}' not found or not tracked."), 404
|
||||
|
||||
# --- Proceed with Deletion ---
|
||||
project_base_path = utils.get_project_base_path(safe_project_name)
|
||||
print(f"Attempting to delete project directory: {project_base_path}")
|
||||
|
||||
try:
|
||||
if os.path.isdir(project_base_path):
|
||||
# THE DANGEROUS PART: Delete the entire directory tree
|
||||
shutil.rmtree(project_base_path)
|
||||
print(f"Successfully deleted directory: {project_base_path}")
|
||||
else:
|
||||
# This shouldn't happen if it's tracked, but good to check
|
||||
print(f"Warning: Project '{safe_project_name}' was tracked but directory not found: {project_base_path}")
|
||||
# Proceed to remove from state anyway
|
||||
|
||||
# --- Update Global State (inside lock) ---
|
||||
with repo_lock:
|
||||
if safe_project_name in all_projects:
|
||||
all_projects.remove(safe_project_name)
|
||||
project_last_commit.pop(safe_project_name, None)
|
||||
project_progress_data.pop(safe_project_name, None)
|
||||
project_status.pop(safe_project_name, None)
|
||||
|
||||
print(f"Removed project '{safe_project_name}' from global state.")
|
||||
# Signal that the project list/state has changed
|
||||
data_updated_event.set()
|
||||
data_updated_event.clear()
|
||||
|
||||
return jsonify(success=True, message=f"Project '{safe_project_name}' deleted successfully.")
|
||||
|
||||
except OSError as e:
|
||||
error_msg = f"Error deleting project directory '{project_base_path}': {e}"
|
||||
print(error_msg)
|
||||
# Attempt to update status to reflect the error, even if deletion failed partially
|
||||
# Do this outside the main state update lock if possible, or briefly re-acquire
|
||||
with repo_lock:
|
||||
if safe_project_name in project_status: # Check if it still exists in status
|
||||
project_status[safe_project_name] = f"Error during deletion: {e}"
|
||||
data_updated_event.set(); data_updated_event.clear() # Signal status update
|
||||
return jsonify(success=False, message=error_msg), 500
|
||||
except Exception as e:
|
||||
# Catch any other unexpected errors during deletion
|
||||
error_msg = f"Unexpected error deleting project '{safe_project_name}': {e}"
|
||||
print(error_msg)
|
||||
with repo_lock:
|
||||
if safe_project_name in project_status:
|
||||
project_status[safe_project_name] = f"Unexpected deletion error: {e}"
|
||||
data_updated_event.set(); data_updated_event.clear()
|
||||
return jsonify(success=False, message=error_msg), 500
|
||||
|
||||
|
||||
# --- Main Execution ---
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ def get_control_panel_units(csv_filepath):
|
||||
unique_aliases = set()
|
||||
|
||||
try:
|
||||
with open(csv_filepath, mode='r', encoding='utf-8-sig') as infile:
|
||||
with open(csv_filepath, mode='r', encoding='latin-1') as infile:
|
||||
reader = csv.reader(infile)
|
||||
header = next(reader)
|
||||
try:
|
||||
|
||||
@ -15,7 +15,8 @@ def read_manifest(project_name):
|
||||
optional_cols = {config.CSV_EQ_TYPE_COL, config.CSV_CONV_TYPE_COL}
|
||||
try:
|
||||
# Revert back to 'utf-8-sig' to handle potential BOM from Excel
|
||||
with open(csv_filepath, mode='r', newline='', encoding='utf-8-sig') as infile:
|
||||
# Changed encoding to latin-1 to handle potential non-utf8 characters
|
||||
with open(csv_filepath, mode='r', newline='', encoding='latin-1') as infile:
|
||||
reader = csv.DictReader(infile)
|
||||
# Strip whitespace from headers for reliable matching
|
||||
headers = set(h.strip() for h in reader.fieldnames if h) # Added 'if h' to handle potential empty headers
|
||||
|
||||
@ -20,7 +20,7 @@ def read_aliases_from_manifest(csv_filepath, alias_column_name='Alias'):
|
||||
"""Reads the specified column from a CSV file into a set."""
|
||||
aliases = set()
|
||||
try:
|
||||
with open(csv_filepath, mode='r', newline='', encoding='utf-8') as infile:
|
||||
with open(csv_filepath, mode='r', newline='', encoding='latin-1') as infile:
|
||||
reader = csv.DictReader(infile)
|
||||
if alias_column_name not in reader.fieldnames:
|
||||
print(f"Error: Column '{alias_column_name}' not found in {csv_filepath}")
|
||||
|
||||
@ -1,587 +0,0 @@
|
||||
2'-1.5" EL
|
||||
2'-3" EL
|
||||
TRASH
|
||||
PALLETSTAGINGAREA
|
||||
PROBLEM SOLV.CART 80/20
|
||||
PROB. SOLV. CART
|
||||
TRASH
|
||||
TRASH
|
||||
PALLETSTAGINGAREA
|
||||
PROBLEM SOLV.CART 80/20
|
||||
PROB. SOLV. CART
|
||||
TRASH
|
||||
TRASH
|
||||
PALLETSTAGINGAREA
|
||||
PROBLEM SOLV.CART 80/20
|
||||
PROB. SOLV. CART
|
||||
TRASH
|
||||
TRASH
|
||||
PALLETSTAGINGAREA
|
||||
PROBLEM SOLV.CART 80/20
|
||||
PROB. SOLV. CART
|
||||
TRASH
|
||||
TRASH
|
||||
PALLETSTAGINGAREA
|
||||
PROBLEM SOLV.CART 80/20
|
||||
PROB. SOLV. CART
|
||||
TRASH
|
||||
TRASH
|
||||
PALLETSTAGINGAREA
|
||||
PROBLEM SOLV.CART 80/20
|
||||
PROB. SOLV. CART
|
||||
TRASH
|
||||
TRASH
|
||||
PALLETSTAGINGAREA
|
||||
PROBLEM SOLV.CART 80/20
|
||||
PROB. SOLV. CART
|
||||
TRASH
|
||||
TRASH
|
||||
PALLETSTAGINGAREA
|
||||
PROBLEM SOLV.CART 80/20
|
||||
PROB. SOLV. CART
|
||||
TRASH
|
||||
TRASH
|
||||
PALLETSTAGINGAREA
|
||||
PROBLEM SOLV.CART 80/20
|
||||
PROB. SOLV. CART
|
||||
TRASH
|
||||
TRASH
|
||||
PALLETSTAGINGAREA
|
||||
PROBLEM SOLV.CART 80/20
|
||||
PROB. SOLV. CART
|
||||
TRASH
|
||||
ERSCERSCERSCERSCERSCERSCERSCERSCERSCERSCERSCERSCERSCERSCERSC MDR POWER SUPPLY80AMDR POWER SUPPLY80A
|
||||
V
|
||||
11'-2" EL
|
||||
1'-4" EL
|
||||
FL_DPM1MCM04
|
||||
FL_DPM2MCM04
|
||||
ULGLB_DPM1MCM04
|
||||
ULGLC_DPM1MCM04
|
||||
MCM04
|
||||
PRS1_DPM1MCM04
|
||||
CH_DPM2MCM04
|
||||
FL1038_2_PE1ABBB
|
||||
FL1038_1CH_PE1
|
||||
FL1038_2_PE2
|
||||
FL1038_2_VFD15HP350FPM
|
||||
FL1038_3CH_PE1BBBWFL1038_2_JR2WFL1038_2_JR1
|
||||
WFL1038_1_JR1
|
||||
ULC8_3_VFD12HP80FPMULC7_3_VFD12HP80FPMULC6_3_VFD12HP80FPMULC5_3_VFD12HP80FPM
|
||||
PS10_1_VFD15HP150FPMPS10_2_VFD115HP200FPM
|
||||
PS10_5_VFD115HP240FPM
|
||||
PS11_1_VFD15HP150FPMPS11_2_VFD15HP200FPMPS11_3_VFD110HP240FPM
|
||||
PS11_4_VFD110HP240FPM
|
||||
PS11_6_VFD110HP240FPM PS11_7_VFD120HP240FPM PS11_8_VFD115HP240FPM PS11_9_VFD115HP240FPM
|
||||
PS11_11_VFD115HP240FPMULC8_3_JPE2ULC7_3_JPE2ULC6_3_JPE2ULC5_3_JPE2
|
||||
PS10_1_JPE2
|
||||
PS10_1_JPE1PS10_1_JPE3PS11_1_JPE1
|
||||
PS11_1_JPE2
|
||||
PS11_1_JPE3PS10_3_JPE1 PS11_2_JPE1
|
||||
PS10_5CH2_FPE1PS10_5CH2_JPE1
|
||||
PS10_5CH5_FPE2
|
||||
PS10_6CH_FPE1
|
||||
PS10_5_JPE4
|
||||
PS10_5_JPE5
|
||||
PS10_5_JPE3
|
||||
PS10_5_JPE2
|
||||
PS10_5_JPE1
|
||||
PS11_3_JPE2
|
||||
PS11_4_JPE1
|
||||
PS11_11_JPE1
|
||||
PS11_11_JPE2
|
||||
PS11_11_JPE3
|
||||
PS11_11_JPE4
|
||||
PS11_11_JPE5
|
||||
PS11_11CH1_JPE1
|
||||
EPCULC5_3_EPC1
|
||||
EPCULC5_3_EPC2
|
||||
EPCULC6_3_EPC1
|
||||
EPCULC7_3_EPC1
|
||||
EPCULC7_3_EPC2
|
||||
EPCULC8_3_EPC1
|
||||
EPCPS10_1_EPC1EPCPS11_1_EPC1
|
||||
EPCPS10_5_EPC1
|
||||
EPCPS11_11_EPC1
|
||||
WPS10_1_JR1
|
||||
WULC8_3_JR1
|
||||
WULC7_3_JR1
|
||||
WULC6_3_JR1
|
||||
WULC5_3_JR1W
|
||||
PS11_1_JR2
|
||||
PS11_9_JPE1
|
||||
WPS11_11_JR1
|
||||
WPS11_11_JR2
|
||||
WPS10_5_JR1
|
||||
WPS10_5_JR2ABB
|
||||
ABB
|
||||
ABBR
|
||||
GPS10_3_JR1
|
||||
GPS11_11_S1
|
||||
RABB RABB RABB RABB
|
||||
ABBABBR ABBR
|
||||
ABBR
|
||||
ABB
|
||||
ABB
|
||||
PS11_1_FIO1PS10_1_FIO2
|
||||
PS10_5_FIO1
|
||||
PS10_5_FIO3
|
||||
PS11_11_FIO1
|
||||
PS10_5DIV1_LS1LSPS10_5DIV1_LS2LS
|
||||
PS10_5DIV2_LS1LSPS10_5DIV2_LS2LS
|
||||
PS10_5DIV3_LS1LSPS10_5DIV3_LS2LS
|
||||
PS10_5DIV5_LS1LSPS10_5DIV5_LS2LS
|
||||
PS11_11DIV1_LS1LSPS11_11DIV1_LS2LS
|
||||
PS11_11DIV2_LS1LSPS11_11DIV2_LS2LS
|
||||
PS11_11DIV3_LS1LSPS11_11DIV3_LS2LS
|
||||
PS11_11DIV4_LS1LSPS11_11DIV4_LS2LS
|
||||
PS11_11DIV5_LS1LSPS11_11DIV5_LS2LS
|
||||
PS11_11DIV6_LS1LSPS11_11DIV6_LS2LS
|
||||
SOLPS10_5DIV5_SOL1SOLPS10_5DIV5_SOL2
|
||||
SOLPS10_5DIV3_SOL1SOLPS10_5DIV3_SOL2
|
||||
SOLPS10_5DIV2_SOL1SOLPS10_5DIV2_SOL2SOLPS10_5DIV1_SOL1SOLPS10_5DIV1_SOL2
|
||||
SOLPS11_11DIV1_SOL1SOLPS11_11DIV1_SOL2
|
||||
SOLPS11_11DIV2_SOL1SOLPS11_11DIV2_SOL2
|
||||
SOLPS11_11DIV3_SOL1SOLPS11_11DIV3_SOL2
|
||||
SOLPS11_11DIV4_SOL1SOLPS11_11DIV4_SOL2
|
||||
SOLPS11_11DIV5_SOL1SOLPS11_11DIV5_SOL2
|
||||
SOLPS11_11DIV6_SOL1SOLPS11_11DIV6_SOL2
|
||||
PS10_5CH2_FPE2PS10_5CH1_JPE1PS10_5CH1_FPE1PS10_5CH1_FPE2
|
||||
PS10_5CH5_FPE1PS10_5CH5_JPE1
|
||||
PS10_1_JPE4
|
||||
PS11_11CH1_FPE1PS11_11CH1_FPE2PS11_11CH2_JPE1
|
||||
PS11_12CH_FPE1ULC8_3_JPE1ULC7_3_JPE1ULC6_3_JPE1
|
||||
ULC5_3_JPE1
|
||||
PS11_1_JPE4
|
||||
PRS3_2B_VFD12HP120 FPMPRS3_3B_VFD13HP120 FPM
|
||||
PRS3_2A_VFD12HP120 FPMPRS3_3A_VFD12HP120 FPM
|
||||
PRS3_5_VFD12HP120 FPM
|
||||
ERSCPRS3_6_ERSC1ERSCPRS3_6_ERSC2ERSCPRS3_6_ERSC3ERSCPRS3_6_ERSC4ERSCPRS3_6_ERSC5ERSCPRS3_6_ERSC6ERSCPRS3_6_ERSC7ERSCPRS3_6_ERSC8ERSCPRS3_6_ERSC9ERSCPRS3_6_ERSC10ERSCPRS3_6_ERSC11ERSCPRS3_6_ERSC12ERSCPRS3_6_ERSC13ERSCPRS3_6_ERSC14ERSCPRS3_6_ERSC15 MDR POWER SUPPLYPRS3_6_PSU140AMDR POWER SUPPLYPRS3_6_PSU240A
|
||||
PRS3_6_PE1PRS3_6_PE2PRS3_6_PE3PRS3_6_PE4PRS3_6_PE5PRS3_6_PE6PRS3_6_PE7PRS3_6_PE8PRS3_6_PE9PRS3_6_PE10PRS3_6_PE11PRS3_6_PE12PRS3_6_PE13PRS3_6_PE14PRS3_6_PE15PRS3_6_PE16PRS3_6_PE17PRS3_6_PE18PRS3_6_PE19PRS3_6_PE20PRS3_6_PE21PRS3_6_PE22PRS3_6_PE23PRS3_6_PE24PRS3_6_PE25PRS3_6_PE26PRS3_6_PE27PRS3_6_PE28PRS3_6_PE29PRS3_6_PE30
|
||||
PRS3_2B_JPE1 PRS3_2A_JPE1
|
||||
PRS3_1BCH_JPE1 PRS3_1ACH_JPE1
|
||||
PRS3_5_JPE1
|
||||
PRS3_3B_JPE1PRS3_3B_JPE2PRS3_3A_JPE2 PRS3_3A_JPE1
|
||||
PRS3_5_JPE2
|
||||
PRS4_1_JPE1PRS4_2_JPE1 EPCPRS3_5_EPC1
|
||||
EPCPRS3_5_EPC2
|
||||
EPCPRS4_1_EPC2
|
||||
EPCPRS4_1_EPC1
|
||||
EPCPRS4_2_EPC1
|
||||
EPCPRS4_2_EPC2
|
||||
GPRS4_2_S2ABBRGPRS4_1_S2GPRS4_1_S1
|
||||
GPRS4_2_S1
|
||||
GPRS3_5_S1GPRS3_5_S2
|
||||
BBR
|
||||
BBR
|
||||
ABBR BBR
|
||||
ABB ABB
|
||||
WPRS3_3B_JR1
|
||||
WPRS3_3A_JR1WPRS3_2B_JR1
|
||||
WPRS3_1BCH_JR1 WPRS3_1ACH_JR1
|
||||
WPRS3_2A_JR1
|
||||
ABB
|
||||
ABB
|
||||
ABB
|
||||
ABB
|
||||
BABB
|
||||
PRS3_4CH_FPE1
|
||||
PRS3_4CH_FPE2B B
|
||||
BB
|
||||
GBBAB
|
||||
GBBABGBBAB
|
||||
GBBABGBBAB
|
||||
GBBAB
|
||||
GPS11_11CH1_S1
|
||||
GPS11_11CH2_S1
|
||||
GPS11_11CH3_S1
|
||||
GPS11_11CH4_S1
|
||||
GPS11_11CH5_S1
|
||||
GPS11_11CH6_S1
|
||||
GPS10_5CH2_S1
|
||||
GPS10_5CH1_S1
|
||||
GPS10_5CH5_S1
|
||||
GPS10_5CH3_S1
|
||||
GBBAB
|
||||
GBBAB
|
||||
GBBAB
|
||||
GBBAB
|
||||
PS10_1_ENC1PS10_2_ENC1
|
||||
PS10_5_ENC1
|
||||
PS11_3_ENC1
|
||||
PS11_6_ENC1 PS11_7_ENC1 PS11_8_ENC1 PS11_9_ENC1
|
||||
PS11_11_ENC1
|
||||
EPCPS11_7_EPC2
|
||||
EPCPS11_7_EPC1
|
||||
BBR
|
||||
BBR
|
||||
G
|
||||
PS11_7_S2
|
||||
GPS11_7_S1
|
||||
ULC8_3_ENC1ULC7_3_ENC1ULC6_3_ENC1ULC5_3_ENC1
|
||||
PRS4_2_ENC1
|
||||
PRS4_1_ENC1
|
||||
PS10_5_PS1PS
|
||||
PS11_11_PS1PS
|
||||
PS10_5CH3_JPE1PS10_5CH3_FPE1PS10_5CH3_FPE2
|
||||
PS11_11CH2_FPE1PS11_11CH2_FPE2
|
||||
EPCPS11_8_EPC1BBR
|
||||
G
|
||||
PRS4_2_JPE2PS11_3_JPE1
|
||||
EPCPS11_4_EPC2
|
||||
EPCPS11_4_EPC1EPCPS11_3_EPC1
|
||||
EPCPS11_3_EPC2
|
||||
ABBR
|
||||
BBR
|
||||
BBR
|
||||
GPS11_3_S1ABBR
|
||||
GPS11_3_S2
|
||||
PS11_2_ENC1PS10_3_VFD115HP240FPMPS10_3_ENC1 PS11_1_ENC1
|
||||
PS11_4_ENC1
|
||||
PS11_11_JPE6WPS11_11_JR3ABB
|
||||
PRS4_1_VFD17.5HP120FPM
|
||||
PRS4_2_VFD15HP120FPM
|
||||
RGULC5_3_SS1RGULC5_3_SS2
|
||||
PS11_1_FIO2
|
||||
RGULC6_3_SS2
|
||||
PS10_1_FIO1
|
||||
RGPS10_1_SS1
|
||||
PS10_2_TPE
|
||||
PS10_5_FIO2
|
||||
PS10_5_FIO4PS10_5_FIO5
|
||||
WPS11_1_JR1
|
||||
PS11_3_FIO1
|
||||
GPS11_4_S2
|
||||
GPS11_4_S1
|
||||
PS11_4_FIO1
|
||||
PS11_7_FIO1
|
||||
PS11_11_FIO2
|
||||
PS11_11_FIO3
|
||||
PS11_11_FIO4
|
||||
PS11_11_FIO5
|
||||
PS11_11_FIO6
|
||||
PRS3_2B_FIO1 PRS3_2A_FIOM1
|
||||
PRS3_4CH_FIO1
|
||||
PRS4_2_FIO1
|
||||
WPRS4_2_JR1
|
||||
RGULC7_3_SS1
|
||||
RGULC7_3_SS2RGULC8_3_SS1
|
||||
PS10_5_TPE1
|
||||
PS11_6_TPE1
|
||||
PS11_6_TPE2
|
||||
PS11_7_TPE1
|
||||
PS11_8_TPE1 PS11_11_TPE1
|
||||
PS11_11CH3_JPE1PS11_11CH3_FPE1PS11_11CH3_FPE2PS11_11CH4_JPE1PS11_11CH4_FPE1PS11_11CH4_FPE2
|
||||
PS11_11CH5_JPE1PS11_11CH5_FPE1PS11_11CH5_FPE2PS11_11CH6_JPE1PS11_11CH6_FPE1PS11_11CH6_FPE2
|
||||
GDTC_NCH1_EN1
|
||||
CH_DPM1_FIOM1
|
||||
GDTC_NCH2_EN1
|
||||
GDTC_NCH3_EN1
|
||||
GDTC_NCH4_EN1CH_DPM1MCM04
|
||||
GDTC_NCH5_EN1
|
||||
CH_DPM1_FIOM2
|
||||
GDTC_NCH6_EN1
|
||||
GDTC_NCH7_EN1
|
||||
GDTC_NCH8_EN1
|
||||
GDTC_NCH9_EN1
|
||||
CH_DPM1_FIOM3
|
||||
GDTC_NCH10_EN1
|
||||
GDTC_NCH11_EN1
|
||||
GDTC_NCH12_EN1
|
||||
GDTC_NCH13_EN1
|
||||
CH_DPM1_FIOM4
|
||||
GDTC_NCH14_EN1
|
||||
GDTC_NCH15_EN1
|
||||
GDTC_NCH16_EN1
|
||||
CH_DPM1_FIOM5
|
||||
GDTC_NCH17_EN1
|
||||
FL1034_2_PE1ABBBFL1034_1CH_PE1
|
||||
FL1034_2_PE2
|
||||
FL1034_2_VFD15HP350FPM
|
||||
FL1034_3CH_PE1BBBWFL1034_2_JR2WFL1034_2_JR1
|
||||
WFL1034_1_JR1FL1026_2_PE1ABBB
|
||||
FL1026_1CH_PE1
|
||||
FL1026_2_PE2FL1026_3CH_PE1BBBWFL1026_2_JR2WFL1026_2_JR1
|
||||
WFL1026_1_JR1FL1022_2_PE1
|
||||
ABBB
|
||||
FL1022_1CH_PE1
|
||||
FL1022_2_PE2FL1022_3CH_PE1BBBWFL1022_2_JR2WFL1022_2_JR1
|
||||
WFL1022_1_JR1FL1018_2_PE1ABBB
|
||||
FL1018_1CH_PE1
|
||||
FL1018_2_PE2FL1018_3CH_PE1BBBWFL1018_2_JR2WFL1018_2_JR1
|
||||
WFL1018_1_JR1
|
||||
FL1014_2_PE1ABBB
|
||||
FL1014_1CH_PE1
|
||||
FL1014_2_PE2FL1014_3CH_PE1BBBWFL1014_2_JR2WFL1014_2_JR1
|
||||
WFL1014_1_JR1 FL3012_2_PE1
|
||||
ABBB
|
||||
FL3012_1CH_PE1
|
||||
FL3012_2_PE2FL3012_3CH_PE1BBBWFL3012_2_JR2WFL3012_2_JR1
|
||||
WFL3012_1_JR1FL3016_2_PE1
|
||||
ABBB
|
||||
FL3016_1CH_PE1
|
||||
FL3016_2_PE2FL3016_3CH_PE1BBBWFL3016_2_JR2WFL3016_2_JR1
|
||||
WFL3016_1_JR1FL3020_2_PE1
|
||||
ABBB
|
||||
FL3020_1CH_PE1
|
||||
FL3020_2_PE2FL3020_3CH_PE1BBBWFL3020_2_JR2WFL3020_2_JR1
|
||||
WFL3020_1_JR1FL3024_2_PE1
|
||||
ABBB
|
||||
FL3024_1CH_PE1
|
||||
FL3024_2_PE2FL3024_3CH_PE1BBBWFL3024_2_JR2WFL3024_2_JR1
|
||||
WFL3024_1_JR1
|
||||
FL1026_2_VFD15HP350FPMFL1022_2_VFD15HP350FPMFL1018_2_VFD15HP350FPMFL1014_2_VFD15HP350FPM FL3012_2_VFD15HP350FPMFL3016_2_VFD15HP350FPMFL3020_2_VFD15HP350FPMFL3024_2_VFD15HP350FPM
|
||||
CH_DPM2_FIOM2CH_DPM2_FIOM1
|
||||
CH_DPM2_FIOM4CH_DPM2_FIOM3CH_DPM2_FIOM6CH_DPM2_FIOM5
|
||||
BBGABBBGBBBGBBBGB BBGBBBGBBBGB BBGBBBGBBBGB BBGBBBGB BBGBBBGAB BBGAB BBGAB BBGAB
|
||||
BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB
|
||||
CH_DPM3MCM04 CH_DPM4MCM04 CH_DPM5MCM04 CH_DPM6MCM04
|
||||
PB_NCH1_FIOH1PB_NCH3_FIOH1PB_NCH5_FIOH1PB_NCH7_FIOH1PB_NCH9_FIOH1PB_NCH10_FIOH1PB_NCH11_FIOH1PB_NCH12_FIOH1PB_NCH13_FIOH1PB_NCH14_FIOH1PB_NCH15_FIOH1PB_NCH16_FIOH1PB_NCH18_FIOH1PB_NCH20_FIOH1
|
||||
CH_DPM3_FIOM2CH_DPM3_FIOM1CH_DPM3_FIOM4CH_DPM3_FIOM3CH_DPM3_FIOM6CH_DPM3_FIOM5CH_DPM3_FIOM8CH_DPM3_FIOM7
|
||||
PB_NCH21_FIOH1PB_NCH24_FIOH1PB_NCH25_FIOH1PB_NCH26_FIOH1PB_NCH27_FIOH1PB_NCH28_FIOH1PB_NCH29_FIOH1PB_NCH31_FIOH1PB_NCH33_FIOH1PB_NCH35_FIOH1PB_NCH37_FIOH1PB_NCH39_FIOH1PB_NCH41_FIOH1PB_NCH43_FIOH1PB_NCH45_FIOH1PB_NCH46_FIOH1PB_NCH48_FIOH1PB_NCH50_FIOH1PB_NCH52_FIOH1PB_NCH54_FIOH1PB_NCH56_FIOH1PB_NCH58_FIOH1PB_NCH60_FIOH1
|
||||
CH_DPM4_FIOM1CH_DPM4_FIOM3CH_DPM4_FIOM2CH_DPM4_FIOM5CH_DPM4_FIOM4CH_DPM4_FIOM7CH_DPM4_FIOM6CH_DPM5_FIOM2CH_DPM5_FIOM1CH_DPM5_FIOM4CH_DPM5_FIOM3CH_DPM5_FIOM6CH_DPM5_FIOM5CH_DPM5_FIOM8CH_DPM5_FIOM7CH_DPM6_FIOM2CH_DPM6_FIOM1CH_DPM6_FIOM4CH_DPM6_FIOM3CH_DPM6_FIOM6CH_DPM6_FIOM5CH_DPM6_FIOM8CH_DPM6_FIOM7
|
||||
BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGABBBGBBBGBBBGB BBGBBBGBBBGB BBGB BBGB BBGB BBGB BBGB BBGBBBGBBBGB BBGB BBGBBBGB BBGB BBGB BBGB BBGB BBGB BBGB BBGBBBGB BBGBBBGBBBGB BBGBBBGB BBGB BBGBBBGB BBGBBBGB BBGBBBGBBBGB BBGBBBGB BBGB
|
||||
GDTC_NCH18_EN1
|
||||
GDTC_NCH19_EN1
|
||||
GDTC_NCH20_EN1
|
||||
GDTC_NCH21_EN1
|
||||
GDTC_NCH22_EN1
|
||||
GDTC_NCH23_EN1
|
||||
GDTC_NCH24_EN1
|
||||
GDTC_NCH25_EN1
|
||||
GDTC_NCH26_EN1
|
||||
GDTC_NCH27_EN1
|
||||
GDTC_NCH28_EN1
|
||||
GDTC_NCH29_EN1
|
||||
GDTC_NCH30_EN1
|
||||
GDTC_NCH31_EN1
|
||||
GDTC_NCH32_EN1
|
||||
GDTC_NCH33_EN1
|
||||
GDTC_NCH34_EN1
|
||||
GDTC_NCH35_EN1
|
||||
GDTC_NCH36_EN1
|
||||
GDTC_NCH37_EN1
|
||||
GDTC_NCH38_EN1
|
||||
GDTC_NCH39_EN1
|
||||
GDTC_CH40_EN1
|
||||
GDTC_CH41_EN1
|
||||
GDTC_CH42_EN1
|
||||
GDTC_CH43_EN1
|
||||
GDTC_CH44_EN1
|
||||
GDTC_CH45_EN1
|
||||
GDTC_CH46_EN1
|
||||
GDTC_CH47_EN1
|
||||
GDTC_CH48_EN1
|
||||
GDTC_CH49_EN1
|
||||
GDTC_CH50_EN1
|
||||
GDTC_NCH51_EN1
|
||||
GDTC_NCH52_EN1
|
||||
GDTC_NCH53_EN1
|
||||
GDTC_NCH54_EN1
|
||||
GDTC_NCH55_EN1
|
||||
GDTC_NCH56_EN1
|
||||
GDTC_NCH57_EN1
|
||||
GDTC_NCH58_EN1
|
||||
GDTC_NCH59_EN1
|
||||
GDTC_NCH60_EN1
|
||||
GDTC_NCH61_EN1
|
||||
GDTC_NCH62_EN1
|
||||
GDTC_NCH63_EN1
|
||||
GDTC_NCH64_EN1
|
||||
GDTC_NCH65_EN1
|
||||
GDTC_NCH66_EN1
|
||||
GDTC_NCH67_EN1
|
||||
GDTC_NCH68_EN1
|
||||
GDTC_NCH69_EN1
|
||||
GDTC_NCH70_EN1
|
||||
GDTC_NCH71_EN1
|
||||
GDTC_NCH72_EN1
|
||||
GDTC_NCH73_EN1
|
||||
GDTC_NCH74_EN1
|
||||
GDTC_NCH75_EN1
|
||||
GDTC_NCH76_EN1
|
||||
PB_NCH1_PE1
|
||||
PB_NCH1_PE2
|
||||
BBGABBBGB
|
||||
WPB_NCH1_PKGREL_PB1SOLPB_NCH1_PKGREL_SOL1
|
||||
PB_NCH2_PE1
|
||||
PB_NCH2_PE2
|
||||
WPB_NCH2_PKGREL_PB1SOLPB_NCH2_PKGREL_SOL1
|
||||
PB_NCH3_PE1
|
||||
PB_NCH3_PE2
|
||||
WPB_NCH3_PKGREL_PB1SOLPB_NCH3_PKGREL_SOL1
|
||||
PB_NCH4_PE1
|
||||
PB_NCH4_PE2
|
||||
WPB_NCH4_PKGREL_PB1SOLPB_NCH4_PKGREL_SOL1
|
||||
PB_NCH5_PE1
|
||||
PB_NCH5_PE2
|
||||
WPB_NCH5_PKGREL_PB1SOLPB_NCH5_PKGREL_SOL1
|
||||
PB_NCH6_PE1
|
||||
PB_NCH6_PE2
|
||||
WPB_NCH6_PKGREL_PB1SOLPB_NCH6_PKGREL_SOL1
|
||||
PB_NCH7_PE1
|
||||
PB_NCH7_PE2
|
||||
WPB_NCH7_PKGREL_PB1SOLPB_NCH7_PKGREL_SOL1
|
||||
PB_NCH8_PE1
|
||||
PB_NCH8_PE2
|
||||
WPB_NCH8_PKGREL_PB1SOLPB_NCH8_PKGREL_SOL1
|
||||
PB_NCH9_PE1
|
||||
PB_NCH9_PE2
|
||||
WPB_NCH9_PKGREL_PB1SOLPB_NCH9_PKGREL_SOL1
|
||||
PB_NCH10_PE1
|
||||
PB_NCH10_PE2
|
||||
WPB_NCH10_PKGREL_PB1SOLPB_NCH10_PKGREL_SOL1
|
||||
PB_NCH11_PE1
|
||||
PB_NCH11_PE2
|
||||
WPB_NCH11_PKGREL_PB1SOLPB_NCH11_PKGREL_SOL1
|
||||
PB_NCH12_PE1
|
||||
PB_NCH12_PE2
|
||||
WPB_NCH12_PKGREL_PB1SOLPB_NCH12_PKGREL_SOL1
|
||||
PB_NCH13_PE1
|
||||
PB_NCH13_PE2
|
||||
WPB_NCH13_PKGREL_PB1SOLPB_NCH13_PKGREL_SOL1
|
||||
PB_NCH14_PE1
|
||||
PB_NCH14_PE2
|
||||
WPB_NCH14_PKGREL_PB1SOLPB_NCH14_PKGREL_SOL1
|
||||
PB_NCH15_PE1
|
||||
PB_NCH15_PE2
|
||||
WPB_NCH15_PKGREL_PB1SOLPB_NCH15_PKGREL_SOL1
|
||||
PB_NCH16_PE1
|
||||
PB_NCH16_PE2
|
||||
WPB_NCH16_PKGREL_PB1SOLPB_NCH16_PKGREL_SOL1
|
||||
PB_NCH17_PE1
|
||||
PB_NCH17_PE2
|
||||
WPB_NCH17_PKGREL_PB1SOLPB_NCH17_PKGREL_SOL1
|
||||
PB_NCH18_PE1
|
||||
PB_NCH18_PE2
|
||||
WPB_NCH18_PKGREL_PB1SOLPB_NCH18_PKGREL_SOL1
|
||||
PB_NCH19_PE1
|
||||
PB_NCH19_PE2
|
||||
WPB_NCH19_PKGREL_PB1SOLPB_NCH19_PKGREL_SOL1
|
||||
PB_NCH20_PE1
|
||||
PB_NCH20_PE2
|
||||
WPB_NCH20_PKGREL_PB1SOLPB_NCH20_PKGREL_SOL1
|
||||
PB_NCH21_PE1
|
||||
PB_NCH21_PE2
|
||||
WPB_NCH21_PKGREL_PB1SOLPB_NCH21_PKGREL_SOL1
|
||||
PB_NCH22_PE1
|
||||
PB_NCH22_PE2
|
||||
WPB_NCH22_PKGREL_PB1SOLPB_NCH22_PKGREL_SOL1
|
||||
PB_NCH23_PE1
|
||||
PB_NCH23_PE2
|
||||
WPB_NCH23_PKGREL_PB1SOLPB_NCH23_PKGREL_SOL1
|
||||
PB_NCH24_PE1
|
||||
PB_NCH24_PE2
|
||||
WPB_NCH24_PKGREL_PB1SOLPB_NCH24_PKGREL_SOL1
|
||||
PB_NCH25_PE1
|
||||
PB_NCH25_PE2
|
||||
WPB_NCH25_PKGREL_PB1SOLPB_NCH25_PKGREL_SOL1
|
||||
PB_NCH26_PE1
|
||||
PB_NCH26_PE2
|
||||
WPB_NCH26_PKGREL_PB1SOLPB_NCH26_PKGREL_SOL1
|
||||
PB_NCH27_PE1
|
||||
PB_NCH27_PE2
|
||||
WPB_NCH27_PKGREL_PB1SOLPB_NCH27_PKGREL_SOL1
|
||||
PB_NCH28_PE1
|
||||
PB_NCH28_PE2
|
||||
WPB_NCH28_PKGREL_PB1SOLPB_NCH28_PKGREL_SOL1
|
||||
PB_NCH29_PE1
|
||||
PB_NCH29_PE2
|
||||
WPB_NCH29_PKGREL_PB1SOLPB_NCH29_PKGREL_SOL1
|
||||
PB_NCH30_PE1
|
||||
PB_NCH30_PE2
|
||||
WPB_NCH30_PKGREL_PB1SOLPB_NCH30_PKGREL_SOL1
|
||||
PB_NCH31_PE1
|
||||
PB_NCH31_PE2
|
||||
WPB_NCH31_PKGREL_PB1SOLPB_NCH31_PKGREL_SOL1
|
||||
PB_NCH32_PE1
|
||||
PB_NCH32_PE2
|
||||
WPB_NCH32_PKGREL_PB1SOLPB_NCH32_PKGREL_SOL1
|
||||
PB_NCH33_PE1
|
||||
PB_NCH33_PE2
|
||||
WPB_NCH33_PKGREL_PB1SOLPB_NCH33_PKGREL_SOL1
|
||||
PB_NCH34_PE1
|
||||
PB_NCH34_PE2
|
||||
WPB_NCH34_PKGREL_PB1SOLPB_NCH34_PKGREL_SOL1
|
||||
PB_NCH35_PE1
|
||||
PB_NCH35_PE2
|
||||
WPB_NCH35_PKGREL_PB1SOLPB_NCH35_PKGREL_SOL1
|
||||
PB_NCH36_PE1
|
||||
PB_NCH36_PE2
|
||||
WPB_NCH36_PKGREL_PB1SOLPB_NCH36_PKGREL_SOL1
|
||||
PB_NCH37_PE1
|
||||
PB_NCH37_PE2
|
||||
WPB_NCH37_PKGREL_PB1SOLPB_NCH37_PKGREL_SOL1
|
||||
PB_NCH38_PE1
|
||||
PB_NCH38_PE2
|
||||
WPB_NCH38_PKGREL_PB1SOLPB_NCH38_PKGREL_SOL1
|
||||
PB_NCH39_PE1
|
||||
PB_NCH39_PE2
|
||||
WPB_NCH39_PKGREL_PB1SOLPB_NCH39_PKGREL_SOL1
|
||||
PB_NCH40_PE1
|
||||
PB_NCH40_PE2
|
||||
WPB_NCH40_PKGREL_PB1SOLPB_NCH40_PKGREL_SOL1
|
||||
PB_NCH41_PE1
|
||||
PB_NCH41_PE2
|
||||
WPB_NCH41_PKGREL_PB1SOLPB_NCH41_PKGREL_SOL1
|
||||
PB_NCH42_PE1
|
||||
PB_NCH42_PE2
|
||||
WPB_NCH42_PKGREL_PB1SOLPB_NCH42_PKGREL_SOL1
|
||||
PB_NCH43_PE1
|
||||
PB_NCH43_PE2
|
||||
WPB_NCH43_PKGREL_PB1SOLPB_NCH43_PKGREL_SOL1
|
||||
PB_NCH44_PE1
|
||||
PB_NCH44_PE2
|
||||
WPB_NCH44_PKGREL_PB1SOLPB_NCH44_PKGREL_SOL1
|
||||
PB_NCH45_PE1
|
||||
PB_NCH45_PE2
|
||||
WPB_NCH45_PKGREL_PB1SOLPB_NCH45_PKGREL_SOL1
|
||||
PB_NCH46_PE1
|
||||
PB_NCH46_PE2
|
||||
WPB_NCH46_PKGREL_PB1SOLPB_NCH46_PKGREL_SOL1
|
||||
PB_NCH47_PE1
|
||||
PB_NCH47_PE2
|
||||
WPB_NCH47_PKGREL_PB1SOLPB_NCH47_PKGREL_SOL1
|
||||
PB_NCH48_PE1
|
||||
PB_NCH48_PE2
|
||||
WPB_NCH48_PKGREL_PB1SOLPB_NCH48_PKGREL_SOL1
|
||||
PB_NCH49_PE1
|
||||
PB_NCH49_PE2
|
||||
WPB_NCH49_PKGREL_PB1SOLPB_NCH49_PKGREL_SOL1
|
||||
PB_NCH50_PE1
|
||||
PB_NCH50_PE2
|
||||
WPB_NCH50_PKGREL_PB1SOLPB_NCH50_PKGREL_SOL1
|
||||
PB_NCH51_PE1
|
||||
PB_NCH51_PE2
|
||||
WPB_NCH51_PKGREL_PB1SOLPB_NCH51_PKGREL_SOL1
|
||||
PB_NCH52_PE1
|
||||
PB_NCH52_PE2
|
||||
WPB_NCH52_PKGREL_PB1SOLPB_NCH52_PKGREL_SOL1
|
||||
PB_NCH53_PE1
|
||||
PB_NCH53_PE2
|
||||
WPB_NCH53_PKGREL_PB1SOLPB_NCH53_PKGREL_SOL1
|
||||
PB_NCH54_PE1
|
||||
PB_NCH54_PE2
|
||||
WPB_NCH54_PKGREL_PB1SOLPB_NCH54_PKGREL_SOL1
|
||||
PB_NCH55_PE1
|
||||
PB_NCH55_PE2
|
||||
WPB_NCH55_PKGREL_PB1SOLPB_NCH55_PKGREL_SOL1
|
||||
PB_NCH56_PE1
|
||||
PB_NCH56_PE2
|
||||
WPB_NCH56_PKGREL_PB1SOLPB_NCH56_PKGREL_SOL1
|
||||
PB_NCH57_PE1
|
||||
PB_NCH57_PE2
|
||||
WPB_NCH57_PKGREL_PB1SOLPB_NCH57_PKGREL_SOL1
|
||||
PB_NCH58_PE1
|
||||
PB_NCH58_PE2
|
||||
WPB_NCH58_PKGREL_PB1SOLPB_NCH58_PKGREL_SOL1
|
||||
PB_NCH59_PE1
|
||||
PB_NCH59_PE2
|
||||
WPB_NCH59_PKGREL_PB1SOLPB_NCH59_PKGREL_SOL1
|
||||
PB_NCH60_PE1
|
||||
PB_NCH60_PE2
|
||||
WPB_NCH60_PKGREL_PB1SOLPB_NCH60_PKGREL_SOL1
|
||||
PB_NCH61_PE1
|
||||
PB_NCH61_PE2
|
||||
WPB_NCH61_PKGREL_PB1SOLPB_NCH61_PKGREL_SOL1
|
||||
BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGAB BBGABBBGBBBGB BBGBBBGBBBGB BBGB BBGB BBGB BBGBBBGB BBGBBBGB BBGBBBGBBBGB BBGB BBGB BBGB BBGBBBGBBBGB BBGBBBGBBBGB BBGBBBGBBBGB BBGBBBGB BBGBBBGBBBGB BBGBBBGBBBGB BBGBBBGBBBGB BBGBBBGBBBGB
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -472,4 +472,4 @@ PS4_10_VFD110HP300 FPM
|
||||
PS4_10_TPE2
|
||||
PS4_11_TPE1
|
||||
PS4_12_JPE1
|
||||
MCM01
|
||||
MCM01
|
||||
@ -398,4 +398,4 @@ PS7_12_VFD13HP300 FPM
|
||||
PS7_12_JPE1PS7_13_VFD13HP300 FPMPS7_14_TPE1
|
||||
RGPS7_14_SS1PS7_14_FIO1
|
||||
BBA
|
||||
MCM02
|
||||
MCM02
|
||||
@ -178,4 +178,4 @@ RBB
|
||||
GNCP1_8_S1
|
||||
GNCP1_8_S2
|
||||
NCP1_8_TPE1NCP1_8_FIO1
|
||||
MCM03
|
||||
MCM03
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,2 +1,2 @@
|
||||
ProjectName: MTN6_Test
|
||||
ProjectName: MTN6_with_mcm02
|
||||
RepoURL: http://192.168.5.191:3000/ilia-gurielidze-autstand/MTN6_SCADA.git
|
||||
File diff suppressed because it is too large
Load Diff
2
projects/MTN6_with_mcm04/project_info.txt
Normal file
2
projects/MTN6_with_mcm04/project_info.txt
Normal file
@ -0,0 +1,2 @@
|
||||
ProjectName: MTN6_with_mcm04
|
||||
RepoURL: http://192.168.5.191:3000/ilia-gurielidze-autstand/MTN6_SCADA.git
|
||||
@ -5,6 +5,13 @@ let currentProjectData = {}; // Stores the LATEST full data received from SSE {
|
||||
let selectedProjectName = null; // Track the currently selected project
|
||||
let detailsModalInstance = null;
|
||||
let currentVisibleSection = 'scada'; // Track visible section: 'scada', 'drawing', 'conflicts'
|
||||
let eventSource = null;
|
||||
let allProjectData = {}; // Holds combined status, progress, commit for all projects
|
||||
let activeCharts = {}; // Store chart instances to prevent duplicates
|
||||
let selectedProject = null; // Store the currently selected project name
|
||||
let initialStatuses = initialServerData.status || {}; // Use embedded status initially
|
||||
const projectSelector = document.getElementById('projectSelector');
|
||||
const manageFilesBtn = document.getElementById('manageFilesBtn'); // Get manage files button
|
||||
|
||||
// --- Chart Configurations ---
|
||||
const scadaChartLabels = ['Found in SCADA', 'Not Found in SCADA'];
|
||||
@ -738,11 +745,112 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
showSection('scada');
|
||||
|
||||
setupAddProjectForm(); // Call the setup function for the new form
|
||||
|
||||
// Event Listener for Project Selector Change
|
||||
projectSelector.addEventListener('change', (event) => {
|
||||
selectedProject = event.target.value;
|
||||
console.log("Project selected:", selectedProject);
|
||||
updateUIForSelectedProject();
|
||||
// Enable manage files button if a project is selected, disable if no project
|
||||
manageFilesBtn.disabled = !selectedProject;
|
||||
// If no project selected (e.g., placeholder), update display accordingly
|
||||
if (!selectedProject) {
|
||||
document.querySelectorAll('.project-name-display').forEach(span => span.textContent = 'No Project Selected');
|
||||
document.getElementById('status-message').textContent = 'N/A';
|
||||
document.getElementById('last-commit').textContent = 'N/A';
|
||||
document.getElementById('selected-project-status-name').textContent = '...';
|
||||
// Optionally clear charts/tables or show a placeholder message
|
||||
clearAllProjectSpecificUI(); // Assuming a function to clear UI elements
|
||||
}
|
||||
});
|
||||
|
||||
// Event Listener for Add Project Form
|
||||
const addProjectForm = document.getElementById('addProjectForm');
|
||||
if (addProjectForm) {
|
||||
setupAddProjectForm(); // Call the setup function for the new form
|
||||
}
|
||||
|
||||
// --- NEW: Event Listeners for Manage Files Modal ---
|
||||
const manageFilesModalElement = document.getElementById('manageFilesModal');
|
||||
const uploadPdfsForm = document.getElementById('uploadPdfsForm');
|
||||
const triggerAnalysisBtn = document.getElementById('triggerAnalysisBtn');
|
||||
|
||||
if (manageFilesBtn && manageFilesModalElement && uploadPdfsForm && triggerAnalysisBtn) {
|
||||
// When the Manage Files modal is shown, fetch the PDF list
|
||||
manageFilesModalElement.addEventListener('show.bs.modal', async () => {
|
||||
if (!selectedProject) return; // Should not happen if button is enabled correctly
|
||||
// Update modal title *before* loading
|
||||
manageFilesModalElement.querySelectorAll('.project-name-display').forEach(span => {
|
||||
span.textContent = selectedProject;
|
||||
});
|
||||
// Clear previous statuses immediately
|
||||
clearManageFilesStatusMessages();
|
||||
// Load files
|
||||
await loadAndDisplayPdfs(selectedProject);
|
||||
});
|
||||
|
||||
// Handle PDF Upload Form Submission
|
||||
uploadPdfsForm.addEventListener('submit', handlePdfUploadSubmit);
|
||||
|
||||
// Handle Trigger Analysis Button Click
|
||||
triggerAnalysisBtn.addEventListener('click', handleTriggerAnalysisClick);
|
||||
|
||||
// --- NEW: Handle Delete Project Button Click ---
|
||||
const deleteProjectBtn = document.getElementById('deleteProjectBtn');
|
||||
if (deleteProjectBtn) {
|
||||
deleteProjectBtn.addEventListener('click', handleDeleteProjectClick);
|
||||
} else {
|
||||
console.warn("Delete Project button (deleteProjectBtn) not found.");
|
||||
}
|
||||
|
||||
// --- NEW: Handle Manifest Upload Form Submit ---
|
||||
const uploadManifestForm = document.getElementById('uploadManifestForm');
|
||||
if (uploadManifestForm) {
|
||||
uploadManifestForm.addEventListener('submit', handleManifestUploadSubmit);
|
||||
} else {
|
||||
console.warn("Upload Manifest form (uploadManifestForm) not found.");
|
||||
}
|
||||
|
||||
} else {
|
||||
console.warn("Manage Files Modal elements (button, modal, form, trigger btn) not all found. File management disabled.");
|
||||
if(manageFilesBtn) manageFilesBtn.style.display = 'none'; // Hide button if modal isn't functional
|
||||
}
|
||||
|
||||
// --- Adjust Initial State Setting ---
|
||||
const initialProjects = initialServerData.projects || [];
|
||||
if (projectSelector.options.length > 0 && projectSelector.value) {
|
||||
selectedProject = projectSelector.value; // Get initial value from selector
|
||||
console.log("Initial project selected:", selectedProject);
|
||||
if (manageFilesBtn) manageFilesBtn.disabled = !selectedProject; // Enable button if a project is initially selected
|
||||
updateUIForSelectedProject(); // Update based on initially selected project
|
||||
connectEventSource(); // Connect to SSE
|
||||
} else {
|
||||
console.log("No projects found initially or selector empty.");
|
||||
if (manageFilesBtn) manageFilesBtn.disabled = true; // Ensure button is disabled
|
||||
document.querySelectorAll('.project-name-display').forEach(span => span.textContent = 'No Projects');
|
||||
updateStatusDisplay('No projects discovered.', '...', 'N/A');
|
||||
clearAllProjectSpecificUI();
|
||||
}
|
||||
|
||||
// Initialize details modal instance (if it exists)
|
||||
const detailsModalElement = document.getElementById('detailsModal');
|
||||
if (detailsModalElement) {
|
||||
detailsModalInstance = new bootstrap.Modal(detailsModalElement);
|
||||
}
|
||||
|
||||
// Add event listeners for navigation tabs
|
||||
document.querySelectorAll('#viewTabs .nav-link').forEach(tab => {
|
||||
tab.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const newView = event.target.getAttribute('data-view');
|
||||
switchView(newView);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --- Connect to SSE stream (Single connection) ---
|
||||
console.log("Initializing SSE connection...");
|
||||
const eventSource = new EventSource("/stream");
|
||||
eventSource = new EventSource("/stream");
|
||||
|
||||
eventSource.onmessage = function(event) {
|
||||
try {
|
||||
@ -857,9 +965,22 @@ function setupAddProjectForm() {
|
||||
statusDiv.classList.remove('alert-info');
|
||||
if (response.ok && result.success) {
|
||||
statusDiv.classList.add('alert-success');
|
||||
statusDiv.textContent = result.message + ' Please restart the server for the new project to appear.';
|
||||
statusDiv.textContent = result.message || 'Project added successfully.';
|
||||
form.reset(); // Clear the form on success
|
||||
// Keep button disabled after successful submission
|
||||
|
||||
// Hide the modal on success
|
||||
const modalElement = form.closest('.modal');
|
||||
if (modalElement) {
|
||||
const modalInstance = bootstrap.Modal.getInstance(modalElement);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
} else {
|
||||
console.warn('Could not get modal instance to hide it.');
|
||||
}
|
||||
}
|
||||
// Re-enable button shortly after modal starts closing (optional, could leave disabled)
|
||||
setTimeout(() => { submitButton.disabled = false; }, 500);
|
||||
|
||||
} else {
|
||||
statusDiv.classList.add('alert-danger');
|
||||
statusDiv.textContent = 'Error: ' + (result.message || 'Unknown error occurred.');
|
||||
@ -874,4 +995,546 @@ function setupAddProjectForm() {
|
||||
}
|
||||
statusDiv.style.display = 'block'; // Ensure status is visible
|
||||
});
|
||||
}
|
||||
|
||||
// --- NEW: File Management Functions ---
|
||||
|
||||
async function loadAndDisplayPdfs(projectName) {
|
||||
const pdfListDiv = document.getElementById('existingPdfList');
|
||||
const statusDiv = document.getElementById('manageFilesStatus');
|
||||
if (!pdfListDiv || !statusDiv) return;
|
||||
|
||||
pdfListDiv.innerHTML = '<div class="list-group-item text-muted">Loading files...</div>'; // Show loading state within list-group
|
||||
statusDiv.style.display = 'none'; // Hide general status
|
||||
|
||||
try {
|
||||
const response = await fetch(`/list_pdfs/${encodeURIComponent(projectName)}`);
|
||||
const data = await response.json();
|
||||
|
||||
pdfListDiv.innerHTML = ''; // Clear loading state
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.message || `Failed to list PDF files (HTTP ${response.status})`);
|
||||
}
|
||||
|
||||
if (data.files && data.files.length > 0) {
|
||||
data.files.forEach(filename => {
|
||||
const listItem = document.createElement('div');
|
||||
listItem.className = 'list-group-item d-flex justify-content-between align-items-center';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.textContent = filename;
|
||||
nameSpan.title = filename; // Show full name on hover if needed
|
||||
nameSpan.style.overflow = 'hidden';
|
||||
nameSpan.style.textOverflow = 'ellipsis';
|
||||
nameSpan.style.whiteSpace = 'nowrap';
|
||||
nameSpan.style.marginRight = '10px';
|
||||
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.className = 'btn btn-danger btn-sm flex-shrink-0'; // Prevent button shrinking
|
||||
deleteButton.textContent = 'Delete';
|
||||
deleteButton.title = `Delete ${filename}`;
|
||||
deleteButton.onclick = () => handleDeletePdfClick(projectName, filename);
|
||||
|
||||
listItem.appendChild(nameSpan);
|
||||
listItem.appendChild(deleteButton);
|
||||
pdfListDiv.appendChild(listItem);
|
||||
});
|
||||
} else {
|
||||
pdfListDiv.innerHTML = '<div class="list-group-item text-muted">No PDF files found for this project.</div>';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF list:', error);
|
||||
pdfListDiv.innerHTML = '<div class="list-group-item text-danger">Error loading files.</div>';
|
||||
showManageFilesStatus(`Error loading PDF list: ${error.message}`, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeletePdfClick(projectName, filename) {
|
||||
if (!confirm(`Are you sure you want to delete the file: ${filename}? This cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Requesting deletion of ${filename} from project ${projectName}`);
|
||||
clearManageFilesStatusMessages(); // Clear previous messages
|
||||
showManageFilesStatus(`Deleting ${filename}...`, 'info'); // Show deleting status
|
||||
|
||||
try {
|
||||
const response = await fetch(`/delete_pdf/${encodeURIComponent(projectName)}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ filename: filename })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.message || `Failed to delete PDF file (HTTP ${response.status})`);
|
||||
}
|
||||
|
||||
showManageFilesStatus(data.message || `Successfully deleted ${filename}.`, 'success');
|
||||
// Refresh the list after deletion
|
||||
await loadAndDisplayPdfs(projectName);
|
||||
// Recommend triggering analysis
|
||||
showAnalysisTriggerStatus('File deleted. Trigger analysis to update progress.', 'info');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting PDF:', error);
|
||||
showManageFilesStatus(`Error deleting file: ${error.message}`, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePdfUploadSubmit(event) {
|
||||
event.preventDefault();
|
||||
if (!selectedProject) return;
|
||||
|
||||
const form = event.target;
|
||||
const formData = new FormData(form);
|
||||
const fileInput = document.getElementById('newPdfFiles');
|
||||
const uploadStatusDiv = document.getElementById('uploadStatus');
|
||||
|
||||
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
|
||||
showUploadStatus('Please select at least one PDF file to upload.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Uploading ${fileInput.files.length} file(s) to project ${selectedProject}`);
|
||||
clearManageFilesStatusMessages();
|
||||
showUploadStatus('Uploading files...', 'info', false); // Show persistent uploading message
|
||||
|
||||
try {
|
||||
const response = await fetch(`/upload_pdfs/${encodeURIComponent(selectedProject)}`, {
|
||||
method: 'POST',
|
||||
body: formData // FormData handles multipart/form-data automatically
|
||||
});
|
||||
|
||||
// Try to parse JSON regardless of status for potential error messages
|
||||
let data = {};
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch(e) {
|
||||
console.warn("Could not parse JSON response from upload endpoint.");
|
||||
// If JSON parsing fails on error, create a basic error object
|
||||
if (!response.ok) {
|
||||
data = { success: false, message: `Upload failed with status ${response.status}. No error details available.` };
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.message || `File upload failed (HTTP ${response.status})`);
|
||||
}
|
||||
|
||||
showUploadStatus(data.message || `Successfully uploaded files.`, 'success');
|
||||
form.reset(); // Clear the file input
|
||||
await loadAndDisplayPdfs(selectedProject); // Refresh the list
|
||||
// Recommend triggering analysis
|
||||
showAnalysisTriggerStatus('Files uploaded. Trigger analysis to update progress.', 'info');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error uploading PDFs:', error);
|
||||
showUploadStatus(`Upload failed: ${error.message}`, 'danger');
|
||||
} finally {
|
||||
// Ensure the 'Uploading files...' message is cleared if it wasn't replaced by success/error
|
||||
if (uploadStatusDiv && uploadStatusDiv.textContent === 'Uploading files...') {
|
||||
uploadStatusDiv.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTriggerAnalysisClick() {
|
||||
if (!selectedProject) return;
|
||||
|
||||
console.log(`Requesting manual analysis trigger for project ${selectedProject}`);
|
||||
clearManageFilesStatusMessages();
|
||||
showAnalysisTriggerStatus('Triggering analysis...', 'info', false);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/trigger_analysis/${encodeURIComponent(selectedProject)}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.message || `Failed to trigger analysis (HTTP ${response.status})`);
|
||||
}
|
||||
|
||||
showAnalysisTriggerStatus(data.message || 'Analysis triggered successfully. Monitor status bar for updates.', 'success', false); // Keep success message visible
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error triggering analysis:', error);
|
||||
showAnalysisTriggerStatus(`Error: ${error.message}`, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
function showManageFilesStatus(message, type = 'info') {
|
||||
const statusDiv = document.getElementById('manageFilesStatus');
|
||||
if (!statusDiv) return;
|
||||
statusDiv.className = `mt-3 alert alert-${type}`;
|
||||
statusDiv.textContent = message;
|
||||
statusDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
function showUploadStatus(message, type = 'info', autoClear = true) {
|
||||
const statusDiv = document.getElementById('uploadStatus');
|
||||
if (!statusDiv) return;
|
||||
statusDiv.className = `mt-2 text-${type}`;
|
||||
if (type === 'danger' || type === 'warning' || type === 'success') {
|
||||
statusDiv.className += ' fw-bold';
|
||||
}
|
||||
statusDiv.textContent = message;
|
||||
statusDiv.style.display = 'block';
|
||||
// Clear previous timeouts if any
|
||||
if (statusDiv.timeoutId) clearTimeout(statusDiv.timeoutId);
|
||||
if (autoClear) {
|
||||
statusDiv.timeoutId = setTimeout(() => { statusDiv.style.display = 'none'; statusDiv.timeoutId = null; }, 5000); // Hide after 5 seconds
|
||||
}
|
||||
}
|
||||
|
||||
function showAnalysisTriggerStatus(message, type = 'info', autoClear = true) {
|
||||
const statusDiv = document.getElementById('analysisTriggerStatus');
|
||||
if (!statusDiv) return;
|
||||
statusDiv.className = `mt-2 text-${type}`;
|
||||
if (type === 'danger' || type === 'warning' || type === 'success') {
|
||||
statusDiv.className += ' fw-bold';
|
||||
}
|
||||
statusDiv.textContent = message;
|
||||
statusDiv.style.display = 'block';
|
||||
// Clear previous timeouts if any
|
||||
if (statusDiv.timeoutId) clearTimeout(statusDiv.timeoutId);
|
||||
if (autoClear) {
|
||||
statusDiv.timeoutId = setTimeout(() => { statusDiv.style.display = 'none'; statusDiv.timeoutId = null; }, 8000); // Hide after 8 seconds (longer for analysis trigger)
|
||||
}
|
||||
}
|
||||
|
||||
function clearManageFilesStatusMessages() {
|
||||
const manageStatus = document.getElementById('manageFilesStatus');
|
||||
const uploadStatus = document.getElementById('uploadStatus');
|
||||
const analysisStatus = document.getElementById('analysisTriggerStatus');
|
||||
const deleteStatus = document.getElementById('deleteProjectStatus');
|
||||
const manifestStatus = document.getElementById('uploadManifestStatus'); // Added
|
||||
if(manageStatus) manageStatus.style.display = 'none';
|
||||
if(uploadStatus) { uploadStatus.style.display = 'none'; if (uploadStatus.timeoutId) clearTimeout(uploadStatus.timeoutId); uploadStatus.timeoutId = null; }
|
||||
if(analysisStatus) { analysisStatus.style.display = 'none'; if (analysisStatus.timeoutId) clearTimeout(analysisStatus.timeoutId); analysisStatus.timeoutId = null; }
|
||||
if(deleteStatus) { deleteStatus.style.display = 'none'; if (deleteStatus.timeoutId) clearTimeout(deleteStatus.timeoutId); deleteStatus.timeoutId = null; } // Clear timeout for delete too
|
||||
if(manifestStatus) { manifestStatus.style.display = 'none'; if (manifestStatus.timeoutId) clearTimeout(manifestStatus.timeoutId); manifestStatus.timeoutId = null; } // Added
|
||||
}
|
||||
|
||||
function clearAllProjectSpecificUI() {
|
||||
// Clear Overall Charts & Text
|
||||
updateOverallChart('overall-scada', 0, 0, 0); // Assumes updateOverallChart exists and handles zero data
|
||||
updateOverallChart('overall-drawing', 0, 0, 0); // Assumes updateOverallChart exists and handles zero data
|
||||
const overallScadaText = document.getElementById('overall-scada-text');
|
||||
if (overallScadaText) overallScadaText.textContent = 'Found in SCADA: 0/0 (0%)';
|
||||
const overallDrawingText = document.getElementById('overall-drawing-text');
|
||||
if (overallDrawingText) overallDrawingText.textContent = 'Found in Drawing: 0/0 (0%)';
|
||||
|
||||
// Clear Panel Sections
|
||||
document.getElementById('scada-panels-progress').innerHTML = '<p>Select a project to view data.</p>';
|
||||
document.getElementById('drawing-panels-progress').innerHTML = '<p>Select a project to view data.</p>';
|
||||
document.getElementById('panels-conflicts').innerHTML = '<p>Select a project to view data.</p>';
|
||||
const conflictCount = document.getElementById('conflict-count');
|
||||
if (conflictCount) conflictCount.textContent = '0';
|
||||
// Clear charts in case they were drawn
|
||||
destroyAllCharts();
|
||||
}
|
||||
|
||||
function destroyAllCharts() {
|
||||
Object.keys(activeCharts).forEach(key => {
|
||||
if (activeCharts[key]) {
|
||||
activeCharts[key].destroy();
|
||||
delete activeCharts[key];
|
||||
}
|
||||
});
|
||||
// console.log("Destroyed all active charts."); // Optional log
|
||||
}
|
||||
|
||||
// Add a helper to update overall chart
|
||||
function updateOverallChart(chartId, foundScada, foundDrawing, total) {
|
||||
const chartCanvas = document.getElementById(chartId);
|
||||
if (chartCanvas) {
|
||||
const ctx = chartCanvas.getContext('2d');
|
||||
const chart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Found in SCADA', 'Found in Drawing', 'Total'],
|
||||
datasets: [{
|
||||
label: 'Match Count',
|
||||
data: [foundScada, foundDrawing, total],
|
||||
backgroundColor: ['rgba(13, 110, 253, 0.5)', 'rgba(25, 135, 84, 0.5)', 'rgba(75, 192, 192, 0.5)'],
|
||||
borderColor: ['rgb(13, 110, 253)', 'rgb(25, 135, 84)', 'rgb(75, 192, 192)'],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add a helper to create panel card
|
||||
function createPanelCard(panelName, foundScada, foundDrawing, total) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'panel-card';
|
||||
card.innerHTML = `
|
||||
<h5>${panelName}</h5>
|
||||
<p>Found in SCADA: ${foundScada}</p>
|
||||
<p>Found in Drawing: ${foundDrawing}</p>
|
||||
<p>Total: ${total}</p>
|
||||
`;
|
||||
return card;
|
||||
}
|
||||
|
||||
// Add a helper to populate details modal
|
||||
function populateDetailsModal(projectName, identifier, categoryType, context) {
|
||||
let sourceData = null;
|
||||
let panelNameDisplay = "";
|
||||
const listKeysMap = context === 'scada' ? scadaListKeysMap : drawingListKeysMap;
|
||||
const listTypeLabel = categoryType === 'found'
|
||||
? (context === 'scada' ? 'Found in SCADA' : 'Found in Drawing')
|
||||
: (context === 'scada' ? 'Not Found in SCADA' : 'Not Found in Drawing');
|
||||
|
||||
// Get the specific project's progress data
|
||||
const projectProgress = (currentProjectData[projectName] && currentProjectData[projectName].progress) ? currentProjectData[projectName].progress : {};
|
||||
|
||||
if (identifier === '__overall__') {
|
||||
sourceData = projectProgress.overall || null;
|
||||
panelNameDisplay = `Overall (${projectName})`;
|
||||
} else {
|
||||
sourceData = (projectProgress.panels) ? projectProgress.panels[identifier] : null;
|
||||
panelNameDisplay = `${identifier} (${projectName})`;
|
||||
}
|
||||
|
||||
if (!sourceData) {
|
||||
console.error(`Could not find source data for modal. Project: ${projectName}, Identifier: ${identifier}, Context: ${context}`);
|
||||
alert("Error: Could not load details data.");
|
||||
return;
|
||||
}
|
||||
|
||||
const backendListKeys = listKeysMap[categoryType];
|
||||
if (!backendListKeys) { /* ... error handling ... */ return; }
|
||||
|
||||
let combinedDataList = [];
|
||||
backendListKeys.forEach(key => {
|
||||
if (sourceData[key]) {
|
||||
combinedDataList = combinedDataList.concat(sourceData[key]);
|
||||
}
|
||||
});
|
||||
|
||||
if (combinedDataList.length === 0) { /* ... alert handling ... */ return; }
|
||||
|
||||
const modalTitleElement = document.getElementById('detailsModalLabel');
|
||||
const modalTableBody = document.querySelector('#detailsModal .modal-body tbody');
|
||||
|
||||
modalTitleElement.innerHTML = `${listTypeLabel} Items for ${panelNameDisplay} <span class="badge bg-secondary ms-2">${combinedDataList.length}</span>`;
|
||||
modalTableBody.innerHTML = '';
|
||||
|
||||
combinedDataList.sort((a, b) => a.alias.localeCompare(b.alias)).forEach(item => {
|
||||
const row = document.createElement('tr');
|
||||
row.insertCell().textContent = item.alias;
|
||||
row.insertCell().textContent = item.control_panel;
|
||||
const scadaCell = row.insertCell(); scadaCell.innerHTML = item.found_scada ? '<span class="status-yes">Yes</span>' : '<span class="status-no">No</span>';
|
||||
const drawingCell = row.insertCell(); drawingCell.innerHTML = item.found_drawing ? '<span class="status-yes">Yes</span>' : '<span class="status-no">No</span>';
|
||||
row.insertCell().textContent = item.equipment_type || 'N/A';
|
||||
row.insertCell().textContent = item.conveyor_type || 'N/A';
|
||||
if (item.found_scada && !item.found_drawing) { row.classList.add('table-warning'); }
|
||||
modalTableBody.appendChild(row);
|
||||
});
|
||||
|
||||
if (!detailsModalInstance) {
|
||||
detailsModalInstance = new bootstrap.Modal(document.getElementById('detailsModal'));
|
||||
}
|
||||
detailsModalInstance.show();
|
||||
}
|
||||
|
||||
// Add a helper to update UI for selected project
|
||||
function updateUIForSelectedProject() {
|
||||
// Implement the logic to update the UI for the selected project
|
||||
console.log("Updating UI for selected project:", selectedProject);
|
||||
}
|
||||
|
||||
// Add a helper to switch view
|
||||
function switchView(newView) {
|
||||
console.log("Switching to view:", newView);
|
||||
showSection(newView);
|
||||
}
|
||||
|
||||
// Add a helper to update status display
|
||||
function updateStatusDisplay(statusMessage, projectName, commitHash) {
|
||||
const statusMsgSpan = document.getElementById('status-message');
|
||||
const statusProjSpan = document.getElementById('selected-project-status-name');
|
||||
const commitSpan = document.getElementById('last-commit');
|
||||
|
||||
if (statusMsgSpan) statusMsgSpan.textContent = statusMessage || 'N/A';
|
||||
if (statusProjSpan) statusProjSpan.textContent = projectName || '...';
|
||||
if (commitSpan) commitSpan.textContent = commitHash ? commitHash.substring(0, 7) : 'N/A'; // Show abbreviated hash
|
||||
}
|
||||
|
||||
// --- NEW: Delete Project Function ---
|
||||
async function handleDeleteProjectClick() {
|
||||
if (!selectedProject) {
|
||||
showDeleteProjectStatus('No project selected to delete.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// VERY IMPORTANT: Double confirmation with project name
|
||||
const confirmation = prompt(
|
||||
`This action is IRREVERSIBLE and will permanently delete the project '${selectedProject}' and all its data (manifest, PDFs, text files, cloned repository) from the server.\n\nType the project name '${selectedProject}' below to confirm deletion:`
|
||||
);
|
||||
|
||||
if (confirmation !== selectedProject) {
|
||||
showDeleteProjectStatus('Project deletion cancelled or confirmation mismatch.', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Requesting deletion of project ${selectedProject}`);
|
||||
clearManageFilesStatusMessages(); // Clear other messages
|
||||
showDeleteProjectStatus(`Deleting project '${selectedProject}'...`, 'info', false); // Show persistent status
|
||||
// Optionally disable buttons while deleting
|
||||
document.getElementById('deleteProjectBtn').disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/delete_project/${encodeURIComponent(selectedProject)}`, {
|
||||
method: 'POST' // Backend route expects POST
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.message || `Failed to delete project (HTTP ${response.status})`);
|
||||
}
|
||||
|
||||
showDeleteProjectStatus(`Project '${selectedProject}' deleted successfully.`, 'success', false);
|
||||
// Give user time to read the success message before closing modal
|
||||
setTimeout(() => {
|
||||
// Close the modal
|
||||
const manageModalElem = document.getElementById('manageFilesModal');
|
||||
const manageModalInstance = bootstrap.Modal.getInstance(manageModalElem);
|
||||
if (manageModalInstance) {
|
||||
manageModalInstance.hide();
|
||||
}
|
||||
|
||||
// Remove the project from the selector
|
||||
const selector = document.getElementById('projectSelector');
|
||||
let newSelectedIndex = -1;
|
||||
for (let i = 0; i < selector.options.length; i++) {
|
||||
if (selector.options[i].value === selectedProject) {
|
||||
// Determine next index to select (previous or first)
|
||||
newSelectedIndex = (i > 0) ? i - 1 : 0;
|
||||
selector.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Select the next/previous/first remaining project or clear UI
|
||||
if (selector.options.length > 0) {
|
||||
selector.selectedIndex = newSelectedIndex >= 0 ? newSelectedIndex : 0;
|
||||
// Manually trigger the change handler for the new selection
|
||||
handleProjectChange();
|
||||
} else {
|
||||
// No projects left
|
||||
selector.innerHTML = '<option value="" disabled>No projects found</option>';
|
||||
selectedProject = null;
|
||||
selectedProjectName = null; // Clear global state var too
|
||||
document.querySelectorAll('.project-name-display').forEach(span => span.textContent = 'No Projects');
|
||||
updateStatusDisplay('No projects available.', '...', 'N/A');
|
||||
clearAllProjectSpecificUI(); // Clear charts/tables
|
||||
manageFilesBtn.disabled = true; // Disable manage files button
|
||||
}
|
||||
}, 1500); // Delay closing modal
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting project:', error);
|
||||
showDeleteProjectStatus(`Error: ${error.message}`, 'danger', false);
|
||||
// Re-enable button on error
|
||||
document.getElementById('deleteProjectBtn').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function showDeleteProjectStatus(message, type = 'info', autoClear = true) {
|
||||
const statusDiv = document.getElementById('deleteProjectStatus');
|
||||
if (!statusDiv) return;
|
||||
statusDiv.className = `mt-2 text-${type}`;
|
||||
if (type === 'danger' || type === 'warning' || type === 'success') {
|
||||
statusDiv.className += ' fw-bold';
|
||||
}
|
||||
statusDiv.textContent = message;
|
||||
statusDiv.style.display = 'block';
|
||||
|
||||
if (autoClear) {
|
||||
// Clear previous timeouts if any
|
||||
if (statusDiv.timeoutId) clearTimeout(statusDiv.timeoutId);
|
||||
statusDiv.timeoutId = setTimeout(() => { statusDiv.style.display = 'none'; statusDiv.timeoutId = null; }, 6000);
|
||||
}
|
||||
}
|
||||
|
||||
// --- NEW: Handle Manifest Upload Form Submission ---
|
||||
async function handleManifestUploadSubmit(event) {
|
||||
event.preventDefault();
|
||||
if (!selectedProject) return;
|
||||
|
||||
const form = event.target;
|
||||
const formData = new FormData(form);
|
||||
const fileInput = document.getElementById('newManifestFile');
|
||||
const statusDiv = document.getElementById('uploadManifestStatus');
|
||||
|
||||
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
|
||||
showUploadManifestStatus('Please select a manifest CSV file to upload.', 'warning');
|
||||
return;
|
||||
}
|
||||
const manifestFile = fileInput.files[0];
|
||||
if (!manifestFile.name.toLowerCase().endsWith('.csv')) {
|
||||
showUploadManifestStatus('Invalid file type. Please select a .csv file.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Uploading new manifest for project ${selectedProject}`);
|
||||
clearManageFilesStatusMessages();
|
||||
showUploadManifestStatus('Uploading manifest...', 'info', false);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/upload_manifest/${encodeURIComponent(selectedProject)}`, {
|
||||
method: 'POST', // Backend route expects POST
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.message || `Failed to upload manifest (HTTP ${response.status})`);
|
||||
}
|
||||
|
||||
showUploadManifestStatus(data.message || 'Manifest updated successfully.', 'success');
|
||||
form.reset(); // Clear the file input
|
||||
// Recommend triggering analysis
|
||||
showAnalysisTriggerStatus('Manifest updated. Trigger analysis to see changes.', 'info');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error uploading manifest:', error);
|
||||
showUploadManifestStatus(`Upload failed: ${error.message}`, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
function showUploadManifestStatus(message, type = 'info', autoClear = true) {
|
||||
const statusDiv = document.getElementById('uploadManifestStatus');
|
||||
if (!statusDiv) return;
|
||||
statusDiv.className = `mt-2 text-${type}`;
|
||||
if (type === 'danger' || type === 'warning' || type === 'success') {
|
||||
statusDiv.className += ' fw-bold';
|
||||
}
|
||||
statusDiv.textContent = message;
|
||||
statusDiv.style.display = 'block';
|
||||
|
||||
if (autoClear) {
|
||||
if (statusDiv.timeoutId) clearTimeout(statusDiv.timeoutId);
|
||||
statusDiv.timeoutId = setTimeout(() => { statusDiv.style.display = 'none'; statusDiv.timeoutId = null; }, 5000);
|
||||
}
|
||||
}
|
||||
@ -32,6 +32,12 @@
|
||||
Add Project
|
||||
</button>
|
||||
</div>
|
||||
<!-- Manage Files Button -->
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-info" id="manageFilesBtn" data-bs-toggle="modal" data-bs-target="#manageFilesModal" disabled>
|
||||
Manage Files
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation between views (remains the same) -->
|
||||
@ -149,7 +155,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="repoUrl" class="form-label">Git Repository URL</label>
|
||||
<input type="url" class="form-control" id="repoUrl" name="repoUrl" required>
|
||||
<div class="form-text">The URL will be used for cloning after restart.</div>
|
||||
<!-- <div class="form-text">The URL will be used for cloning.</div> --> <!-- Commented out as cloning happens immediately -->
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="manifestFile" class="form-label">Manifest CSV File</label>
|
||||
@ -160,7 +166,7 @@
|
||||
<input class="form-control" type="file" id="pdfFiles" name="pdfFiles" accept=".pdf" multiple required>
|
||||
</div>
|
||||
<div id="addProjectStatus" class="mt-3" style="display: none;"></div>
|
||||
<button type="submit" class="btn btn-primary">Add Project & Prepare for Restart</button>
|
||||
<button type="submit" class="btn btn-primary">Add Project</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -168,13 +174,88 @@
|
||||
</div>
|
||||
<!-- End Add Project Modal -->
|
||||
|
||||
<!-- NEW: Manage Project Files Modal -->
|
||||
<div class="modal fade" id="manageFilesModal" tabindex="-1" aria-labelledby="manageFilesModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="manageFilesModalLabel">Manage Files for Project: <span class="project-name-display"></span></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Existing Files List -->
|
||||
<h6>Existing Drawing PDFs:</h6>
|
||||
<div id="existingPdfList" class="list-group mb-3" style="max-height: 200px; overflow-y: auto;">
|
||||
<!-- Files will be listed here by JS -->
|
||||
<span class="text-muted">Loading files...</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Upload New Files Form -->
|
||||
<h6>Upload New PDF Files:</h6>
|
||||
<form id="uploadPdfsForm" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="file" id="newPdfFiles" name="pdfFiles" accept=".pdf" multiple required>
|
||||
<div class="form-text">Select one or more PDF files to add to the project.</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">Upload Selected PDFs</button>
|
||||
<div id="uploadStatus" class="mt-2" style="display: none;"></div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- NEW: Upload/Replace Manifest -->
|
||||
<h6>Update Manifest File:</h6>
|
||||
<form id="uploadManifestForm" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label for="newManifestFile" class="form-label">Select new manifest.csv</label>
|
||||
<input class="form-control" type="file" id="newManifestFile" name="manifestFile" accept=".csv" required>
|
||||
<div class="form-text">This will replace the existing manifest for the project.</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Update Manifest</button>
|
||||
<div id="uploadManifestStatus" class="mt-2" style="display: none;"></div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Trigger Analysis -->
|
||||
<h6>Manual Analysis:</h6>
|
||||
<p class="form-text">After deleting or uploading files, or updating the manifest, you should manually trigger an analysis to update the progress metrics.</p>
|
||||
<button type="button" id="triggerAnalysisBtn" class="btn btn-warning">Trigger Project Analysis</button>
|
||||
<div id="analysisTriggerStatus" class="mt-2" style="display: none;"></div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- NEW: Delete Project Section -->
|
||||
<h6>Delete Project (Warning: Irreversible)</h6>
|
||||
<p class="form-text text-danger">Deleting the project will remove all associated files (Manifest, PDFs, Extracted Text, Cloned Repo) from the server permanently.</p>
|
||||
<button type="button" id="deleteProjectBtn" class="btn btn-danger">Delete This Project</button>
|
||||
<div id="deleteProjectStatus" class="mt-2" style="display: none;"></div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div id="manageFilesStatus" class="mt-3 alert" style="display: none;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Manage Project Files Modal -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- Pass initial data to JavaScript -->
|
||||
<script>
|
||||
// Render Jinja data into intermediate JS variables
|
||||
const initialProjectsJson = {{ projects | tojson }};
|
||||
const initialStatusJson = {{ initial_statuses | tojson }};
|
||||
|
||||
// Embed initial data directly into the page for faster initial load
|
||||
const initialServerData = {
|
||||
projects: {{ projects | tojson }},
|
||||
status: {{ initial_statuses | tojson }}
|
||||
projects: initialProjectsJson,
|
||||
status: initialStatusJson
|
||||
// Note: Full progress data is not embedded initially, fetched via SSE
|
||||
};
|
||||
</script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user