Add GE-Enforce export-to-share + fleet-compliance UI (Milestone 1 UX)
Rounds out the Milestone 1 admin experience: author + publish in shopdb, push to the share by a button, and see what the fleet actually did. Export to share: - GET/PUT /api/geenforce/config stores the on-share export root (Setting geenforce_share_root); POST /scopes/<id>/export-share writes the current published JSON to <shareroot>/<scope>/manifest.json (preinstall.json for the preinstall phase), backing up the existing file to _meta/history first. geenforce.publish gated. The engine and PCs are untouched - this is the safe Milestone 1 push whose rollback is restoring the history backup. - Editor: a share-root config row + an "Export to Share" button per scope. - 3 tests (config roundtrip, export writes the file, second export backs up). Fleet-compliance UI (Settings > Enforcement Reports): - New page over GET /reports + /reports/<id>: latest report per PC with received (applied vs latest published version), status (ok/selfhealed/failed), and install/skip/fail counts; row detail shows per-entry outcomes with self-heal flags, exit codes, and messages. Hostname/PC-type filters. - ADR-010 settings card + ADR-009 plugin-gated route. Full suite 883 green; frontend build + naming green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,9 +16,11 @@ from flask_jwt_extended import jwt_required
|
||||
|
||||
from shopdb.api import (
|
||||
db, success_response, error_response, ErrorCodes, require_permission,
|
||||
service_token_authorized,
|
||||
service_token_authorized, Setting,
|
||||
)
|
||||
|
||||
SHAREROOT_SETTING = 'geenforce_share_root'
|
||||
|
||||
from ..models import (
|
||||
ManifestScope, ManifestEntry, ManifestPublishedVersion,
|
||||
ManifestEnforcementReport, ENTRY_TYPES, PHASES,
|
||||
@@ -421,6 +423,49 @@ def get_version(scopeid, versionnumber):
|
||||
'manifest': _json.loads(version.manifestjson)})
|
||||
|
||||
|
||||
@geenforce_bp.route('/config', methods=['GET'])
|
||||
@jwt_required()
|
||||
@require_permission('geenforce.manage')
|
||||
def get_config():
|
||||
"""Plugin config: the on-share export root (for export-to-share)."""
|
||||
setting = Setting.query.filter_by(key=SHAREROOT_SETTING).first()
|
||||
return success_response({'shareroot': setting.value if setting else ''})
|
||||
|
||||
|
||||
@geenforce_bp.route('/config', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('geenforce.publish')
|
||||
def put_config():
|
||||
payload = request.get_json(silent=True) or {}
|
||||
Setting.set(SHAREROOT_SETTING, (payload.get('shareroot') or '').strip(),
|
||||
valuetype='string', category='geenforce',
|
||||
description='On-share export root for GE-Enforce manifests')
|
||||
db.session.commit()
|
||||
return success_response({'shareroot': (payload.get('shareroot') or '').strip()})
|
||||
|
||||
|
||||
@geenforce_bp.route('/scopes/<int:scopeid>/export-share', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('geenforce.publish')
|
||||
def export_share_route(scopeid):
|
||||
"""Write the scope's current published JSON to the configured share root
|
||||
(backing up the old file to _meta/history first)."""
|
||||
scope = db.session.get(ManifestScope, scopeid)
|
||||
if not scope:
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'No such scope', http_code=404)
|
||||
setting = Setting.query.filter_by(key=SHAREROOT_SETTING).first()
|
||||
shareroot = setting.value if setting else ''
|
||||
if not shareroot:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
'Configure the share root first', http_code=400)
|
||||
try:
|
||||
path = service.export_scope_to_share(scope.scopename, scope.phase,
|
||||
shareroot)
|
||||
except (ValueError, OSError) as exc:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, str(exc), http_code=400)
|
||||
return success_response({'path': path})
|
||||
|
||||
|
||||
@geenforce_bp.route('/scopes/<int:scopeid>/rollback', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('geenforce.publish')
|
||||
|
||||
Reference in New Issue
Block a user