feat: Add custom model training annotation UI#4667
feat: Add custom model training annotation UI#4667pliablepixels wants to merge 27 commits intoZoneMinder:masterfrom
Conversation
Add canvas-based bounding box annotation editor to the event view for correcting object detection results and building YOLO training datasets. - Two new config options: ZM_OPT_TRAINING (toggle) and ZM_TRAINING_DATA_DIR - AJAX backend (training.php) with load/save/delete/status actions - Canvas annotation editor (training.js) with draw/resize/relabel/undo - Frame navigation (alarm/snapshot/objdetect/numbered frames) - Roboflow-compatible YOLO output (images/all/, labels/all/, data.yaml) - Training data statistics with per-class image counts and guidance - Full i18n support via en_gb.php SLANG/OLANG entries - Label validation, YOLO coordinate clamping, audit logging - DB migration for existing installs (zm_update-1.39.2.sql) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ZM_DIR_EVENTS may not exist in the Config table on fresh builds, causing REPLACE to return NULL and the UPDATE to fail with ERROR 1048 on NOT NULL column. Use COALESCE to skip the update when the derived path is NULL. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ZM_DIR_EVENTS is a compile-time substitution, never in the Config table. The default is set via ConfigData.pm.in (@ZM_CACHEDIR@/training) and the PHP fallback in getTrainingDataDir() handles empty values. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove objdetect from frame selector and fid validation - Fix 500 error: replace non-existent ZM\AuditAction() with ZM\Info() - Scale canvas text/borders/handles relative to canvas width for readability - Change annotate button icon from fa-pencil-square-o to fa-crosshairs - Bump CSS font sizes from 0.75-0.9rem to 0.95-1rem - Use orange (#ff8c00) stroke for in-progress bounding box drawing - Flash save button green (btn-saved class) on successful save - Hide #eventVideo when annotation panel is open - Skip empty label files in getTrainingStats() count - Reject save with empty annotations array - Add undo support for delete and AJAX delete when all boxes removed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ZM_TRAINING_DETECT_SCRIPT config option for specifying the path to an external detection script (e.g. zm_detect.py). When configured, a Detect button appears in the annotation editor that: - Runs the script with -f <image> -m <monitor_id> - Parses the --SPLIT-- JSON output for labels, boxes, confidences - Displays results as orange (pending) bounding boxes - Users can accept (checkmark) or reject (x) each detection - Only accepted annotations are saved to the training set Also adds accept/reject UI in the sidebar object list, with orange color for pending detections and normal colors for accepted ones. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Was hardcoded to %06d but the actual padding depends on the ZM_EVENT_IMAGE_DIGITS config (typically 5). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Accepted and manually-drawn annotations render in green (#28a745) - Pending detections remain orange (#ff8c00) - Removed the label select dropdown from the action bar (clutter) - Left button text-transform as-is (ZM-wide uppercase) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Cancel button renamed to Exit - Action bar buttons use smaller font (0.8rem) and padding - Status text truncates with ellipsis instead of wrapping buttons - Action bar uses flex-wrap: nowrap to keep buttons on one line Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove empty annotations rejection from PHP save action - Empty label file = background/negative image in YOLO format - JS prompts user when saving with no objects: "Save as background image? Background images help reduce false positives." - Stats now show background image count separately - Delete Box button already works on selected pending boxes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add SLANG entries for all training UI strings and wire them through the trainingTranslations object: - AcceptDetection, BackgroundImageConfirm, BackgroundImages - DetectFailed, DetectedObjects - FailedToLoadEvent, FailedToLoadFrame, LoadFrameFirst - NoFrameLoaded, NoTrainingData, SaveFailed All user-visible text in training.js now uses this.translations.Key || 'fallback' pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Literal \n in the PHP string was output as newlines inside a JS string literal in event.js.php, causing a SyntaxError that broke the entire page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ll, UI polish - Add Annotate button to objdetect modal (events list) with auto-open via ?annotate=1 URL parameter - Compact stats layout: label and value on same row, trash icon to delete all training data with typed confirmation prompt - Add delete_all AJAX action to training.php - Reduce all annotation panel font sizes to 0.8rem to match ZM base - Fix label picker white-on-white by setting explicit colors - Add beforeunload guard for unsaved annotations - Switch detect action from shell_exec to exec for better error handling - Show "no objects detected" as info status, not error - Increase status message display time to 10 seconds - Add i18n strings: ConfirmDeleteTrainingData, DeleteAllTrainingData, TrainingDataDeleted Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…lete - Rewrite browse overlay as two-panel layout: directory tree (left) + file listing/preview (right) - Add browse_file PHP action to serve images and text files from training dir - Add browse_delete PHP action with class ID remapping to keep data.yaml consistent - Overlay bounding boxes on image previews from corresponding label files - Add per-file delete button (trash icon) that removes both image and label pair - Move delete-all trash icon to sidebar header, status messages below sidebar - Use existing ZM utilities: human_filesize() for file sizes, detaintPath() for path security - Add SelectBoxFirst translation for delete-without-selection feedback - Fix orphaned class ID bug: remap IDs in all label files when classes are removed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rename migration to zm_update-1.39.1.sql, add DEALLOCATE PREPARE, fix Requires format to 'ZM_OPT_TRAINING=1' - Bump version.txt to 1.39.1 - Remove docs/plans/ and utils/deploy-training.sh from repo - training.php: remove PHP fallback path, add validFrameId() helper, move buildTree() to top-level with symlink skip and depth limit, split canView/canEdit permissions, require POST for destructive ops, remove path disclosure, sanitize log output, add is_executable check - training.js: remove console.log calls, fix _setStatus boolean to string type, fix double undo push, switch delete ops to POST, replace hardcoded English strings with translation keys - event.js.php: remove window.annotationEditor global, add new translation keys to trainingTranslations - event.php: add frame skip and browse buttons, break up long HTML - en_gb.php: add missing translation keys in alphabetical order - training.css: add frame browse overlay and pagination styles Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace beforeunload with click-capture nav guard to avoid blank screen caused by ZM's global beforeunload content-hide animation - Wrap streamPrev/streamNext to prompt before stream teardown - Warn when saving with unaccepted (orange) detection boxes - Add PHP fallback for empty TRAINING_DATA_DIR using ZM_DIR_CACHE - Show spinning loader overlay with blur while frame images load - Change detection running status to blue (info) color - Fix label picker uppercase caused by ZM global button CSS - Add TrainingPendingOnly, TrainingPendingDiscard translation keys Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new, config-gated (“ZM_OPT_TRAINING”) annotation workflow in the classic skin event view to correct object detections and persist training data in YOLO format for custom model training.
Changes:
- Adds a canvas-based annotation editor UI to the event view, including frame navigation and an overlay-based training-data browser.
- Introduces a new AJAX backend (
web/ajax/training.php) to load/save annotations, browse/delete dataset files, and optionally run an external detection script. - Adds new configuration options (DB migration + ConfigData + translations) and bumps version to
1.39.1.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
web/skins/classic/views/js/training.js |
New AnnotationEditor implementation (drawing/editing boxes, browsing frames/data, saving YOLO). |
web/skins/classic/views/js/event.js.php |
Initializes the editor and wires UI handlers when ZM_OPT_TRAINING is enabled. |
web/skins/classic/views/event.php |
Adds the Annotate button, annotation panel markup, and loads training CSS/JS when enabled. |
web/skins/classic/css/base/views/training.css |
Styles for the annotation panel, frame browser, and training-data browser overlays. |
web/ajax/training.php |
Backend endpoints for training editor (load/save/status/browse/delete/detect). |
web/ajax/modals/objdetect.php |
Adds a shortcut button into the objdetect modal to open the annotation UI. |
web/lang/en_gb.php |
Adds translation keys for the new feature and new config option prompts/help. |
scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in |
Adds new options ZM_OPT_TRAINING, ZM_TRAINING_DATA_DIR, ZM_TRAINING_DETECT_SCRIPT. |
db/zm_update-1.39.1.sql |
Adds Config entries for the new options. |
version.txt |
Version bump to 1.39.1. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
web/ajax/training.php
Outdated
| $reqPath = detaintPath($_REQUEST['path']); | ||
| $fullPath = realpath($base.'/'.$reqPath); | ||
|
|
||
| // Validate file is within the training directory | ||
| if ($fullPath === false || strpos($fullPath, realpath($base)) !== 0 || !is_file($fullPath)) { | ||
| ZM\Warning('Training: browse_file path rejected: '.validHtmlStr($_REQUEST['path'])); | ||
| ajaxError('File not found or access denied'); | ||
| break; | ||
| } |
There was a problem hiding this comment.
The realpath containment check uses strpos($fullPath, realpath($base)) !== 0, which is vulnerable to prefix-matching paths (e.g. $base='/data/training' and $fullPath='/data/training_evil/file' passes). Use a boundary-safe check (e.g. compare against $baseReal . DIRECTORY_SEPARATOR or verify the next char is /).
There was a problem hiding this comment.
Fixed in 62678b2. Now uses rtrim(realpath($base), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR before the strpos containment check, preventing prefix-matching against sibling directories.
web/ajax/training.php
Outdated
| $reqPath = detaintPath($_REQUEST['path']); | ||
| $fullPath = realpath($base.'/'.$reqPath); | ||
|
|
||
| if ($fullPath === false || strpos($fullPath, realpath($base)) !== 0 || !is_file($fullPath)) { | ||
| ajaxError('File not found or access denied'); | ||
| break; | ||
| } | ||
|
|
There was a problem hiding this comment.
Same path containment issue as browse_file: strpos($fullPath, realpath($base)) !== 0 can be bypassed by sibling directories that share the same prefix as $base. Tighten the check to ensure $fullPath is actually under $base (boundary-aware).
| $reqPath = detaintPath($_REQUEST['path']); | |
| $fullPath = realpath($base.'/'.$reqPath); | |
| if ($fullPath === false || strpos($fullPath, realpath($base)) !== 0 || !is_file($fullPath)) { | |
| ajaxError('File not found or access denied'); | |
| break; | |
| } | |
| $baseReal = realpath($base); | |
| if ($baseReal === false) { | |
| ajaxError('Training data directory not found or invalid'); | |
| break; | |
| } | |
| $reqPath = detaintPath($_REQUEST['path']); | |
| $fullPath = realpath($baseReal . '/' . $reqPath); | |
| if ($fullPath === false) { | |
| ajaxError('File not found or access denied'); | |
| break; | |
| } | |
| $basePrefix = rtrim($baseReal, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; | |
| if (strpos($fullPath, $basePrefix) !== 0 || !is_file($fullPath)) { | |
| ajaxError('File not found or access denied'); | |
| break; | |
| } |
There was a problem hiding this comment.
Fixed in 62678b2. Same fix applied — appending DIRECTORY_SEPARATOR to the base path before strpos check.
web/ajax/training.php
Outdated
| $tmpFile = tempnam(sys_get_temp_dir(), 'zm_detect_').'.jpg'; | ||
| if (!copy($srcImage, $tmpFile)) { |
There was a problem hiding this comment.
tempnam(...).'.jpg' creates (and leaks) the original temp file returned by tempnam() while writing to a different filename. This leaves stray files in the temp dir. Use the tempnam() return value directly (optionally rename it) and ensure it’s always cleaned up (also on error paths).
| $tmpFile = tempnam(sys_get_temp_dir(), 'zm_detect_').'.jpg'; | |
| if (!copy($srcImage, $tmpFile)) { | |
| $tmpFile = tempnam(sys_get_temp_dir(), 'zm_detect_'); | |
| if ($tmpFile === false) { | |
| ajaxError('Failed to create temp file for detection'); | |
| break; | |
| } | |
| if (!copy($srcImage, $tmpFile)) { | |
| if (file_exists($tmpFile)) { | |
| unlink($tmpFile); | |
| } |
There was a problem hiding this comment.
Fixed in 62678b2. Now renames the tempnam() base file to .jpg instead of creating a second file, so no orphan is left behind. Also added cleanup on copy failure.
db/zm_update-1.39.1.sql
Outdated
| DefaultValue='', | ||
| Hint='', | ||
| Prompt='Training data directory', | ||
| Help='Filesystem path where corrected annotation images and YOLO label files are stored. The directory will be created automatically if it does not exist. Uses Roboflow-compatible YOLO directory layout (images/all/, labels/all/, data.yaml). If left empty, defaults to a training folder alongside your events storage.', |
There was a problem hiding this comment.
The migration Help text says an empty ZM_TRAINING_DATA_DIR defaults alongside events storage, but the runtime fallback is ZM_DIR_CACHE/training and ConfigData.pm.in defaults to @ZM_CACHEDIR@/training. Please align the migration help text with the actual default behavior (or change the default).
| Help='Filesystem path where corrected annotation images and YOLO label files are stored. The directory will be created automatically if it does not exist. Uses Roboflow-compatible YOLO directory layout (images/all/, labels/all/, data.yaml). If left empty, defaults to a training folder alongside your events storage.', | |
| Help='Filesystem path where corrected annotation images and YOLO label files are stored. The directory will be created automatically if it does not exist. Uses Roboflow-compatible YOLO directory layout (images/all/, labels/all/, data.yaml). If left empty, defaults to a training folder in the ZoneMinder cache directory (e.g. ZM_DIR_CACHE/training).', |
There was a problem hiding this comment.
Fixed in 62678b2. Help text now says "defaults to a training folder inside the ZoneMinder cache directory" which matches the actual ZM_DIR_CACHE/training fallback.
web/skins/classic/views/event.php
Outdated
| <button class="btn btn-normal btn-sm" data-frame="skip-forward" title="<?php echo translate('TrainingSkipForward') ?>"><i class="fa fa-forward"></i></button> | ||
| <button class="btn btn-normal btn-sm" id="annotationBrowseFramesBtn" title="<?php echo translate('TrainingBrowseFrames') ?>"><i class="fa fa-th"></i></button> | ||
| <input type="number" class="form-control form-control-sm frame-input" id="annotationFrameInput" min="1" max="<?php echo $Event->Frames() ?>" placeholder="#" title="<?php echo translate('GoToFrame') ?>"/> | ||
| <button class="btn btn-normal btn-sm" id="annotationGoToFrame"><?php echo translate('GoToFrame') ?></button> |
There was a problem hiding this comment.
training.js updates a .frame-total element inside #annotationFrameSelector, but the markup here doesn't include any element with class frame-total, so the total frame count will never render. Add the missing element (or adjust the JS to target an existing node).
| <button class="btn btn-normal btn-sm" id="annotationGoToFrame"><?php echo translate('GoToFrame') ?></button> | |
| <button class="btn btn-normal btn-sm" id="annotationGoToFrame"><?php echo translate('GoToFrame') ?></button> | |
| <span class="frame-total"><?php echo $Event->Frames() ?></span> |
There was a problem hiding this comment.
Fixed in 62678b2. Added the missing <span class="frame-total"></span> element to the frame selector markup in event.php.
| } else if (e.key === 'Delete' || e.key === 'Backspace') { | ||
| if (self.selectedIndex >= 0) { | ||
| e.preventDefault(); | ||
| self._pushUndo(); |
There was a problem hiding this comment.
The Delete/Backspace key handler calls _pushUndo() and then calls deleteAnnotation(), which also calls _pushUndo(). This causes a single delete action to require two undos and bloats the undo stack. Remove one of the _pushUndo() calls (typically keep it inside deleteAnnotation()).
| self._pushUndo(); |
There was a problem hiding this comment.
Fixed in 62678b2. Removed the extra _pushUndo() call from the Delete/Backspace keydown handler — deleteAnnotation() already calls it internally.
web/ajax/training.php
Outdated
| ZM\Debug('Training: detect found '.count($detections).' objects for event '.$eid.' frame '.$fid); | ||
| ajaxResponse([ | ||
| 'detections' => $detections, | ||
| 'raw_output' => $output, |
There was a problem hiding this comment.
The detect endpoint returns raw_output from the external detection script. The front-end doesn't appear to use it, and it can leak internal details or error output to the browser unnecessarily. Consider omitting it (or gating it behind a debug flag / admin-only).
| 'raw_output' => $output, |
There was a problem hiding this comment.
Fixed in 62678b2. Removed raw_output from the detect response. The script output is still logged server-side via ZM\Debug for troubleshooting.
| label files are stored. The directory will be created | ||
| automatically if it does not exist. Uses Roboflow-compatible | ||
| YOLO directory layout (images/all/, labels/all/, data.yaml). | ||
| If left empty, defaults to a training folder alongside your events storage. |
There was a problem hiding this comment.
The help text says an empty ZM_TRAINING_DATA_DIR defaults to a folder alongside events storage, but the configured default is @ZM_CACHEDIR@/training and web/ajax/training.php falls back to ZM_DIR_CACHE.'/training'. Update the help text (or the implementation) so the documented default matches reality.
| If left empty, defaults to a training folder alongside your events storage. | |
| If left empty (or set to the default), ZoneMinder uses a "training" | |
| subdirectory under the cache directory (ZM_DIR_CACHE.'/training', | |
| typically @ZM_CACHEDIR@/training). |
There was a problem hiding this comment.
Fixed in 62678b2. Same correction as the migration — now says "inside the ZoneMinder cache directory."
| case 'save': | ||
| // Save annotation (image + YOLO label file) | ||
| if (!canEdit('Events')) { | ||
| ajaxError('Insufficient permissions'); | ||
| break; | ||
| } | ||
| if (empty($_REQUEST['eid']) || !isset($_REQUEST['fid'])) { | ||
| ajaxError('Event ID and Frame ID required'); | ||
| break; | ||
| } | ||
|
|
||
| $eid = validCardinal($_REQUEST['eid']); | ||
| $fid = $_REQUEST['fid']; | ||
|
|
||
| if (!validFrameId($fid)) { | ||
| ajaxError('Invalid frame ID'); | ||
| break; | ||
| } | ||
|
|
||
| $Event = ZM\Event::find_one(['Id' => $eid]); | ||
| if (!$Event) { | ||
| ajaxError('Event not found'); | ||
| break; | ||
| } | ||
|
|
||
| if (!ensureTrainingDirs()) break; | ||
|
|
||
| $annotations = json_decode($_REQUEST['annotations'], true); | ||
| if (!is_array($annotations)) { | ||
| ajaxError('Invalid annotations data'); | ||
| break; | ||
| } | ||
|
|
||
| $imgWidth = intval($_REQUEST['width']); | ||
| $imgHeight = intval($_REQUEST['height']); | ||
| if ($imgWidth <= 0 || $imgHeight <= 0) { | ||
| ajaxError('Invalid image dimensions'); | ||
| break; | ||
| } |
There was a problem hiding this comment.
State-changing training endpoints like action=save are callable via ?request=training without any CSRF protection, so an attacker can forge POST requests from another site that run in a logged-in user’s browser and modify or delete training annotations. If a user with canEdit('Events') visits a malicious page while logged in, that page can silently submit a form to ?request=training&action=save/delete/delete_all/browse_delete and corrupt or wipe the training dataset. To fix this, enforce CSRF tokens or the existing csrf-magic mechanism for request=training actions and validate them on these write operations before applying any changes.
There was a problem hiding this comment.
Skipping this one. ZoneMinder's existing AJAX endpoints (event delete, monitor config, etc. in web/ajax/) do not enforce CSRF tokens either — this is a codebase-wide pattern, not specific to the training feature. Adding CSRF validation here alone would be inconsistent. This should be addressed as a separate codebase-wide effort.
… help text - Fix path traversal via directory prefix matching: append DIRECTORY_SEPARATOR to base path before strpos containment check (2 locations in training.php) - Fix temp file leak: rename tempnam() base file to .jpg instead of creating a second file, leaving the original orphaned - Remove raw_output from detect response (information disclosure) - Fix double _pushUndo on Delete key (keydown handler + deleteAnnotation) - Fix help text: "alongside events storage" → "inside the ZoneMinder cache directory" to match actual ZM_DIR_CACHE default (2 locations) - Add missing .frame-total span element in event.php Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Parses event_{eid}_frame_{fid} from training image filenames and adds
a pencil button in the preview header. Clicking it navigates to the
event view with ?annotate=1&frame=FRAME_ID, auto-opening the annotation
editor on that specific frame. Adds initialFrame parameter to open().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change default training data directory from @ZM_CACHEDIR@/training to @ZM_CONTENTDIR@/training so it lives alongside events storage instead of inside the web cache folder. Update PHP fallback to use dirname(ZM_DIR_EVENTS) accordingly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Hold Shift to bypass hit-testing on existing boxes and force draw mode, allowing new boxes to be drawn inside larger ones. Cursor reverts to crosshair while Shift is held. Adds internationalized hint text below the canvas. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add load_saved PHP action that reads YOLO label files and converts normalized coordinates back to pixels. When navigating to an event via the browse edit button, saved annotations are loaded into the editor so users can review and modify previous work. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add browse_objects PHP action that groups annotated images by class label and collects background images separately. Inject a virtual "Objects" folder at the top of the browse tree with fa-tags icon. Expanding it shows per-class sub-items with count badges and a background images category. Clicking a class displays a thumbnail grid; clicking a thumbnail navigates to the annotation editor for that event/frame. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the annotation/training interface from an inline panel in event.php to a dedicated ?view=training screen. This cleanly separates the training workflow from event playback and enables switching between events without a full page reload. - Add web/skins/classic/views/training.php as a standalone training view - Add web/skins/classic/views/js/training.js.php with view-specific init and translations (moved from event.js.php) - Add switchEvent() method to AnnotationEditor for in-place event switching when clicking thumbnails or edit buttons in the browse panel - Update page title, URL bar, and back button on event switch - Change annotate button in event.php from toggle to navigation link - Update objdetect modal to link to training view - Remove annotation panel HTML and training init code from event view - Fix pre-existing broken </span tag on toggleZonesButton in event.php - Add training view layout CSS, inline browse panel styles Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Save annotations now refreshes browse tree and file listing - Delete all training data now refreshes browse panel - Delete file in browse always refreshes editor stats - Invalidate objects cache on save, delete, and delete-all - Expose browse state on editor instance for cross-panel sync - Update training guidance text to 50+/100+ images per class Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ning files - Rebuild data.yaml when deleting annotations (delete and browse_delete actions) so unused class names are removed and IDs re-indexed - Extract rebuildDataYaml() helper, replacing duplicated inline rebuild logic in browse_delete - Make sidebar trash button always visible for recovery from inconsistent states; _deleteAllTrainingData now also clears editor annotations - Consolidate permission/POST checks into arrays before switch statement - Extract helper functions: resolveTrainingPath, getFrameStem, getFramePaths, _loadEventData, _confirmDiscardIfDirty, _resetAnnotationState, _applyEventData, _getNormalizedRect - Extract 8 pure browse utility functions to module level - Add floatval() validation on bbox coordinates in save action - Add LOCK_EX to writeDataYaml, add .fail(logAjaxFail) to AJAX calls - Replace var with let/const, add 'use strict', remove IIFEs - CSS custom properties for repeated values, requestAnimationFrame throttle for canvas mousemove - Align with ZM conventions: error messages include username/action, trailing ajaxError after switch, "use strict" at line 1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split the config value on the first whitespace so the executable path is validated separately and extra arguments (e.g. -c config.yml) are passed through to the command line. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| global $monitor; | ||
| ?> | ||
|
|
||
| const eventData = { |
There was a problem hiding this comment.
I wonder if we could just output $Event->to_json() and make this a 1-liner.
There was a problem hiding this comment.
Fixed in ca74d11. Replaced with const eventData = <?php echo $Event->to_json() ?>; — single line, uses the inherited Object::to_json() method.
web/skins/classic/views/training.php
Outdated
| return; | ||
| } | ||
|
|
||
| zm_session_start(); |
There was a problem hiding this comment.
these should not be here
There was a problem hiding this comment.
Fixed in ca74d11. Removed both lines — the training view never reads or writes session data, so the session open/close was a no-op.
db/zm_update-1.39.1.sql
Outdated
| -- Add AUDIT logging level between PANIC (-4) and NOLOG. | ||
| -- AUDIT is now -5; NOLOG shifts from -5 to -6. | ||
| -- Migrate any stored NOLOG config values from -5 to -6. | ||
| -- This updates a 1.39.0 database to 1.39.1 |
There was a problem hiding this comment.
We don't need this. This wouldn't actually correctly update the config table anyways. zmupdate.pl -f will automatically add this field to config.
There was a problem hiding this comment.
Fixed in ca74d11. Deleted the migration file entirely — the three config entries are already defined in ConfigData.pm.in and zmupdate.pl -f handles syncing them to the database.
- Replace manual eventData construction with $Event->to_json() - Remove unused zm_session_start()/session_write_close() from training view - Delete zm_update-1.39.1.sql migration (zmupdate.pl -f handles Config entries via ConfigData.pm) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
ZM_OPT_TRAININGconfig option — zero impact when disabledRefs #4666
Test plan
🤖 Generated with Claude Code