Update probe related models, routes and templates
This commit is contained in:
@@ -93,6 +93,23 @@ class Probe:
|
||||
raise Exception('Failed to create probe')
|
||||
return cls(**result.data[0])
|
||||
|
||||
def retire(self):
|
||||
"""Mark probe as retired with current timestamp"""
|
||||
supabase = get_supabase()
|
||||
result = supabase.table('probes').update({
|
||||
'retired_at': datetime.utcnow().isoformat()
|
||||
}).eq('id', self.id).execute()
|
||||
if not result.data:
|
||||
raise Exception('Failed to retire probe')
|
||||
self.retired_at = datetime.utcnow()
|
||||
|
||||
def delete(self):
|
||||
"""Delete probe from database"""
|
||||
supabase = get_supabase()
|
||||
result = supabase.table('probes').delete().eq('id', self.id).execute()
|
||||
if not result.data:
|
||||
raise Exception('Failed to delete probe')
|
||||
|
||||
@dataclass
|
||||
class Channel:
|
||||
"""Channel model representing individual measurement channels"""
|
||||
@@ -131,6 +148,19 @@ class Channel:
|
||||
channels.append(cls(**row))
|
||||
return channels
|
||||
|
||||
@classmethod
|
||||
def get_by_serial(cls, serial_number: str):
|
||||
"""Get channel by serial number if it exists (case-sensitive exact match)"""
|
||||
serial_upper = serial_number.upper()
|
||||
supabase = get_supabase()
|
||||
result = supabase.table('channels').select("*").eq('serial_number', serial_upper).execute()
|
||||
if not result.data:
|
||||
return None
|
||||
row = result.data[0]
|
||||
# Parse datetime string
|
||||
row['created_at'] = datetime.fromisoformat(row['created_at']) if row['created_at'] else None
|
||||
return cls(**row)
|
||||
|
||||
@classmethod
|
||||
def create(cls, probe_id: str, serial_number: str, parameter_id: str):
|
||||
"""Create a new channel"""
|
||||
@@ -259,6 +289,25 @@ class Calibration:
|
||||
result = supabase.table('calibrations').select("*").eq('work_order_id', work_order_id).execute()
|
||||
return [cls(**row) for row in result.data] if result.data else []
|
||||
|
||||
@classmethod
|
||||
def get_by_channel(cls, channel_id: str):
|
||||
"""Get all calibrations for a specific channel"""
|
||||
supabase = get_supabase()
|
||||
result = supabase.table('calibrations').select("*").eq('channel_id', channel_id).execute()
|
||||
calibrations = []
|
||||
for row in result.data if result.data else []:
|
||||
try:
|
||||
# Parse datetime strings
|
||||
for date_field in ['std_cal_date', 'std_cal_due', 'date']:
|
||||
if row.get(date_field):
|
||||
row[date_field] = datetime.fromisoformat(row[date_field])
|
||||
|
||||
calibrations.append(cls(**row))
|
||||
except Exception as e:
|
||||
print(f"Error parsing calibration data: {e}")
|
||||
continue
|
||||
return calibrations
|
||||
|
||||
@dataclass
|
||||
class Customer:
|
||||
"""Customer model representing clients who request calibrations"""
|
||||
|
||||
@@ -162,9 +162,14 @@ def review_calibration(calibration_id):
|
||||
flash('Calibration not found', 'error')
|
||||
return redirect(url_for('calibrations.index'))
|
||||
|
||||
user = {
|
||||
'user_name': session.get('user_name'),
|
||||
'can_calibrate': session.get('can_calibrate'),
|
||||
'can_review': session.get('can_review')
|
||||
}
|
||||
return render_template('calibration_review.html',
|
||||
calibration=calibration.data[0],
|
||||
user=session)
|
||||
user=user)
|
||||
|
||||
@calibrations_bp.route('/new')
|
||||
def new_calibration():
|
||||
@@ -189,13 +194,18 @@ def new_calibration():
|
||||
# Get all parameters (using correct column name)
|
||||
parameters = supabase.table('parameters').select('id, parameter_name').execute()
|
||||
|
||||
user = {
|
||||
'user_name': session.get('user_name'),
|
||||
'can_calibrate': session.get('can_calibrate'),
|
||||
'can_review': session.get('can_review')
|
||||
}
|
||||
return render_template('calibration_form.html',
|
||||
work_orders=work_orders.data,
|
||||
standards=standards.data,
|
||||
channels=channels.data,
|
||||
probe_models=probe_models.data,
|
||||
parameters=parameters.data,
|
||||
user=session)
|
||||
user=user)
|
||||
|
||||
@calibrations_bp.route('/filtered-channels')
|
||||
def get_filtered_channels():
|
||||
@@ -248,6 +258,11 @@ def index():
|
||||
calibrated_by:calibrated_by(name)
|
||||
''').order('date', desc=True).limit(10).execute()
|
||||
|
||||
user = {
|
||||
'user_name': session.get('user_name'),
|
||||
'can_calibrate': session.get('can_calibrate'),
|
||||
'can_review': session.get('can_review')
|
||||
}
|
||||
return render_template('calibration_dashboard.html',
|
||||
summary={
|
||||
'total_calibrations': total,
|
||||
@@ -255,7 +270,7 @@ def index():
|
||||
'failed_calibrations': failed
|
||||
},
|
||||
recent_calibrations=recent_calibrations.data,
|
||||
user=session)
|
||||
user=user)
|
||||
|
||||
@calibrations_bp.route('/probe/<probe_id>')
|
||||
def calibrations_for_probe(probe_id):
|
||||
@@ -278,10 +293,15 @@ def calibrations_for_probe(probe_id):
|
||||
# Get probe details
|
||||
probe = supabase.table('probes').select('*').eq('id', probe_id).execute()
|
||||
|
||||
user = {
|
||||
'user_name': session.get('user_name'),
|
||||
'can_calibrate': session.get('can_calibrate'),
|
||||
'can_review': session.get('can_review')
|
||||
}
|
||||
return render_template('calibration_history.html',
|
||||
calibrations=calibrations.data,
|
||||
probe=probe.data[0] if probe.data else None,
|
||||
user=session)
|
||||
user=user)
|
||||
|
||||
@calibrations_bp.route('/probe/<probe_id>/trends')
|
||||
def calibration_trends(probe_id):
|
||||
@@ -344,6 +364,11 @@ def view_calibration(calibration_id):
|
||||
flash('Calibration not found', 'error')
|
||||
return redirect(url_for('calibrations.index'))
|
||||
|
||||
user = {
|
||||
'user_name': session.get('user_name'),
|
||||
'can_calibrate': session.get('can_calibrate'),
|
||||
'can_review': session.get('can_review')
|
||||
}
|
||||
return render_template('calibration_view.html',
|
||||
calibration=calibration.data[0],
|
||||
user=session)
|
||||
user=user)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, session
|
||||
from app.models import Channel, Calibration
|
||||
from app.routes.auth import role_required
|
||||
|
||||
@@ -16,6 +16,12 @@ def view_channel(channel_id):
|
||||
# Get calibration history for this channel
|
||||
calibrations = Calibration.get_by_channel(channel_id)
|
||||
|
||||
user = {
|
||||
'user_name': session.get('user_name'),
|
||||
'can_calibrate': session.get('can_calibrate'),
|
||||
'can_review': session.get('can_review')
|
||||
}
|
||||
return render_template('channel_view.html',
|
||||
channel=channel,
|
||||
calibrations=calibrations)
|
||||
calibrations=calibrations,
|
||||
user=user)
|
||||
|
||||
@@ -1,20 +1,48 @@
|
||||
import re
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, session
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify
|
||||
from app.models import Probe, ProbeModel, Channel, Parameter, ProbeLocation
|
||||
from app.routes.auth import role_required
|
||||
|
||||
probes_bp = Blueprint('probes', __name__)
|
||||
|
||||
@probes_bp.route('/api/check-channel-serials', methods=['POST'], endpoint='check_channel_serials')
|
||||
@role_required('review')
|
||||
def check_channel_serials():
|
||||
"""Check if channel serials already exist in database"""
|
||||
data = request.get_json()
|
||||
if not data or 'serials' not in data:
|
||||
return jsonify({'error': 'Invalid request'}), 400
|
||||
|
||||
duplicates = []
|
||||
for serial in data['serials']:
|
||||
if Channel.get_by_serial(serial):
|
||||
duplicates.append(serial)
|
||||
|
||||
return jsonify({'duplicates': duplicates})
|
||||
|
||||
@probes_bp.route('/')
|
||||
@role_required('review')
|
||||
def list_probes():
|
||||
"""List all probes with optional active filter"""
|
||||
active_only = request.args.get('active_only', 'true').lower() == 'true'
|
||||
probes = Probe.get_all(active_only=active_only)
|
||||
|
||||
# Get channels for each probe
|
||||
probes_with_channels = []
|
||||
for probe in probes:
|
||||
probe_dict = probe.__dict__
|
||||
probe_dict['channels'] = Channel.get_by_probe(probe.id)
|
||||
probes_with_channels.append(probe_dict)
|
||||
|
||||
user = {
|
||||
'user_name': session.get('user_name'),
|
||||
'can_calibrate': session.get('can_calibrate'),
|
||||
'can_review': session.get('can_review')
|
||||
}
|
||||
return render_template('probe_list.html',
|
||||
probes=probes,
|
||||
probes=probes_with_channels,
|
||||
active_only=active_only,
|
||||
user=session)
|
||||
user=user)
|
||||
|
||||
@probes_bp.route('/new')
|
||||
@role_required('review')
|
||||
@@ -22,36 +50,51 @@ def new_probe():
|
||||
"""Display form to create new probe"""
|
||||
probe_models = ProbeModel.get_all()
|
||||
parameters = Parameter.get_all()
|
||||
user = {
|
||||
'user_name': session.get('user_name'),
|
||||
'can_calibrate': session.get('can_calibrate'),
|
||||
'can_review': session.get('can_review')
|
||||
}
|
||||
return render_template('probe_form.html',
|
||||
probe_models=probe_models,
|
||||
parameters=parameters,
|
||||
user=session)
|
||||
user=user)
|
||||
|
||||
@probes_bp.route('/', methods=['POST'])
|
||||
@role_required('review')
|
||||
def create_probe():
|
||||
"""Handle new probe creation with channels"""
|
||||
try:
|
||||
# First create the probe
|
||||
probe = Probe.create(
|
||||
model_id=request.form['model_id'],
|
||||
serial_number=request.form['serial_number'],
|
||||
description='' # Empty description since field was removed
|
||||
)
|
||||
|
||||
# Then create channels if provided
|
||||
# First validate all channels
|
||||
channel_serials = request.form.getlist('channel_serials[]')
|
||||
parameter_ids = request.form.getlist('parameter_ids[]')
|
||||
|
||||
if channel_serials and parameter_ids and len(channel_serials) == len(parameter_ids):
|
||||
for i in range(len(channel_serials)):
|
||||
# Validate channel serial format [0-9A-F]{16}
|
||||
if not re.fullmatch(r'^[0-9A-F]{16}$', channel_serials[i].upper()):
|
||||
serial_upper = channel_serials[i].upper()
|
||||
if not re.fullmatch(r'^[0-9A-F]{16}$', serial_upper):
|
||||
raise ValueError(f'Invalid channel serial format: {channel_serials[i]}. Must be 16 hex characters (0-9, A-F)')
|
||||
|
||||
# Check for duplicate serial
|
||||
existing_channel = Channel.get_by_serial(serial_upper)
|
||||
if existing_channel:
|
||||
raise ValueError(f'Channel serial already exists: {channel_serials[i]}')
|
||||
|
||||
# Only create probe if all channels are valid
|
||||
probe = Probe.create(
|
||||
model_id=request.form['model_id'],
|
||||
serial_number='', # Default empty string since field was removed from form
|
||||
description='' # Empty description since field was removed
|
||||
)
|
||||
|
||||
# Create channels after successful probe creation
|
||||
if channel_serials and parameter_ids and len(channel_serials) == len(parameter_ids):
|
||||
for i in range(len(channel_serials)):
|
||||
serial_upper = channel_serials[i].upper()
|
||||
Channel.create(
|
||||
probe_id=probe.id,
|
||||
serial_number=channel_serials[i].upper(), # Store in uppercase
|
||||
serial_number=serial_upper,
|
||||
parameter_id=parameter_ids[i]
|
||||
)
|
||||
|
||||
@@ -79,8 +122,104 @@ def view_probe(probe_id):
|
||||
# Get location history
|
||||
locations = ProbeLocation.get_by_probe(probe_id)
|
||||
|
||||
# Get parameters for channel form
|
||||
parameters = Parameter.get_all()
|
||||
|
||||
user = {
|
||||
'user_name': session.get('user_name'),
|
||||
'can_calibrate': session.get('can_calibrate'),
|
||||
'can_review': session.get('can_review')
|
||||
}
|
||||
return render_template('probe_view.html',
|
||||
probe=probe,
|
||||
channels=channels,
|
||||
locations=locations,
|
||||
user=session)
|
||||
parameters=parameters,
|
||||
user=user)
|
||||
|
||||
@probes_bp.route('/<probe_id>/channels', methods=['POST'])
|
||||
@role_required('review')
|
||||
def add_channel(probe_id):
|
||||
"""Add a channel to an existing probe"""
|
||||
try:
|
||||
probe = Probe.get_by_id(probe_id)
|
||||
if not probe:
|
||||
flash('Probe not found', 'danger')
|
||||
return redirect(url_for('probes.list_probes'))
|
||||
|
||||
if probe.retired_at:
|
||||
flash('Cannot add channels to retired probe', 'danger')
|
||||
return redirect(url_for('probes.view_probe', probe_id=probe_id))
|
||||
|
||||
serial = request.form.get('channel_serial', '').upper()
|
||||
parameter_id = request.form.get('parameter_id')
|
||||
|
||||
# Validate channel serial format [0-9A-F]{16}
|
||||
if not re.fullmatch(r'^[0-9A-F]{16}$', serial):
|
||||
raise ValueError(f'Invalid channel serial format: {serial}. Must be 16 hex characters (0-9, A-F)')
|
||||
|
||||
# Validate parameter_id exists and is valid
|
||||
if not parameter_id:
|
||||
raise ValueError('Parameter ID is required')
|
||||
if not Parameter.get_by_id(parameter_id):
|
||||
raise ValueError('Invalid parameter ID')
|
||||
|
||||
# Check for duplicate serial
|
||||
existing_channel = Channel.get_by_serial(serial)
|
||||
if existing_channel:
|
||||
raise ValueError(f'Channel serial already exists: {serial}')
|
||||
|
||||
# Create channel
|
||||
Channel.create(
|
||||
probe_id=probe_id,
|
||||
serial_number=serial,
|
||||
parameter_id=parameter_id
|
||||
)
|
||||
|
||||
flash('Channel added successfully', 'success')
|
||||
return redirect(url_for('probes.view_probe', probe_id=probe_id))
|
||||
except ValueError as ve:
|
||||
flash(f'Validation error: {str(ve)}', 'danger')
|
||||
return redirect(url_for('probes.view_probe', probe_id=probe_id))
|
||||
except Exception as e:
|
||||
flash(f'Error adding channel: {str(e)}', 'danger')
|
||||
return redirect(url_for('probes.view_probe', probe_id=probe_id))
|
||||
|
||||
@probes_bp.route('/<probe_id>/retire', methods=['POST'])
|
||||
@role_required('review')
|
||||
def retire_probe(probe_id):
|
||||
"""Mark a probe as retired"""
|
||||
try:
|
||||
probe = Probe.get_by_id(probe_id)
|
||||
if not probe:
|
||||
flash('Probe not found', 'danger')
|
||||
return redirect(url_for('probes.list_probes'))
|
||||
|
||||
if probe.retired_at:
|
||||
flash('Probe is already retired', 'warning')
|
||||
return redirect(url_for('probes.view_probe', probe_id=probe_id))
|
||||
|
||||
probe.retire()
|
||||
flash('Probe retired successfully', 'success')
|
||||
return redirect(url_for('probes.view_probe', probe_id=probe_id))
|
||||
except Exception as e:
|
||||
flash(f'Error retiring probe: {str(e)}', 'danger')
|
||||
return redirect(url_for('probes.view_probe', probe_id=probe_id))
|
||||
|
||||
@probes_bp.route('/<probe_id>', methods=['DELETE'])
|
||||
@role_required('review')
|
||||
def delete_probe(probe_id):
|
||||
"""Delete a probe (only if it has no channels)"""
|
||||
try:
|
||||
probe = Probe.get_by_id(probe_id)
|
||||
if not probe:
|
||||
return jsonify({'error': 'Probe not found'}), 404
|
||||
|
||||
channels = Channel.get_by_probe(probe_id)
|
||||
if channels:
|
||||
return jsonify({'error': 'Cannot delete probe with channels'}), 400
|
||||
|
||||
probe.delete()
|
||||
return jsonify({'message': 'Probe deleted successfully'}), 200
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@@ -13,11 +13,6 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="serial_number" class="form-label">Serial Number</label>
|
||||
<input type="text" class="form-control" id="serial_number" name="serial_number" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<h4>Channels</h4>
|
||||
<div id="channels-container">
|
||||
@@ -41,9 +36,75 @@
|
||||
<button type="button" class="btn btn-secondary" id="add-channel">Add Another Channel</button>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Create Probe</button>
|
||||
<button type="submit" class="btn btn-primary" id="create-probe">Create Probe</button>
|
||||
|
||||
<script>
|
||||
// Channel serial validation regex
|
||||
const CHANNEL_SERIAL_REGEX = /^[0-9A-F]{16}$/i;
|
||||
|
||||
async function validateChannelSerials() {
|
||||
const serialInputs = document.querySelectorAll('input[name="channel_serials[]"]');
|
||||
const serials = new Set();
|
||||
let hasValidChannel = false;
|
||||
|
||||
// First validate format and duplicates in form
|
||||
for (const input of serialInputs) {
|
||||
const serial = input.value.trim().toUpperCase();
|
||||
if (!serial) continue;
|
||||
|
||||
if (!CHANNEL_SERIAL_REGEX.test(serial)) {
|
||||
input.classList.add('is-invalid');
|
||||
alert(`Invalid channel serial format: ${serial}. Must be 16 hex characters (0-9, A-F)`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (serials.has(serial)) {
|
||||
input.classList.add('is-invalid');
|
||||
alert(`Duplicate channel serial in form: ${serial}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
serials.add(serial);
|
||||
input.classList.remove('is-invalid');
|
||||
hasValidChannel = true;
|
||||
}
|
||||
|
||||
if (!hasValidChannel) {
|
||||
alert('At least one channel must have a valid serial number');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for duplicates in database
|
||||
try {
|
||||
const response = await fetch('/probes/api/check-channel-serials', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({serials: Array.from(serials)})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.duplicates && data.duplicates.length > 0) {
|
||||
alert(`Channel serial already exists in database: ${data.duplicates[0]}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking channel serials:', error);
|
||||
alert('Error validating channel serials. Please try again.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
document.getElementById('create-probe').addEventListener('click', async function(e) {
|
||||
e.preventDefault();
|
||||
if (await validateChannelSerials()) {
|
||||
this.form.submit();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('add-channel').addEventListener('click', function() {
|
||||
const container = document.getElementById('channels-container');
|
||||
const count = container.children.length;
|
||||
@@ -61,7 +122,11 @@
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="channel_serial_${count}" class="form-label">Channel Serial</label>
|
||||
<input type="text" class="form-control" id="channel_serial_${count}" name="channel_serials[]" required>
|
||||
<input type="text" class="form-control" id="channel_serial_${count}" name="channel_serials[]" required
|
||||
pattern="[0-9A-F]{16}" title="16-character hex serial number">
|
||||
<div class="invalid-feedback">
|
||||
Must be a 16-character hex number (0-9, A-F)
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto d-flex align-items-end">
|
||||
<button type="button" class="btn btn-danger remove-channel">Remove</button>
|
||||
|
||||
@@ -24,41 +24,44 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Model</th>
|
||||
<th>Serial Number</th>
|
||||
<th>Description</th>
|
||||
<th>Created</th>
|
||||
<th>Status</th>
|
||||
<th>Retired</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set current_model = none %}
|
||||
{% for probe in probes|sort(attribute='model_name') %}
|
||||
{% if probe.model_name != current_model %}
|
||||
<tr class="table-info">
|
||||
<td colspan="6">
|
||||
<strong>{{ probe.model_name }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{% set current_model = probe.model_name %}
|
||||
{% endif %}
|
||||
<tr class="probe-row" data-active="{{ 'true' if not probe.retired_at else 'false' }}">
|
||||
<td></td> <!-- Empty cell under model header -->
|
||||
<td>{{ probe.serial_number }}</td>
|
||||
<td>{{ probe.description }}</td>
|
||||
<td>{{ probe.model_name }}</td>
|
||||
<td>{{ probe.created_at.strftime('%Y-%m-%d') }}</td>
|
||||
<td>
|
||||
{% if probe.retired_at %}
|
||||
<span class="badge bg-secondary">Retired</span>
|
||||
<span class="badge bg-secondary">Yes</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
<span class="badge bg-success">No</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('probes.view_probe', probe_id=probe.id) }}"
|
||||
class="btn btn-sm btn-outline-primary">View</a>
|
||||
class="btn btn-sm btn-outline-primary">View/Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% if probe.channels %}
|
||||
{% for channel in probe.channels %}
|
||||
<tr class="channel-row">
|
||||
<td colspan="2">
|
||||
<small class="text-muted">
|
||||
Channel: {{ channel.parameter.parameter_name }} ({{ channel.serial_number }})
|
||||
</small>
|
||||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="{{ url_for('channels.view_channel', channel_id=channel.id) }}"
|
||||
class="btn btn-sm btn-outline-secondary">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -65,6 +65,33 @@
|
||||
<div class="alert alert-info mt-4">No channels found for this probe</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not probe.retired_at %}
|
||||
<div class="mt-4">
|
||||
<h5>Add Channel</h5>
|
||||
<form method="POST" action="{{ url_for('probes.add_channel', probe_id=probe.id) }}">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="parameter_id" class="form-label">Parameter</label>
|
||||
<select class="form-select" id="parameter_id" name="parameter_id" required>
|
||||
{% for param in parameters %}
|
||||
<option value="{{ param.id }}">{{ param.parameter_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="channel_serial" class="form-label">Channel Serial</label>
|
||||
<input type="text" class="form-control" id="channel_serial" name="channel_serial"
|
||||
required pattern="[0-9A-F]{16}" title="16-character hex serial number">
|
||||
<div class="invalid-feedback">
|
||||
Must be a 16-character hex number (0-9, A-F)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mt-3">Add Channel</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if locations %}
|
||||
<div class="mt-4">
|
||||
<h5>Location History</h5>
|
||||
@@ -102,7 +129,41 @@
|
||||
<div class="mt-4">
|
||||
<a href="{{ url_for('probes.list_probes') }}"
|
||||
class="btn btn-outline-secondary">Back to List</a>
|
||||
|
||||
{% if not probe.retired_at %}
|
||||
<form method="POST" action="{{ url_for('probes.retire_probe', probe_id=probe.id) }}" class="d-inline">
|
||||
<button type="submit" class="btn btn-warning ms-2">Retire Probe</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if not channels %}
|
||||
<button class="btn btn-danger ms-2" id="delete-probe">Delete Probe</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('delete-probe')?.addEventListener('click', async function() {
|
||||
if (confirm('Are you sure you want to delete this probe?')) {
|
||||
try {
|
||||
const response = await fetch("{{ url_for('probes.delete_probe', probe_id=probe.id) }}", {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.href = "{{ url_for('probes.list_probes') }}";
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.error || 'Failed to delete probe');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error deleting probe: ' + error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user