Coords holds a space-separated list of "x,y" points, written with two
decimal places by pointsToCoords()/mapCoords(). Since 1.39.2 the Zones
values are percentages rather than pixels, which costs up to 13 bytes per
point ("100.00,100.00") against the 9 a pixel pair used ("1920,1080").
tinytext caps at 255 bytes, so a zone of roughly 19 points or more
overflows and is silently truncated, leaving a corrupt polygon. In pixel
form the same zone fit, so this only starts biting at the conversion.
Widen the column in 1.39.2 before its conversion runs, and again in a new
1.39.23 for installations that ran that conversion before the widening
existed - zmupdate.pl only applies files at or above the database version,
so those never revisit 1.39.2. ALTER ... MODIFY to the same type is a
no-op, so both are safe to re-run.
Maps stores coordinates in the same format and is widened to match. It
was added to zm_create.sql.in in 1.32 without a matching update script,
so installations older than that have never had the table; guard that
statement on the table existing rather than failing the whole migration.
Update zm_create.sql.in for both tables so a fresh install matches an
upgraded one, and bump version.txt so the new update script is reached.
The Coords pixel-to-percent conversion is already idempotent: the cursor
loop skips any zone whose Coords contain a '.', and CAST(... AS
DECIMAL(10,2)) always renders two decimals, so a converted zone can never
be mistaken for a pixel one.
The Area rescale that followed it had no such guard. It ran as a separate
statement over every zone with Area > 0, with nothing to distinguish a
zone converted by this run from one converted by an earlier one, so each
run divided Area by (Width * Height) / 10000 again. Re-running does not
repair it.
This is not hypothetical. zmupdate.pl applies every update file whose
version is >= the database version, so the file matching the current
version is re-run on each upgrade. A live database had a full-frame zone
on a 1920x1080 monitor holding Area 48, which is
ROUND(10000 * 10000.0 / 2073600) - the rescale applied to an already
rescaled value. Two sibling zones had reached 0. Their Coords were
intact throughout, confirming the conversion held while Area decayed.
Fold the rescale into the UPDATE inside the loop so it can only touch a
zone whose Coords were just converted. Zones skipped as already-percent
keep their Area.
Area is recoverable without a backup: web/includes/actions/zone.php
recomputes it with getPolyArea() from Coords whenever a zone is saved.
A DateTime lower bound was written as
COALESCE(E.EndDateTime, '9999-12-31 23:59:59') >= T1
reading "an event that has not ended yet never ends". EndDateTime is NULL for
an event that is still recording, but also for one zmc was killed part way
through, and that stays NULL forever - so every abandoned event matched every
window from then on. Wrapping the column in a function also meant no index
could range over it: with the only MonitorId key being MonitorId alone, every
montage review request read all of that monitor's events and filtered them in
memory, however narrow the window.
Length tells the two cases apart. It is flushed during recording, so a live
event's effective end keeps advancing while an abandoned one's is frozen at
whatever was recorded. Use the same CASE the SELECT list in ajax/events.php
already uses, and bound it below by StartDateTime.
The lower bound now emits three conjuncts:
E.StartDateTime >= DATE_SUB(T1, INTERVAL 1 DAY)
AND (E.EndDateTime IS NULL OR E.EndDateTime >= T1)
AND <effective end> >= T1
The floor bounds the scan for a window in the past and drops events abandoned
more than a day earlier - events do not outlive the nightly logrotate SIGHUP,
which stops and restarts them, so a day is comfortably beyond any real event.
The middle conjunct is implied by the third and exists only to give the
optimiser a second indexable handle: it is narrow exactly when the floor is
wide, so between them a window at either end of the retention period has
something cheap to range over. Verified the optimiser picks correctly for both,
unprompted. The third is the residual that gets the semantics right.
Add the two composite keys those conjuncts need. Two range columns cannot both
narrow one B-tree, and these are mirror images - EndDateTime >= T1 is
open-ended upwards, StartDateTime <= T2 downwards:
Events_MonitorId_StartDateTime_idx (MonitorId, StartDateTime)
Events_EndDateTime_MonitorId_idx (EndDateTime, MonitorId)
MonitorId leads the first because it is the equality; past a range column later
columns can no longer narrow the scan, and (StartDateTime, MonitorId) measured
3.5x slower for the same rows. EndDateTime leads the second so it also covers
zmaudit's hunt for events that were never closed, which has no monitor to scope
it - 9 rows with a key against a 22,948 row table scan without.
Both replacements are added before the keys they supersede are dropped:
Events_MonitorId_idx (MonitorId) is now a leftmost prefix of the new key.
Events_EndDateTime_DiskSpace (EndDateTime, DiskSpace) existed for the scan
that hunted events with no DiskSpace set; DiskSpace is set when the event is
finalised in C++, so nothing scans for it any more.
Net index count on Events is unchanged.
Measured on a 7,167 event monitor. Default one hour window: 7,055 rows
examined / 25.4ms -> 173 rows / 4ms. Window scrubbed a week back, which
persists in the zmFilter_StartDateTime cookie: 6,995 rows / 28ms -> 19 rows /
0.15ms.
Adds migration db/zm_update-1.39.22.sql. Extends t/filter_sql.t; the PHP and Perl were
checked to emit identical SQL, and the semantics checked against a table of
seven event shapes - abandoned long ago, abandoned recently, overlapping,
inside, before, still recording, and still recording for 17 hours.
Events_Lock holds advisory locks over events, so that two filters do not work
on the same event at the same time. Claiming an event is a single autocommitted
INSERT of one row, which means no InnoDB lock is held while the filter actually
works on the event.
No foreign key to Events on purpose: it would make every event delete take a
lock in this table, which is the coupling this exists to avoid. Rows left
behind for deleted events are harmless and expire.
ZM_FILTER_LOCK_TIMEOUT (default 3600) is how long a claim lasts. A filter that
is killed part way through an event cannot release anything, so claims have to
expire on their own. It needs to be longer than the slowest thing a filter does
to a single event, which is why it is configurable rather than fixed.
Adds migration db/zm_update-1.39.21.sql.
Three values in the entry added a few commits ago were wrong, all for the
same reason: I read them off http status codes, in the entry whose own
comment says http status codes cannot be trusted here. Re-derived with
amcrest_probe.pl, which decides every flag by comparing frames.
CanMoveDiag was 1 because LeftUp returns 200. It shifts the picture by
1.8 against a 6.0 threshold, so nothing moves. Now 0.
Pan and tilt speed were 1..8 from the Dahua documentation. Measured
displacement is 31.5, 36.2, 41.4 for speeds 1, 2, 4 and then flat through
8, 16, 32 and 64, so the usable range is 1..4 even though the firmware
accepts far higher.
NumPresets was 255, the firmware ceiling. That is the wrong thing to
measure: the classic skin draws one button per preset, and the camera
refuses GotoPreset for any index not yet stored, so 255 gives a wall of
buttons where all but the defined ones error. Now 25, matching the
sibling Dahua/Amcrest RPC entries.
CanMoveAbs stays 0 but for a better reason than before. PositionABS is
accepted, reaches distinct positions and reproduces a revisited
coordinate, so it passes the obvious tests. Its targets are 90 degrees
apart though, and the median step shifts the view 16.4 where a single one
second nudge shifts it 42.5 - the pan argument is being clamped to a
fraction of what was asked, so a moveMap click would not land where it
was aimed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzHrvNtF13vyYBAJjktam6
The 49 control protocol definitions were sitting inline in
zm_create.sql.in, which makes adding a model-specific entry a scroll
through a wall of positional INSERTs. Move them to db/controls.sql and
pull them in with
source @PKGDATADIR@/db/controls.sql
which is how User_Preferences.sql, manufacturers.sql, models.sql,
triggers.sql, Object_Types.sql, AI_Models.sql and coco_dataset.sql are
already handled, and the existing install(FILES ${dbfileslist}) glob puts
the new file where that path resolves to. The 49 rows move across
byte-identical.
The new entry is the Amcrest IP5M-1190EW, verified against firmware
2.810.00AC004.0.R. The generic 'Amcrest HTTP API' entry advertises zoom
and continuous zoom and no presets, which is backwards for this model:
pan, tilt, the diagonals and continuous move all physically move it, arg2
really is the speed, and GotoPreset works, while zoom, focus and iris do
not. ZoomTele, ZoomWide, FocusNear and FocusFar answer OK and do
nothing; autoFocus, getFocusStatus, IrisLarge and AutoPanOn 400.
PositionABS answers OK and is inert, so CanMoveAbs and CanMoveMap stay 0.
The white light exists in Lighting_V2 but is not drivable, so CanLight
stays 0.
Worth recording for anyone rechecking this: ptz.cgi?action=getStatus on
that firmware always reports Postion=0/16.65 MoveStatus=Idle no matter
what the camera is doing, so status cannot tell you whether a move
worked. All of the above was confirmed by diffing snapshots.
zm_update-1.39.20.sql carries the same row to existing installs, guarded
with NOT EXISTS so it can be re-run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzHrvNtF13vyYBAJjktam6
Nothing enforced uniqueness, but every reader already assumed one row per
name: User::Preferences() hashes the rows by Name, montagereview.php and
User_Preference::find_one() take the first match, and TagOrder::load()
reads a single Value. With duplicates present those pick an arbitrary
row, so a write could land on one row while reads returned another --
the tag order would appear to stop updating.
Replace the UserId-only index with UNIQUE (UserId, Name) and make Name
NOT NULL. The unique index still serves UserId-only lookups and the
UserId foreign key as a leftmost prefix, so the old index is redundant
and is dropped. Name has to be NOT NULL for the constraint to mean
anything, since a unique index permits any number of NULLs; a NULL-named
row is unreachable regardless, as a preference is only ever looked up by
name.
zm_update-1.39.19.sql discards NULL-named rows, makes the column NOT
NULL, collapses existing duplicates keeping the highest Id (the most
recently inserted), adds the unique index, then drops the old one. The
index is added before the old one is dropped so the foreign key is never
left without a usable index. Each schema change is guarded against
INFORMATION_SCHEMA so re-running is a no-op. db/User_Preferences.sql
gets the same shape for fresh installs.
With the constraint in place, TagOrder::recordUsage() replaces its
SELECT-then-INSERT-or-UPDATE with a single INSERT ... ON DUPLICATE KEY
UPDATE. That drops a query and closes the race where two concurrent tag
additions by the same user both see no row and both insert.
Verified on MariaDB 11.8.8 against a seeded table: duplicate rows
collapse to the expected survivors, NULL-named rows are removed, a
second run is a clean no-op, NULL/duplicate/bad-UserId inserts are
rejected (1048/1062/1452, so the foreign key survives losing its index),
and mysqldump of a fresh install matches a migrated database exactly.
Bump version.txt and the redhat spec Version to 1.39.19.
Server.php's ReadStats() query (SELECT ... WHERE ServerId=? ORDER BY
TimeStamp DESC LIMIT 1) had no supporting index, forcing a full table
scan on every page load that shows server stats. Add a composite
(ServerId, TimeStamp) index via zm_update-1.39.18.sql (idempotent,
checked against INFORMATION_SCHEMA.STATISTICS) and zm_create.sql.in
for fresh installs. Keep the existing TimeStamp-only index, since
zmstats.pl's prune DELETE filters by TimeStamp alone.
Also query ReadStats() with the server's actual Id() instead of
coercing Id() <= 1 to 0.
Bump version.txt and the redhat spec Version to 1.39.18.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7jeoG8ptXRpXxif2JqkD7
Add the schema backing per-monitor object detection and the AI dataset/
model/class management UI:
- Monitors gains AnalysisImageOpacity and ObjectDetection,
ObjectDetectionModel, ObjectDetectionObjectThreshold,
ObjectDetectionNMSThreshold columns.
- New tables AI_Datasets, AI_Models, AI_Object_Classes,
AI_Detection_Settings and AI_Detections.
- Seed the COCO 2017 dataset (80 classes) and default per-class
detection settings via db/coco_dataset.sql.
Existing installs get zm_update-1.39.17.sql, which is idempotent and
adds the columns with their final VARCHAR(16) ObjectDetection shape
directly (no enum-churn intermediates). Fresh installs create the same
objects from zm_create.sql.in sourcing AI_Models.sql and
coco_dataset.sql. Both paths were verified to produce identical schema.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the menu customization work from the ai_server branch onto master:
- Add/edit/delete custom navbar/sidebar menu entries via Options > Menu
- Per-entry Link column (new Menu_Items.Link) with ?view= fallback derived
the same way as built-in items; custom entries render via buildMenuItem
- Live icon preview (material/font awesome) with fixed-width preview cell
- Per-row delete icon; add/reset buttons with tooltips
- Surface DB errors when saving options config: capture the swallowed PDO
error via dbLastError() and show it in the page error banner instead of
redirecting past it
Menu_Items.Link is added to zm_create.sql.in and as an idempotent ALTER in
zm_update-1.39.16.sql.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Front Culdesac (a CMIP3CD42WI-28AISP ColorVu camera) exposes the same
colorVuWhiteLight supplement-light interface as the CMIP1342WE-28MDA but was
assigned a non-HikVision control, so it showed no Light button. Add a
model-specific Controls row (HikVision protocol, CanLight only; fixed camera
with no PTZ/focus/iris) in zm_create.sql.in and migration 1.39.16.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add lightOn/lightOff/lightStatus to the HikVision control module, driving the
camera's ISAPI supplement light (ISAPI/Image/channels/<n>/supplementLight).
"On" selects the white light where the model has one (colorVuWhiteLight), else
IR; "off" restores the camera's smart/auto default (eventIntelligence) so night
IR keeps working. The methods GET the current document and rewrite only
<supplementLightMode>, preserving the std-cgi namespace and the sibling
brightness fields the firmware rejects PUTs without. Mode selection is driven by
the model's advertised supplementLight/capabilities. lightStatus returns
{ WhiteLight => 'On'|'Off'|undef } in the shape the existing web toggle consumes,
so no web changes are needed (the CanLight UI is already generic).
Add a model-specific Controls row for the LTS CMIP1342WE-28MDA (a fixed ColorVu
camera: white light, reboot, no PTZ/focus/iris), in zm_create.sql.in and
migration zm_update-1.39.16.sql. Bump version to 1.39.16.
The pure mode-selection/XML helpers are covered by t/hikvision_light.t. Verified
live against a CMIP1342WE-28MDA: the GET-modify-PUT round-trip turns the white
light on and restores the prior mode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CanLight and CanIndicatorLight columns were added to the Controls table
(between MaxWhiteSpeed and HasPresets) without updating the legacy positional
INSERT ... VALUES (NULL, ...) seed rows. All 43 of those rows were left two
values short, so a fresh `mysql < zm_create.sql` load failed with ERROR 1136
(Column count doesn't match value count).
Splice CanLight=0,CanIndicatorLight=0 into each positional row at the matching
column position. None of these legacy models support either capability, so 0,0
is correct; existing installs are unaffected (the columns were added there by
the 1.39.13/1.39.14 ALTERs with the same default). Named-column rows already
specified the values and are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a control protocol module for cameras built on the HiSilicon
Hi3510 SoC which expose cgi-bin/hi3510/ptzctrl.cgi rather than the
older Foscam decoder_control.cgi interface. Contributed by Turgut
Kalfaoglu on the forums, tested on a Tenvis TH661.
Supports continuous pan/tilt with auto-stop, emulated diagonals,
presets 1-8, and horizontal/vertical patrol via presets 9/10.
Credential and host parsing uses the Control.pm guess_credentials
helper; the camera-tested wire format (usr/pwd query parameters)
is preserved.
Adds the Controls table row to zm_create.sql.in and an idempotent
zm_update-1.39.15.sql migration, and bumps version to 1.39.15.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a CanIndicatorLight capability and status-aware Indicator toggle button. The indicator LED on the ASH21-B, ADC2W and ASH42-B is controlled via the LightGlobal config (configManager get/setConfig); add indicatorLightOn/Off/Status to Dahua_RPC and a model-specific 'Amcrest ASH42-B RPC' Controls row, with the capability also enabled on the ASH21-B/ADC2W/generic rows. Migration zm_update-1.39.13.sql adds the column.
Add Dahua_RPC keepAlive (global.keepAlive) wired into a 30s zmcontrol idle tick, plus session-expiry re-login retry in set_config and the status queries, so the long-running control daemon does not silently fail after the ~60s session timeout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a CanLight control capability rendering a single status-aware Light toggle button. The ADC2W white light is driven via CoaxialControlIO.control (Type 1, numeric IO); the button queries live state and reflects it (amber when on).
To get device state to the browser, add an opt-in two-way response path to the control protocol: zmcontrol writes a JSON result back only when a request sets wants_response (fire-and-forget commands unchanged, SIGPIPE-safe); Monitor::sendControlCommandWithResponse and ajax/control.php return it.
Also adds get_config/set_config/probe to Dahua_RPC for characterising cameras, the CanLight column (migration zm_update-1.39.12.sql), edit-UI checkbox, and a config unit test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ZoneMinder::Control::Dahua_RPC, a PTZ control module driving Amcrest Smart Home (ASH21/ASH42/ADC2W) and Dahua cameras over the JSON-RPC /RPC2 interface, since their cgi-bin API is disabled and ONVIF exposes no PTZ service. Two-stage MD5-challenge login with session reuse and self-healing re-login; continuous pan/tilt + diagonals, stop, presets, zoom, focus, reboot.
Adds a generic 'Dahua/Amcrest RPC' Controls row plus model-specific rows for the ASH21-B (pan/tilt only) and ADC2W (reboot only), the ASH21-B/ADC2W models, migration zm_update-1.39.11.sql, and a login-hash unit test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
zmupdate.pl reruns migrations where the file version is >= the current
DB version, so once the DB reaches 1.39.10 the script fires again. The
unconditional ALTER TABLE Logs ADD/DROP INDEX statements failed on the
second run with "Duplicate key name".
Guard both Logs index changes with INFORMATION_SCHEMA.STATISTICS checks
(same pattern used for the Sessions index), and DEALLOCATE PREPARE
after each block so prepared-statement names don't accumulate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous wording named a fictional Event::createNotification function;
the bucket+ES insert sequence in src/zm_event.cpp lives in the Event
constructor (line 46). Point future readers at the right symbol so they
can grep and verify the path.
Adds zm_event.cpp's create path (Events INSERT -> bucket INSERTs ->
Event_Summaries UPDATE; event_insert_trigger itself is commented out so
zmc does this directly), and lists zmaudit.pl alongside zmstats.pl, so
the comment enumerates every writer obligated to follow the canonical
order rather than just the trigger paths.
The session garbage collector ran DELETE FROM Sessions WHERE access < ?
against an unindexed column, forcing a full table scan and taking gap
locks across the access range. With REPLACE INTO Sessions happening on
every authenticated request, this is a deadlock hotspot.
- Add Sessions_access_idx on Sessions(access) in both fresh-install
schema (zm_create.sql.in) and a migration (zm_update-1.39.10.sql).
- Rewrite ZMSessionHandler::gc to a two-phase delete: SELECT up to 100
expired ids via the new index (consistent read, no locks), then
DELETE WHERE id IN (...) by primary key. InnoDB takes record locks
only on the matched rows, not gap locks on the access range.
- Bump version to 1.39.10 so zmupdate.pl picks up the new migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The stock preset MinAlarm/MinFilter/MinBlob values were authored for
~320x240 cameras, where 5% of zone area was a 62x62 motion blob.
At 1080p, the same 5% requires a 322x322 blob to trigger — so even
the "Fast, high sensitivity" preset rarely fires on a person walking.
New scale (% of zone area): low=3, medium~0.5, high=0.1.
MaxPixelThreshold (per-pixel grayscale delta, 0-255) is unchanged.
Updates zm_create.sql.in for fresh installs and adds a migration
that rewrites Ids 1-7 on existing installs. Custom presets (Id > 7)
are left alone. Bumps version to 1.39.10.
Adds a curated, per-encoder parameter-template library to ZoneMinder:
- Monitor edit page: a new Template row above the EncoderParameters
textarea offers per-encoder templates (Balanced / Archival / Low
Power / Low CPU). Apply merges the template's params into the
textarea, preserving user-only keys. Advisory lint flags option
keys that aren't recognised for the selected encoder. Switching
encoders offers a same-name template on the new encoder via a
native confirm.
- Options page: a new Encoder Templates tab with full CRUD —
list / edit / copy / delete — backed by a new CakePHP REST API
at /api/encoder_templates.
- Storage: a new EncoderTemplates DB table seeded with 14 shipped
defaults across libx264 / libx265 / h264_nvenc / hevc_nvenc /
h264_vaapi / hevc_vaapi. The table is mutable; ZM upgrades do not
re-seed user-edited rows.
- valid_keys (the lint allow-list) stays in PHP code as ffmpeg
vocabulary, not user data.
- Default params explicitly include pix_fmt to avoid the yuvj420p
HEVC HW-decode rejection issue we hit earlier.
No C++ change. The textarea content is parsed by the existing
av_dict_parse_string call in src/zm_videostore.cpp.
version.txt -> 1.39.6.
Specs: docs/superpowers/specs/2026-05-0{1,2}-*.md
Plans: docs/superpowers/plans/2026-05-0{1,2}-*.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records the InnoDB X-lock order that all writers (zma trigger path, zmstats
prune+resync, zmfilter/Event::delete) must follow to avoid the deadlock cycles
fixed in the surrounding commits. Comment only; no behavior change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a CreatedBy column to the Reports table and a canEdit() method on
the Report class so $report->canEdit() (already called from
web/ajax/reports.php) resolves to a real check. canEdit() permits the
report owner (CreatedBy == user) or any user/role with System=Edit.
Wire actions/report.php to stamp CreatedBy on first save and refuse
save/delete on existing reports the current user cannot edit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Accidentally removed in 4bd56c22d when refactoring menu rendering. The
1.39.3 migration seeds a 'Watch' Menu_Items row, but there was no
function registered for that key, so the navbar entry never rendered.
Restore getCycleHTML with the current menu-function signature
(custom label, skipTranslate) and map the 'Watch' menu key to it.
Add the 'Watch' row to zm_create.sql.in for fresh installs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The migration procedure computed correct sub-1.0 percentage values
(e.g. 3000 pixels → 0.26%) but the ALTER TABLE that changed the
columns from INT to DECIMAL(10,2) ran *after* the conversion. MySQL
silently truncated values like 0.26 to 0 when storing into INT
columns, zeroing out MinAlarmPixels, MinFilterPixels, MinBlobPixels,
MaxAlarmPixels, MaxFilterPixels, and MaxBlobPixels for all zones.
Move the ALTER TABLE statements before the CALL so the columns can
hold decimal values when the UPDATE writes them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous migration relied on Zones.Area to convert pixel thresholds
to percentages, but this column is often 0 (never populated by the C++
daemon which calculates polygon area at runtime). Replace the single
UPDATE with a cursor-based stored procedure that computes the polygon
area directly from percentage coordinates using the shoelace formula,
then derives the pixel area for accurate threshold conversion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ROUND() without a precision argument rounds to integers, zeroing out
any converted percentage below 0.5%. Use ROUND(..., 2) to match the
DECIMAL(10,2) column type and keep values like 0.96%.
Fixes https://forums.zoneminder.com/viewtopic.php?t=34657
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a boolean web config option to control visibility of the Back and
Refresh navigation buttons shown at the top of most views. Uses a body
class and CSS rule so no individual view files need changes.
Also remove the ZM_WEB_BUTTON_STYLE INSERT from the migration SQL since
zmupdate.pl handles Config table inserts from ConfigData.pm.in.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a web config option to control toolbar button display:
- icons+text (default): show both icon and label
- icons: show only the icon, hide text labels
- text: show only the label, hide icons on buttons that have labels
Body class (btn-icons-only / btn-text-only) is set in getBodyTopHTML() and
CSS rules in skin.css toggle visibility of .text spans and icon elements.
Add title tooltips to console.php buttons so they remain usable in icon-only
mode. Migration appended to zm_update-1.39.4.sql.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change ZM_OPT_USE_REMEMBER_ME from a boolean to a tri-state string:
- None: checkbox hidden, sessions persist for ZM_COOKIE_LIFETIME (old disabled)
- Yes: checkbox shown and pre-checked by default
- No: checkbox shown and unchecked by default (old enabled behavior)
Update ConfigData.pm.in with new type definition, login.php to honor the
checked state, and session/action handlers to recognize the new values.
Migration in zm_update-1.39.4.sql maps old '1' to 'No' and '0' to 'None'.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change import button label from "Save" to "Import" in add_monitors modal
- Fix FormData TypeError by using correct form id and extracting DOM element
- Add INSERT IGNORE to menu_items migration to backfill any missing entries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>