fix: mount openclaw routes at /openclaw prefix + fix docker.client wrapper + strip duplicate /apps/ paths across sub-routers

- openClawRoutes was mounted at root causing /status vs /openclaw/status mismatch
- ctx.docker is a typed wrapper {client,pull,...} — all calls now use docker.client.*
- templates/deploy/removal/restore sub-routers had /apps/ hardcoded in inner routes
  causing double-stacking when mounted under /apps (→ /apps/apps/templates etc)
- openclaw.js: GET /status, POST /deploy, GET/POST /proxy/*, DELETE /
This commit is contained in:
Hermes
2026-05-27 22:20:21 -07:00
parent 17edb3bc90
commit e07375f642
7 changed files with 300 additions and 16 deletions
+2 -2
View File
@@ -227,7 +227,7 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
}
// Check for existing container before deployment
router.post('/apps/check-existing', asyncHandler(async (req, res) => {
router.post('/check-existing', asyncHandler(async (req, res) => {
const { appId } = req.body;
const template = ctx.APP_TEMPLATES[appId];
if (!template) throw new ValidationError('Invalid app template');
@@ -240,7 +240,7 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
}, 'check-existing'));
// Deploy new app
router.post('/apps/deploy', asyncHandler(async (req, res) => {
router.post('/deploy', asyncHandler(async (req, res) => {
const { appId, config } = req.body;
if (!appId || typeof appId !== 'string') {
throw new ValidationError('appId is required');
+15 -5
View File
@@ -45,11 +45,21 @@ module.exports = function(ctx) {
// Mount sub-routes — pass full ctx so sub-routes can reference ctx.* properties
const subCtx = Object.assign({}, ctx, { helpers });
router.use(initDeploy(subCtx));
router.use(initRemoval(subCtx));
router.use(initTemplates(subCtx));
router.use(initRestore(subCtx));
router.use(initCompose(subCtx));
try { router.use('/deploy', initDeploy(subCtx)); }
catch(e) { (ctx.log || console).error('[apps] deploy routes init failed:', e.message); }
try { router.use('/remove', initRemoval(subCtx)); }
catch(e) { (ctx.log || console).error('[apps] removal routes init failed:', e.message); }
try { router.use('/apps', initTemplates(subCtx)); }
catch(e) { (ctx.log || console).error('[apps] templates routes init failed:', e.message); }
try { router.use('/restore', initRestore(subCtx)); }
catch(e) { (ctx.log || console).error('[apps] restore routes init failed:', e.message); }
try { router.use('/compose', initCompose(subCtx)); }
catch(e) { (ctx.log || console).error('[apps] compose routes init failed:', e.message); }
return router;
};
+1 -1
View File
@@ -35,7 +35,7 @@ module.exports = function({
* @param {Function} deps.safeErrorMessage - Safe error message formatter
* @returns {express.Router}
*/
router.delete('/apps/:appId', asyncHandler(async (req, res) => {
router.delete('/:appId', asyncHandler(async (req, res) => {
const { appId } = req.params;
const { containerId, subdomain, ip, deleteContainer } = req.query;
const shouldDeleteContainer = deleteContainer === 'true';
+3 -3
View File
@@ -30,7 +30,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
* Pulls image, creates container, starts it, recreates Caddy config.
* Skips if container is already running.
*/
router.post('/apps/:appId/restore', asyncHandler(async (req, res) => {
router.post('/:appId/restore', asyncHandler(async (req, res) => {
const { appId } = req.params;
const services = await servicesStateManager.read();
const service = services.find(s => s.id === appId);
@@ -50,7 +50,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
* Restore all services that have deployment manifests.
* Returns per-service results.
*/
router.post('/apps/restore-all', asyncHandler(async (req, res) => {
router.post('/restore-all', asyncHandler(async (req, res) => {
const services = await servicesStateManager.read();
const restoreable = services.filter(s => s.deploymentManifest);
@@ -91,7 +91,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
/**
* List all services and their restore status.
*/
router.get('/apps/restore-status', asyncHandler(async (req, res) => {
router.get('/restore-status', asyncHandler(async (req, res) => {
const services = await servicesStateManager.read();
const status = [];
+5 -5
View File
@@ -41,7 +41,7 @@ module.exports = function({
};
// Get available app templates
router.get('/apps/templates', asyncHandler(async (req, res) => {
router.get('/templates', asyncHandler(async (req, res) => {
res.json({
success: true,
templates: ctx.APP_TEMPLATES,
@@ -51,7 +51,7 @@ module.exports = function({
}, 'apps-templates'));
// Get specific app template
router.get('/apps/templates/:appId', asyncHandler(async (req, res) => {
router.get('/templates/:appId', asyncHandler(async (req, res) => {
const { appId } = req.params;
const template = ctx.APP_TEMPLATES[appId];
if (!template) {
@@ -62,7 +62,7 @@ module.exports = function({
}, 'apps-template-detail'));
// Check port availability
router.get('/apps/ports/:port/check', asyncHandler(async (req, res) => {
router.get('/ports/:port/check', asyncHandler(async (req, res) => {
const port = req.params.port;
const conflicts = await helpers.checkPortConflicts([port]);
if (conflicts.length > 0) {
@@ -74,7 +74,7 @@ module.exports = function({
}, 'check-port'));
// Get suggested available port
router.get('/apps/ports/:basePort/suggest', asyncHandler(async (req, res) => {
router.get('/ports/:basePort/suggest', asyncHandler(async (req, res) => {
const basePort = parseInt(req.params.basePort) || 8080;
const maxAttempts = 100;
const usedPorts = await docker.getUsedPorts();
@@ -88,7 +88,7 @@ module.exports = function({
}, 'suggest-port'));
// Update subdomain for deployed app
router.post('/apps/update-subdomain', asyncHandler(async (req, res) => {
router.post('/update-subdomain', asyncHandler(async (req, res) => {
const { serviceId, oldSubdomain, newSubdomain, containerId, ip } = req.body;
const { ValidationError } = require('../../errors');