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:
root 2025-04-10 02:50:00 +00:00
parent 12a85c2fc3
commit 952ab3a906
34 changed files with 5025 additions and 4091 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

373
app.py
View File

@ -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 ---

View File

@ -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:

View File

@ -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

View File

@ -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}")

View File

@ -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

View File

@ -472,4 +472,4 @@ PS4_10_VFD110HP300 FPM
PS4_10_TPE2
PS4_11_TPE1
PS4_12_JPE1
MCM01
MCM01

View File

@ -398,4 +398,4 @@ PS7_12_VFD13HP300 FPM
PS7_12_JPE1PS7_13_VFD13HP300 FPMPS7_14_TPE1
RGPS7_14_SS1PS7_14_FIO1
BBA
MCM02
MCM02

View File

@ -178,4 +178,4 @@ RBB
GNCP1_8_S1
GNCP1_8_S2
NCP1_8_TPE1NCP1_8_FIO1
MCM03
MCM03

View File

@ -1,2 +1,2 @@
ProjectName: MTN6_Test
ProjectName: MTN6_with_mcm02
RepoURL: http://192.168.5.191:3000/ilia-gurielidze-autstand/MTN6_SCADA.git

View File

@ -0,0 +1,2 @@
ProjectName: MTN6_with_mcm04
RepoURL: http://192.168.5.191:3000/ilia-gurielidze-autstand/MTN6_SCADA.git

View File

@ -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);
}
}

View File

@ -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>