fmeshsync fix with synology,vm's and lxc
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
# AssetManager installation guide
|
||||
|
||||
This guide describes a Docker image based installation and the first-start behavior of AssetManager.
|
||||
|
||||
## 1. Prepare the installation directory
|
||||
|
||||
Create a dedicated directory and the persistent data directories:
|
||||
|
||||
```bash
|
||||
mkdir -p /srv/docker/assetmanager
|
||||
cd /srv/docker/assetmanager
|
||||
mkdir -p data/config data/uploads data/logs data/backups data/scripts data/postgres
|
||||
```
|
||||
|
||||
Create a `.env` file. Use strong, unique values for all secrets:
|
||||
|
||||
```env
|
||||
ASSETMANAGER_VERSION=0.5.5.47
|
||||
APP_PORT=8088
|
||||
POSTGRES_DB=assetmanager
|
||||
POSTGRES_USER=assetmanager
|
||||
POSTGRES_PASSWORD=CHANGE_ME
|
||||
SESSION_SECRET=CHANGE_ME_LONG_RANDOM
|
||||
LOCAL_ADMIN_USERNAME=emergency-admin
|
||||
LOCAL_ADMIN_PASSWORD=CHANGE_ME_MIN_12_CHARS
|
||||
MESHCENTRAL_PASSWORD=
|
||||
LDAP_BIND_PASSWORD=
|
||||
BACKUP_INTERVAL_HOURS=8
|
||||
BACKUP_RETENTION_DAYS=3
|
||||
```
|
||||
|
||||
Keep `.env` private. Do not commit it to a public repository.
|
||||
|
||||
## 2. Docker Compose file for a registry image
|
||||
|
||||
Use the published image instead of building the application locally:
|
||||
|
||||
```yaml
|
||||
name: assetmanager
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
container_name: assetmanager-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-assetmanager}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-assetmanager}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change-me}
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-assetmanager} -d ${POSTGRES_DB:-assetmanager}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
app:
|
||||
image: git.jusaro.de/roland/assetmanager:${ASSETMANAGER_VERSION:-0.5.5.47}
|
||||
container_name: assetmanager-app
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-assetmanager}:${POSTGRES_PASSWORD:-change-me}@db:5432/${POSTGRES_DB:-assetmanager}
|
||||
APP_TITLE: AssetManager
|
||||
APP_CONFIG: /app/config/config.json
|
||||
APPINFO_PATH: /app/config/APPINFO.json
|
||||
BACKUP_DIR: /data/backups
|
||||
BACKUP_INTERVAL_HOURS: ${BACKUP_INTERVAL_HOURS:-8}
|
||||
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-3}
|
||||
MESHCENTRAL_PASSWORD: ${MESHCENTRAL_PASSWORD:-}
|
||||
SYNC_LOG_DIR: /app/data/logs/sync
|
||||
DIAGNOSTIC_DIR: /app/data/logs/diagnostics
|
||||
LDAP_BIND_PASSWORD: ${LDAP_BIND_PASSWORD:-}
|
||||
SESSION_SECRET: ${SESSION_SECRET:-}
|
||||
LOCAL_ADMIN_USERNAME: ${LOCAL_ADMIN_USERNAME:-}
|
||||
LOCAL_ADMIN_PASSWORD: ${LOCAL_ADMIN_PASSWORD:-}
|
||||
ports:
|
||||
- "${APP_PORT:-8088}:8000"
|
||||
volumes:
|
||||
- ./data/config:/app/config
|
||||
- ./data/uploads:/app/app/static/uploads
|
||||
- ./data/logs:/app/data/logs
|
||||
- ./data/backups:/data/backups
|
||||
- ./data/scripts:/scripts
|
||||
```
|
||||
|
||||
If the registry is private, sign in once on the Docker host:
|
||||
|
||||
```bash
|
||||
docker login git.jusaro.de
|
||||
```
|
||||
|
||||
## 3. First start
|
||||
|
||||
Pull and start the containers:
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
On the first start, the container creates the following files only when they do not already exist:
|
||||
|
||||
```text
|
||||
data/config/config.json
|
||||
data/config/APPINFO.json
|
||||
```
|
||||
|
||||
Existing files are preserved during subsequent container starts and updates.
|
||||
|
||||
## 4. Important: authentication is initially disabled
|
||||
|
||||
A fresh default configuration starts with authentication disabled. This is intentional so the initial configuration can be completed, but the installation must not be exposed to an untrusted network in this state.
|
||||
|
||||
The web interface displays a warning banner while authentication is disabled. To enable local authentication:
|
||||
|
||||
1. Open `data/config/config.json`.
|
||||
2. Locate the `authentication` section.
|
||||
3. Set `mode` to `local`.
|
||||
4. Restart only the application container:
|
||||
|
||||
```bash
|
||||
docker compose restart app
|
||||
```
|
||||
|
||||
5. Sign in using `LOCAL_ADMIN_USERNAME` and `LOCAL_ADMIN_PASSWORD` from `.env`.
|
||||
|
||||
The local administrator is the protected emergency account and should remain available even when LDAP/Active Directory is configured later.
|
||||
|
||||
## 5. Configure the callback base address
|
||||
|
||||
Remote jobs call back to AssetManager after execution. In **Software settings**, configure the callback base address when clients must reach AssetManager through DNS, a reverse proxy, VPN, or the Internet.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
https://assetmanager.example.org
|
||||
https://assetmanager.example.org:8443
|
||||
http://192.168.1.40:8088
|
||||
```
|
||||
|
||||
For Internet-facing use, HTTPS through a properly configured reverse proxy is strongly recommended. The callback endpoint must be reachable from the managed client.
|
||||
|
||||
If no callback base address is configured, AssetManager derives the address from the current request.
|
||||
|
||||
## 6. Verify the installation
|
||||
|
||||
Check the application status and logs:
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
docker compose logs --tail=100 -f
|
||||
```
|
||||
|
||||
After signing in, verify at least:
|
||||
|
||||
- application version
|
||||
- database connection
|
||||
- local emergency administrator login
|
||||
- MeshCentral connectivity, if used
|
||||
- one test inventory or remote job
|
||||
- successful callback processing
|
||||
|
||||
## 7. Persistent data and backups
|
||||
|
||||
The following paths contain persistent runtime data and must survive image replacement:
|
||||
|
||||
```text
|
||||
data/config/
|
||||
data/postgres/
|
||||
data/uploads/
|
||||
data/logs/
|
||||
data/backups/
|
||||
data/scripts/
|
||||
.env
|
||||
```
|
||||
|
||||
Back up `.env` and the complete `data/` directory before significant updates.
|
||||
|
||||
## 8. Updating an image-based installation
|
||||
|
||||
Set the desired fixed image version in `.env`, for example:
|
||||
|
||||
```env
|
||||
ASSETMANAGER_VERSION=0.5.5.47
|
||||
```
|
||||
|
||||
Then update:
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
docker compose logs --tail=100 -f
|
||||
```
|
||||
|
||||
Using a fixed version tag is recommended for controlled production updates.
|
||||
|
||||
## 9. Log files
|
||||
|
||||
Application logs are stored below `data/logs/`. The main files include `errors.log` and, when LDAP is used, `ldap.log`.
|
||||
|
||||
Starting with version 0.5.5.45, these file loggers detect if their active log file was deleted or replaced externally. On the next log event, the file is reopened and created again automatically; an application container restart is no longer required.
|
||||
|
||||
For normal log maintenance, rotation/truncation mechanisms are preferable to manually deleting active log files. Docker container output remains available through:
|
||||
|
||||
```bash
|
||||
docker compose logs --tail=100 -f
|
||||
```
|
||||
|
||||
## 10. Troubleshooting
|
||||
|
||||
### The web interface logs in as guest
|
||||
|
||||
The default configuration still has authentication disabled. Set `authentication.mode` to `local` in `data/config/config.json` and restart the app container.
|
||||
|
||||
### A remote job remains at “callback pending”
|
||||
|
||||
Check:
|
||||
|
||||
- whether the callback base address is reachable from the client
|
||||
- reverse proxy/firewall rules
|
||||
- application logs under `data/logs/`
|
||||
- `docker compose logs --tail=100 -f`
|
||||
|
||||
### A configuration file is missing
|
||||
|
||||
If `config.json` or `APPINFO.json` is absent, restart the application container. The entrypoint creates a missing default file without overwriting an existing one.
|
||||
@@ -1,168 +0,0 @@
|
||||
# Proposal: Client communication and future software deployment
|
||||
|
||||
## Target architecture
|
||||
|
||||
A small Windows client ("AssetManager Agent") runs on every managed computer. A Netlogon or GPO startup script only installs or updates this agent. Inventory and software jobs are then controlled through a secured HTTPS API.
|
||||
|
||||
## Why not execute every command directly in a Netlogon script?
|
||||
|
||||
A startup script is useful for bootstrap and repair, but unsuitable for permanent job control:
|
||||
|
||||
- User and computer startup is delayed.
|
||||
- Results and retries are difficult to track.
|
||||
- A network share or domain controller may not yet be reachable during startup.
|
||||
- Software installations require status, timeout, exit-code, and restart handling.
|
||||
- Passwords or global API keys must not be stored in the script.
|
||||
|
||||
## Recommended components
|
||||
|
||||
### 1. Bootstrap through GPO or Netlogon
|
||||
|
||||
The startup script:
|
||||
|
||||
1. creates `C:\ProgramData\AssetManager\agent`,
|
||||
2. downloads a signed agent version from an internal HTTPS address,
|
||||
3. installs a Windows service,
|
||||
4. stores only the server URL and a one-time registration identifier,
|
||||
5. starts the service.
|
||||
|
||||
### 2. Device identity
|
||||
|
||||
During initial registration, the agent reports:
|
||||
|
||||
- computer name,
|
||||
- AD domain,
|
||||
- BIOS serial number,
|
||||
- Windows MachineGuid,
|
||||
- optionally, the MeshCentral node ID.
|
||||
|
||||
The server then creates a device-specific token. Windows DPAPI protects the token locally. Do not use a shared API key for all clients.
|
||||
|
||||
### 3. Agent polling
|
||||
|
||||
For example, the client requests a job every five minutes over HTTPS:
|
||||
|
||||
`GET /api/agent/v1/jobs/next`
|
||||
|
||||
Response when no job is available:
|
||||
|
||||
```json
|
||||
{"job": null, "next_poll_seconds": 300}
|
||||
```
|
||||
|
||||
Response with a job:
|
||||
|
||||
```json
|
||||
{
|
||||
"job": {
|
||||
"id": 4711,
|
||||
"type": "software_inventory",
|
||||
"expires_at": "2026-07-22T23:00:00Z",
|
||||
"payload": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The client first acknowledges receipt and later reports the result, exit code, log excerpt, and timestamps.
|
||||
|
||||
### 4. Software inventory
|
||||
|
||||
The first agent function should:
|
||||
|
||||
- read installed MSI applications from the 32-bit and 64-bit registry,
|
||||
- optionally include AppX packages,
|
||||
- collect name, version, publisher, installation date, and uninstall string,
|
||||
- send the compressed result to AssetManager,
|
||||
- store inventory runs per asset on the server.
|
||||
|
||||
Do not use `Win32_Product`, because that WMI class may trigger MSI repairs and can be very slow.
|
||||
|
||||
### 5. Software catalog for future deployment
|
||||
|
||||
Server-side tables:
|
||||
|
||||
- `software_packages`
|
||||
- `software_package_versions`
|
||||
- `software_jobs`
|
||||
- `software_job_results`
|
||||
- `agent_devices`
|
||||
- `agent_tokens`
|
||||
- `agent_inventory_runs`
|
||||
|
||||
A software package contains:
|
||||
|
||||
- name and version,
|
||||
- installation source,
|
||||
- SHA-256 hash,
|
||||
- installation command,
|
||||
- uninstall command,
|
||||
- detection rule,
|
||||
- accepted exit codes,
|
||||
- restart behavior,
|
||||
- timeout,
|
||||
- target architecture.
|
||||
|
||||
### 6. Installation source
|
||||
|
||||
Packages may initially remain on a file server, for example:
|
||||
|
||||
`\\fileserver\software\packages\ExampleApp\1.0\`
|
||||
|
||||
A job must not contain arbitrary PowerShell code. It refers to an approved package definition. The agent verifies that:
|
||||
|
||||
1. the package is approved on the server,
|
||||
2. the source is below an allowed UNC base path,
|
||||
3. the file hash matches,
|
||||
4. the installation command matches the package definition.
|
||||
|
||||
An internal HTTPS package download is more robust in the long term because it does not require machine access to a share and is easier to audit.
|
||||
|
||||
### 7. Security rules
|
||||
|
||||
- use HTTPS only, with an internally trusted certificate,
|
||||
- use a separate revocable token per device,
|
||||
- sign jobs on the server or deliver them through an authenticated TLS connection,
|
||||
- do not provide unrestricted shell or PowerShell input in the web interface,
|
||||
- execute commands only through defined job types,
|
||||
- verify package files with SHA-256,
|
||||
- keep a complete audit log,
|
||||
- use roles and permissions for approval and execution,
|
||||
- enforce maximum runtime and retry limits,
|
||||
- run the agent as SYSTEM only when a specific job requires it.
|
||||
|
||||
## Suggested implementation phases
|
||||
|
||||
### Phase 1 – Inventory
|
||||
|
||||
- agent registration
|
||||
- heartbeat and last-contact tracking
|
||||
- operating-system and software inventory
|
||||
- display on the asset detail page
|
||||
- manual inventory job
|
||||
|
||||
### Phase 2 – Job model
|
||||
|
||||
- generic job queue
|
||||
- statuses: pending, claimed, running, success, failed, expired
|
||||
- exit codes, logs, timeout, and retry
|
||||
- live status in AssetManager
|
||||
|
||||
### Phase 3 – Software catalog
|
||||
|
||||
- package definitions
|
||||
- detection rules
|
||||
- installation and uninstallation
|
||||
- single devices and device groups
|
||||
- maintenance windows
|
||||
|
||||
### Phase 4 – Rollout safety
|
||||
|
||||
- pilot groups
|
||||
- approval workflow
|
||||
- staged deployment
|
||||
- abort on excessive failure rate
|
||||
- restart coordination
|
||||
|
||||
## Recommendation for the next development step
|
||||
|
||||
Implement Phase 1 first. The agent should not yet execute arbitrary commands. Once registration, heartbeat, and software inventory are stable, the same secured communication can be used for the job queue and later software deployment.
|
||||
@@ -4,12 +4,29 @@
|
||||
|
||||
Release notes are stored outside the project root to keep the repository overview compact.
|
||||
|
||||
The current release is **0.5.5.30**.
|
||||
The current release is **0.5.5.47**.
|
||||
|
||||
Older notes are concise English summaries migrated from the original release documents. Git history remains authoritative for exact implementation details.
|
||||
|
||||
## Releases
|
||||
|
||||
- [0.5.5.47](UPDATE-0.5.5.47.md)
|
||||
- [0.5.5.46](UPDATE-0.5.5.46.md)
|
||||
- [0.5.5.45](UPDATE-0.5.5.45.md)
|
||||
- [0.5.5.44](UPDATE-0.5.5.44.md)
|
||||
- [0.5.5.43](UPDATE-0.5.5.43.md)
|
||||
- [0.5.5.42](UPDATE-0.5.5.42.md)
|
||||
- [0.5.5.41](UPDATE-0.5.5.41.md)
|
||||
- [0.5.5.40](UPDATE-0.5.5.40.md)
|
||||
- [0.5.5.39](UPDATE-0.5.5.39.md)
|
||||
- [0.5.5.38](UPDATE-0.5.5.38.md)
|
||||
- [0.5.5.37](UPDATE-0.5.5.37.md)
|
||||
- [0.5.5.36](UPDATE-0.5.5.36.md)
|
||||
- [0.5.5.35](UPDATE-0.5.5.35.md)
|
||||
- [0.5.5.34](UPDATE-0.5.5.34.md)
|
||||
- [0.5.5.33](UPDATE-0.5.5.33.md)
|
||||
- [0.5.5.32](UPDATE-0.5.5.32.md)
|
||||
- [0.5.5.31](UPDATE-0.5.5.31.md)
|
||||
- [0.5.5.30](UPDATE-0.5.5.30.md)
|
||||
- [0.5.5.29](UPDATE-0.5.5.29.md)
|
||||
- [0.5.5.28](UPDATE-0.5.5.28.md)
|
||||
@@ -165,3 +182,6 @@ See [UPDATE-NOTE.md](UPDATE-NOTE.md).
|
||||
- [0.5.5.41](UPDATE-0.5.5.41.md) – Configurable MeshCentral matching and synchronization dry run.
|
||||
|
||||
- [0.5.5.42](UPDATE-0.5.5.42.md) — public repository documentation cleanup
|
||||
|
||||
|
||||
- [0.5.5.50](UPDATE-0.5.5.50.md) - safer MeshCentral identity matching for containers and virtual machines
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Version 0.5.5.43
|
||||
# AssetManager 0.5.5.43
|
||||
|
||||
- Added drag handles to resize every standard table column.
|
||||
- Stored column widths independently for each table and column.
|
||||
- Kept the asset list's existing horizontal-scroll width calculation in sync.
|
||||
- Added keyboard resizing with the left and right arrow keys.
|
||||
- Added double-click on a separator to reset a column to its default width.
|
||||
## Changes
|
||||
|
||||
- Added first-start initialization for persistent `config.json` and `APPINFO.json` when using the Docker image.
|
||||
- Existing persistent configuration files are never overwritten by the container entrypoint.
|
||||
- Added an image ownership cleanup for the bundled MeshCentral Node.js dependencies to avoid invalid UID/GID extraction errors on other Docker hosts.
|
||||
- Prepared the Docker image for registry-based installations and updates with persistent configuration volumes.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# AssetManager 0.5.5.44
|
||||
|
||||
- Sanitize embedded NUL characters in software inventory callback data before PostgreSQL persistence.
|
||||
- Add a visible first-start warning banner while authentication is disabled.
|
||||
- Document the first-start authentication procedure for image-based installations.
|
||||
@@ -0,0 +1,6 @@
|
||||
# AssetManager 0.5.5.45
|
||||
|
||||
- Added a detailed Docker image installation and first-start guide.
|
||||
- Documented automatic first-start creation of `config.json` and `APPINFO.json`.
|
||||
- Documented the initial authentication-disabled state, warning banner, local emergency administrator activation, callback base address, image updates, and troubleshooting.
|
||||
- Changed application and LDAP file logging to watched file handlers so externally deleted or replaced active log files are recreated automatically on the next log event without restarting the application container.
|
||||
@@ -0,0 +1,13 @@
|
||||
# AssetManager 0.5.5.46
|
||||
|
||||
## MeshCentral device identity and placeholder serial numbers
|
||||
|
||||
- Added configurable global serial-number placeholder values.
|
||||
- Added manufacturer-specific placeholder rules using `Manufacturer | Serial number`.
|
||||
- Placeholder serial numbers are not used for asset matching.
|
||||
- Placeholder serial numbers do not overwrite an existing asset serial number during synchronization.
|
||||
- The MeshCentral node ID remains the stable unique device identity and is used before serial-number matching by default.
|
||||
- The original MeshCentral source value remains available in stored source data when source-data storage is enabled.
|
||||
- Dry-run and live synchronization use the same placeholder detection logic.
|
||||
|
||||
The default configuration includes common textual placeholders and a Synology-specific rule for `123456789` and `Unknown`.
|
||||
@@ -0,0 +1,12 @@
|
||||
# AssetManager 0.5.5.47
|
||||
|
||||
## Changes
|
||||
|
||||
- MeshCentral network mapping now selects a plausible primary interface instead of relying on the first network record.
|
||||
- Common Ethernet, Linux predictable-interface, bridge, bond, wireless and virtualization interface names are ranked case-insensitively.
|
||||
- Loopback, container and tunnel interfaces are de-prioritized.
|
||||
- Invalid all-zero or malformed MAC addresses are ignored.
|
||||
- IP and MAC mapping prefer the same active interface with usable IPv4 data whenever possible.
|
||||
- The asset list now visibly shows active category and duplicate-selection filters with removable filter chips.
|
||||
- Duplicate-filter record counts now reflect the duplicate result set, including `0 of 0` when no duplicates are found.
|
||||
- Duplicate-search result text is fully localized.
|
||||
@@ -0,0 +1,8 @@
|
||||
# AssetManager 0.5.5.48
|
||||
|
||||
## Restored table state and row-number resizing
|
||||
|
||||
- Restores persistent client-side table sorting across page reloads and partial table reloads.
|
||||
- Restores the visual row-number column on data tables.
|
||||
- Restores resizing of the row-number column and persists its width per table in browser local storage.
|
||||
- Keeps the current 0.5.5.47 duplicate-filter, network-interface, authentication-banner, callback, logging, and MeshCentral identity changes intact.
|
||||
@@ -0,0 +1,9 @@
|
||||
# AssetManager 0.5.5.49
|
||||
|
||||
## Persistent resizing for all table columns
|
||||
|
||||
- Extends table resizing from the visual row-number column to every visible data-table column.
|
||||
- Stores widths per table and column in browser local storage.
|
||||
- Restores saved widths after page reloads and partial/AJAX table reloads.
|
||||
- Keeps persistent sorting, filters, row numbering and newer 0.5.5.44-0.5.5.48 fixes intact.
|
||||
- Adds translated resize hints for German and English.
|
||||
@@ -0,0 +1,11 @@
|
||||
# AssetManager 0.5.5.50
|
||||
|
||||
## Safer MeshCentral identity matching for containers and virtual machines
|
||||
|
||||
- Keeps MeshCentral node ID as the primary device identity.
|
||||
- Adds optional exact MAC-address matching between node ID and serial-number matching.
|
||||
- Detects serial-number identities that occur more than once in the current MeshCentral inventory and excludes them from serial-number matching.
|
||||
- Prevents a serial-number match when both the incoming device and the existing asset have valid but different MAC addresses.
|
||||
- This avoids false matches for LXC/VM guests that inherit the same host DMI/mainboard serial number.
|
||||
- Existing configured dummy-serial rules remain unchanged.
|
||||
- Dry-run and real synchronization use the same matching logic.
|
||||
@@ -0,0 +1,9 @@
|
||||
# AssetManager 0.5.5.51
|
||||
|
||||
## MeshCentral category mapping UI
|
||||
|
||||
- Moves the existing MeshCentral device-type to asset-category mapping from the general settings template to the MeshCentral synchronization page.
|
||||
- Adds a dedicated save action for the fallback category and MeshCentral types 1 through 8.
|
||||
- Keeps the existing configuration keys (`meshcentral.fallback_category_id` and `meshcentral.category_mapping`) and synchronization behavior unchanged.
|
||||
- Validates submitted category IDs server-side.
|
||||
- Removes the unreachable hidden MeshCentral category-mapping block from the general settings page.
|
||||
@@ -0,0 +1,7 @@
|
||||
# AssetManager 0.5.5.52
|
||||
|
||||
## MeshCentral category page fix
|
||||
|
||||
- Fixes a crash when opening the MeshCentral synchronization page after the category-mapping UI was moved there.
|
||||
- Uses the existing category name ordering because the `Category` model has no `sort_order` column.
|
||||
- Keeps the category-mapping storage and synchronization logic unchanged.
|
||||
Reference in New Issue
Block a user