Create MeshCentral workspace
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
|
||||
.env
|
||||
*.env
|
||||
|
||||
vendor/MeshCentral/
|
||||
|
||||
dist/packages/
|
||||
*.zip
|
||||
|
||||
.old/
|
||||
*.old
|
||||
*.old-*
|
||||
*.bak.*
|
||||
*..old
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,44 @@
|
||||
# MeshCentral Workspace
|
||||
|
||||
This repository collects Christ IT's MeshCentral-related work in one place.
|
||||
|
||||
The workspace is intentionally source-first. It contains the code we own around MeshCentral operations, WordPress control, and MeshCentral plugin distribution. A full upstream MeshCentral checkout should stay outside this repository unless a specific patch or fork is being maintained.
|
||||
|
||||
## Project Lanes
|
||||
|
||||
### WordPress Control
|
||||
|
||||
`wordpress/plugins/mesh-press/`
|
||||
|
||||
MeshPress is the WordPress-side control plugin. It talks to the bridge service, caches fleet state, exposes MeshCentral group/device workflows, receives protected bridge events, and can forward useful lifecycle events into OmniLog.
|
||||
|
||||
### Bridge Service
|
||||
|
||||
`bridge/meshpress-bridge/`
|
||||
|
||||
MeshPress Bridge is the Node/Express service that runs beside MeshCentral. It talks to MeshCentral through `meshctrl`, exposes token-protected API routes to MeshPress, pushes fleet snapshots to WordPress, and gates higher-risk operations behind explicit environment flags.
|
||||
|
||||
### MeshCentral Plugins
|
||||
|
||||
`meshcentral/plugins/`
|
||||
|
||||
These are plugins that run inside MeshCentral itself.
|
||||
|
||||
- `gracepress/` is the GracePress Catalog Connector for MeshCentral plugin delivery and diagnostics.
|
||||
- `gracepress-theme-studio/` is the first managed MeshCentral plugin, currently focused on theme profile editing.
|
||||
|
||||
### Current Distribution Artifacts
|
||||
|
||||
`dist/current/`
|
||||
|
||||
This folder keeps the current generated manifests, changelog copies, and MeshCentral facade files imported from the GracePress catalog. These are copied here as release evidence and migration reference. The long-term goal is for this workspace to own its own build/release scripts instead of depending on the GracePress workspace.
|
||||
|
||||
## External References
|
||||
|
||||
`vendor/`
|
||||
|
||||
Use this folder for notes about external dependencies or local clones. Do not commit a full upstream MeshCentral source tree by default.
|
||||
|
||||
## Current Migration Rule
|
||||
|
||||
GracePress remains the active release/catalog source until this workspace has equivalent build and ship tooling. Do not delete the original GracePress copies until the new repository can build, package, and publish the same artifacts.
|
||||
@@ -0,0 +1,109 @@
|
||||
# meshpress-bridge Changelog
|
||||
|
||||
## Current
|
||||
- Fixed agent-link generation to pass the resolved group object instead of a boolean match flag.
|
||||
- Added selective WordPress event pushes for high-signal bridge lifecycle events so MeshPress can forward them into OmniLog.
|
||||
- Added bridge status visibility for WordPress event push configuration.
|
||||
|
||||
## 0.4.3 - 2026-06-12
|
||||
- Fixed agent-link generation to pass the resolved group object instead of a boolean match flag.
|
||||
- Added selective WordPress event pushes for high-signal bridge lifecycle events so MeshPress can forward them into OmniLog.
|
||||
- Added bridge status visibility for WordPress event push configuration.
|
||||
|
||||
## 0.4.2 - 2026-05-25
|
||||
- Added redacted MeshCentral config auditing for MySQL/MariaDB, framing, logging, backup, and maintenance posture.
|
||||
- Added real MeshCentral `settings.maintenanceMode` toggling with config backup and service restart support.
|
||||
- Replaced the data-only backup with a guarded full snapshot that includes MeshCentral files, config, manifest, checksum, and a MySQL/MariaDB dump when configured.
|
||||
- Added a configurable database dump timeout (`MESHPRESS_DB_DUMP_TIMEOUT_SECONDS`) so backup failures produce clear diagnostics instead of hanging.
|
||||
|
||||
## 0.4.1 - 2026-05-25
|
||||
- Resolved MeshCentral groups before generating agent links, so WordPress punctuation drift no longer breaks installer commands.
|
||||
- Generated Linux install commands from the canonical group short id, including MeshCentral `@` characters.
|
||||
- Returned the matched group record and group name with agent-link responses for clearer MeshPress output.
|
||||
- Returned normalized group `short_id` values so agent-link generation can preserve MeshCentral punctuation across the full path.
|
||||
- Added installer-shape diagnostics and an explicit warning when the resolved short Mesh ID looks too short.
|
||||
- Added a curl fallback command while keeping the native MeshCentral wget installer command as the primary Linux path.
|
||||
|
||||
## 0.4.0 - 2026-05-24
|
||||
- Resolved MeshCentral groups before generating agent links, so WordPress punctuation drift no longer breaks installer commands.
|
||||
- Generated Linux install commands from the canonical group short id, including MeshCentral `@` characters.
|
||||
- Returned the matched group record and group name with agent-link responses for clearer MeshPress output.
|
||||
|
||||
## 0.3.9 - 2026-05-24
|
||||
- Replaced the bridge updater with a staged, verified, rollback-capable update flow that does not delete PM2 before the new copy is ready.
|
||||
- Added catalog update checks and a runtime maintenance-mode toggle endpoint for MeshPress.
|
||||
- Made group-id resolution tolerant of MeshCentral punctuation drift, and switched Linux agent commands to MeshCentral's native install-script pattern.
|
||||
|
||||
## 0.3.8 - 2026-05-24
|
||||
- Group edits now verify the refreshed MeshCentral group record before returning success.
|
||||
- Added extra group edit diagnostics when MeshCtrl accepts a command but the group does not actually change.
|
||||
- Tried both `--desc` and `--description` edit forms for better MeshCtrl version compatibility.
|
||||
|
||||
## 0.3.7 - 2026-05-24
|
||||
- Added guarded MeshCentral maintenance endpoints for version/update checks, data backups, and update triggering.
|
||||
- Added agent uninstall diagnostics and richer copy-friendly bridge logging for lifecycle testing.
|
||||
- Hardened the bridge installer/updater so running from the live `/opt` target refuses instead of deleting itself.
|
||||
|
||||
## 0.3.6 - 2026-05-24
|
||||
- Rejected MeshCtrl failure text even when MeshCtrl exits with code 0, preventing false-success group edit responses.
|
||||
- Added richer group edit candidate attempts and copyable JSON diagnostics for invalid group identifiers.
|
||||
- Expanded agent link generation with Windows, Linux, macOS, Mesh Assistant, and Linux install command output.
|
||||
|
||||
## 0.3.5 - 2026-05-24
|
||||
- Changed MeshCentral session links to prefer the working `viewmode-first + gotonode` URL pattern.
|
||||
- Added short-node-id extraction for `gotonode` links so full `node//...` ids do not break direct console sessions.
|
||||
- Added `/api/session-diagnostics/:nodeId` plus guarded device rename and move endpoints for manual MeshPress testing.
|
||||
|
||||
## 0.3.4 - 2026-05-23
|
||||
- Added bridge runtime version reporting and a guarded manual update endpoint for MeshPress admin.
|
||||
- Added richer MeshCentral session-link generation for Desktop, Console, and Files with multiple direct-link candidates.
|
||||
- Added `console` and `shell` aliases for MeshCentral terminal sessions.
|
||||
|
||||
## 0.3.3 - 2026-05-23
|
||||
- Added `/api/version`, `/api/update-now`, and `/api/session-links/:nodeId`.
|
||||
- Added all-link candidate generation for MeshCentral session testing.
|
||||
- Restarted bridge updates with `pm2 --update-env` so env/config changes are picked up.
|
||||
|
||||
## 0.3.2 - 2026-05-23
|
||||
- Hardened bridge authentication for public-port exposure by requiring a configured bearer token on all endpoints.
|
||||
- Added optional exact-IP allowlisting and failed-auth rate limiting.
|
||||
- Moved WordPress fleet push authentication from query string to Authorization header.
|
||||
- Added startup security warnings for all-interface binding without app-layer allowlisting.
|
||||
|
||||
## 0.3.1 - 2026-05-23
|
||||
- Added bridge self-test, server-info, users, events, and device-info diagnostics for MeshPress.
|
||||
- Added configurable MeshCentral direct-link style with `MESHPRESS_LINK_MODE`.
|
||||
- Added guarded provisioning endpoints for group edit/remove, group user assignment, invite links, and simple device message/toast commands.
|
||||
- Added cache clearing and richer status output for faster troubleshooting.
|
||||
|
||||
## 0.3.0 - 2026-05-22
|
||||
- Added Phase 5.2 MeshCentral provisioning endpoints for Service Desk.
|
||||
- Added authenticated agent/provisioning link generation for MeshCentral device groups.
|
||||
- Added guarded device group creation through MeshCtrl `adddevicegroup`.
|
||||
- Required `MESHPRESS_ALLOW_COMMANDS=1` before the bridge will create MeshCentral groups, keeping provisioning actions opt-in.
|
||||
- Returned Windows x64, x86, ARM64, and interactive Windows agent links for linked MeshCentral groups.
|
||||
|
||||
<!-- GP-RECONSTRUCTED-CHANGELOG:START -->
|
||||
|
||||
## Reconstructed History
|
||||
|
||||
These entries were inferred from local distribution archives, module headers, and `.old` backups. They are intentionally conservative and may not capture the full intent of each change.
|
||||
|
||||
### 0.2.0 - reconstructed
|
||||
- Reconstructed from archive `meshpress-bridge-v0.2.0.zip` (2026-05-20).
|
||||
- Header description at this version: GracePress MeshPress Bridge. Pushes MeshCentral fleet snapshots to WordPress MeshPress.
|
||||
- Catalog/category marker: Hidden.
|
||||
|
||||
### 0.1.1 - reconstructed
|
||||
- Reconstructed from 2 file backup(s) in `tools/meshpress-bridge/.old`.
|
||||
- Exact intent unknown; this confirms a recoverable historical version existed.
|
||||
|
||||
### 0.1.0 - reconstructed
|
||||
- Reconstructed from 1 file backup(s) in `tools/meshpress-bridge/.old`.
|
||||
- Exact intent unknown; this confirms a recoverable historical version existed.
|
||||
|
||||
### VERSION, - reconstructed
|
||||
- Reconstructed from 2 file backup(s) in `tools/meshpress-bridge/.old`.
|
||||
- Exact intent unknown; this confirms a recoverable historical version existed.
|
||||
|
||||
<!-- GP-RECONSTRUCTED-CHANGELOG:END -->
|
||||
@@ -0,0 +1,184 @@
|
||||
# GracePress MeshPress Bridge
|
||||
|
||||
Small local HTTP bridge between WordPress/MeshPress and MeshCentral.
|
||||
|
||||
The bridge runs beside MeshCentral and uses MeshCentral's official `meshctrl` tool to read device groups and devices. WordPress talks to this bridge instead of trying to speak MeshCentral websocket protocol directly.
|
||||
|
||||
## Endpoints
|
||||
|
||||
- `GET /api/health`
|
||||
- `GET /api/status`
|
||||
- `GET /api/self-test`
|
||||
- `GET /api/groups`
|
||||
- `GET /api/users`
|
||||
- `GET /api/events?limit=50`
|
||||
- `GET /api/serverinfo`
|
||||
- `GET /api/devices`
|
||||
- `GET /api/device/:nodeId`
|
||||
- `GET /api/fleet`
|
||||
- `POST /api/push-now`
|
||||
- `POST /api/cache/clear`
|
||||
- `GET /api/link/desktop/:nodeId`
|
||||
- `GET /api/link/terminal/:nodeId`
|
||||
- `GET /api/link/files/:nodeId`
|
||||
- `GET /api/link/:mode/:nodeId?link_mode=both`
|
||||
- `GET /api/agent-links/:groupId`
|
||||
- `POST /api/groups/create`
|
||||
- `POST /api/groups/edit`
|
||||
- `POST /api/groups/remove`
|
||||
- `POST /api/groups/add-user`
|
||||
- `POST /api/groups/remove-user`
|
||||
- `POST /api/invite-link`
|
||||
- `POST /api/device/message`
|
||||
- `POST /api/device/toast`
|
||||
- `POST /api/action`
|
||||
|
||||
All endpoints require `Authorization: Bearer TOKEN`. If `MESHPRESS_BRIDGE_TOKEN` is empty, protected requests are rejected.
|
||||
|
||||
`/api/action` is intentionally locked down for now. It returns a clear unsupported-command response until each broad remote-command workflow is whitelisted and tested.
|
||||
|
||||
Provisioning and device-command endpoints require:
|
||||
|
||||
```bash
|
||||
MESHPRESS_ALLOW_COMMANDS="1"
|
||||
```
|
||||
|
||||
Without that flag the bridge will read MeshCentral state but refuse provisioning actions.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
cd /storage/meshcentral-workspace/bridge/meshpress-bridge
|
||||
sudo ./install.sh
|
||||
sudo nano /etc/gracepress/meshpress-bridge.env
|
||||
pm2 restart gp-meshpress-bridge
|
||||
```
|
||||
|
||||
## Required Config
|
||||
|
||||
```bash
|
||||
MESHPRESS_BRIDGE_PORT="3099"
|
||||
MESHPRESS_BRIDGE_BIND="127.0.0.1"
|
||||
MESHPRESS_BRIDGE_TOKEN="generated-secret"
|
||||
MESHPRESS_ALLOWED_IPS=""
|
||||
MESHPRESS_AUTH_WINDOW_SECONDS="900"
|
||||
MESHPRESS_AUTH_MAX_FAILURES="30"
|
||||
MESHCTRL_URL="wss://127.0.0.1"
|
||||
PUBLIC_MESH_URL="https://mesh.yourdomain.com"
|
||||
MESHCTRL_USER="admin"
|
||||
MESHCTRL_PASS=""
|
||||
MESHCTRL_DOMAIN=""
|
||||
MESHCTRL_LOGIN_KEY_FILE="/etc/gracepress/meshcentral-login.key"
|
||||
MESHCTRL_PATH=""
|
||||
MESHCTRL_IGNORE_CERT="0"
|
||||
MESHPRESS_LINK_HIDE=""
|
||||
MESHPRESS_LINK_MODE="gotonode"
|
||||
MESHPRESS_ALLOW_COMMANDS="0"
|
||||
WP_DOMAIN="https://christp2p.com"
|
||||
WP_API_KEY="same-token-used-in-meshpress"
|
||||
MESHPRESS_PUSH_INTERVAL_SECONDS="3600"
|
||||
MESHPRESS_PUSH_EVENTS="1"
|
||||
```
|
||||
|
||||
If auto-detection cannot find `meshctrl`, set `MESHCTRL_PATH` to the full path of `meshctrl.js`.
|
||||
|
||||
When `WP_DOMAIN` and `WP_API_KEY` are set, the bridge pushes fleet snapshots to WordPress at startup and then every hour.
|
||||
When `MESHPRESS_PUSH_EVENTS` is enabled, selected bridge lifecycle events are posted to MeshPress so WordPress can record them through OmniLog.
|
||||
|
||||
## Public Exposure Hardening
|
||||
|
||||
Do not expose port `3099` to the public internet without firewall/VPN controls. Preferred options:
|
||||
|
||||
1. Bind to localhost and proxy privately:
|
||||
|
||||
```bash
|
||||
MESHPRESS_BRIDGE_BIND="127.0.0.1"
|
||||
```
|
||||
|
||||
2. Bind to a Tailscale address only:
|
||||
|
||||
```bash
|
||||
MESHPRESS_BRIDGE_BIND="100.x.y.z"
|
||||
```
|
||||
|
||||
3. If the bridge must listen on all interfaces temporarily, restrict source IPs at both the firewall and app layer:
|
||||
|
||||
```bash
|
||||
MESHPRESS_BRIDGE_BIND="0.0.0.0"
|
||||
MESHPRESS_ALLOWED_IPS="WORDPRESS_SERVER_IP,YOUR_ADMIN_IP"
|
||||
```
|
||||
|
||||
The app-layer allowlist only accepts exact IP addresses. It is a backstop, not a replacement for firewall rules.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Use MeshPress > Diagnostics & Manual Actions first. It calls these bridge endpoints:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer TOKEN" http://127.0.0.1:3099/api/health
|
||||
curl -H "Authorization: Bearer TOKEN" http://127.0.0.1:3099/api/version
|
||||
curl -H "Authorization: Bearer TOKEN" http://127.0.0.1:3099/api/status
|
||||
curl -H "Authorization: Bearer TOKEN" http://127.0.0.1:3099/api/self-test
|
||||
curl -H "Authorization: Bearer TOKEN" http://127.0.0.1:3099/api/groups?refresh=1
|
||||
curl -H "Authorization: Bearer TOKEN" http://127.0.0.1:3099/api/devices?refresh=1
|
||||
```
|
||||
|
||||
`/api/self-test` verifies the bridge can find `meshctrl`, has an auth method, has a public MeshCentral URL, and can list device groups and devices.
|
||||
|
||||
For direct session links, test both URL styles:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer TOKEN" "http://127.0.0.1:3099/api/session-links/NODEID"
|
||||
curl -H "Authorization: Bearer TOKEN" "http://127.0.0.1:3099/api/link/console/NODEID?link_mode=all"
|
||||
```
|
||||
|
||||
To trigger a catalog-managed bridge update from MeshPress admin or curl:
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Authorization: Bearer TOKEN" http://127.0.0.1:3099/api/update-now
|
||||
```
|
||||
|
||||
To test the first provisioning path, enable commands and create a temporary group:
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"MeshPress Test Group","description":"Manual bridge validation"}' \
|
||||
http://127.0.0.1:3099/api/groups/create
|
||||
```
|
||||
|
||||
## MeshCentral Auth
|
||||
|
||||
The bridge supports either a MeshCentral login key file or a dedicated integration username/password.
|
||||
|
||||
For username/password auth, set:
|
||||
|
||||
```bash
|
||||
MESHCTRL_USER="your-generated-integration-user"
|
||||
MESHCTRL_PASS="your-generated-integration-password"
|
||||
```
|
||||
|
||||
If `MESHCTRL_LOGIN_KEY_FILE` exists, the bridge will prefer it. If the file does not exist and `MESHCTRL_PASS` is set, it will use username/password auth.
|
||||
|
||||
## MeshCentral Login Key Option
|
||||
|
||||
On the MeshCentral server:
|
||||
|
||||
```bash
|
||||
cd /opt/meshcentral
|
||||
sudo node node_modules/meshcentral --loginTokenKey > /etc/gracepress/meshcentral-login.key
|
||||
sudo chmod 600 /etc/gracepress/meshcentral-login.key
|
||||
```
|
||||
|
||||
MeshCentral must have login tokens enabled in `meshcentral-data/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": {
|
||||
"allowLoginToken": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Restart MeshCentral after changing config.
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
COMPONENT="meshpress-bridge"
|
||||
TARGET_DIR="/opt/gracepress-meshpress-bridge"
|
||||
ENV_DIR="/etc/gracepress"
|
||||
ENV_FILE="$ENV_DIR/meshpress-bridge.env"
|
||||
CRON_FILE="/etc/cron.d/gracepress-meshpress-bridge"
|
||||
SERVICE_NAME="gp-meshpress-bridge"
|
||||
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
VERSION="$(grep -m 1 '^# Version:' "$SOURCE_DIR/meshpress-bridge" | sed -E 's/^# Version:[[:space:]]*//')"
|
||||
SOURCE_REAL="$(realpath "$SOURCE_DIR")"
|
||||
TARGET_REAL="$(realpath -m "$TARGET_DIR")"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Please run as root: sudo ./install.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$SOURCE_REAL" = "$TARGET_REAL" ]; then
|
||||
echo "Same-directory install detected; delegating to safe in-place updater."
|
||||
exec "$TARGET_DIR/update.sh"
|
||||
fi
|
||||
|
||||
mkdir -p "$ENV_DIR"
|
||||
touch "$ENV_FILE"
|
||||
chmod 600 "$ENV_FILE"
|
||||
|
||||
rand_token() {
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
openssl rand -hex 32
|
||||
else
|
||||
date +%s%N | sha256sum | awk '{print $1}'
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_env() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
if ! grep -q "^${key}=" "$ENV_FILE"; then
|
||||
echo "${key}=\"${value}\"" >> "$ENV_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_env "MESHPRESS_BRIDGE_PORT" "${MESHPRESS_BRIDGE_PORT:-3099}"
|
||||
ensure_env "MESHPRESS_BRIDGE_BIND" "${MESHPRESS_BRIDGE_BIND:-127.0.0.1}"
|
||||
ensure_env "MESHPRESS_BRIDGE_TOKEN" "${MESHPRESS_BRIDGE_TOKEN:-$(rand_token)}"
|
||||
ensure_env "MESHPRESS_ALLOWED_IPS" "${MESHPRESS_ALLOWED_IPS:-}"
|
||||
ensure_env "MESHPRESS_AUTH_WINDOW_SECONDS" "${MESHPRESS_AUTH_WINDOW_SECONDS:-900}"
|
||||
ensure_env "MESHPRESS_AUTH_MAX_FAILURES" "${MESHPRESS_AUTH_MAX_FAILURES:-30}"
|
||||
ensure_env "MESHCTRL_URL" "${MESHCTRL_URL:-wss://127.0.0.1}"
|
||||
ensure_env "PUBLIC_MESH_URL" "${PUBLIC_MESH_URL:-https://mesh.example.com}"
|
||||
ensure_env "MESHCTRL_USER" "${MESHCTRL_USER:-admin}"
|
||||
ensure_env "MESHCTRL_PASS" "${MESHCTRL_PASS:-}"
|
||||
ensure_env "MESHCTRL_DOMAIN" "${MESHCTRL_DOMAIN:-}"
|
||||
ensure_env "MESHCTRL_LOGIN_KEY_FILE" "${MESHCTRL_LOGIN_KEY_FILE:-/etc/gracepress/meshcentral-login.key}"
|
||||
ensure_env "MESHCTRL_PATH" "${MESHCTRL_PATH:-}"
|
||||
ensure_env "MESHCTRL_IGNORE_CERT" "${MESHCTRL_IGNORE_CERT:-0}"
|
||||
ensure_env "MESHPRESS_CACHE_TTL_SECONDS" "${MESHPRESS_CACHE_TTL_SECONDS:-60}"
|
||||
ensure_env "MESHPRESS_LINK_HIDE" "${MESHPRESS_LINK_HIDE:-}"
|
||||
ensure_env "MESHPRESS_ALLOW_COMMANDS" "${MESHPRESS_ALLOW_COMMANDS:-0}"
|
||||
ensure_env "MESHPRESS_ALLOW_MAINTENANCE" "${MESHPRESS_ALLOW_MAINTENANCE:-0}"
|
||||
ensure_env "MESHCENTRAL_DIR" "${MESHCENTRAL_DIR:-}"
|
||||
ensure_env "MESHCENTRAL_DATA_DIR" "${MESHCENTRAL_DATA_DIR:-}"
|
||||
ensure_env "MESHCENTRAL_SERVICE_NAME" "${MESHCENTRAL_SERVICE_NAME:-meshcentral}"
|
||||
ensure_env "MESHPRESS_BACKUP_DIR" "${MESHPRESS_BACKUP_DIR:-/var/backups/gracepress-meshcentral}"
|
||||
ensure_env "WP_DOMAIN" "${WP_DOMAIN:-}"
|
||||
ensure_env "WP_API_KEY" "${WP_API_KEY:-}"
|
||||
ensure_env "MESHPRESS_PUSH_INTERVAL_SECONDS" "${MESHPRESS_PUSH_INTERVAL_SECONDS:-3600}"
|
||||
ensure_env "MESHPRESS_PUSH_EVENTS" "${MESHPRESS_PUSH_EVENTS:-1}"
|
||||
ensure_env "AUTO_UPDATE" "${AUTO_UPDATE:-1}"
|
||||
|
||||
apt-get update -y >/dev/null
|
||||
apt-get install -y curl ca-certificates >/dev/null
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null
|
||||
apt-get install -y nodejs >/dev/null
|
||||
npm install -g pm2 >/dev/null
|
||||
|
||||
pm2 install pm2-logrotate >/dev/null 2>&1 || true
|
||||
pm2 set pm2-logrotate:max_size 25M >/dev/null 2>&1 || true
|
||||
pm2 set pm2-logrotate:retain 5 >/dev/null 2>&1 || true
|
||||
|
||||
STAGE_DIR="${TARGET_DIR}.installing.$$"
|
||||
rm -rf "$STAGE_DIR"
|
||||
mkdir -p "$STAGE_DIR"
|
||||
cp -a "$SOURCE_DIR"/. "$STAGE_DIR"/
|
||||
echo "$VERSION" > "$STAGE_DIR/VERSION"
|
||||
cd "$STAGE_DIR"
|
||||
npm install --omit=dev >/dev/null
|
||||
node --check server.js >/dev/null
|
||||
chmod +x "$STAGE_DIR/update.sh" "$STAGE_DIR/meshpress-bridge"
|
||||
|
||||
pm2 delete "$SERVICE_NAME" >/dev/null 2>&1 || true
|
||||
if [ -d "$TARGET_DIR" ]; then
|
||||
BACKUP_DIR="${TARGET_DIR}.old.$(date +%Y%m%d%H%M%S)"
|
||||
mv "$TARGET_DIR" "$BACKUP_DIR"
|
||||
echo "Previous install moved to $BACKUP_DIR"
|
||||
fi
|
||||
mv "$STAGE_DIR" "$TARGET_DIR"
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
pm2 start server.js --name "$SERVICE_NAME" --time --max-memory-restart 512M >/dev/null
|
||||
pm2 save >/dev/null
|
||||
pm2 startup | grep "sudo" | bash >/dev/null 2>&1 || true
|
||||
|
||||
cat > "$CRON_FILE" <<EOF
|
||||
SHELL=/bin/bash
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
17 * * * * root [ -f "$ENV_FILE" ] && . "$ENV_FILE"; [ "\${AUTO_UPDATE:-1}" = "1" ] && "$TARGET_DIR/update.sh" >/var/log/gracepress-meshpress-bridge-update.log 2>&1
|
||||
EOF
|
||||
chmod 644 "$CRON_FILE"
|
||||
|
||||
echo "GracePress MeshPress Bridge v$VERSION installed."
|
||||
echo "Config: $ENV_FILE"
|
||||
echo "Token: $(grep '^MESHPRESS_BRIDGE_TOKEN=' "$ENV_FILE" | cut -d= -f2- | sed 's/^\"//;s/\"$//')"
|
||||
echo "Next: edit $ENV_FILE and set MESHCTRL_URL, PUBLIC_MESH_URL, MESHCTRL_USER, MESHCTRL_PASS or MESHCTRL_LOGIN_KEY_FILE, MESHCTRL_PATH, WP_DOMAIN, and WP_API_KEY."
|
||||
echo "Then restart: pm2 restart $SERVICE_NAME --update-env"
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
# Description: GracePress MeshPress Bridge. Pushes MeshCentral fleet snapshots to WordPress MeshPress.
|
||||
# Category: Hidden
|
||||
# Version: 0.4.3
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec node "$DIR/server.js" "$@"
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "gracepress-meshpress-bridge",
|
||||
"version": "0.4.3",
|
||||
"private": true,
|
||||
"description": "GracePress bridge service for MeshCentral fleet sync and device links",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"check": "node --check server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.3"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,109 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
COMPONENT="meshpress-bridge"
|
||||
TARGET_DIR="/opt/gracepress-meshpress-bridge"
|
||||
ENV_FILE="/etc/gracepress/meshpress-bridge.env"
|
||||
SERVICE_NAME="gp-meshpress-bridge"
|
||||
LOG_PREFIX="[meshpress-bridge-update]"
|
||||
LOCK_FILE="/tmp/gracepress-meshpress-bridge-update.lock"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
STAGE_DIR=""
|
||||
BACKUP_DIR=""
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
if [ -n "$STAGE_DIR" ] && [ -d "$STAGE_DIR" ]; then rm -rf "$STAGE_DIR"; fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
log() { echo "$LOG_PREFIX $(date -Is) $*"; }
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
log "Please run as root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec 9>"$LOCK_FILE"
|
||||
if command -v flock >/dev/null 2>&1; then
|
||||
flock -n 9 || { log "Another bridge update is already running."; exit 1; }
|
||||
fi
|
||||
|
||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||
CATALOG_URL="${GRACEPRESS_CATALOG_URL:-https://christit.com/gracepress/api.php?endpoint=manifest&slug=meshpress-bridge}"
|
||||
|
||||
apt-get update -y >/dev/null
|
||||
apt-get install -y curl unzip python3 ca-certificates nodejs npm >/dev/null
|
||||
if ! command -v pm2 >/dev/null 2>&1; then
|
||||
npm install -g pm2 >/dev/null
|
||||
fi
|
||||
|
||||
log "Checking catalog: $CATALOG_URL"
|
||||
curl -fsSL "$CATALOG_URL&nocache=$(date +%s)" -o "$TMP_DIR/manifest.json"
|
||||
DOWNLOAD_URL="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("download_url",""))' "$TMP_DIR/manifest.json")"
|
||||
VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("version","0.0.0"))' "$TMP_DIR/manifest.json")"
|
||||
|
||||
if [ -z "$DOWNLOAD_URL" ]; then
|
||||
log "No download_url found in catalog manifest."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT="$(cat "$TARGET_DIR/VERSION" 2>/dev/null || echo 0.0.0)"
|
||||
if [ "$CURRENT" = "$VERSION" ]; then
|
||||
log "$COMPONENT is already current: $CURRENT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "Updating $COMPONENT from $CURRENT to $VERSION"
|
||||
curl -fL "$DOWNLOAD_URL" -o "$TMP_DIR/package.zip"
|
||||
unzip -q "$TMP_DIR/package.zip" -d "$TMP_DIR/pkg"
|
||||
PKG_DIR="$(find "$TMP_DIR/pkg" -maxdepth 3 -type f -name server.js -printf '%h\n' | head -n 1)"
|
||||
if [ -z "$PKG_DIR" ] || [ ! -f "$PKG_DIR/package.json" ]; then
|
||||
log "Package does not contain a valid bridge payload."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STAGE_DIR="${TARGET_DIR}.next.${VERSION}.$$"
|
||||
rm -rf "$STAGE_DIR"
|
||||
mkdir -p "$STAGE_DIR"
|
||||
cp -a "$PKG_DIR"/. "$STAGE_DIR"/
|
||||
echo "$VERSION" > "$STAGE_DIR/VERSION"
|
||||
chmod +x "$STAGE_DIR/update.sh" "$STAGE_DIR/meshpress-bridge" 2>/dev/null || true
|
||||
|
||||
log "Installing node dependencies in staging directory."
|
||||
(cd "$STAGE_DIR" && npm install --omit=dev >/dev/null && node --check server.js >/dev/null)
|
||||
|
||||
BACKUP_DIR="${TARGET_DIR}.old.$(date +%Y%m%d%H%M%S)"
|
||||
if [ -d "$TARGET_DIR" ]; then
|
||||
mv "$TARGET_DIR" "$BACKUP_DIR"
|
||||
log "Previous install moved to $BACKUP_DIR"
|
||||
fi
|
||||
|
||||
mv "$STAGE_DIR" "$TARGET_DIR"
|
||||
STAGE_DIR=""
|
||||
|
||||
rollback() {
|
||||
log "Update failed after swap; attempting rollback."
|
||||
local failed="${TARGET_DIR}.failed.$(date +%Y%m%d%H%M%S)"
|
||||
if [ -d "$TARGET_DIR" ]; then mv "$TARGET_DIR" "$failed"; fi
|
||||
if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then mv "$BACKUP_DIR" "$TARGET_DIR"; fi
|
||||
if [ -f "$TARGET_DIR/server.js" ]; then
|
||||
pm2 start "$TARGET_DIR/server.js" --name "$SERVICE_NAME" --time --max-memory-restart 512M >/dev/null 2>&1 || pm2 restart "$SERVICE_NAME" --update-env >/dev/null 2>&1 || true
|
||||
pm2 save >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
if pm2 describe "$SERVICE_NAME" >/dev/null 2>&1; then
|
||||
if ! (cd "$TARGET_DIR" && pm2 restart "$SERVICE_NAME" --update-env >/dev/null); then
|
||||
rollback
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if ! (cd "$TARGET_DIR" && pm2 start server.js --name "$SERVICE_NAME" --time --max-memory-restart 512M >/dev/null); then
|
||||
rollback
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
pm2 save >/dev/null
|
||||
log "$COMPONENT updated to $VERSION"
|
||||
@@ -0,0 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.3 - Profile editor foundation
|
||||
- Added Theme Studio profile storage with a locked MeshCentral Default profile and editable cloned theme profiles.
|
||||
- Added profile dropdown selection, clone current profile, save profile, and export JSON actions.
|
||||
- Replaced the raw inventory-first screen with grouped Brand, Colors, Typography, Layout, Icons, and Advanced controls.
|
||||
- Added dropdown/select controls for fonts, density, icon set, icon size, spacing-style values, and standard color pickers for color fields.
|
||||
- Kept Apply and startup CSS hook controls disabled until generated CSS, rollback, and service-load behavior are implemented.
|
||||
|
||||
## 0.0.2 - MeshCentral-safe runtime identity
|
||||
- Changed the MeshCentral runtime `shortName` to `gpthemestudio` so MeshCentral's alphanumeric admin route guard can open the backend panel.
|
||||
- Renamed the runtime entrypoint to `gpthemestudio.js` and aligned the JavaScript export with the MeshCentral loader.
|
||||
- Preserved the GracePress catalog slug `gracepress-theme-studio` and the existing catalog facade URLs.
|
||||
|
||||
## 0.0.1 - Default theme inventory
|
||||
- Created the initial GracePress Theme Studio MeshCentral plugin.
|
||||
- Captured the current classic MeshCentral UI as a locked `MeshCentral Default` theme profile.
|
||||
- Added a backend admin panel for reviewing theme variables and future theming capabilities.
|
||||
- Kept this first release read-only; it does not inject CSS, alter templates, or modify MeshCentral core files.
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/**
|
||||
* GracePress MeshCentral plugin download facade.
|
||||
* Generated by scripts/build; do not edit manually.
|
||||
*/
|
||||
$slug = 'gracepress-theme-studio';
|
||||
$manifest = __DIR__ . '/' . $slug . '.json';
|
||||
|
||||
if (!is_file($manifest)) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo 'Manifest not found';
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode((string) file_get_contents($manifest), true);
|
||||
$download = is_array($data) ? (string)($data['download_url'] ?? '') : '';
|
||||
$prefix = 'https://christit.com/gracepress/';
|
||||
|
||||
if ($download === '' || strncmp($download, $prefix, strlen($prefix)) !== 0) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo 'Download not found';
|
||||
exit;
|
||||
}
|
||||
|
||||
$zip = basename(parse_url($download, PHP_URL_PATH));
|
||||
if (!preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $zip)) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo 'Invalid download';
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = __DIR__ . '/' . $zip;
|
||||
if (!is_file($file)) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo 'Package not found';
|
||||
exit;
|
||||
}
|
||||
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
header('Content-Type: application/zip');
|
||||
header('Content-Disposition: attachment; filename="' . $zip . '"');
|
||||
header('Content-Length: ' . filesize($file));
|
||||
header('X-GracePress-Download-Url: ' . $download);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'HEAD') {
|
||||
readfile($file);
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "GracePress Theme Studio",
|
||||
"slug": "gracepress-theme-studio",
|
||||
"type": "mcplugin",
|
||||
"category": "MeshCentral",
|
||||
"version": "0.0.3",
|
||||
"stage": "Alpha",
|
||||
"release_channel": "Internal",
|
||||
"public_release": "No",
|
||||
"public_candidate": "No",
|
||||
"wp_org_slug": "",
|
||||
"description": "MeshCentral UI theme inventory and profile editor for GracePress-managed MeshCentral servers.",
|
||||
"icon": "🧩",
|
||||
"requires_php": "8.0",
|
||||
"changelog_url": "https://christit.com/gracepress/api.php?endpoint=changelog&slug=gracepress-theme-studio",
|
||||
"changelog_updated": "2026-06-27 18:51:46 UTC",
|
||||
"download_url": "https://christit.com/gracepress/gracepress-theme-studio-v0.0.3.zip",
|
||||
"file": "gracepress-theme-studio/gpthemestudio.js",
|
||||
"settings_slug": "gracepress-theme-studio",
|
||||
"last_updated": "2026-06-27 18:53:57 UTC",
|
||||
"meshcentral": {
|
||||
"short_name": "gpthemestudio",
|
||||
"has_admin_panel": true,
|
||||
"main": "gpthemestudio.js",
|
||||
"config_url": "https://christit.com/gracepress/gracepress-theme-studio.mcplugin.json"
|
||||
},
|
||||
"meshcentral_config_url": "https://christit.com/gracepress/gracepress-theme-studio.mcplugin.json",
|
||||
"install_type": "meshcentral-plugin"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "GracePress Theme Studio",
|
||||
"shortName": "gpthemestudio",
|
||||
"version": "0.0.3",
|
||||
"description": "MeshCentral UI theme profile editor for GracePress-managed MeshCentral servers.",
|
||||
"author": "Jason Eugene Garrison",
|
||||
"homepage": "https://christit.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://christit.com/gracepress/api.php?endpoint=manifest&slug=gracepress-theme-studio"
|
||||
},
|
||||
"hasAdminPanel": true,
|
||||
"main": "gpthemestudio.js",
|
||||
"configUrl": "https://christit.com/gracepress/gracepress-theme-studio.mcplugin.json",
|
||||
"downloadUrl": "https://christit.com/gracepress/gracepress-theme-studio.download.php",
|
||||
"changelogUrl": "https://christit.com/gracepress/api.php?endpoint=changelog&slug=gracepress-theme-studio",
|
||||
"versionHistoryUrl": "https://christit.com/gracepress/gracepress-theme-studio.tags.json",
|
||||
"meshCentralCompat": ">=1.1.0",
|
||||
"download_url": "https://christit.com/gracepress/gracepress-theme-studio-v0.0.3.zip",
|
||||
"gracepress": {
|
||||
"slug": "gracepress-theme-studio",
|
||||
"type": "mcplugin",
|
||||
"built_at": "2026-06-27 18:53:57 UTC",
|
||||
"manifest_url": "https://christit.com/gracepress/api.php?endpoint=manifest&slug=gracepress-theme-studio"
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"name": "0.0.3",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-theme-studio-v0.0.3.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-theme-studio-v0.0.3.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-theme-studio-v0.0.3.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.2",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-theme-studio-v0.0.2.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-theme-studio-v0.0.2.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-theme-studio-v0.0.2.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.1",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-theme-studio-v0.0.1.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-theme-studio-v0.0.1.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-theme-studio-v0.0.1.zip"
|
||||
}
|
||||
]
|
||||
Vendored
+91
@@ -0,0 +1,91 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.18 - Theme Studio 0.0.3 catalog seed
|
||||
- Updated the built-in GracePress Theme Studio catalog seed to version `0.0.3`.
|
||||
- Changed the Theme Studio description to match the new profile editor role.
|
||||
- Keeps the MeshCentral package manager authoritative over GracePress-managed plugin rows while preserving unrelated third-party plugin rows.
|
||||
|
||||
## 0.0.17 - GracePress row ownership cleanup
|
||||
- Resolves MeshCentral plugin registry row IDs from `_id`, `id`, or `pluginId` so cleanup works across MeshCentral database adapters.
|
||||
- Archives and removes GracePress-owned legacy rows when their short name no longer matches the GracePress catalog entry.
|
||||
- Archives and removes duplicate GracePress-owned rows for the same managed MeshCentral short name while preserving the active row when one exists.
|
||||
- Reports missing row IDs as explicit sync errors instead of silently leaving obsolete GracePress rows in the MeshCentral plugin catalog.
|
||||
- Keeps cleanup scoped to GracePress-managed plugin entries so unrelated third-party MeshCentral plugins are not touched.
|
||||
|
||||
## 0.0.16 - Legacy row quarantine
|
||||
- Archives and removes obsolete legacy MeshCentral plugin rows such as `gracepress-theme-studio` after preserving their registry document under the live plugin `.old` folder.
|
||||
- Preserves the installed/active status of the corrected `gpthemestudio` row during catalog sync instead of forcing managed rows back to disabled.
|
||||
- Keeps Theme Studio migration in the rolling catalog update path while avoiding duplicate disabled rows in MeshCentral's plugin list.
|
||||
|
||||
## 0.0.15 - MeshCentral-safe Theme Studio seed
|
||||
- Updated the built-in Theme Studio seed to use the MeshCentral-safe runtime short name `gpthemestudio`.
|
||||
- Preserved the public GracePress catalog slug `gracepress-theme-studio` while separating it from MeshCentral's admin-route identity.
|
||||
- Keeps the catalog-manager rolling update path as the migration mechanism for correcting installed MeshCentral plugin rows.
|
||||
- Migrates the legacy `gracepress-theme-studio` MeshCentral row forward to `gpthemestudio` instead of creating a duplicate row.
|
||||
|
||||
## 0.0.14 - Built-in MeshCentral plugin catalog seed
|
||||
- Added a deterministic built-in GracePress MeshCentral plugin catalog seed for `GracePress Theme Studio`.
|
||||
- Changed discovery to write disabled plugin rows through MeshCentral's database helper directly instead of the higher-level plugin wrapper.
|
||||
- Added explicit sync-stage logging so MeshCentral service logs show whether discovery starts, reads existing rows, inserts rows, or fails.
|
||||
- Kept the dynamic catalog URL in metadata for a later pass, but removed runtime catalog fetch as a dependency for exposing the first managed plugin.
|
||||
|
||||
## 0.0.13 - Constructor startup sync
|
||||
- Moved GracePress MeshCentral plugin discovery into the plugin constructor path because MeshCentral does not call `server_startup()` for already-installed plugins during ordinary service reload.
|
||||
- Kept the `server_startup()` hook for install/upgrade compatibility while also scheduling discovery shortly after plugin load.
|
||||
- Added an admin-panel fallback that starts discovery if the connector panel opens before the scheduled sync has run.
|
||||
|
||||
## 0.0.12 - Catalog plugin discovery
|
||||
- Added startup sync from the GracePress `mcplugins` catalog bucket into MeshCentral's plugin registry.
|
||||
- Missing GracePress MeshCentral plugins are now inserted as disabled/available rows, ready for MeshCentral's normal Install action.
|
||||
- Existing disabled rows are refreshed from their current `.mcplugin.json` config while active installed plugins are left alone.
|
||||
- Added backend panel sync status so catalog discovery can be inspected from the connector admin view.
|
||||
|
||||
## 0.0.11 - Facade confirmation update
|
||||
- Published a marker-only update to confirm the MeshCentral git facade shape after the successful `0.0.10` upgrade.
|
||||
- Preserved the MeshCentral-facing `downloadUrl` and `versionHistoryUrl` facade URLs in package config.
|
||||
- Updated the backend panel test marker to `0.0.11`.
|
||||
|
||||
## 0.0.10 - MeshCentral git facade test
|
||||
- Added a marker-only update that uses GracePress-generated MeshCentral git facade URLs.
|
||||
- Switched MeshCentral `downloadUrl` to the no-query `gracepress.download.php` package facade.
|
||||
- Switched MeshCentral `versionHistoryUrl` to `gracepress.tags.json`, shaped like a GitHub tags response with `name` and `url` fields.
|
||||
- Updated the backend panel test marker to `0.0.10`.
|
||||
|
||||
## 0.0.9 - Versioned download URL retest
|
||||
- Published a marker-only update that uses the versioned package URL as MeshCentral `downloadUrl`.
|
||||
- Avoided the cached `gracepress-latest.zip` alias and the nginx path-info download route while validating the built-in Upgrade action.
|
||||
- Updated the backend panel test marker to `0.0.9`.
|
||||
|
||||
## 0.0.8 - Upgrade flow retest
|
||||
- Published a small marker-only update for validating MeshCentral's built-in Upgrade action after the MySQL plugin registry repair.
|
||||
- Updated the backend panel test marker to `0.0.8` so a successful install can be confirmed from the MeshCentral plugin panel.
|
||||
|
||||
## 0.0.7 - Direct MeshCentral latest zip
|
||||
- Replaced the query-string latest-download endpoint with a stable `gracepress-latest.zip` file because MeshCentral's plugin downloader builds requests from `URL.pathname` and drops query strings.
|
||||
- Kept versioned GracePress release artifacts while adding a MeshCentral-safe latest zip alias for the built-in Upgrade action.
|
||||
- Updated package metadata so installed MeshCentral config rows keep using the stable latest zip URL after upgrade.
|
||||
|
||||
## 0.0.6 - Stable MeshCentral upgrade download
|
||||
- Added a GracePress `mcplugin-download` endpoint for MeshCentral's built-in Upgrade action.
|
||||
- Changed the MeshCentral-facing `downloadUrl` to a stable latest-download endpoint because MeshCentral upgrades from the installed DB row, not the freshly fetched latest config object.
|
||||
- Kept the GracePress manifest `download_url` versioned for catalog and rollback clarity.
|
||||
|
||||
## 0.0.5 - Upgrade detection test
|
||||
- Published a small catalog-only test update for MeshCentral's built-in Upgrade action.
|
||||
- Added a visible backend panel note so successful upgrade can be confirmed after MeshCentral installs the package.
|
||||
|
||||
## 0.0.4 - MeshCentral lifecycle alignment
|
||||
- Renamed the display/package name to GracePress Catalog Connector while preserving the stable `gracepress` slug and MeshCentral `shortName`.
|
||||
- Changed MeshCentral repository metadata to `git` so MeshCentral's built-in installer and upgrade action can download the GracePress-hosted zip.
|
||||
- Documented the active plugin identity rule: folder name, `shortName`, JS export, config row, and catalog slug must line up.
|
||||
- Prepared the live registry cleanup from duplicate `gracepress` rows into one active catalog-managed plugin.
|
||||
|
||||
## 0.0.3 - Catalog-managed backend panel
|
||||
- Moved the live GracePress MeshCentral dummy plugin into the GracePress workspace as an `mcplugin` package.
|
||||
- Added internal MeshCentral plugin header metadata for GracePress catalog builds.
|
||||
- Added backend admin panel catalog checks against the GracePress manifest endpoint.
|
||||
- Updated MeshCentral plugin config URLs to use the GracePress catalog-managed package, config, and changelog artifacts.
|
||||
|
||||
## 0.0.2 - Backend hello-world route
|
||||
- Replaced device-tab test behavior with a backend/admin plugin panel.
|
||||
- Kept MeshCentral plugin permissions unused while the current MeshCentral database adapter reports missing plugin-permission helpers.
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/**
|
||||
* GracePress MeshCentral plugin download facade.
|
||||
* Generated by scripts/build; do not edit manually.
|
||||
*/
|
||||
$slug = 'gracepress';
|
||||
$manifest = __DIR__ . '/' . $slug . '.json';
|
||||
|
||||
if (!is_file($manifest)) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo 'Manifest not found';
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode((string) file_get_contents($manifest), true);
|
||||
$download = is_array($data) ? (string)($data['download_url'] ?? '') : '';
|
||||
$prefix = 'https://christit.com/gracepress/';
|
||||
|
||||
if ($download === '' || strncmp($download, $prefix, strlen($prefix)) !== 0) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo 'Download not found';
|
||||
exit;
|
||||
}
|
||||
|
||||
$zip = basename(parse_url($download, PHP_URL_PATH));
|
||||
if (!preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $zip)) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo 'Invalid download';
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = __DIR__ . '/' . $zip;
|
||||
if (!is_file($file)) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo 'Package not found';
|
||||
exit;
|
||||
}
|
||||
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
header('Content-Type: application/zip');
|
||||
header('Content-Disposition: attachment; filename="' . $zip . '"');
|
||||
header('Content-Length: ' . filesize($file));
|
||||
header('X-GracePress-Download-Url: ' . $download);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'HEAD') {
|
||||
readfile($file);
|
||||
}
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "GracePress Catalog Connector",
|
||||
"slug": "gracepress",
|
||||
"type": "mcplugin",
|
||||
"category": "MeshCentral",
|
||||
"version": "0.0.18",
|
||||
"stage": "Alpha",
|
||||
"release_channel": "Internal",
|
||||
"public_release": "No",
|
||||
"public_candidate": "No",
|
||||
"wp_org_slug": "",
|
||||
"description": "MeshCentral backend connector for GracePress catalog-managed plugin delivery, updates, and diagnostics.",
|
||||
"icon": "🧩",
|
||||
"requires_php": "8.0",
|
||||
"changelog_url": "https://christit.com/gracepress/api.php?endpoint=changelog&slug=gracepress",
|
||||
"changelog_updated": "2026-06-27 18:52:52 UTC",
|
||||
"download_url": "https://christit.com/gracepress/gracepress-v0.0.18.zip",
|
||||
"file": "gracepress/gracepress.js",
|
||||
"settings_slug": "gracepress",
|
||||
"last_updated": "2026-06-27 18:53:57 UTC",
|
||||
"meshcentral": {
|
||||
"short_name": "gracepress",
|
||||
"has_admin_panel": true,
|
||||
"main": "gracepress.js",
|
||||
"config_url": "https://christit.com/gracepress/gracepress.mcplugin.json"
|
||||
},
|
||||
"meshcentral_config_url": "https://christit.com/gracepress/gracepress.mcplugin.json",
|
||||
"install_type": "meshcentral-plugin"
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "GracePress Catalog Connector",
|
||||
"shortName": "gracepress",
|
||||
"version": "0.0.18",
|
||||
"description": "MeshCentral backend connector for GracePress catalog-managed plugin delivery, updates, and diagnostics.",
|
||||
"author": "Jason Eugene Garrison",
|
||||
"homepage": "https://christit.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://christit.com/gracepress/api.php?endpoint=manifest&slug=gracepress"
|
||||
},
|
||||
"hasAdminPanel": true,
|
||||
"main": "gracepress.js",
|
||||
"configUrl": "https://christit.com/gracepress/gracepress.mcplugin.json",
|
||||
"downloadUrl": "https://christit.com/gracepress/gracepress.download.php",
|
||||
"changelogUrl": "https://christit.com/gracepress/api.php?endpoint=changelog&slug=gracepress",
|
||||
"versionHistoryUrl": "https://christit.com/gracepress/gracepress.tags.json",
|
||||
"meshCentralCompat": ">=1.1.0",
|
||||
"download_url": "https://christit.com/gracepress/gracepress-v0.0.18.zip",
|
||||
"gracepress": {
|
||||
"slug": "gracepress",
|
||||
"type": "mcplugin",
|
||||
"built_at": "2026-06-27 18:53:57 UTC",
|
||||
"manifest_url": "https://christit.com/gracepress/api.php?endpoint=manifest&slug=gracepress"
|
||||
}
|
||||
}
|
||||
Vendored
+98
@@ -0,0 +1,98 @@
|
||||
[
|
||||
{
|
||||
"name": "0.0.18",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-v0.0.18.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-v0.0.18.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-v0.0.18.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.17",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-v0.0.17.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-v0.0.17.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-v0.0.17.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.16",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-v0.0.16.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-v0.0.16.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-v0.0.16.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.15",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-v0.0.15.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-v0.0.15.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-v0.0.15.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.14",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-v0.0.14.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-v0.0.14.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-v0.0.14.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.13",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-v0.0.13.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-v0.0.13.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-v0.0.13.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.12",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-v0.0.12.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-v0.0.12.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-v0.0.12.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.11",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-v0.0.11.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-v0.0.11.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-v0.0.11.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.10",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-v0.0.10.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-v0.0.10.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-v0.0.10.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.9",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-v0.0.9.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-v0.0.9.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-v0.0.9.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.8",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-v0.0.8.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-v0.0.8.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-v0.0.8.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.7",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-v0.0.7.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-v0.0.7.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-v0.0.7.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.6",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-v0.0.6.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-v0.0.6.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-v0.0.6.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.5",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-v0.0.5.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-v0.0.5.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-v0.0.5.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.4",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-v0.0.4.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-v0.0.4.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-v0.0.4.zip"
|
||||
},
|
||||
{
|
||||
"name": "0.0.3",
|
||||
"zipball_url": "https://christit.com/gracepress/gracepress-v0.0.3.zip",
|
||||
"tarball_url": "https://christit.com/gracepress/gracepress-v0.0.3.zip",
|
||||
"url": "https://christit.com/gracepress/gracepress-v0.0.3.zip"
|
||||
}
|
||||
]
|
||||
Vendored
+163
@@ -0,0 +1,163 @@
|
||||
# GracePress: MeshPress Sync Changelog
|
||||
|
||||
## Current
|
||||
- Added a new-server onboarding wizard for bridge URL, token, public MeshCentral URL, advanced mode, and first health checks.
|
||||
- Added a connection health panel for WordPress-to-bridge, bridge-to-MeshCentral, public URL, fleet freshness, bridge push, and OmniLog event flow.
|
||||
- Added an advanced-mode gate for MeshCentral Ops and raw troubleshooting controls.
|
||||
- Added an agent enrollment check to confirm newly installed agents appear in the selected MeshCentral group.
|
||||
- Added a protected bridge event receiver so MeshPress Bridge lifecycle events can flow into OmniLog.
|
||||
|
||||
## 1.13.0 - 2026-06-12
|
||||
- Added a new-server onboarding wizard for bridge URL, token, public MeshCentral URL, advanced mode, and first health checks.
|
||||
- Added a connection health panel for WordPress-to-bridge, bridge-to-MeshCentral, public URL, fleet freshness, bridge push, and OmniLog event flow.
|
||||
- Added an advanced-mode gate for MeshCentral Ops and raw troubleshooting controls.
|
||||
- Added an agent enrollment check to confirm newly installed agents appear in the selected MeshCentral group.
|
||||
- Added a protected bridge event receiver so MeshPress Bridge lifecycle events can flow into OmniLog.
|
||||
|
||||
## 1.12.7 - 2026-05-25
|
||||
- Added separate controls for bridge maintenance and real MeshCentral maintenance mode.
|
||||
- Added redacted MeshCentral config audit output for safer copy/paste diagnostics.
|
||||
- Upgraded backups to request full MeshCentral snapshots, including MySQL/MariaDB dumps when configured.
|
||||
|
||||
## 1.12.6 - 2026-05-25
|
||||
- Rolled MeshPress admin back to the last known stable 1.12.1 layout after later rescue edits broke tabs, buttons, and fleet loading.
|
||||
- Preserved MeshCentral `@` characters in group/device identifiers before sending requests to the bridge.
|
||||
- Removed browser dark-mode override CSS that inverted MeshPress cards in light mode.
|
||||
- Restored explicit MeshCentral maintenance-mode enable/disable controls now that the bridge endpoint is stable again.
|
||||
- Simplified agent provisioning into one primary Linux install command plus a compact installer/link picker.
|
||||
- Added clearer Mesh ID diagnostics without forcing operators to manually paste or understand the raw MeshCentral group id.
|
||||
|
||||
## 1.12.5 - 2026-05-24
|
||||
- Rolled MeshPress admin back to the last known stable 1.12.1 layout after later rescue edits broke tabs, buttons, and fleet loading.
|
||||
- Preserved MeshCentral `@` characters in group/device identifiers before sending requests to the bridge.
|
||||
- Removed browser dark-mode override CSS that inverted MeshPress cards in light mode.
|
||||
|
||||
## 1.12.0 - 2026-05-24
|
||||
- Added a MeshCentral Ops tab for runtime/update checks, guarded backups, and agent uninstall diagnostics through the bridge.
|
||||
- Added an operator-only uninstall-agent test button to the device troubleshooting flow.
|
||||
- Added admin output panels designed for copyable diagnostics when bridge maintenance or MeshCentral commands fail.
|
||||
|
||||
## 1.11.9 - 2026-05-24
|
||||
- Improved the fleet overview with grouped device cards, search, group filtering, status filtering, and direct Desktop/Terminal/Files actions.
|
||||
- Expanded agent deployment output for Windows, macOS, Linux, Mesh Assistant, and copyable Linux install commands.
|
||||
- Added clearer provisioning guidance and bridge diagnostic output for group edit failures.
|
||||
|
||||
## 1.11.8 - 2026-05-24
|
||||
- Routed fleet Desktop/Console/Files buttons through bridge-generated session links instead of locally guessed URLs.
|
||||
- Preserved MeshCentral `$` node ids through AJAX sanitization and made `viewmode-first + gotonode` the tested primary path.
|
||||
- Added session diagnostics plus manual group edit, device rename, and device move controls for bridge testing.
|
||||
|
||||
## 1.11.7 - 2026-05-23
|
||||
- Added bridge runtime version/status visibility and manual update triggering from MeshPress admin.
|
||||
- Added a richer Desktop/Console/Files session link tester with multiple MeshCentral direct-link candidates.
|
||||
- Cleaned mojibake labels from fleet action buttons.
|
||||
|
||||
## 1.11.6 - 2026-05-23
|
||||
- Added Bridge Runtime Status controls for version checks and manual bridge updates.
|
||||
- Added support for bridge `/api/session-links` and console link candidate testing.
|
||||
- Renamed Terminal actions to Console in the UI to match the workflow being tested.
|
||||
|
||||
## 1.11.5 - 2026-05-23
|
||||
- Added a Diagnostics & Manual Actions tab for bridge probing and first-party MeshCentral validation.
|
||||
- Added manual MeshCentral group creation from MeshPress, with clear output and generated agent links.
|
||||
- Added read-only bridge probes for health, status, self-test, groups, devices, users, events, server info, and device info.
|
||||
- Added direct session link testing for Desktop, Terminal, and Files in both `gotonode` and `node` URL formats.
|
||||
|
||||
<!-- GP-RECONSTRUCTED-CHANGELOG:START -->
|
||||
|
||||
## Reconstructed History
|
||||
|
||||
These entries were inferred from local distribution archives, module headers, and `.old` backups. They are intentionally conservative and may not capture the full intent of each change.
|
||||
|
||||
### 1.11.4 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.11.4.zip` (2026-05-21).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.
|
||||
- Header advertised WordPress compatibility through 7.0.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.11.3 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.11.3.zip` (2026-05-20).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.11.2 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.11.2.zip` (2026-05-20).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.11.1 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.11.1.zip` (2026-05-20).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.11.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.11.0.zip` (2026-05-20).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.10.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.10.0.zip` (2026-05-20).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.9.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.9.0.zip` (2026-05-20).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.8.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.8.0.zip` (2026-05-20).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.7.1 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.7.1.zip` (2026-05-20).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects to the custom MeshCentral GracePress API plugin for flawless fleet syncing and management.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.7.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.7.0.zip` (2026-05-18).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects to the custom MeshCentral GracePress API plugin for flawless fleet syncing and management.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.5.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.5.0.zip` (2026-05-18).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Uses a secure Cookie-Based REST API bridge to sync your MeshCentral fleet into WordPress.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.4.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.4.0.zip` (2026-05-18).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Uses a native WebSocket payload engine to sync your MeshCentral fleet into WordPress.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.3.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.3.0.zip` (2026-05-18).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Uses a native WebSocket engine to sync your MeshCentral fleet into WordPress.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.1.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.1.0.zip` (2026-05-18).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Syncs your MeshCentral fleet into WordPress for instant remote desktops and API-driven automation.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.0.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.0.0.zip` (2026-05-18).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Syncs your MeshCentral fleet into WordPress for instant remote desktops and API-driven automation.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
<!-- GP-RECONSTRUCTED-CHANGELOG:END -->
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "GracePress: MeshPress Sync",
|
||||
"slug": "mesh-press",
|
||||
"type": "plugin",
|
||||
"category": "Utility",
|
||||
"version": "1.13.0",
|
||||
"stage": "Production",
|
||||
"description": "Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.",
|
||||
"icon": "🌐",
|
||||
"requires_php": "8.0",
|
||||
"changelog_url": "https://christit.com/gracepress/api.php?endpoint=changelog&slug=mesh-press",
|
||||
"changelog_updated": "2026-06-12 19:28:43 UTC",
|
||||
"download_url": "https://christit.com/gracepress/mesh-press-v1.13.0.zip",
|
||||
"file": "mesh-press/mesh-press.php",
|
||||
"settings_slug": "mesh-press",
|
||||
"last_updated": "2026-06-12 19:29:09 UTC"
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
# meshpress-bridge Changelog
|
||||
|
||||
## Current
|
||||
- Fixed agent-link generation to pass the resolved group object instead of a boolean match flag.
|
||||
- Added selective WordPress event pushes for high-signal bridge lifecycle events so MeshPress can forward them into OmniLog.
|
||||
- Added bridge status visibility for WordPress event push configuration.
|
||||
|
||||
## 0.4.3 - 2026-06-12
|
||||
- Fixed agent-link generation to pass the resolved group object instead of a boolean match flag.
|
||||
- Added selective WordPress event pushes for high-signal bridge lifecycle events so MeshPress can forward them into OmniLog.
|
||||
- Added bridge status visibility for WordPress event push configuration.
|
||||
|
||||
## 0.4.2 - 2026-05-25
|
||||
- Added redacted MeshCentral config auditing for MySQL/MariaDB, framing, logging, backup, and maintenance posture.
|
||||
- Added real MeshCentral `settings.maintenanceMode` toggling with config backup and service restart support.
|
||||
- Replaced the data-only backup with a guarded full snapshot that includes MeshCentral files, config, manifest, checksum, and a MySQL/MariaDB dump when configured.
|
||||
- Added a configurable database dump timeout (`MESHPRESS_DB_DUMP_TIMEOUT_SECONDS`) so backup failures produce clear diagnostics instead of hanging.
|
||||
|
||||
## 0.4.1 - 2026-05-25
|
||||
- Resolved MeshCentral groups before generating agent links, so WordPress punctuation drift no longer breaks installer commands.
|
||||
- Generated Linux install commands from the canonical group short id, including MeshCentral `@` characters.
|
||||
- Returned the matched group record and group name with agent-link responses for clearer MeshPress output.
|
||||
- Returned normalized group `short_id` values so agent-link generation can preserve MeshCentral punctuation across the full path.
|
||||
- Added installer-shape diagnostics and an explicit warning when the resolved short Mesh ID looks too short.
|
||||
- Added a curl fallback command while keeping the native MeshCentral wget installer command as the primary Linux path.
|
||||
|
||||
## 0.4.0 - 2026-05-24
|
||||
- Resolved MeshCentral groups before generating agent links, so WordPress punctuation drift no longer breaks installer commands.
|
||||
- Generated Linux install commands from the canonical group short id, including MeshCentral `@` characters.
|
||||
- Returned the matched group record and group name with agent-link responses for clearer MeshPress output.
|
||||
|
||||
## 0.3.9 - 2026-05-24
|
||||
- Replaced the bridge updater with a staged, verified, rollback-capable update flow that does not delete PM2 before the new copy is ready.
|
||||
- Added catalog update checks and a runtime maintenance-mode toggle endpoint for MeshPress.
|
||||
- Made group-id resolution tolerant of MeshCentral punctuation drift, and switched Linux agent commands to MeshCentral's native install-script pattern.
|
||||
|
||||
## 0.3.8 - 2026-05-24
|
||||
- Group edits now verify the refreshed MeshCentral group record before returning success.
|
||||
- Added extra group edit diagnostics when MeshCtrl accepts a command but the group does not actually change.
|
||||
- Tried both `--desc` and `--description` edit forms for better MeshCtrl version compatibility.
|
||||
|
||||
## 0.3.7 - 2026-05-24
|
||||
- Added guarded MeshCentral maintenance endpoints for version/update checks, data backups, and update triggering.
|
||||
- Added agent uninstall diagnostics and richer copy-friendly bridge logging for lifecycle testing.
|
||||
- Hardened the bridge installer/updater so running from the live `/opt` target refuses instead of deleting itself.
|
||||
|
||||
## 0.3.6 - 2026-05-24
|
||||
- Rejected MeshCtrl failure text even when MeshCtrl exits with code 0, preventing false-success group edit responses.
|
||||
- Added richer group edit candidate attempts and copyable JSON diagnostics for invalid group identifiers.
|
||||
- Expanded agent link generation with Windows, Linux, macOS, Mesh Assistant, and Linux install command output.
|
||||
|
||||
## 0.3.5 - 2026-05-24
|
||||
- Changed MeshCentral session links to prefer the working `viewmode-first + gotonode` URL pattern.
|
||||
- Added short-node-id extraction for `gotonode` links so full `node//...` ids do not break direct console sessions.
|
||||
- Added `/api/session-diagnostics/:nodeId` plus guarded device rename and move endpoints for manual MeshPress testing.
|
||||
|
||||
## 0.3.4 - 2026-05-23
|
||||
- Added bridge runtime version reporting and a guarded manual update endpoint for MeshPress admin.
|
||||
- Added richer MeshCentral session-link generation for Desktop, Console, and Files with multiple direct-link candidates.
|
||||
- Added `console` and `shell` aliases for MeshCentral terminal sessions.
|
||||
|
||||
## 0.3.3 - 2026-05-23
|
||||
- Added `/api/version`, `/api/update-now`, and `/api/session-links/:nodeId`.
|
||||
- Added all-link candidate generation for MeshCentral session testing.
|
||||
- Restarted bridge updates with `pm2 --update-env` so env/config changes are picked up.
|
||||
|
||||
## 0.3.2 - 2026-05-23
|
||||
- Hardened bridge authentication for public-port exposure by requiring a configured bearer token on all endpoints.
|
||||
- Added optional exact-IP allowlisting and failed-auth rate limiting.
|
||||
- Moved WordPress fleet push authentication from query string to Authorization header.
|
||||
- Added startup security warnings for all-interface binding without app-layer allowlisting.
|
||||
|
||||
## 0.3.1 - 2026-05-23
|
||||
- Added bridge self-test, server-info, users, events, and device-info diagnostics for MeshPress.
|
||||
- Added configurable MeshCentral direct-link style with `MESHPRESS_LINK_MODE`.
|
||||
- Added guarded provisioning endpoints for group edit/remove, group user assignment, invite links, and simple device message/toast commands.
|
||||
- Added cache clearing and richer status output for faster troubleshooting.
|
||||
|
||||
## 0.3.0 - 2026-05-22
|
||||
- Added Phase 5.2 MeshCentral provisioning endpoints for Service Desk.
|
||||
- Added authenticated agent/provisioning link generation for MeshCentral device groups.
|
||||
- Added guarded device group creation through MeshCtrl `adddevicegroup`.
|
||||
- Required `MESHPRESS_ALLOW_COMMANDS=1` before the bridge will create MeshCentral groups, keeping provisioning actions opt-in.
|
||||
- Returned Windows x64, x86, ARM64, and interactive Windows agent links for linked MeshCentral groups.
|
||||
|
||||
<!-- GP-RECONSTRUCTED-CHANGELOG:START -->
|
||||
|
||||
## Reconstructed History
|
||||
|
||||
These entries were inferred from local distribution archives, module headers, and `.old` backups. They are intentionally conservative and may not capture the full intent of each change.
|
||||
|
||||
### 0.2.0 - reconstructed
|
||||
- Reconstructed from archive `meshpress-bridge-v0.2.0.zip` (2026-05-20).
|
||||
- Header description at this version: GracePress MeshPress Bridge. Pushes MeshCentral fleet snapshots to WordPress MeshPress.
|
||||
- Catalog/category marker: Hidden.
|
||||
|
||||
### 0.1.1 - reconstructed
|
||||
- Reconstructed from 2 file backup(s) in `tools/meshpress-bridge/.old`.
|
||||
- Exact intent unknown; this confirms a recoverable historical version existed.
|
||||
|
||||
### 0.1.0 - reconstructed
|
||||
- Reconstructed from 1 file backup(s) in `tools/meshpress-bridge/.old`.
|
||||
- Exact intent unknown; this confirms a recoverable historical version existed.
|
||||
|
||||
### VERSION, - reconstructed
|
||||
- Reconstructed from 2 file backup(s) in `tools/meshpress-bridge/.old`.
|
||||
- Exact intent unknown; this confirms a recoverable historical version existed.
|
||||
|
||||
<!-- GP-RECONSTRUCTED-CHANGELOG:END -->
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "meshpress-bridge",
|
||||
"slug": "meshpress-bridge",
|
||||
"type": "tool",
|
||||
"category": "Hidden",
|
||||
"version": "0.4.3",
|
||||
"stage": "Production",
|
||||
"description": "GracePress MeshPress Bridge. Pushes MeshCentral fleet snapshots to WordPress MeshPress.",
|
||||
"icon": "⚙️",
|
||||
"requires_php": "7.4",
|
||||
"changelog_url": "https://christit.com/gracepress/api.php?endpoint=changelog&slug=meshpress-bridge",
|
||||
"changelog_updated": "2026-06-12 19:28:43 UTC",
|
||||
"download_url": "https://christit.com/gracepress/meshpress-bridge-v0.4.3.zip",
|
||||
"file": "meshpress-bridge/meshpress-bridge",
|
||||
"settings_slug": "",
|
||||
"last_updated": "2026-06-12 19:29:32 UTC"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# Workspace Map
|
||||
|
||||
## Imported Source
|
||||
|
||||
| Area | New Path | Original GracePress Path |
|
||||
| --- | --- | --- |
|
||||
| WordPress plugin | `wordpress/plugins/mesh-press/` | `plugins/mesh-press/` |
|
||||
| Bridge service | `bridge/meshpress-bridge/` | `tools/meshpress-bridge/` |
|
||||
| Catalog connector | `meshcentral/plugins/gracepress/` | `meshcentral-plugins/gracepress/` |
|
||||
| Theme Studio plugin | `meshcentral/plugins/gracepress-theme-studio/` | `meshcentral-plugins/gracepress-theme-studio/` |
|
||||
| Current artifacts | `dist/current/` | `dist/` selected MeshPress and MeshCentral artifacts |
|
||||
|
||||
## Boundaries
|
||||
|
||||
- WordPress plugin code belongs under `wordpress/plugins/`.
|
||||
- Node helper services belong under `bridge/`.
|
||||
- MeshCentral runtime plugins belong under `meshcentral/plugins/`.
|
||||
- Generated release evidence belongs under `dist/current/` until dedicated build output rules exist.
|
||||
- Upstream MeshCentral source belongs outside the tracked repository by default.
|
||||
|
||||
## Follow-Up Work
|
||||
|
||||
- Port or rewrite the GracePress `mcplugin` build flow so this workspace can package MeshCentral plugins independently.
|
||||
- Decide whether MeshPress should remain catalog-released through GracePress or gain a separate release channel.
|
||||
- Normalize mojibake metadata in imported manifests and headers during the first real source change.
|
||||
- Add smoke checks for the bridge service and MeshCentral plugin identity rules.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.3 - Profile editor foundation
|
||||
- Added Theme Studio profile storage with a locked MeshCentral Default profile and editable cloned theme profiles.
|
||||
- Added profile dropdown selection, clone current profile, save profile, and export JSON actions.
|
||||
- Replaced the raw inventory-first screen with grouped Brand, Colors, Typography, Layout, Icons, and Advanced controls.
|
||||
- Added dropdown/select controls for fonts, density, icon set, icon size, spacing-style values, and standard color pickers for color fields.
|
||||
- Kept Apply and startup CSS hook controls disabled until generated CSS, rollback, and service-load behavior are implemented.
|
||||
|
||||
## 0.0.2 - MeshCentral-safe runtime identity
|
||||
- Changed the MeshCentral runtime `shortName` to `gpthemestudio` so MeshCentral's alphanumeric admin route guard can open the backend panel.
|
||||
- Renamed the runtime entrypoint to `gpthemestudio.js` and aligned the JavaScript export with the MeshCentral loader.
|
||||
- Preserved the GracePress catalog slug `gracepress-theme-studio` and the existing catalog facade URLs.
|
||||
|
||||
## 0.0.1 - Default theme inventory
|
||||
- Created the initial GracePress Theme Studio MeshCentral plugin.
|
||||
- Captured the current classic MeshCentral UI as a locked `MeshCentral Default` theme profile.
|
||||
- Added a backend admin panel for reviewing theme variables and future theming capabilities.
|
||||
- Kept this first release read-only; it does not inject CSS, alter templates, or modify MeshCentral core files.
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "GracePress Theme Studio",
|
||||
"shortName": "gpthemestudio",
|
||||
"version": "0.0.3",
|
||||
"description": "MeshCentral UI theme profile editor for GracePress-managed MeshCentral servers.",
|
||||
"author": "Jason Eugene Garrison",
|
||||
"homepage": "https://christit.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://christit.com/gracepress/api.php?endpoint=manifest&slug=gracepress-theme-studio"
|
||||
},
|
||||
"hasAdminPanel": true,
|
||||
"main": "gpthemestudio.js",
|
||||
"configUrl": "https://christit.com/gracepress/gracepress-theme-studio.mcplugin.json",
|
||||
"downloadUrl": "https://christit.com/gracepress/gracepress-theme-studio.download.php",
|
||||
"changelogUrl": "https://christit.com/gracepress/api.php?endpoint=changelog&slug=gracepress-theme-studio",
|
||||
"versionHistoryUrl": "https://christit.com/gracepress/gracepress-theme-studio.tags.json",
|
||||
"meshCentralCompat": ">=1.1.0"
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"id": "meshcentral-default",
|
||||
"name": "MeshCentral Default",
|
||||
"description": "Captured baseline for the current Christ IT Connections MeshCentral classic UI before GracePress theming is applied.",
|
||||
"locked": true,
|
||||
"capturedAt": "2026-06-27",
|
||||
"source": "GracePress Theme Studio initial default profile",
|
||||
"variables": {
|
||||
"brand.title": "Christ IT Connections",
|
||||
"brand.subtitle": "mesh.christit.com",
|
||||
"brand.logoUrl": "",
|
||||
"brand.footerText": "Terms & Privacy",
|
||||
"font.family": "Arial, Helvetica, sans-serif",
|
||||
"font.baseSize": "16px",
|
||||
"font.smallSize": "12px",
|
||||
"font.headingSize": "32px",
|
||||
"layout.mode": "classic-leftbar",
|
||||
"layout.density": "default",
|
||||
"layout.contentMaxWidth": "none",
|
||||
"layout.leftbarWidth": "75px",
|
||||
"layout.iconRailWidth": "75px",
|
||||
"layout.footerHeight": "48px",
|
||||
"layout.borderRadius": "0px",
|
||||
"color.pageBackground": "#000000",
|
||||
"color.headerBackground": "#003a70",
|
||||
"color.headerText": "#d7d7d7",
|
||||
"color.headerAccent": "#ffffff",
|
||||
"color.leftbarBackground": "#1f1f2a",
|
||||
"color.leftbarActiveBackground": "#8bc8ff",
|
||||
"color.leftbarActiveBorder": "#66c27a",
|
||||
"color.sidebarBackground": "#144c83",
|
||||
"color.sidebarButtonBackground": "#06417a",
|
||||
"color.sidebarButtonActiveBackground": "#0d5ea8",
|
||||
"color.tabBackground": "#8b8b8b",
|
||||
"color.tabActiveBackground": "#004b8d",
|
||||
"color.tabText": "#ffffff",
|
||||
"color.panelBackground": "#000000",
|
||||
"color.panelText": "#d7d7d7",
|
||||
"color.tableHeaderText": "#ffffff",
|
||||
"color.tableRowText": "#d7d7d7",
|
||||
"color.link": "#5dade2",
|
||||
"color.success": "#00cc44",
|
||||
"color.warning": "#b45309",
|
||||
"color.danger": "#cc3333",
|
||||
"color.modalOverlay": "rgba(0,0,0,0.5)",
|
||||
"color.modalBackground": "#ffffff",
|
||||
"color.modalText": "#111111",
|
||||
"color.modalBorder": "#d9dde5",
|
||||
"button.primaryBackground": "#007bff",
|
||||
"button.primaryText": "#ffffff",
|
||||
"button.secondaryBackground": "#f4f4f6",
|
||||
"button.secondaryText": "#111111",
|
||||
"button.border": "#aeb4bf",
|
||||
"table.border": "#303030",
|
||||
"table.headerBackground": "#000000",
|
||||
"table.rowBackground": "#000000",
|
||||
"table.rowAltBackground": "#050505",
|
||||
"icons.set": "meshcentral-classic",
|
||||
"icons.size": "48px",
|
||||
"icons.monochrome": false,
|
||||
"icons.packPath": "",
|
||||
"effects.textShadow": "2px 2px 3px rgba(0,0,0,0.85)",
|
||||
"effects.panelShadow": "none",
|
||||
"effects.modalShadow": "0 0 24px rgba(0,0,0,0.6)",
|
||||
"advanced.customCss": ""
|
||||
},
|
||||
"notes": [
|
||||
"This profile is intentionally descriptive. It records the current classic UI appearance as variables before any global CSS injection exists.",
|
||||
"Values can be refined from MeshCentral CSS inspection in a later pass.",
|
||||
"Do not mutate this profile in-place; clone it into a new theme when applying changes."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,685 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Plugin Name: GracePress Theme Studio
|
||||
* Description: MeshCentral UI theme inventory and profile editor for GracePress-managed MeshCentral servers.
|
||||
* Version: 0.0.3
|
||||
* Stage: Alpha
|
||||
* Release Channel: Internal
|
||||
* Public Release: No
|
||||
* Public Candidate: No
|
||||
* Category: MeshCentral
|
||||
* Settings Slug: gracepress-theme-studio
|
||||
* WordPress.org Slug:
|
||||
* MeshCentral Short Name: gpthemestudio
|
||||
* MeshCentral Config URL: https://christit.com/gracepress/gracepress-theme-studio.mcplugin.json
|
||||
* MeshCentral Compatibility: >=1.1.0
|
||||
* Requires PHP: 8.0
|
||||
* Icon: 🧩
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const querystring = require('querystring');
|
||||
|
||||
const PLUGIN = {
|
||||
name: 'GracePress Theme Studio',
|
||||
shortName: 'gpthemestudio',
|
||||
version: '0.0.3',
|
||||
};
|
||||
|
||||
const PROFILE_DIR = 'themes';
|
||||
const DEFAULT_PROFILE_ID = 'meshcentral-default';
|
||||
const ACTIVE_FILE = 'active-theme.json';
|
||||
|
||||
const CONTROL_GROUPS = [
|
||||
{
|
||||
id: 'brand',
|
||||
label: 'Brand',
|
||||
description: 'Visible identity and non-CSS presentation values.',
|
||||
fields: [
|
||||
{ key: 'brand.title', label: 'Site title', type: 'text' },
|
||||
{ key: 'brand.subtitle', label: 'Subtitle', type: 'text' },
|
||||
{ key: 'brand.logoUrl', label: 'Logo URL', type: 'text', placeholder: 'https://...' },
|
||||
{ key: 'brand.footerText', label: 'Footer text', type: 'text', placeholder: 'Terms & Privacy' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'color',
|
||||
label: 'Colors',
|
||||
description: 'Primary MeshCentral chrome, panel, link, and state colors.',
|
||||
fields: [
|
||||
{ key: 'color.headerBackground', label: 'Header background', type: 'color' },
|
||||
{ key: 'color.headerText', label: 'Header text', type: 'color' },
|
||||
{ key: 'color.leftbarBackground', label: 'Icon rail background', type: 'color' },
|
||||
{ key: 'color.sidebarBackground', label: 'Sidebar background', type: 'color' },
|
||||
{ key: 'color.tabActiveBackground', label: 'Active tab', type: 'color' },
|
||||
{ key: 'color.panelBackground', label: 'Panel background', type: 'color' },
|
||||
{ key: 'color.panelText', label: 'Panel text', type: 'color' },
|
||||
{ key: 'color.link', label: 'Links', type: 'color' },
|
||||
{ key: 'color.success', label: 'Success', type: 'color' },
|
||||
{ key: 'color.warning', label: 'Warning', type: 'color' },
|
||||
{ key: 'color.danger', label: 'Danger', type: 'color' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'font',
|
||||
label: 'Typography',
|
||||
description: 'Dropdown-driven font decisions instead of blank freehand values.',
|
||||
fields: [
|
||||
{
|
||||
key: 'font.family',
|
||||
label: 'Font family',
|
||||
type: 'select',
|
||||
options: [
|
||||
'Arial, Helvetica, sans-serif',
|
||||
'Inter, Arial, Helvetica, sans-serif',
|
||||
'Segoe UI, Arial, Helvetica, sans-serif',
|
||||
'Roboto, Arial, Helvetica, sans-serif',
|
||||
'Georgia, serif',
|
||||
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||
],
|
||||
},
|
||||
{ key: 'font.baseSize', label: 'Base size', type: 'select', options: ['14px', '15px', '16px', '17px', '18px'] },
|
||||
{ key: 'font.headingSize', label: 'Heading size', type: 'select', options: ['24px', '28px', '32px', '36px', '40px'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'layout',
|
||||
label: 'Layout',
|
||||
description: 'Stable layout tokens for later CSS generation.',
|
||||
fields: [
|
||||
{ key: 'layout.density', label: 'Density', type: 'select', options: ['compact', 'default', 'comfortable'] },
|
||||
{ key: 'layout.leftbarWidth', label: 'Icon rail width', type: 'select', options: ['60px', '68px', '75px', '84px', '96px'] },
|
||||
{ key: 'layout.borderRadius', label: 'Border radius', type: 'select', options: ['0px', '3px', '5px', '8px'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'icons',
|
||||
label: 'Icons',
|
||||
description: 'Icon pack selection. Custom pack upload/generation comes after the mapping is proven.',
|
||||
fields: [
|
||||
{ key: 'icons.set', label: 'Icon set', type: 'select', options: ['meshcentral-classic', 'gracepress-classic', 'gracepress-flat', 'custom-pack'] },
|
||||
{ key: 'icons.size', label: 'Icon size', type: 'select', options: ['40px', '48px', '56px', '64px'] },
|
||||
{ key: 'icons.packPath', label: 'Custom pack path', type: 'text', placeholder: '/path/to/icons' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
label: 'Advanced',
|
||||
description: 'Recorded but not globally injected until rollback and startup loading are built.',
|
||||
fields: [
|
||||
{ key: 'advanced.customCss', label: 'Custom CSS notes', type: 'textarea' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
module.exports.gpthemestudio = function(parent) {
|
||||
const obj = { parent };
|
||||
obj.exports = [];
|
||||
|
||||
ensureProfileStore(parent);
|
||||
console.log(`${PLUGIN.name} loaded.`);
|
||||
|
||||
obj.handleAdminReq = function(req, res, user) {
|
||||
handleAdminRequest(parent, req, res, user);
|
||||
};
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
function handleAdminRequest(parent, req, res, user) {
|
||||
if (req && req.method === 'POST') {
|
||||
readBody(req, function(err, body) {
|
||||
const username = getUsername(user);
|
||||
let notice = '';
|
||||
let error = '';
|
||||
|
||||
if (err) {
|
||||
error = err.message;
|
||||
} else {
|
||||
const result = handleAction(parent, body);
|
||||
notice = result.notice || '';
|
||||
error = result.error || '';
|
||||
}
|
||||
|
||||
const state = loadState(parent);
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.end(renderAdmin(username, state, notice, error));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const state = loadState(parent, req);
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.end(renderAdmin(getUsername(user), state, '', ''));
|
||||
}
|
||||
|
||||
function readBody(req, done) {
|
||||
let body = '';
|
||||
req.on('data', function(chunk) {
|
||||
body += chunk;
|
||||
if (body.length > 262144) req.destroy(new Error('Request body too large.'));
|
||||
});
|
||||
req.on('end', function() {
|
||||
try {
|
||||
done(null, querystring.parse(body));
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
req.on('error', done);
|
||||
}
|
||||
|
||||
function handleAction(parent, data) {
|
||||
const action = getPostedValue(data.action);
|
||||
if (action === 'clone') return cloneProfile(parent, data);
|
||||
if (action === 'save') return saveProfile(parent, data);
|
||||
if (action === 'select') return selectActiveProfile(parent, data.profileId);
|
||||
if (action === 'export') return exportProfile(parent, data.profileId);
|
||||
return { error: 'Unknown Theme Studio action.' };
|
||||
}
|
||||
|
||||
function cloneProfile(parent, data) {
|
||||
const base = getProfileById(parent, String(data.sourceProfileId || DEFAULT_PROFILE_ID)) || loadDefaultTheme(parent);
|
||||
const name = cleanProfileName(data.newProfileName || 'Christ IT Theme');
|
||||
const id = uniqueProfileId(parent, slugify(name));
|
||||
const clone = JSON.parse(JSON.stringify(base));
|
||||
|
||||
clone.id = id;
|
||||
clone.name = name;
|
||||
clone.locked = false;
|
||||
clone.capturedAt = new Date().toISOString().slice(0, 10);
|
||||
clone.source = `Cloned from ${base.name || base.id || DEFAULT_PROFILE_ID}`;
|
||||
clone.description = `Editable theme profile cloned from ${base.name || 'MeshCentral Default'}.`;
|
||||
clone.notes = ['Editable GracePress Theme Studio profile.'];
|
||||
clone.branding = clone.branding || {};
|
||||
clone.generated = clone.generated || {};
|
||||
|
||||
writeProfile(parent, clone);
|
||||
writeActiveProfile(parent, id);
|
||||
return { notice: `Created editable theme profile "${name}".` };
|
||||
}
|
||||
|
||||
function saveProfile(parent, data) {
|
||||
const profileId = String(data.profileId || '');
|
||||
const profile = getProfileById(parent, profileId);
|
||||
if (!profile) return { error: 'Selected profile could not be found.' };
|
||||
if (profile.locked) return { error: 'The default MeshCentral profile is locked. Clone it before editing.' };
|
||||
|
||||
profile.name = cleanProfileName(data.profileName || profile.name || profile.id);
|
||||
profile.description = String(data.profileDescription || profile.description || '');
|
||||
profile.updatedAt = new Date().toISOString();
|
||||
profile.variables = profile.variables || {};
|
||||
|
||||
getEditableFields().forEach(function(field) {
|
||||
const formKey = formFieldName(field.key);
|
||||
if (Object.prototype.hasOwnProperty.call(data, formKey)) {
|
||||
profile.variables[field.key] = normalizeFieldValue(field, data[formKey]);
|
||||
}
|
||||
});
|
||||
|
||||
writeProfile(parent, profile);
|
||||
writeActiveProfile(parent, profile.id);
|
||||
return { notice: `Saved "${profile.name}".` };
|
||||
}
|
||||
|
||||
function selectActiveProfile(parent, profileId) {
|
||||
const profile = getProfileById(parent, String(profileId || ''));
|
||||
if (!profile) return { error: 'Selected profile could not be found.' };
|
||||
writeActiveProfile(parent, profile.id);
|
||||
return { notice: `Selected "${profile.name}" as the active editing profile.` };
|
||||
}
|
||||
|
||||
function exportProfile(parent, profileId) {
|
||||
const profile = getProfileById(parent, String(profileId || ''));
|
||||
if (!profile) return { error: 'Selected profile could not be found.' };
|
||||
const exportDir = path.join(getProfileDir(parent), 'exports');
|
||||
fs.mkdirSync(exportDir, { recursive: true });
|
||||
const file = path.join(exportDir, `${safeFileName(profile.id)}-${Date.now()}.json`);
|
||||
fs.writeFileSync(file, JSON.stringify(profile, null, 2) + '\n', 'utf8');
|
||||
return { notice: `Exported "${profile.name}" to ${file}.` };
|
||||
}
|
||||
|
||||
function ensureProfileStore(parent) {
|
||||
const dir = getProfileDir(parent);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const defaultFile = path.join(dir, `${DEFAULT_PROFILE_ID}.json`);
|
||||
if (!fs.existsSync(defaultFile)) {
|
||||
writeProfile(parent, loadDefaultTheme(parent));
|
||||
}
|
||||
if (!fs.existsSync(path.join(dir, ACTIVE_FILE))) {
|
||||
writeActiveProfile(parent, DEFAULT_PROFILE_ID);
|
||||
}
|
||||
}
|
||||
|
||||
function loadState(parent, req) {
|
||||
ensureProfileStore(parent);
|
||||
const profiles = loadProfiles(parent);
|
||||
const requestedId = getRequestProfileId(req);
|
||||
const activeId = requestedId || readActiveProfile(parent) || DEFAULT_PROFILE_ID;
|
||||
const active = profiles.find(function(profile) { return profile.id === activeId; }) || profiles[0] || loadDefaultTheme(parent);
|
||||
return {
|
||||
profiles,
|
||||
active,
|
||||
activeId: active.id,
|
||||
storagePath: getProfileDir(parent),
|
||||
};
|
||||
}
|
||||
|
||||
function getRequestProfileId(req) {
|
||||
if (!req || !req.url || req.url.indexOf('?') === -1) return '';
|
||||
const query = querystring.parse(req.url.split('?').slice(1).join('?'));
|
||||
return String(query.profileId || '');
|
||||
}
|
||||
|
||||
function loadProfiles(parent) {
|
||||
const dir = getProfileDir(parent);
|
||||
const files = fs.readdirSync(dir)
|
||||
.filter(function(file) { return file.endsWith('.json') && file !== ACTIVE_FILE; })
|
||||
.sort();
|
||||
const profiles = files.map(function(file) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}).filter(Boolean);
|
||||
|
||||
profiles.sort(function(left, right) {
|
||||
if (left.id === DEFAULT_PROFILE_ID) return -1;
|
||||
if (right.id === DEFAULT_PROFILE_ID) return 1;
|
||||
return String(left.name || left.id).localeCompare(String(right.name || right.id));
|
||||
});
|
||||
|
||||
return profiles.length ? profiles : [loadDefaultTheme(parent)];
|
||||
}
|
||||
|
||||
function getProfileById(parent, profileId) {
|
||||
return loadProfiles(parent).find(function(profile) { return profile.id === profileId; }) || null;
|
||||
}
|
||||
|
||||
function getProfileDir(parent) {
|
||||
return path.join(__dirname, PROFILE_DIR);
|
||||
}
|
||||
|
||||
function readActiveProfile(parent) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(path.join(getProfileDir(parent), ACTIVE_FILE), 'utf8')).activeProfileId || DEFAULT_PROFILE_ID;
|
||||
} catch (err) {
|
||||
return DEFAULT_PROFILE_ID;
|
||||
}
|
||||
}
|
||||
|
||||
function writeActiveProfile(parent, profileId) {
|
||||
fs.writeFileSync(path.join(getProfileDir(parent), ACTIVE_FILE), JSON.stringify({
|
||||
activeProfileId: profileId || DEFAULT_PROFILE_ID,
|
||||
selectedAt: new Date().toISOString(),
|
||||
applied: false,
|
||||
}, null, 2) + '\n', 'utf8');
|
||||
}
|
||||
|
||||
function writeProfile(parent, profile) {
|
||||
fs.mkdirSync(getProfileDir(parent), { recursive: true });
|
||||
fs.writeFileSync(path.join(getProfileDir(parent), `${safeFileName(profile.id)}.json`), JSON.stringify(profile, null, 2) + '\n', 'utf8');
|
||||
}
|
||||
|
||||
function loadDefaultTheme(parent) {
|
||||
const candidates = [
|
||||
path.join(__dirname, 'default-theme.json'),
|
||||
].filter(Boolean);
|
||||
|
||||
for (const file of candidates) {
|
||||
try {
|
||||
if (fs.existsSync(file)) {
|
||||
const theme = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
theme.id = DEFAULT_PROFILE_ID;
|
||||
theme.name = theme.name || 'MeshCentral Default';
|
||||
theme.locked = true;
|
||||
theme.variables = withProfileDefaults(theme.variables || {});
|
||||
return theme;
|
||||
}
|
||||
} catch (err) {
|
||||
return fallbackDefaultTheme(file, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackDefaultTheme('', '');
|
||||
}
|
||||
|
||||
function fallbackDefaultTheme(source, error) {
|
||||
return {
|
||||
schema: 1,
|
||||
id: DEFAULT_PROFILE_ID,
|
||||
name: 'MeshCentral Default',
|
||||
description: error ? 'Default theme profile could not be loaded.' : 'Default theme profile is missing from this plugin package.',
|
||||
locked: true,
|
||||
capturedAt: '',
|
||||
source,
|
||||
variables: withProfileDefaults({}),
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
function withProfileDefaults(variables) {
|
||||
const defaults = {
|
||||
'brand.logoUrl': '',
|
||||
'brand.footerText': 'Terms & Privacy',
|
||||
'icons.packPath': '',
|
||||
'advanced.customCss': '',
|
||||
};
|
||||
return Object.assign({}, defaults, variables);
|
||||
}
|
||||
|
||||
function renderAdmin(username, state, notice, error) {
|
||||
const profile = state.active || loadDefaultTheme();
|
||||
const variables = withProfileDefaults(profile.variables || {});
|
||||
const locked = !!profile.locked;
|
||||
const profiles = state.profiles || [];
|
||||
const rawGroups = groupVariables(variables);
|
||||
const rawRows = renderRawInventory(rawGroups);
|
||||
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>${escapeHtml(PLUGIN.name)}</title>
|
||||
<style>
|
||||
:root {
|
||||
--gp-bg: #f5f7fa;
|
||||
--gp-panel: #ffffff;
|
||||
--gp-ink: #1f2937;
|
||||
--gp-muted: #667085;
|
||||
--gp-line: #d8dee8;
|
||||
--gp-accent: #0f5e9c;
|
||||
--gp-accent-2: #087f5b;
|
||||
--gp-warn: #b45309;
|
||||
--gp-danger: #b42318;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; padding: 24px; background: var(--gp-bg); color: var(--gp-ink); font-family: Arial, Helvetica, sans-serif; }
|
||||
.wrap { max-width: 1180px; }
|
||||
.hero { border-left: 5px solid var(--gp-accent); padding: 0 0 0 16px; margin-bottom: 18px; }
|
||||
h1 { margin: 0 0 6px; font-size: 24px; }
|
||||
h2 { margin: 0 0 12px; font-size: 17px; }
|
||||
h3 { margin: 0 0 8px; font-size: 15px; }
|
||||
p { line-height: 1.45; }
|
||||
.muted { color: var(--gp-muted); }
|
||||
.grid { display: grid; grid-template-columns: repeat(4, minmax(150px, 1fr)); gap: 10px; margin: 16px 0; }
|
||||
.metric, .panel { background: var(--gp-panel); border: 1px solid var(--gp-line); border-radius: 6px; padding: 14px; }
|
||||
.metric .label { color: var(--gp-muted); font-size: 12px; font-weight: 700; text-transform: uppercase; }
|
||||
.metric .value { font-size: 18px; font-weight: 700; margin-top: 5px; }
|
||||
.panels { display: grid; grid-template-columns: 1fr; gap: 14px; }
|
||||
.profilebar { display: grid; grid-template-columns: minmax(260px, 1fr) auto auto; gap: 10px; align-items: end; }
|
||||
.notice, .error { border-radius: 5px; padding: 9px 11px; margin: 0 0 12px; }
|
||||
.notice { border: 1px solid #9bd7bd; background: #edfdf5; color: #087f5b; }
|
||||
.error { border: 1px solid #f2aca8; background: #fff1f0; color: var(--gp-danger); }
|
||||
label { display: block; color: #374151; font-weight: 700; font-size: 12px; margin: 0 0 5px; text-transform: uppercase; }
|
||||
input, select, textarea { width: 100%; border: 1px solid #bfc7d4; border-radius: 5px; padding: 8px; font: inherit; color: #111827; background: #fff; }
|
||||
input[type="color"] { min-height: 38px; padding: 3px; }
|
||||
textarea { min-height: 92px; resize: vertical; }
|
||||
input[readonly], textarea[readonly], select:disabled { background: #f3f5f8; color: #6b7280; }
|
||||
.section-grid { display: grid; grid-template-columns: repeat(2, minmax(240px, 1fr)); gap: 12px; }
|
||||
.field { margin-bottom: 10px; }
|
||||
.field.color { display: grid; grid-template-columns: 54px 1fr; gap: 8px; align-items: end; }
|
||||
.field.color label { grid-column: 1 / -1; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
||||
button { border: 1px solid var(--gp-line); background: #eef2f7; color: #111827; border-radius: 5px; padding: 8px 11px; cursor: pointer; }
|
||||
button.primary { background: var(--gp-accent); border-color: var(--gp-accent); color: #fff; }
|
||||
button:disabled { opacity: .55; cursor: not-allowed; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 13px; }
|
||||
th, td { border-top: 1px solid var(--gp-line); padding: 9px 10px; text-align: left; vertical-align: middle; }
|
||||
th { background: #f0f3f8; color: #374151; font-size: 12px; text-transform: uppercase; }
|
||||
code { background: #eef2f7; padding: 2px 5px; border-radius: 3px; }
|
||||
.chip { display: inline-block; border: 1px solid var(--gp-line); background: #f8fafc; border-radius: 999px; padding: 3px 8px; font-size: 12px; color: #334155; }
|
||||
.chip.ok { border-color: #9bd7bd; color: var(--gp-accent-2); background: #edfdf5; }
|
||||
.chip.warn { border-color: #f2c98d; color: var(--gp-warn); background: #fff7ed; }
|
||||
.swatch { display: inline-block; width: 18px; height: 18px; border: 1px solid #9ca3af; border-radius: 3px; vertical-align: middle; margin-right: 8px; background: var(--swatch); }
|
||||
.raw { max-height: 360px; overflow: auto; border: 1px solid var(--gp-line); border-radius: 5px; }
|
||||
@media (max-width: 840px) { .grid, .section-grid, .profilebar { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="hero">
|
||||
<h1>${escapeHtml(PLUGIN.name)}</h1>
|
||||
<div class="muted">Profile editor baseline for MeshCentral UI theming. Logged in as ${escapeHtml(username)}.</div>
|
||||
</div>
|
||||
|
||||
${notice ? `<div class="notice">${escapeHtml(notice)}</div>` : ''}
|
||||
${error ? `<div class="error">${escapeHtml(error)}</div>` : ''}
|
||||
|
||||
<div class="grid">
|
||||
<div class="metric"><div class="label">Plugin</div><div class="value">${escapeHtml(PLUGIN.version)}</div></div>
|
||||
<div class="metric"><div class="label">Profile</div><div class="value">${escapeHtml(profile.name || '')}</div></div>
|
||||
<div class="metric"><div class="label">Variables</div><div class="value">${Object.keys(variables).length}</div></div>
|
||||
<div class="metric"><div class="label">Mode</div><div class="value">${locked ? '<span class="chip ok">Locked default</span>' : '<span class="chip warn">Editable</span>'}</div></div>
|
||||
</div>
|
||||
|
||||
<div class="panels">
|
||||
<section class="panel">
|
||||
<form method="post" class="profilebar">
|
||||
<input type="hidden" name="action" value="select">
|
||||
<div>
|
||||
<label for="profileId">Theme profile</label>
|
||||
<select id="profileId" name="profileId">${renderProfileOptions(profiles, profile.id)}</select>
|
||||
</div>
|
||||
<button class="primary" type="submit">Load Profile</button>
|
||||
<button type="submit" name="action" value="export">Export JSON</button>
|
||||
</form>
|
||||
<form method="post" class="actions">
|
||||
<input type="hidden" name="action" value="clone">
|
||||
<input type="hidden" name="sourceProfileId" value="${escapeAttr(profile.id)}">
|
||||
<input name="newProfileName" value="${escapeAttr(suggestCloneName(profile.name))}" aria-label="New profile name">
|
||||
<button class="primary" type="submit">Clone Current Profile</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<form method="post" class="panel">
|
||||
<input type="hidden" name="action" value="save">
|
||||
<input type="hidden" name="profileId" value="${escapeAttr(profile.id)}">
|
||||
<h2>Profile Editor</h2>
|
||||
<p class="muted">${locked ? 'This is the locked MeshCentral Default profile. Clone it to create an editable theme.' : 'Edit values, save the profile, then export or apply after the global CSS hook is built.'}</p>
|
||||
<div class="section-grid">
|
||||
<div class="field">
|
||||
<label for="profileName">Profile name</label>
|
||||
<input id="profileName" name="profileName" value="${escapeAttr(profile.name || '')}" ${locked ? 'readonly' : ''}>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="profileDescription">Description</label>
|
||||
<input id="profileDescription" name="profileDescription" value="${escapeAttr(profile.description || '')}" ${locked ? 'readonly' : ''}>
|
||||
</div>
|
||||
</div>
|
||||
${CONTROL_GROUPS.map(function(group) { return renderControlGroup(group, variables, locked); }).join('')}
|
||||
<div class="actions">
|
||||
<button class="primary" type="submit" ${locked ? 'disabled' : ''}>Save Profile</button>
|
||||
<button type="button" disabled>Apply Theme</button>
|
||||
<button type="button" disabled>Install Startup CSS Hook</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Implementation Boundary</h2>
|
||||
<p class="muted">Theme Studio now saves editable profiles. Apply is intentionally disabled until the generated CSS hook, startup reapply behavior, and rollback marker are implemented.</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><th>ID</th><td><code>${escapeHtml(profile.id || '')}</code></td></tr>
|
||||
<tr><th>Storage</th><td><code>${escapeHtml(state.storagePath || '')}</code></td></tr>
|
||||
<tr><th>Source</th><td>${escapeHtml(profile.source || '')}</td></tr>
|
||||
<tr><th>Updated</th><td>${escapeHtml(profile.updatedAt || profile.capturedAt || '')}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Raw Variable Inventory</h2>
|
||||
<p class="muted">Kept for audit and future mapping work. The editor above is the primary workflow.</p>
|
||||
<div class="raw">${rawRows || '<p class="muted">No variables were found.</p>'}</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function renderProfileOptions(profiles, activeId) {
|
||||
return profiles.map(function(profile) {
|
||||
const label = `${profile.name || profile.id}${profile.locked ? ' (locked)' : ''}`;
|
||||
return `<option value="${escapeAttr(profile.id)}" ${profile.id === activeId ? 'selected' : ''}>${escapeHtml(label)}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderControlGroup(group, variables, locked) {
|
||||
const fields = group.fields.map(function(field) {
|
||||
return renderField(field, variables[field.key], locked);
|
||||
}).join('');
|
||||
|
||||
return `<section>
|
||||
<h3>${escapeHtml(group.label)}</h3>
|
||||
<p class="muted">${escapeHtml(group.description)}</p>
|
||||
<div class="section-grid">${fields}</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function renderField(field, value, locked) {
|
||||
const name = formFieldName(field.key);
|
||||
const id = `field-${name}`;
|
||||
const current = value === undefined || value === null ? '' : String(value);
|
||||
const disabled = locked ? 'disabled' : '';
|
||||
const readonly = locked ? 'readonly' : '';
|
||||
const fieldClass = field.type === 'color' ? 'field color' : 'field';
|
||||
let control = '';
|
||||
|
||||
if (field.type === 'select') {
|
||||
control = `<select id="${escapeAttr(id)}" name="${escapeAttr(name)}" ${disabled}>${renderSelectOptions(field.options || [], current)}</select>`;
|
||||
} else if (field.type === 'textarea') {
|
||||
control = `<textarea id="${escapeAttr(id)}" name="${escapeAttr(name)}" ${readonly}>${escapeHtml(current)}</textarea>`;
|
||||
} else if (field.type === 'color') {
|
||||
const color = isHexColor(current) ? current : '#000000';
|
||||
control = `<input type="color" id="${escapeAttr(id)}-color" name="${escapeAttr(name)}" value="${escapeAttr(color)}" ${disabled}><input type="text" value="${escapeAttr(current)}" ${readonly} aria-label="${escapeAttr(field.label)} text value">`;
|
||||
} else {
|
||||
control = `<input id="${escapeAttr(id)}" name="${escapeAttr(name)}" value="${escapeAttr(current)}" placeholder="${escapeAttr(field.placeholder || '')}" ${readonly}>`;
|
||||
}
|
||||
|
||||
return `<div class="${fieldClass}"><label for="${escapeAttr(id)}">${escapeHtml(field.label)}</label>${control}</div>`;
|
||||
}
|
||||
|
||||
function renderSelectOptions(options, current) {
|
||||
const seen = new Set();
|
||||
const all = options.slice();
|
||||
if (current && all.indexOf(current) === -1) all.unshift(current);
|
||||
return all.filter(function(option) {
|
||||
if (seen.has(option)) return false;
|
||||
seen.add(option);
|
||||
return true;
|
||||
}).map(function(option) {
|
||||
return `<option value="${escapeAttr(option)}" ${option === current ? 'selected' : ''}>${escapeHtml(option)}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderRawInventory(groups) {
|
||||
const order = ['brand', 'font', 'layout', 'color', 'button', 'table', 'icons', 'effects', 'advanced', 'other'];
|
||||
return order.filter(function(group) { return groups[group] && groups[group].length; }).map(function(group) {
|
||||
const body = groups[group].map(function(row) {
|
||||
return `<tr><td><code>${escapeHtml(row.key)}</code></td><td>${renderValue(row.value)}</td></tr>`;
|
||||
}).join('');
|
||||
|
||||
return `<h3>${escapeHtml(titleCase(group))}</h3><table><thead><tr><th>Variable</th><th>Value</th></tr></thead><tbody>${body}</tbody></table>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function getEditableFields() {
|
||||
return CONTROL_GROUPS.reduce(function(fields, group) {
|
||||
return fields.concat(group.fields);
|
||||
}, []);
|
||||
}
|
||||
|
||||
function groupVariables(variables) {
|
||||
const groups = {};
|
||||
Object.keys(variables).sort().forEach(function(key) {
|
||||
const dot = key.indexOf('.');
|
||||
const group = dot > 0 ? key.substring(0, dot) : 'other';
|
||||
if (!groups[group]) groups[group] = [];
|
||||
groups[group].push({ key, value: variables[key] });
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
function renderValue(value) {
|
||||
const text = String(value);
|
||||
const color = isColorValue(text) ? `<span class="swatch" style="--swatch:${escapeAttr(text)}"></span>` : '';
|
||||
return `${color}<code>${escapeHtml(text)}</code>`;
|
||||
}
|
||||
|
||||
function normalizeFieldValue(field, value) {
|
||||
value = getPostedValue(value);
|
||||
value = String(value || '').trim();
|
||||
if (field.type === 'color' && !isHexColor(value)) return '#000000';
|
||||
return value;
|
||||
}
|
||||
|
||||
function getPostedValue(value) {
|
||||
if (Array.isArray(value)) return String(value[value.length - 1] || '');
|
||||
return String(value || '');
|
||||
}
|
||||
|
||||
function formFieldName(key) {
|
||||
return `var_${String(key).replace(/[^a-zA-Z0-9]/g, '_')}`;
|
||||
}
|
||||
|
||||
function uniqueProfileId(parent, base) {
|
||||
base = base || 'theme';
|
||||
const existing = new Set(loadProfiles(parent).map(function(profile) { return profile.id; }));
|
||||
if (!existing.has(base)) return base;
|
||||
let index = 2;
|
||||
while (existing.has(`${base}-${index}`)) index += 1;
|
||||
return `${base}-${index}`;
|
||||
}
|
||||
|
||||
function cleanProfileName(value) {
|
||||
const name = String(value || '').trim().replace(/\s+/g, ' ');
|
||||
return name || 'Untitled Theme';
|
||||
}
|
||||
|
||||
function slugify(value) {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 60) || 'theme';
|
||||
}
|
||||
|
||||
function safeFileName(value) {
|
||||
return String(value || 'theme').replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
}
|
||||
|
||||
function suggestCloneName(name) {
|
||||
if (!name || name === 'MeshCentral Default') return 'Christ IT Theme';
|
||||
return `${name} Copy`;
|
||||
}
|
||||
|
||||
function getUsername(user) {
|
||||
return (user && (user.name || user._id)) ? (user.name || user._id) : 'MeshCentral admin';
|
||||
}
|
||||
|
||||
function isHexColor(value) {
|
||||
return /^#([0-9a-f]{6})$/i.test(String(value || ''));
|
||||
}
|
||||
|
||||
function isColorValue(value) {
|
||||
return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value) || /^rgba?\(/i.test(value);
|
||||
}
|
||||
|
||||
function titleCase(value) {
|
||||
return String(value).replace(/[-_]/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value).replace(/[&<>"']/g, function(c) {
|
||||
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c];
|
||||
});
|
||||
}
|
||||
|
||||
function escapeAttr(value) {
|
||||
return escapeHtml(value).replace(/`/g, '`');
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.18 - Theme Studio 0.0.3 catalog seed
|
||||
- Updated the built-in GracePress Theme Studio catalog seed to version `0.0.3`.
|
||||
- Changed the Theme Studio description to match the new profile editor role.
|
||||
- Keeps the MeshCentral package manager authoritative over GracePress-managed plugin rows while preserving unrelated third-party plugin rows.
|
||||
|
||||
## 0.0.17 - GracePress row ownership cleanup
|
||||
- Resolves MeshCentral plugin registry row IDs from `_id`, `id`, or `pluginId` so cleanup works across MeshCentral database adapters.
|
||||
- Archives and removes GracePress-owned legacy rows when their short name no longer matches the GracePress catalog entry.
|
||||
- Archives and removes duplicate GracePress-owned rows for the same managed MeshCentral short name while preserving the active row when one exists.
|
||||
- Reports missing row IDs as explicit sync errors instead of silently leaving obsolete GracePress rows in the MeshCentral plugin catalog.
|
||||
- Keeps cleanup scoped to GracePress-managed plugin entries so unrelated third-party MeshCentral plugins are not touched.
|
||||
|
||||
## 0.0.16 - Legacy row quarantine
|
||||
- Archives and removes obsolete legacy MeshCentral plugin rows such as `gracepress-theme-studio` after preserving their registry document under the live plugin `.old` folder.
|
||||
- Preserves the installed/active status of the corrected `gpthemestudio` row during catalog sync instead of forcing managed rows back to disabled.
|
||||
- Keeps Theme Studio migration in the rolling catalog update path while avoiding duplicate disabled rows in MeshCentral's plugin list.
|
||||
|
||||
## 0.0.15 - MeshCentral-safe Theme Studio seed
|
||||
- Updated the built-in Theme Studio seed to use the MeshCentral-safe runtime short name `gpthemestudio`.
|
||||
- Preserved the public GracePress catalog slug `gracepress-theme-studio` while separating it from MeshCentral's admin-route identity.
|
||||
- Keeps the catalog-manager rolling update path as the migration mechanism for correcting installed MeshCentral plugin rows.
|
||||
- Migrates the legacy `gracepress-theme-studio` MeshCentral row forward to `gpthemestudio` instead of creating a duplicate row.
|
||||
|
||||
## 0.0.14 - Built-in MeshCentral plugin catalog seed
|
||||
- Added a deterministic built-in GracePress MeshCentral plugin catalog seed for `GracePress Theme Studio`.
|
||||
- Changed discovery to write disabled plugin rows through MeshCentral's database helper directly instead of the higher-level plugin wrapper.
|
||||
- Added explicit sync-stage logging so MeshCentral service logs show whether discovery starts, reads existing rows, inserts rows, or fails.
|
||||
- Kept the dynamic catalog URL in metadata for a later pass, but removed runtime catalog fetch as a dependency for exposing the first managed plugin.
|
||||
|
||||
## 0.0.13 - Constructor startup sync
|
||||
- Moved GracePress MeshCentral plugin discovery into the plugin constructor path because MeshCentral does not call `server_startup()` for already-installed plugins during ordinary service reload.
|
||||
- Kept the `server_startup()` hook for install/upgrade compatibility while also scheduling discovery shortly after plugin load.
|
||||
- Added an admin-panel fallback that starts discovery if the connector panel opens before the scheduled sync has run.
|
||||
|
||||
## 0.0.12 - Catalog plugin discovery
|
||||
- Added startup sync from the GracePress `mcplugins` catalog bucket into MeshCentral's plugin registry.
|
||||
- Missing GracePress MeshCentral plugins are now inserted as disabled/available rows, ready for MeshCentral's normal Install action.
|
||||
- Existing disabled rows are refreshed from their current `.mcplugin.json` config while active installed plugins are left alone.
|
||||
- Added backend panel sync status so catalog discovery can be inspected from the connector admin view.
|
||||
|
||||
## 0.0.11 - Facade confirmation update
|
||||
- Published a marker-only update to confirm the MeshCentral git facade shape after the successful `0.0.10` upgrade.
|
||||
- Preserved the MeshCentral-facing `downloadUrl` and `versionHistoryUrl` facade URLs in package config.
|
||||
- Updated the backend panel test marker to `0.0.11`.
|
||||
|
||||
## 0.0.10 - MeshCentral git facade test
|
||||
- Added a marker-only update that uses GracePress-generated MeshCentral git facade URLs.
|
||||
- Switched MeshCentral `downloadUrl` to the no-query `gracepress.download.php` package facade.
|
||||
- Switched MeshCentral `versionHistoryUrl` to `gracepress.tags.json`, shaped like a GitHub tags response with `name` and `url` fields.
|
||||
- Updated the backend panel test marker to `0.0.10`.
|
||||
|
||||
## 0.0.9 - Versioned download URL retest
|
||||
- Published a marker-only update that uses the versioned package URL as MeshCentral `downloadUrl`.
|
||||
- Avoided the cached `gracepress-latest.zip` alias and the nginx path-info download route while validating the built-in Upgrade action.
|
||||
- Updated the backend panel test marker to `0.0.9`.
|
||||
|
||||
## 0.0.8 - Upgrade flow retest
|
||||
- Published a small marker-only update for validating MeshCentral's built-in Upgrade action after the MySQL plugin registry repair.
|
||||
- Updated the backend panel test marker to `0.0.8` so a successful install can be confirmed from the MeshCentral plugin panel.
|
||||
|
||||
## 0.0.7 - Direct MeshCentral latest zip
|
||||
- Replaced the query-string latest-download endpoint with a stable `gracepress-latest.zip` file because MeshCentral's plugin downloader builds requests from `URL.pathname` and drops query strings.
|
||||
- Kept versioned GracePress release artifacts while adding a MeshCentral-safe latest zip alias for the built-in Upgrade action.
|
||||
- Updated package metadata so installed MeshCentral config rows keep using the stable latest zip URL after upgrade.
|
||||
|
||||
## 0.0.6 - Stable MeshCentral upgrade download
|
||||
- Added a GracePress `mcplugin-download` endpoint for MeshCentral's built-in Upgrade action.
|
||||
- Changed the MeshCentral-facing `downloadUrl` to a stable latest-download endpoint because MeshCentral upgrades from the installed DB row, not the freshly fetched latest config object.
|
||||
- Kept the GracePress manifest `download_url` versioned for catalog and rollback clarity.
|
||||
|
||||
## 0.0.5 - Upgrade detection test
|
||||
- Published a small catalog-only test update for MeshCentral's built-in Upgrade action.
|
||||
- Added a visible backend panel note so successful upgrade can be confirmed after MeshCentral installs the package.
|
||||
|
||||
## 0.0.4 - MeshCentral lifecycle alignment
|
||||
- Renamed the display/package name to GracePress Catalog Connector while preserving the stable `gracepress` slug and MeshCentral `shortName`.
|
||||
- Changed MeshCentral repository metadata to `git` so MeshCentral's built-in installer and upgrade action can download the GracePress-hosted zip.
|
||||
- Documented the active plugin identity rule: folder name, `shortName`, JS export, config row, and catalog slug must line up.
|
||||
- Prepared the live registry cleanup from duplicate `gracepress` rows into one active catalog-managed plugin.
|
||||
|
||||
## 0.0.3 - Catalog-managed backend panel
|
||||
- Moved the live GracePress MeshCentral dummy plugin into the GracePress workspace as an `mcplugin` package.
|
||||
- Added internal MeshCentral plugin header metadata for GracePress catalog builds.
|
||||
- Added backend admin panel catalog checks against the GracePress manifest endpoint.
|
||||
- Updated MeshCentral plugin config URLs to use the GracePress catalog-managed package, config, and changelog artifacts.
|
||||
|
||||
## 0.0.2 - Backend hello-world route
|
||||
- Replaced device-tab test behavior with a backend/admin plugin panel.
|
||||
- Kept MeshCentral plugin permissions unused while the current MeshCentral database adapter reports missing plugin-permission helpers.
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "GracePress Catalog Connector",
|
||||
"shortName": "gracepress",
|
||||
"version": "0.0.18",
|
||||
"description": "MeshCentral backend connector for GracePress catalog-managed plugin delivery, updates, and diagnostics.",
|
||||
"author": "Jason Eugene Garrison",
|
||||
"homepage": "https://christit.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://christit.com/gracepress/api.php?endpoint=manifest&slug=gracepress"
|
||||
},
|
||||
"hasAdminPanel": true,
|
||||
"main": "gracepress.js",
|
||||
"configUrl": "https://christit.com/gracepress/gracepress.mcplugin.json",
|
||||
"downloadUrl": "https://christit.com/gracepress/gracepress.download.php",
|
||||
"changelogUrl": "https://christit.com/gracepress/api.php?endpoint=changelog&slug=gracepress",
|
||||
"versionHistoryUrl": "https://christit.com/gracepress/gracepress.tags.json",
|
||||
"meshCentralCompat": ">=1.1.0"
|
||||
}
|
||||
@@ -0,0 +1,562 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Plugin Name: GracePress Catalog Connector
|
||||
* Description: MeshCentral backend connector for GracePress catalog-managed plugin delivery, updates, and diagnostics.
|
||||
* Version: 0.0.18
|
||||
* Stage: Alpha
|
||||
* Release Channel: Internal
|
||||
* Public Release: No
|
||||
* Public Candidate: No
|
||||
* Category: MeshCentral
|
||||
* Settings Slug: gracepress
|
||||
* WordPress.org Slug:
|
||||
* MeshCentral Short Name: gracepress
|
||||
* MeshCentral Config URL: https://christit.com/gracepress/gracepress.mcplugin.json
|
||||
* MeshCentral Compatibility: >=1.1.0
|
||||
* Requires PHP: 8.0
|
||||
* Icon: 🧩
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PLUGIN = {
|
||||
name: 'GracePress Catalog Connector',
|
||||
shortName: 'gracepress',
|
||||
version: '0.0.18',
|
||||
catalogUrl: 'https://christit.com/gracepress/catalog.json',
|
||||
manifestUrl: 'https://christit.com/gracepress/api.php?endpoint=manifest&slug=gracepress',
|
||||
configUrl: 'https://christit.com/gracepress/gracepress.mcplugin.json',
|
||||
changelogUrl: 'https://christit.com/gracepress/api.php?endpoint=changelog&slug=gracepress',
|
||||
};
|
||||
|
||||
const STATIC_MCPLUGIN_CONFIGS = [
|
||||
{
|
||||
name: 'GracePress Theme Studio',
|
||||
shortName: 'gpthemestudio',
|
||||
legacyShortNames: ['gracepress-theme-studio'],
|
||||
version: '0.0.3',
|
||||
description: 'MeshCentral UI theme profile editor for GracePress-managed MeshCentral servers.',
|
||||
author: 'Jason Eugene Garrison',
|
||||
homepage: 'https://christit.com',
|
||||
repository: {
|
||||
type: 'git',
|
||||
url: 'https://christit.com/gracepress/api.php?endpoint=manifest&slug=gracepress-theme-studio',
|
||||
},
|
||||
hasAdminPanel: true,
|
||||
main: 'gpthemestudio.js',
|
||||
configUrl: 'https://christit.com/gracepress/gracepress-theme-studio.mcplugin.json',
|
||||
downloadUrl: 'https://christit.com/gracepress/gracepress-theme-studio.download.php',
|
||||
changelogUrl: 'https://christit.com/gracepress/api.php?endpoint=changelog&slug=gracepress-theme-studio',
|
||||
versionHistoryUrl: 'https://christit.com/gracepress/gracepress-theme-studio.tags.json',
|
||||
meshCentralCompat: '>=1.1.0',
|
||||
},
|
||||
];
|
||||
|
||||
const syncState = {
|
||||
lastStarted: '',
|
||||
lastFinished: '',
|
||||
running: false,
|
||||
ok: false,
|
||||
added: [],
|
||||
updated: [],
|
||||
skipped: [],
|
||||
errors: [],
|
||||
};
|
||||
|
||||
module.exports.gracepress = function(parent) {
|
||||
const obj = { parent };
|
||||
obj.exports = [];
|
||||
|
||||
console.log(`${PLUGIN.name} loaded.`);
|
||||
console.log(`${PLUGIN.name} scheduling built-in MeshCentral plugin catalog sync.`);
|
||||
|
||||
syncMeshCentralCatalog(parent);
|
||||
scheduleCatalogSync(parent, 5000);
|
||||
|
||||
obj.server_startup = function() {
|
||||
syncMeshCentralCatalog(parent);
|
||||
};
|
||||
|
||||
obj.handleAdminReq = function(req, res, user) {
|
||||
if (!syncState.lastStarted && !syncState.running) {
|
||||
scheduleCatalogSync(parent, 0);
|
||||
}
|
||||
checkCatalog(function(catalog) {
|
||||
const username = (user && (user.name || user._id)) ? (user.name || user._id) : 'MeshCentral admin';
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.end(renderAdmin(username, catalog));
|
||||
});
|
||||
};
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
function scheduleCatalogSync(parent, delayMs) {
|
||||
setTimeout(function() {
|
||||
syncMeshCentralCatalog(parent);
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
function checkCatalog(done) {
|
||||
const url = `${PLUGIN.manifestUrl}&nocache=${Date.now()}`;
|
||||
https.get(url, { timeout: 4500 }, function(response) {
|
||||
let body = '';
|
||||
response.setEncoding('utf8');
|
||||
response.on('data', function(chunk) {
|
||||
body += chunk;
|
||||
if (body.length > 262144) {
|
||||
response.destroy(new Error('Catalog response too large.'));
|
||||
}
|
||||
});
|
||||
response.on('end', function() {
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
done({
|
||||
ok: response.statusCode >= 200 && response.statusCode < 300 && data && !data.error,
|
||||
statusCode: response.statusCode,
|
||||
manifest: data,
|
||||
checkedAt: new Date().toISOString(),
|
||||
error: data && data.error ? data.error : '',
|
||||
});
|
||||
} catch (err) {
|
||||
done({ ok: false, statusCode: response.statusCode, checkedAt: new Date().toISOString(), error: err.message });
|
||||
}
|
||||
});
|
||||
}).on('timeout', function() {
|
||||
this.destroy(new Error('Catalog request timed out.'));
|
||||
}).on('error', function(err) {
|
||||
done({ ok: false, statusCode: 0, checkedAt: new Date().toISOString(), error: err.message });
|
||||
});
|
||||
}
|
||||
|
||||
function syncMeshCentralCatalog(parent) {
|
||||
if (syncState.running) return;
|
||||
|
||||
syncState.running = true;
|
||||
syncState.ok = false;
|
||||
syncState.lastStarted = new Date().toISOString();
|
||||
syncState.lastFinished = '';
|
||||
syncState.added = [];
|
||||
syncState.updated = [];
|
||||
syncState.skipped = [];
|
||||
syncState.errors = [];
|
||||
|
||||
runCatalogSync(parent).then(function(summary) {
|
||||
syncState.ok = true;
|
||||
syncState.added = summary.added;
|
||||
syncState.updated = summary.updated;
|
||||
syncState.skipped = summary.skipped;
|
||||
syncState.errors = summary.errors;
|
||||
console.log(`${PLUGIN.name} synced MeshCentral catalog: added ${summary.added.length}, updated ${summary.updated.length}, skipped ${summary.skipped.length}, errors ${summary.errors.length}.`);
|
||||
}).catch(function(err) {
|
||||
syncState.errors.push(err && err.message ? err.message : String(err));
|
||||
console.log(`${PLUGIN.name} catalog sync failed: ${syncState.errors.join('; ')}`);
|
||||
}).finally(function() {
|
||||
syncState.running = false;
|
||||
syncState.lastFinished = new Date().toISOString();
|
||||
});
|
||||
}
|
||||
|
||||
async function runCatalogSync(parent) {
|
||||
const summary = { added: [], updated: [], skipped: [], errors: [] };
|
||||
const entries = STATIC_MCPLUGIN_CONFIGS.slice();
|
||||
console.log(`${PLUGIN.name} sync starting with ${entries.length} built-in MeshCentral plugin entries.`);
|
||||
const existing = await getExistingPlugins(parent);
|
||||
console.log(`${PLUGIN.name} sync found ${existing.length} MeshCentral plugin registry rows.`);
|
||||
const byShortName = new Map();
|
||||
const rowsByShortName = new Map();
|
||||
|
||||
existing.forEach(function(row) {
|
||||
const shortName = row && row.shortName ? row.shortName : '';
|
||||
if (!shortName) return;
|
||||
if (!byShortName.has(shortName)) byShortName.set(shortName, row);
|
||||
if (!rowsByShortName.has(shortName)) rowsByShortName.set(shortName, []);
|
||||
rowsByShortName.get(shortName).push(row);
|
||||
});
|
||||
|
||||
for (const entry of entries) {
|
||||
const shortName = (entry && entry.shortName) || '';
|
||||
const configUrl = entry && entry.configUrl;
|
||||
|
||||
if (!shortName || !configUrl) {
|
||||
summary.skipped.push(`${entry && entry.name ? entry.name : 'unknown'}: missing MeshCentral config URL`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shortName === PLUGIN.shortName) {
|
||||
summary.skipped.push(`${shortName}: active connector package`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const pluginRow = normalizePluginConfig(entry);
|
||||
if (!pluginRow || !pluginRow.shortName || !pluginRow.downloadUrl) {
|
||||
summary.errors.push(`${shortName}: invalid MeshCentral plugin config`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const legacyRows = [];
|
||||
if (Array.isArray(entry.legacyShortNames)) {
|
||||
for (const legacyShortName of entry.legacyShortNames) {
|
||||
if (rowsByShortName.has(legacyShortName)) legacyRows.push.apply(legacyRows, rowsByShortName.get(legacyShortName));
|
||||
}
|
||||
}
|
||||
|
||||
for (const legacyRow of legacyRows) {
|
||||
if (!isGracePressManagedRow(legacyRow)) continue;
|
||||
const legacyId = getPluginRowId(legacyRow);
|
||||
if (legacyId !== undefined && legacyId !== null) {
|
||||
await archivePluginRow(parent, legacyRow, 'legacy-shortname');
|
||||
await deletePluginRow(parent, legacyId, legacyRow.shortName);
|
||||
byShortName.delete(legacyRow.shortName);
|
||||
summary.updated.push(`${legacyRow.name || legacyRow.shortName} legacy archived`);
|
||||
} else if (legacyRow && legacyRow.shortName) {
|
||||
summary.errors.push(`${legacyRow.shortName}: legacy row id missing`);
|
||||
}
|
||||
}
|
||||
|
||||
const currentRows = rowsByShortName.get(pluginRow.shortName) || [];
|
||||
const current = chooseCurrentPluginRow(currentRows);
|
||||
for (const duplicateRow of currentRows) {
|
||||
if (duplicateRow === current || !isGracePressManagedRow(duplicateRow)) continue;
|
||||
const duplicateId = getPluginRowId(duplicateRow);
|
||||
if (duplicateId !== undefined && duplicateId !== null) {
|
||||
await archivePluginRow(parent, duplicateRow, 'duplicate-shortname');
|
||||
await deletePluginRow(parent, duplicateId, duplicateRow.shortName);
|
||||
summary.updated.push(`${duplicateRow.name || duplicateRow.shortName} duplicate archived`);
|
||||
} else {
|
||||
summary.errors.push(`${duplicateRow.shortName}: duplicate row id missing`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!current) {
|
||||
console.log(`${PLUGIN.name} adding disabled MeshCentral plugin row for ${pluginRow.shortName}.`);
|
||||
await addPluginRow(parent, pluginRow);
|
||||
byShortName.set(pluginRow.shortName, pluginRow);
|
||||
summary.added.push(`${pluginRow.name} ${pluginRow.version}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const refreshed = Object.assign({}, pluginRow, { status: Number(current.status) || 0 });
|
||||
const currentId = getPluginRowId(current);
|
||||
if (currentId !== undefined && currentId !== null) refreshed._id = currentId;
|
||||
|
||||
if (pluginRowsMatch(current, refreshed)) {
|
||||
summary.skipped.push(`${pluginRow.shortName}: already available`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await updatePluginRow(parent, currentId, refreshed);
|
||||
byShortName.set(pluginRow.shortName, refreshed);
|
||||
summary.updated.push(`${pluginRow.name} ${pluginRow.version}`);
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
function getJson(url) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
https.get(url, { timeout: 6000 }, function(response) {
|
||||
let body = '';
|
||||
response.setEncoding('utf8');
|
||||
response.on('data', function(chunk) {
|
||||
body += chunk;
|
||||
if (body.length > 1048576) {
|
||||
response.destroy(new Error('Response too large.'));
|
||||
}
|
||||
});
|
||||
response.on('end', function() {
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
reject(new Error(`HTTP ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(body));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}).on('timeout', function() {
|
||||
this.destroy(new Error('Request timed out.'));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function getExistingPlugins(parent) {
|
||||
const db = parent && parent.parent && parent.parent.db;
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (!db || typeof db.getPlugins !== 'function') {
|
||||
reject(new Error('MeshCentral plugin database helper is unavailable.'));
|
||||
return;
|
||||
}
|
||||
db.getPlugins(function(err, docs) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve((Array.isArray(docs) ? docs : []).map(parsePluginDoc).filter(Boolean));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addPluginRow(parent, pluginRow) {
|
||||
const db = parent && parent.parent && parent.parent.db;
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (!db || typeof db.addPlugin !== 'function') {
|
||||
reject(new Error('MeshCentral addPlugin helper is unavailable.'));
|
||||
return;
|
||||
}
|
||||
db.addPlugin(pluginRow, function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updatePluginRow(parent, id, pluginRow) {
|
||||
const db = parent && parent.parent && parent.parent.db;
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (id === undefined || id === null || !db || typeof db.updatePlugin !== 'function') {
|
||||
reject(new Error(`Cannot update ${pluginRow.shortName}; plugin id or update helper is unavailable.`));
|
||||
return;
|
||||
}
|
||||
db.updatePlugin(id, pluginRow, function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deletePluginRow(parent, id, shortName) {
|
||||
const db = parent && parent.parent && parent.parent.db;
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (id === undefined || id === null || !db || typeof db.deletePlugin !== 'function') {
|
||||
reject(new Error(`Cannot delete legacy ${shortName}; plugin id or delete helper is unavailable.`));
|
||||
return;
|
||||
}
|
||||
db.deletePlugin(id, function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function archivePluginRow(parent, pluginRow, reason) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
try {
|
||||
const pluginPath = parent && parent.pluginPath ? parent.pluginPath : '';
|
||||
if (!pluginPath) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const oldDir = path.join(pluginPath, '.old');
|
||||
fs.mkdirSync(oldDir, { recursive: true });
|
||||
const stamp = new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14);
|
||||
const shortName = String(pluginRow.shortName || 'unknown').replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const id = String(getPluginRowId(pluginRow) || 'unknown').replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const file = path.join(oldDir, `gracepress-catalog-${reason}-${shortName}-${id}-${stamp}.json`);
|
||||
fs.writeFileSync(file, JSON.stringify(pluginRow, null, 2) + '\n', 'utf8');
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parsePluginDoc(doc) {
|
||||
if (!doc) return null;
|
||||
const outerId = getPluginRowId(doc);
|
||||
const attachOuterId = function(parsed) {
|
||||
if (!parsed || typeof parsed !== 'object') return parsed;
|
||||
if (outerId !== undefined && outerId !== null && parsed._id === undefined && parsed.id === undefined) parsed._id = outerId;
|
||||
return parsed;
|
||||
};
|
||||
|
||||
if (typeof doc === 'string') {
|
||||
try { return JSON.parse(doc); } catch (err) { return null; }
|
||||
}
|
||||
if (typeof doc.doc === 'string') {
|
||||
try { return attachOuterId(JSON.parse(doc.doc)); } catch (err) { return null; }
|
||||
}
|
||||
if (doc.doc && typeof doc.doc === 'object') return attachOuterId(doc.doc);
|
||||
return doc;
|
||||
}
|
||||
|
||||
function getPluginRowId(row) {
|
||||
if (!row || typeof row !== 'object') return undefined;
|
||||
if (row._id !== undefined && row._id !== null) return row._id;
|
||||
if (row.id !== undefined && row.id !== null) return row.id;
|
||||
if (row.pluginId !== undefined && row.pluginId !== null) return row.pluginId;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function chooseCurrentPluginRow(rows) {
|
||||
if (!Array.isArray(rows) || !rows.length) return null;
|
||||
return rows.find(function(row) { return Number(row && row.status) === 1; }) || rows[0];
|
||||
}
|
||||
|
||||
function isGracePressManagedRow(row) {
|
||||
if (!row || typeof row !== 'object') return false;
|
||||
const values = [
|
||||
row.name,
|
||||
row.shortName,
|
||||
row.configUrl,
|
||||
row.downloadUrl,
|
||||
row.changelogUrl,
|
||||
row.versionHistoryUrl,
|
||||
row.homepage,
|
||||
row.repository && row.repository.url,
|
||||
].map(function(value) {
|
||||
return String(value || '').toLowerCase();
|
||||
});
|
||||
|
||||
return values.some(function(value) {
|
||||
return value.indexOf('gracepress') !== -1 || value.indexOf('christit.com/gracepress') !== -1;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizePluginConfig(config) {
|
||||
if (!config || !config.shortName || !config.name) return null;
|
||||
|
||||
return {
|
||||
name: config.name,
|
||||
shortName: config.shortName,
|
||||
version: config.version || '0.0.0',
|
||||
description: config.description || '',
|
||||
hasAdminPanel: !!config.hasAdminPanel,
|
||||
homepage: config.homepage || 'https://christit.com',
|
||||
repository: {
|
||||
type: (config.repository && config.repository.type) || 'git',
|
||||
url: (config.repository && config.repository.url) || config.configUrl || '',
|
||||
},
|
||||
configUrl: config.configUrl || '',
|
||||
downloadUrl: config.downloadUrl || '',
|
||||
changelogUrl: config.changelogUrl || '',
|
||||
versionHistoryUrl: config.versionHistoryUrl || '',
|
||||
meshCentralCompat: config.meshCentralCompat || '>=1.1.0',
|
||||
status: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function pluginRowsMatch(left, right) {
|
||||
const fields = [
|
||||
'name',
|
||||
'shortName',
|
||||
'version',
|
||||
'description',
|
||||
'hasAdminPanel',
|
||||
'homepage',
|
||||
'configUrl',
|
||||
'downloadUrl',
|
||||
'changelogUrl',
|
||||
'versionHistoryUrl',
|
||||
'meshCentralCompat',
|
||||
'status',
|
||||
];
|
||||
|
||||
for (const field of fields) {
|
||||
if (String(left[field] === undefined ? '' : left[field]) !== String(right[field] === undefined ? '' : right[field])) return false;
|
||||
}
|
||||
|
||||
const leftRepo = left.repository || {};
|
||||
const rightRepo = right.repository || {};
|
||||
return String(leftRepo.type || '') === String(rightRepo.type || '') && String(leftRepo.url || '') === String(rightRepo.url || '');
|
||||
}
|
||||
|
||||
function renderAdmin(username, catalog) {
|
||||
const manifest = catalog && catalog.manifest ? catalog.manifest : {};
|
||||
const catalogVersion = manifest.version || '';
|
||||
const updateAvailable = catalogVersion && compareVersions(catalogVersion, PLUGIN.version) > 0;
|
||||
const statusText = catalog && catalog.ok ? 'Catalog reachable' : 'Catalog unavailable';
|
||||
const statusClass = catalog && catalog.ok ? 'ok' : 'warn';
|
||||
const updateText = updateAvailable ? `Update available: v${escapeHtml(catalogVersion)}` : 'Installed version is current for this catalog response.';
|
||||
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>${escapeHtml(PLUGIN.name)}</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 24px; color: #20242a; background: #f6f8fb; }
|
||||
.wrap { max-width: 920px; }
|
||||
.box { border: 1px solid #c8d0dc; border-radius: 6px; padding: 18px; background: #fff; margin-bottom: 14px; }
|
||||
h1 { margin: 0 0 10px; font-size: 22px; }
|
||||
h2 { margin: 0 0 10px; font-size: 16px; }
|
||||
p { line-height: 1.45; }
|
||||
code { background: #eef2f7; padding: 2px 5px; border-radius: 3px; }
|
||||
.grid { display: grid; grid-template-columns: 180px 1fr; gap: 8px 14px; }
|
||||
.label { color: #5e6a7d; font-weight: 700; }
|
||||
.ok { color: #087f5b; font-weight: 700; }
|
||||
.warn { color: #b45309; font-weight: 700; }
|
||||
a { color: #0f5e9c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="box">
|
||||
<h1>${escapeHtml(PLUGIN.name)}</h1>
|
||||
<p>This backend panel is served by the GracePress catalog-managed MeshCentral plugin package. Test update channel: 0.0.18.</p>
|
||||
<div class="grid">
|
||||
<div class="label">User</div><div>${escapeHtml(username)}</div>
|
||||
<div class="label">Short name</div><div><code>${escapeHtml(PLUGIN.shortName)}</code></div>
|
||||
<div class="label">Installed version</div><div><code>${escapeHtml(PLUGIN.version)}</code></div>
|
||||
<div class="label">Catalog status</div><div class="${statusClass}">${escapeHtml(statusText)}</div>
|
||||
<div class="label">Last check</div><div>${escapeHtml((catalog && catalog.checkedAt) || '')}</div>
|
||||
<div class="label">Update status</div><div>${escapeHtml(updateText)}</div>
|
||||
<div class="label">Plugin sync</div><div>${escapeHtml(renderSyncSummaryText())}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h2>GracePress Catalog Links</h2>
|
||||
<p><b>Manifest:</b> <a href="${escapeAttr(PLUGIN.manifestUrl)}" target="_blank" rel="noopener">${escapeHtml(PLUGIN.manifestUrl)}</a></p>
|
||||
<p><b>MeshCentral config:</b> <a href="${escapeAttr(PLUGIN.configUrl)}" target="_blank" rel="noopener">${escapeHtml(PLUGIN.configUrl)}</a></p>
|
||||
<p><b>Changelog:</b> <a href="${escapeAttr(PLUGIN.changelogUrl)}" target="_blank" rel="noopener">${escapeHtml(PLUGIN.changelogUrl)}</a></p>
|
||||
${catalog && catalog.error ? `<p class="warn"><b>Catalog error:</b> ${escapeHtml(catalog.error)}</p>` : ''}
|
||||
${renderSyncDetails()}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function renderSyncSummaryText() {
|
||||
if (syncState.running) return `Running since ${syncState.lastStarted}`;
|
||||
if (!syncState.lastFinished) return 'Waiting for MeshCentral startup sync.';
|
||||
if (syncState.ok) {
|
||||
return `Last sync ${syncState.lastFinished}: added ${syncState.added.length}, updated ${syncState.updated.length}, skipped ${syncState.skipped.length}, errors ${syncState.errors.length}.`;
|
||||
}
|
||||
return `Last sync failed ${syncState.lastFinished}: ${syncState.errors.join('; ')}`;
|
||||
}
|
||||
|
||||
function renderSyncDetails() {
|
||||
const lines = [];
|
||||
if (syncState.added.length) lines.push(`<p><b>Added as available:</b> ${escapeHtml(syncState.added.join(', '))}</p>`);
|
||||
if (syncState.updated.length) lines.push(`<p><b>Updated available rows:</b> ${escapeHtml(syncState.updated.join(', '))}</p>`);
|
||||
if (syncState.skipped.length) lines.push(`<p><b>Skipped:</b> ${escapeHtml(syncState.skipped.join(', '))}</p>`);
|
||||
if (syncState.errors.length) lines.push(`<p class="warn"><b>Sync errors:</b> ${escapeHtml(syncState.errors.join('; '))}</p>`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function compareVersions(a, b) {
|
||||
const left = String(a || '0').split('.').map(function(part) { return parseInt(part, 10) || 0; });
|
||||
const right = String(b || '0').split('.').map(function(part) { return parseInt(part, 10) || 0; });
|
||||
const length = Math.max(left.length, right.length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
const diff = (left[i] || 0) - (right[i] || 0);
|
||||
if (diff !== 0) return diff;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value).replace(/[&<>"']/g, function(c) {
|
||||
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c];
|
||||
});
|
||||
}
|
||||
|
||||
function escapeAttr(value) {
|
||||
return escapeHtml(value).replace(/`/g, '`');
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
# Vendor And Reference Code
|
||||
|
||||
This folder is for notes or untracked local clones used while developing MeshCentral integrations.
|
||||
|
||||
The full upstream MeshCentral source tree is intentionally not imported into this repository on the initial consolidation pass. If we need to maintain a real MeshCentral fork later, create that as a deliberate separate decision with a documented upstream remote and patch policy.
|
||||
@@ -0,0 +1,163 @@
|
||||
# GracePress: MeshPress Sync Changelog
|
||||
|
||||
## Current
|
||||
- Added a new-server onboarding wizard for bridge URL, token, public MeshCentral URL, advanced mode, and first health checks.
|
||||
- Added a connection health panel for WordPress-to-bridge, bridge-to-MeshCentral, public URL, fleet freshness, bridge push, and OmniLog event flow.
|
||||
- Added an advanced-mode gate for MeshCentral Ops and raw troubleshooting controls.
|
||||
- Added an agent enrollment check to confirm newly installed agents appear in the selected MeshCentral group.
|
||||
- Added a protected bridge event receiver so MeshPress Bridge lifecycle events can flow into OmniLog.
|
||||
|
||||
## 1.13.0 - 2026-06-12
|
||||
- Added a new-server onboarding wizard for bridge URL, token, public MeshCentral URL, advanced mode, and first health checks.
|
||||
- Added a connection health panel for WordPress-to-bridge, bridge-to-MeshCentral, public URL, fleet freshness, bridge push, and OmniLog event flow.
|
||||
- Added an advanced-mode gate for MeshCentral Ops and raw troubleshooting controls.
|
||||
- Added an agent enrollment check to confirm newly installed agents appear in the selected MeshCentral group.
|
||||
- Added a protected bridge event receiver so MeshPress Bridge lifecycle events can flow into OmniLog.
|
||||
|
||||
## 1.12.7 - 2026-05-25
|
||||
- Added separate controls for bridge maintenance and real MeshCentral maintenance mode.
|
||||
- Added redacted MeshCentral config audit output for safer copy/paste diagnostics.
|
||||
- Upgraded backups to request full MeshCentral snapshots, including MySQL/MariaDB dumps when configured.
|
||||
|
||||
## 1.12.6 - 2026-05-25
|
||||
- Rolled MeshPress admin back to the last known stable 1.12.1 layout after later rescue edits broke tabs, buttons, and fleet loading.
|
||||
- Preserved MeshCentral `@` characters in group/device identifiers before sending requests to the bridge.
|
||||
- Removed browser dark-mode override CSS that inverted MeshPress cards in light mode.
|
||||
- Restored explicit MeshCentral maintenance-mode enable/disable controls now that the bridge endpoint is stable again.
|
||||
- Simplified agent provisioning into one primary Linux install command plus a compact installer/link picker.
|
||||
- Added clearer Mesh ID diagnostics without forcing operators to manually paste or understand the raw MeshCentral group id.
|
||||
|
||||
## 1.12.5 - 2026-05-24
|
||||
- Rolled MeshPress admin back to the last known stable 1.12.1 layout after later rescue edits broke tabs, buttons, and fleet loading.
|
||||
- Preserved MeshCentral `@` characters in group/device identifiers before sending requests to the bridge.
|
||||
- Removed browser dark-mode override CSS that inverted MeshPress cards in light mode.
|
||||
|
||||
## 1.12.0 - 2026-05-24
|
||||
- Added a MeshCentral Ops tab for runtime/update checks, guarded backups, and agent uninstall diagnostics through the bridge.
|
||||
- Added an operator-only uninstall-agent test button to the device troubleshooting flow.
|
||||
- Added admin output panels designed for copyable diagnostics when bridge maintenance or MeshCentral commands fail.
|
||||
|
||||
## 1.11.9 - 2026-05-24
|
||||
- Improved the fleet overview with grouped device cards, search, group filtering, status filtering, and direct Desktop/Terminal/Files actions.
|
||||
- Expanded agent deployment output for Windows, macOS, Linux, Mesh Assistant, and copyable Linux install commands.
|
||||
- Added clearer provisioning guidance and bridge diagnostic output for group edit failures.
|
||||
|
||||
## 1.11.8 - 2026-05-24
|
||||
- Routed fleet Desktop/Console/Files buttons through bridge-generated session links instead of locally guessed URLs.
|
||||
- Preserved MeshCentral `$` node ids through AJAX sanitization and made `viewmode-first + gotonode` the tested primary path.
|
||||
- Added session diagnostics plus manual group edit, device rename, and device move controls for bridge testing.
|
||||
|
||||
## 1.11.7 - 2026-05-23
|
||||
- Added bridge runtime version/status visibility and manual update triggering from MeshPress admin.
|
||||
- Added a richer Desktop/Console/Files session link tester with multiple MeshCentral direct-link candidates.
|
||||
- Cleaned mojibake labels from fleet action buttons.
|
||||
|
||||
## 1.11.6 - 2026-05-23
|
||||
- Added Bridge Runtime Status controls for version checks and manual bridge updates.
|
||||
- Added support for bridge `/api/session-links` and console link candidate testing.
|
||||
- Renamed Terminal actions to Console in the UI to match the workflow being tested.
|
||||
|
||||
## 1.11.5 - 2026-05-23
|
||||
- Added a Diagnostics & Manual Actions tab for bridge probing and first-party MeshCentral validation.
|
||||
- Added manual MeshCentral group creation from MeshPress, with clear output and generated agent links.
|
||||
- Added read-only bridge probes for health, status, self-test, groups, devices, users, events, server info, and device info.
|
||||
- Added direct session link testing for Desktop, Terminal, and Files in both `gotonode` and `node` URL formats.
|
||||
|
||||
<!-- GP-RECONSTRUCTED-CHANGELOG:START -->
|
||||
|
||||
## Reconstructed History
|
||||
|
||||
These entries were inferred from local distribution archives, module headers, and `.old` backups. They are intentionally conservative and may not capture the full intent of each change.
|
||||
|
||||
### 1.11.4 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.11.4.zip` (2026-05-21).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.
|
||||
- Header advertised WordPress compatibility through 7.0.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.11.3 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.11.3.zip` (2026-05-20).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.11.2 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.11.2.zip` (2026-05-20).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.11.1 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.11.1.zip` (2026-05-20).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.11.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.11.0.zip` (2026-05-20).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.10.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.10.0.zip` (2026-05-20).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.9.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.9.0.zip` (2026-05-20).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.8.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.8.0.zip` (2026-05-20).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects WordPress to the GracePress MeshPress Bridge for MeshCentral fleet syncing and client/device workflows.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.7.1 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.7.1.zip` (2026-05-20).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects to the custom MeshCentral GracePress API plugin for flawless fleet syncing and management.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.7.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.7.0.zip` (2026-05-18).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Connects to the custom MeshCentral GracePress API plugin for flawless fleet syncing and management.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.5.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.5.0.zip` (2026-05-18).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Uses a secure Cookie-Based REST API bridge to sync your MeshCentral fleet into WordPress.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.4.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.4.0.zip` (2026-05-18).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Uses a native WebSocket payload engine to sync your MeshCentral fleet into WordPress.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.3.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.3.0.zip` (2026-05-18).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Uses a native WebSocket engine to sync your MeshCentral fleet into WordPress.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.1.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.1.0.zip` (2026-05-18).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Syncs your MeshCentral fleet into WordPress for instant remote desktops and API-driven automation.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
### 1.0.0 - reconstructed
|
||||
- Reconstructed from archive `mesh-press-v1.0.0.zip` (2026-05-18).
|
||||
- Header description at this version: Enterprise Remote Monitoring & Management (RMM) bridge. Syncs your MeshCentral fleet into WordPress for instant remote desktops and API-driven automation.
|
||||
- Catalog/category marker: Utility.
|
||||
- Header icon marker: 🌐.
|
||||
|
||||
<!-- GP-RECONSTRUCTED-CHANGELOG:END -->
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user