Create MeshCentral workspace

This commit is contained in:
2026-07-11 10:44:42 -05:00
commit 37a8586d34
34 changed files with 6930 additions and 0 deletions
+19
View File
@@ -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
+44
View File
@@ -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.
+109
View File
@@ -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 -->
+184
View File
@@ -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.
+117
View File
@@ -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"
+7
View File
@@ -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" "$@"
+15
View File
@@ -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
+109
View File
@@ -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"
+19
View File
@@ -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.
+57
View File
@@ -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
View File
@@ -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"
}
+26
View File
@@ -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
View File
@@ -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"
}
]
+91
View File
@@ -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.
+57
View File
@@ -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);
}
+29
View File
@@ -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
View File
@@ -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"
}
}
+98
View File
@@ -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"
}
]
+163
View File
@@ -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 -->
+17
View File
@@ -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
View File
@@ -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 -->
+17
View File
@@ -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"
}
+26
View File
@@ -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 ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
});
}
function escapeAttr(value) {
return escapeHtml(value).replace(/`/g, '&#96;');
}
@@ -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 ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
});
}
function escapeAttr(value) {
return escapeHtml(value).replace(/`/g, '&#96;');
}
+5
View File
@@ -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.
+163
View File
@@ -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