Upgrade code

This commit is contained in:
2026-03-30 14:04:58 -04:00
parent bf29ccb287
commit 53eaf1c76d
12 changed files with 390 additions and 58 deletions
+77 -13
View File
@@ -38,14 +38,25 @@
{% if ticket_atts %}
<div class="card mb-4">
<div class="card-header"><i class="bi bi-paperclip me-2"></i>Attachments</div>
<div class="card-body d-flex flex-wrap gap-2">
{% for att in ticket_atts %}
<a href="{{ url_for('tickets.download_attachment', att_id=att.id) }}"
class="btn btn-secondary btn-sm">
<i class="bi bi-download me-1"></i>{{ att.filename }}
<span style="font-size:10px;color:var(--muted);">({{ (att.file_size/1024)|int }}KB)</span>
</a>
{% endfor %}
<div class="card-body">
<div class="d-flex flex-wrap gap-2 align-items-start">
{% for att in ticket_atts %}
{% if att.mime_type and att.mime_type.startswith('image/') %}
<a href="{{ url_for('tickets.download_attachment', att_id=att.id) }}"
target="_blank" class="comment-img-link" title="{{ att.filename }}">
<img src="{{ url_for('tickets.download_attachment', att_id=att.id) }}"
alt="{{ att.filename }}"
class="comment-img-thumb"/>
</a>
{% else %}
<a href="{{ url_for('tickets.download_attachment', att_id=att.id) }}"
class="btn btn-secondary btn-sm">
<i class="bi bi-download me-1"></i>{{ att.filename }}
<span style="font-size:10px;color:var(--muted);">({{ (att.file_size/1024)|int }}KB)</span>
</a>
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endif %}
@@ -77,8 +88,14 @@
<div class="comment-card {% if comment.is_internal %}internal{% endif %}" id="comment-{{ comment.id }}">
<div class="d-flex align-items-center justify-content-between mb-2">
<div class="d-flex align-items-center gap-2">
<div style="width:28px;height:28px;border-radius:50%;background:var(--accent2);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;">
<div style="width:28px;height:28px;border-radius:50%;background:var(--accent2);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;overflow:hidden;flex-shrink:0;">
{% if comment.author.avatar_url %}
<img src="{{ url_for('auth.serve_avatar', filename=comment.author.avatar_url) }}"
alt="{{ comment.author.full_name[0].upper() }}"
style="width:28px;height:28px;object-fit:cover;display:block;"/>
{% else %}
{{ comment.author.full_name[0].upper() }}
{% endif %}
</div>
<span class="comment-author">{{ comment.author.full_name }}</span>
{% if comment.author.is_it_staff %}
@@ -102,7 +119,7 @@
{% endif %}
</div>
</div>
<div class="comment-body">{{ comment.body }}</div>
<div class="comment-body">{{ comment.body | safe }}</div>
{% set c_atts = comment.attachments.all() %}
{% if c_atts %}
<div class="mt-2">
@@ -141,7 +158,7 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="mb-3">
<textarea id="comment-body" class="form-control" name="body" rows="4" required
placeholder="Add your update, follow-up, or response here… (you can also paste images directly)"></textarea>
placeholder="Add your update… Markdown supported: **bold**, _italic_, `code`, - lists, > quotes (you can also paste images directly)"></textarea>
</div>
<!-- Image preview strip — populated by paste or file picker -->
<div id="img-preview-strip" style="display:none;flex-wrap:wrap;gap:8px;margin-bottom:12px;"></div>
@@ -200,11 +217,41 @@ if (typeof socket !== 'undefined') {
// on submit, since a paste event cannot modify a real <input type=file>.
let pastedFiles = [];
// ── Attachment limits (enforced client-side here AND server-side in tickets.py)
const MAX_ATTACHMENT_FILES = 5;
const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024; // 25 MB total per comment
function getAttachmentError(allFiles) {
if (allFiles.length > MAX_ATTACHMENT_FILES) {
return `Too many files — maximum ${MAX_ATTACHMENT_FILES} attachments per comment.`;
}
const totalBytes = allFiles.reduce((sum, f) => sum + f.size, 0);
if (totalBytes > MAX_ATTACHMENT_BYTES) {
const mb = (MAX_ATTACHMENT_BYTES / (1024 * 1024)).toFixed(0);
return `Total attachment size exceeds the ${mb} MB limit per comment.`;
}
return null;
}
function rebuildPreviewStrip() {
// Collect all files: pasted images + files chosen via the picker
const pickerFiles = Array.from(document.getElementById('attachment-input').files || []);
const allFiles = [...pastedFiles, ...pickerFiles];
const strip = document.getElementById('img-preview-strip');
const err = document.getElementById('comment-error');
// Show/clear the limit error alongside the preview strip
const limitErr = getAttachmentError(allFiles);
if (limitErr) {
err.textContent = limitErr;
err.style.display = 'block';
} else {
// Only clear the error if it was an attachment limit message
if (err.textContent && err.textContent.includes('attachment')) {
err.style.display = 'none';
err.textContent = '';
}
}
if (allFiles.length === 0) { strip.style.display = 'none'; strip.innerHTML = ''; return; }
@@ -282,6 +329,18 @@ document.getElementById('comment-form').addEventListener('submit', async functio
const fd = new FormData(this);
pastedFiles.forEach(f => fd.append('attachments', f, f.name));
// Client-side attachment limit check — mirrors server-side guard in tickets.py
const pickerFiles = Array.from(document.getElementById('attachment-input').files || []);
const allFiles = [...pastedFiles, ...pickerFiles];
const limitErr = getAttachmentError(allFiles);
if (limitErr) {
err.textContent = limitErr;
err.style.display = 'block';
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-send me-2"></i>Post Comment';
return;
}
try {
const resp = await fetch(window.location.pathname, {
method : 'POST',
@@ -408,11 +467,16 @@ function buildCommentEl(c) {
</a>`;
}).join('');
const avatarInner = c.author_avatar
? `<img src="/auth/avatar/${c.author_avatar}" alt="${c.author_init}"
style="width:28px;height:28px;object-fit:cover;display:block;border-radius:50%;"/>`
: c.author_init;
wrap.innerHTML = `
<div class="d-flex align-items-center justify-content-between mb-2">
<div class="d-flex align-items-center gap-2">
<div style="width:28px;height:28px;border-radius:50%;background:var(--accent2);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;">
${c.author_init}
<div style="width:28px;height:28px;border-radius:50%;background:var(--accent2);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;overflow:hidden;flex-shrink:0;">
${avatarInner}
</div>
<span class="comment-author">${c.author_name}</span>
${itBadge}${intBadge}