Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72b8d60985 | |||
| 917fb26e05 | |||
| a28867b398 | |||
| ac71ea2c49 | |||
| efa1750cac | |||
| aa6e28d818 | |||
| a0381b12cc | |||
| 64eacb28d2 | |||
| ea671535fc |
@@ -539,6 +539,7 @@ function _renderJobConfigFields(key, cfg) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
if (key === 'patchmon_sync' || key === 'semaphore_sync') {
|
if (key === 'patchmon_sync' || key === 'semaphore_sync') {
|
||||||
const label = key === 'semaphore_sync' ? 'API Token (Bearer)' : 'API Token (Basic)';
|
const label = key === 'semaphore_sync' ? 'API Token (Bearer)' : 'API Token (Basic)';
|
||||||
|
const tokenPlaceholder = key === 'patchmon_sync' ? 'token_key:token_secret' : '';
|
||||||
return `
|
return `
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="job-cfg-api-url">API URL</label>
|
<label class="form-label" for="job-cfg-api-url">API URL</label>
|
||||||
@@ -548,6 +549,7 @@ function _renderJobConfigFields(key, cfg) {
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="job-cfg-api-token">${label}</label>
|
<label class="form-label" for="job-cfg-api-token">${label}</label>
|
||||||
<input class="form-input" id="job-cfg-api-token" type="password"
|
<input class="form-input" id="job-cfg-api-token" type="password"
|
||||||
|
placeholder="${tokenPlaceholder}"
|
||||||
value="${esc(cfg.api_token ?? '')}">
|
value="${esc(cfg.api_token ?? '')}">
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
const VERSION = "1.5.0";
|
const VERSION = "1.7.1";
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "catalyst",
|
"name": "catalyst",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server/server.js",
|
"start": "node server/server.js",
|
||||||
|
|||||||
+7
-1
@@ -41,8 +41,14 @@ async function patchmonSyncHandler(cfg) {
|
|||||||
const { api_url, api_token } = cfg;
|
const { api_url, api_token } = cfg;
|
||||||
if (!api_url || !api_token) throw new Error('Patchmon not configured — set API URL and token');
|
if (!api_url || !api_token) throw new Error('Patchmon not configured — set API URL and token');
|
||||||
|
|
||||||
|
// Accept raw "key:secret" (recommended) or a pre-encoded base64 string.
|
||||||
|
// ":" cannot appear in a valid base64 string, so it's a reliable discriminator.
|
||||||
|
const credential = api_token.includes(':')
|
||||||
|
? Buffer.from(api_token).toString('base64')
|
||||||
|
: api_token;
|
||||||
|
|
||||||
const res = await fetch(api_url, {
|
const res = await fetch(api_url, {
|
||||||
headers: { Authorization: `Basic ${api_token}` },
|
headers: { Authorization: `Basic ${credential}` },
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`Patchmon API ${res.status}`);
|
if (!res.ok) throw new Error(`Patchmon API ${res.status}`);
|
||||||
|
|
||||||
|
|||||||
+7
-3
@@ -112,13 +112,17 @@ router.post('/instances', (req, res) => {
|
|||||||
router.put('/instances/:vmid', (req, res) => {
|
router.put('/instances/:vmid', (req, res) => {
|
||||||
const vmid = parseInt(req.params.vmid, 10);
|
const vmid = parseInt(req.params.vmid, 10);
|
||||||
if (!vmid) return res.status(400).json({ error: 'invalid vmid' });
|
if (!vmid) return res.status(400).json({ error: 'invalid vmid' });
|
||||||
if (!getInstance(vmid)) return res.status(404).json({ error: 'instance not found' });
|
|
||||||
|
|
||||||
const errors = validate(req.body);
|
const existing = getInstance(vmid);
|
||||||
|
if (!existing) return res.status(404).json({ error: 'instance not found' });
|
||||||
|
|
||||||
|
const merged = { ...existing, ...(req.body ?? {}) };
|
||||||
|
|
||||||
|
const errors = validate(merged);
|
||||||
if (errors.length) return res.status(400).json({ errors });
|
if (errors.length) return res.status(400).json({ errors });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = normalise(req.body);
|
const data = normalise(merged);
|
||||||
updateInstance(vmid, data);
|
updateInstance(vmid, data);
|
||||||
res.json(getInstance(data.vmid));
|
res.json(getInstance(data.vmid));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -257,6 +257,18 @@ describe('PUT /api/instances/:vmid', () => {
|
|||||||
const res = await request(app).put('/api/instances/100').send({ ...base, vmid: 200 })
|
const res = await request(app).put('/api/instances/100').send({ ...base, vmid: 200 })
|
||||||
expect(res.status).toBe(409)
|
expect(res.status).toBe(409)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('accepts a partial body and preserves unspecified fields', async () => {
|
||||||
|
await request(app).post('/api/instances').send({ ...base, atlas: 1, tailscale_ip: '100.64.0.1' })
|
||||||
|
const res = await request(app).put('/api/instances/100').send({ state: 'degraded' })
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(res.body.state).toBe('degraded')
|
||||||
|
expect(res.body.name).toBe(base.name)
|
||||||
|
expect(res.body.vmid).toBe(100)
|
||||||
|
expect(res.body.stack).toBe(base.stack)
|
||||||
|
expect(res.body.atlas).toBe(1)
|
||||||
|
expect(res.body.tailscale_ip).toBe('100.64.0.1')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// ── DELETE /api/instances/:vmid ───────────────────────────────────────────────
|
// ── DELETE /api/instances/:vmid ───────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user