{"record":{"id":"8e558178f1312fbe","repo":"Crosstalk-Solutions/project-nomad","slug":"only-custom-apps-can-be-updated-this-way","errorCode":null,"errorMessage":"Only custom apps can be updated this way.","messagePattern":"Only custom apps can be updated this way\\.","errorType":"http","errorClass":null,"httpStatus":403,"severity":"warning","filePath":"admin/app/controllers/system_controller.ts","lineNumber":541,"sourceCode":"            })\n        }\n\n        service.custom_url = normalized\n        await service.save()\n\n        return response.send({ success: true, custom_url: service.custom_url })\n    }\n\n    /** Re-pull a custom app's image and recreate its container in place (preserving volumes). */\n    async updateCustomApp_pullLatest({ request, response }: HttpContext) {\n        const payload = await request.validateUsing(installServiceValidator)\n\n        const service = await Service.query().where('service_name', payload.service_name).first()\n        if (!service) {\n            return response.status(404).send({ success: false, message: `Service ${payload.service_name} not found` })\n        }\n        if (!service.is_custom) {\n            return response.status(403).send({ success: false, message: 'Only custom apps can be updated this way.' })\n        }\n\n        const result = await this.dockerService.recreateCustomAppContainer(payload.service_name, {\n            forcePull: true,\n        })\n        if (result.success) {\n            return response.send({ success: true, message: result.message })\n        }\n        return response.status(400).send({ success: false, message: result.message })\n    }\n\n    /** Return the last N lines of a service container's logs. */\n    async getServiceLogs({ params, request, response }: HttpContext) {\n        // Scope to managed services only — otherwise any sibling container's logs (admin app,\n        // database) would be readable by name on this unauthenticated API surface.\n        const service = await Service.query().where('service_name', params.name).first()\n        if (!service) {\n            return response.status(404).send({ success: false, message: `Service ${params.name} not found` })","sourceCodeStart":523,"sourceCodeEnd":559,"githubUrl":"https://github.com/Crosstalk-Solutions/project-nomad/blob/0bd1c6f4f9888d577fe232de06ac144bb8337131/admin/app/controllers/system_controller.ts#L523-L559","documentation":"HTTP 403 returned by the admin API when updateCustomApp_pullLatest is called for a service whose is_custom flag is false. The endpoint only recreates containers of user-created custom apps; curated or dependency services must be edited via updateCustomApp instead.","triggerScenarios":"POST to the custom-app pull-latest endpoint with a service_name that exists in the services table but was seeded/curated (is_custom = false), e.g. pulling latest for a pre-configured app.","commonSituations":"Calling the wrong update endpoint for a curated app; frontend routing both custom and curated apps to the pull-latest action; service rows seeded without the custom flag.","solutions":["Check Service.is_custom before calling and route curated apps to updateCustomApp","If the app should be custom, fix the service record's is_custom flag","Ensure the UI disables 'pull latest' for non-custom apps"],"exampleFix":"// before\nawait fetch(`/api/custom-apps/${name}/pull-latest`, { method: 'POST' })\n// after\nif (app.is_custom) {\n  await fetch(`/api/custom-apps/${name}/pull-latest`, { method: 'POST' })\n} else {\n  await fetch('/api/custom-apps', { method: 'PUT', body: JSON.stringify(app) })\n}","handlingStrategy":"validation","validationCode":"const svc = await api.getService(name)\nif (!svc?.is_custom) throw new Error('Use updateCustomApp for non-custom apps')","typeGuard":"const isCustomApp = (s: Service | null): s is Service & { is_custom: true } => !!s?.is_custom","tryCatchPattern":null,"preventionTips":["Check is_custom before choosing endpoint","Route curated apps to updateCustomApp"],"tags":["http-403","custom-apps","docker"],"backgroundTag":"http-403-forbidden","analyzedSha":"0bd1c6f4f9888d577fe232de06ac144bb8337131","analyzedAt":"2026-08-27T05:34:15.424Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}