Auto-commit local changes before build (2026-01-06 20:50:50)
This commit is contained in:
parent
92716dc13c
commit
71752fb926
@ -1 +1 @@
|
||||
v20260106-22-ticket-link-multiple-jobs
|
||||
v20260106-24-ticket-scope-resolve-popup
|
||||
|
||||
@ -22,14 +22,19 @@ def api_job_run_alerts(run_id: int):
|
||||
db.session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT t.id, t.ticket_code, t.description, t.start_date, t.resolved_at, t.active_from_date
|
||||
SELECT t.id,
|
||||
t.ticket_code,
|
||||
t.description,
|
||||
t.start_date,
|
||||
COALESCE(ts.resolved_at, t.resolved_at) AS resolved_at,
|
||||
t.active_from_date
|
||||
FROM tickets t
|
||||
JOIN ticket_scopes ts ON ts.ticket_id = t.id
|
||||
WHERE ts.job_id = :job_id
|
||||
AND t.active_from_date <= :run_date
|
||||
AND (
|
||||
t.resolved_at IS NULL
|
||||
OR ((t.resolved_at AT TIME ZONE 'UTC' AT TIME ZONE :ui_tz)::date) >= :run_date
|
||||
COALESCE(ts.resolved_at, t.resolved_at) IS NULL
|
||||
OR ((COALESCE(ts.resolved_at, t.resolved_at) AT TIME ZONE 'UTC' AT TIME ZONE :ui_tz)::date) >= :run_date
|
||||
)
|
||||
ORDER BY t.start_date DESC
|
||||
"""
|
||||
@ -169,7 +174,7 @@ def api_tickets():
|
||||
"active_from_date": str(getattr(t, "active_from_date", "") or ""),
|
||||
"start_date": _format_datetime(t.start_date),
|
||||
"resolved_at": _format_datetime(t.resolved_at) if t.resolved_at else "",
|
||||
"active": t.resolved_at is None,
|
||||
"active": (t.resolved_at is None and TicketScope.query.filter_by(ticket_id=t.id, resolved_at=None).first() is not None),
|
||||
}
|
||||
)
|
||||
return jsonify({"status": "ok", "tickets": items})
|
||||
@ -205,42 +210,12 @@ def api_tickets():
|
||||
return jsonify({"status": "error", "message": "Invalid ticket_code format. Expected TYYYYMMDD.####."}), 400
|
||||
|
||||
existing = Ticket.query.filter_by(ticket_code=ticket_code).first()
|
||||
is_new = existing is None
|
||||
|
||||
# If the ticket already exists, link it to this job/run (a single ticket number can apply to multiple jobs).
|
||||
# Create new ticket if it doesn't exist; otherwise reuse the existing one so the same
|
||||
# ticket number can be linked to multiple jobs and job runs.
|
||||
if existing:
|
||||
ticket = existing
|
||||
try:
|
||||
# Ensure there is at least one job scope for this ticket/job combination.
|
||||
if job and job.id:
|
||||
scope = TicketScope.query.filter_by(
|
||||
ticket_id=ticket.id,
|
||||
scope_type="job",
|
||||
job_id=job.id,
|
||||
).first()
|
||||
if not scope:
|
||||
scope = TicketScope(
|
||||
ticket_id=ticket.id,
|
||||
scope_type="job",
|
||||
customer_id=job.customer_id,
|
||||
backup_software=job.backup_software,
|
||||
backup_type=job.backup_type,
|
||||
job_id=job.id,
|
||||
job_name_match=job.job_name,
|
||||
job_name_match_mode="exact",
|
||||
)
|
||||
db.session.add(scope)
|
||||
|
||||
# Link the ticket to this specific run (idempotent due to unique constraint).
|
||||
existing_link = TicketJobRun.query.filter_by(ticket_id=ticket.id, job_run_id=run.id).first()
|
||||
if not existing_link:
|
||||
link = TicketJobRun(ticket_id=ticket.id, job_run_id=run.id, link_source="manual")
|
||||
db.session.add(link)
|
||||
|
||||
db.session.commit()
|
||||
except Exception as exc:
|
||||
db.session.rollback()
|
||||
return jsonify({"status": "error", "message": str(exc) or "Failed to link ticket."}), 500
|
||||
|
||||
else:
|
||||
ticket = Ticket(
|
||||
ticket_code=ticket_code,
|
||||
@ -251,11 +226,17 @@ def api_tickets():
|
||||
resolved_at=None,
|
||||
)
|
||||
|
||||
try:
|
||||
try:
|
||||
if is_new:
|
||||
db.session.add(ticket)
|
||||
db.session.flush()
|
||||
|
||||
# Minimal scope from job
|
||||
# Ensure a scope exists for this job so alerts/popups can show the ticket code.
|
||||
scope = None
|
||||
if job and job.id:
|
||||
scope = TicketScope.query.filter_by(ticket_id=ticket.id, scope_type="job", job_id=job.id).first()
|
||||
|
||||
if not scope:
|
||||
scope = TicketScope(
|
||||
ticket_id=ticket.id,
|
||||
scope_type="job",
|
||||
@ -265,16 +246,27 @@ def api_tickets():
|
||||
job_id=job.id if job else None,
|
||||
job_name_match=job.job_name if job else None,
|
||||
job_name_match_mode="exact",
|
||||
resolved_at=None,
|
||||
)
|
||||
db.session.add(scope)
|
||||
else:
|
||||
# Re-open this ticket for this job if it was previously resolved for this scope.
|
||||
scope.resolved_at = None
|
||||
scope.customer_id = job.customer_id if job else scope.customer_id
|
||||
scope.backup_software = job.backup_software if job else scope.backup_software
|
||||
scope.backup_type = job.backup_type if job else scope.backup_type
|
||||
scope.job_name_match = job.job_name if job else scope.job_name_match
|
||||
scope.job_name_match_mode = "exact"
|
||||
|
||||
# Link ticket to this job run (idempotent)
|
||||
if not TicketJobRun.query.filter_by(ticket_id=ticket.id, job_run_id=run.id).first():
|
||||
link = TicketJobRun(ticket_id=ticket.id, job_run_id=run.id, link_source="manual")
|
||||
db.session.add(link)
|
||||
|
||||
db.session.commit()
|
||||
except Exception as exc:
|
||||
db.session.rollback()
|
||||
return jsonify({"status": "error", "message": str(exc) or "Failed to create ticket."}), 500
|
||||
db.session.commit()
|
||||
except Exception as exc:
|
||||
db.session.rollback()
|
||||
return jsonify({"status": "error", "message": str(exc) or "Failed to create ticket."}), 500
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
@ -285,8 +277,8 @@ def api_tickets():
|
||||
"description": ticket.description or "",
|
||||
"start_date": _format_datetime(ticket.start_date),
|
||||
"active_from_date": str(ticket.active_from_date) if getattr(ticket, "active_from_date", None) else "",
|
||||
"resolved_at": _format_datetime(ticket.resolved_at) if getattr(ticket, "resolved_at", None) else "",
|
||||
"active": getattr(ticket, "resolved_at", None) is None,
|
||||
"resolved_at": "",
|
||||
"active": True,
|
||||
},
|
||||
}
|
||||
)
|
||||
@ -308,9 +300,68 @@ def api_ticket_resolve(ticket_id: int):
|
||||
return jsonify({"status": "error", "message": "Forbidden."}), 403
|
||||
|
||||
ticket = Ticket.query.get_or_404(ticket_id)
|
||||
if ticket.resolved_at is None:
|
||||
ticket.resolved_at = datetime.utcnow()
|
||||
now = datetime.utcnow()
|
||||
|
||||
payload = request.get_json(silent=True) if request.is_json else {}
|
||||
try:
|
||||
run_id = int((payload or {}).get("job_run_id") or 0)
|
||||
except Exception:
|
||||
run_id = 0
|
||||
|
||||
# Job-scoped resolve (from popups / job details): resolve only for the job of the provided job_run_id
|
||||
if run_id > 0:
|
||||
run = JobRun.query.get(run_id)
|
||||
if not run:
|
||||
return jsonify({"status": "error", "message": "Job run not found."}), 404
|
||||
|
||||
job = Job.query.get(run.job_id) if run else None
|
||||
job_id = job.id if job else None
|
||||
|
||||
try:
|
||||
scope = None
|
||||
if job_id:
|
||||
scope = TicketScope.query.filter_by(ticket_id=ticket.id, scope_type="job", job_id=job_id).first()
|
||||
|
||||
if not scope:
|
||||
scope = TicketScope(
|
||||
ticket_id=ticket.id,
|
||||
scope_type="job",
|
||||
customer_id=job.customer_id if job else None,
|
||||
backup_software=job.backup_software if job else None,
|
||||
backup_type=job.backup_type if job else None,
|
||||
job_id=job_id,
|
||||
job_name_match=job.job_name if job else None,
|
||||
job_name_match_mode="exact",
|
||||
resolved_at=now,
|
||||
)
|
||||
db.session.add(scope)
|
||||
else:
|
||||
if scope.resolved_at is None:
|
||||
scope.resolved_at = now
|
||||
|
||||
# Keep the audit link to the run (idempotent)
|
||||
if not TicketJobRun.query.filter_by(ticket_id=ticket.id, job_run_id=run.id).first():
|
||||
db.session.add(TicketJobRun(ticket_id=ticket.id, job_run_id=run.id, link_source="manual"))
|
||||
|
||||
# If all scopes are resolved, also resolve the ticket globally (so the central list shows it as resolved)
|
||||
open_scope = TicketScope.query.filter_by(ticket_id=ticket.id, resolved_at=None).first()
|
||||
if open_scope is None and ticket.resolved_at is None:
|
||||
ticket.resolved_at = now
|
||||
|
||||
db.session.commit()
|
||||
except Exception as exc:
|
||||
db.session.rollback()
|
||||
return jsonify({"status": "error", "message": str(exc) or "Failed to resolve ticket."}), 500
|
||||
|
||||
return jsonify({"status": "ok", "resolved_at": _format_datetime(now)})
|
||||
|
||||
# Global resolve (from central ticket list): resolve ticket and all scopes
|
||||
if ticket.resolved_at is None:
|
||||
ticket.resolved_at = now
|
||||
|
||||
try:
|
||||
# Resolve any still-open scopes
|
||||
TicketScope.query.filter_by(ticket_id=ticket.id, resolved_at=None).update({"resolved_at": now})
|
||||
db.session.commit()
|
||||
except Exception as exc:
|
||||
db.session.rollback()
|
||||
@ -319,11 +370,9 @@ def api_ticket_resolve(ticket_id: int):
|
||||
# If this endpoint is called from a regular HTML form submit (e.g. Tickets/Remarks page),
|
||||
# redirect back instead of showing raw JSON in the browser.
|
||||
if not request.is_json and "application/json" not in (request.headers.get("Accept") or ""):
|
||||
return redirect(request.referrer or url_for("main.tickets_page"))
|
||||
|
||||
return jsonify({"status": "ok", "resolved_at": _format_datetime(ticket.resolved_at)})
|
||||
|
||||
return redirect(request.referrer or url_for("main.tickets"))
|
||||
|
||||
return jsonify({"status": "ok", "resolved_at": _format_datetime(now)})
|
||||
@main_bp.route("/api/tickets/<int:ticket_id>/link-run", methods=["POST"])
|
||||
@login_required
|
||||
@roles_required("admin", "operator", "viewer")
|
||||
|
||||
@ -198,8 +198,8 @@ def job_detail(job_id: int):
|
||||
WHERE ts.job_id = :job_id
|
||||
AND t.active_from_date <= :max_date
|
||||
AND (
|
||||
t.resolved_at IS NULL
|
||||
OR ((t.resolved_at AT TIME ZONE 'UTC' AT TIME ZONE :ui_tz)::date) >= :min_date
|
||||
COALESCE(ts.resolved_at, t.resolved_at) IS NULL
|
||||
OR ((COALESCE(ts.resolved_at, t.resolved_at) AT TIME ZONE 'UTC' AT TIME ZONE :ui_tz)::date) >= :min_date
|
||||
)
|
||||
"""
|
||||
),
|
||||
|
||||
@ -505,8 +505,8 @@ def run_checks_page():
|
||||
WHERE ts.job_id = :job_id
|
||||
AND t.active_from_date <= :run_date
|
||||
AND (
|
||||
t.resolved_at IS NULL
|
||||
OR ((t.resolved_at AT TIME ZONE 'UTC' AT TIME ZONE :ui_tz)::date) >= :run_date
|
||||
COALESCE(ts.resolved_at, t.resolved_at) IS NULL
|
||||
OR ((COALESCE(ts.resolved_at, t.resolved_at) AT TIME ZONE 'UTC' AT TIME ZONE :ui_tz)::date) >= :run_date
|
||||
)
|
||||
LIMIT 1
|
||||
"""
|
||||
|
||||
@ -1351,6 +1351,7 @@ def migrate_object_persistence_tables() -> None:
|
||||
job_id INTEGER REFERENCES jobs(id),
|
||||
job_name_match VARCHAR(255),
|
||||
job_name_match_mode VARCHAR(32),
|
||||
resolved_at TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL
|
||||
);
|
||||
"""))
|
||||
@ -1364,6 +1365,12 @@ def migrate_object_persistence_tables() -> None:
|
||||
UNIQUE(ticket_id, job_run_id)
|
||||
);
|
||||
"""))
|
||||
# Ensure scope-level resolution exists for per-job ticket resolving
|
||||
conn.execute(text("ALTER TABLE ticket_scopes ADD COLUMN IF NOT EXISTS resolved_at TIMESTAMP"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_ticket_scopes_ticket_id ON ticket_scopes (ticket_id)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_ticket_scopes_job_id ON ticket_scopes (job_id)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_ticket_scopes_resolved_at ON ticket_scopes (resolved_at)"))
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS remarks (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
@ -397,6 +397,7 @@ class TicketScope(db.Model):
|
||||
job_name_match = db.Column(db.String(255))
|
||||
job_name_match_mode = db.Column(db.String(32))
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||
resolved_at = db.Column(db.DateTime)
|
||||
|
||||
|
||||
class TicketJobRun(db.Model):
|
||||
|
||||
@ -4,6 +4,13 @@
|
||||
- Prevented duplicate ticket creation errors when reusing an existing ticket_code.
|
||||
- Ensured tickets are reused and linked instead of rejected when already present in the system.
|
||||
|
||||
## v20260106-24-ticket-scope-resolve-popup
|
||||
|
||||
- Fixed missing ticket number display in job and run popups by always creating or reusing a ticket scope when linking an existing ticket to a job.
|
||||
- Updated ticket resolution logic to support per-job resolution when resolving from a job or run context.
|
||||
- Ensured resolving a ticket from the central Tickets view resolves the ticket globally and closes all associated job scopes.
|
||||
- Updated ticket active status determination to be based on open job scopes, allowing the same ticket number to remain active for other jobs when applicable.
|
||||
|
||||
|
||||
================================================================================================================================================
|
||||
## v0.1.17
|
||||
|
||||
Loading…
Reference in New Issue
Block a user