mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 09:19:42 +02:00
Merge #551: remove the broken tunnel upload page and deprecate /api/screenshots
This commit is contained in:
@@ -448,7 +448,6 @@
|
||||
'Remote Access': '远程访问',
|
||||
'Cloudflare Tunnel': 'Cloudflare 隧道',
|
||||
'Tunnel URL': '隧道地址',
|
||||
'Upload URL': '上传地址',
|
||||
Updates: '更新',
|
||||
'Current Version': '当前版本',
|
||||
'Check for Updates': '检查更新',
|
||||
|
||||
@@ -2992,10 +2992,6 @@
|
||||
<button class="btn-icon-sm" id="tunnelQrBtn" onclick="app.showTunnelQR()" title="Show QR code">⊞</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="set-row" id="tunnelUploadUrlRow" style="display:none">
|
||||
<div class="set-row-text"><span class="set-row-label">Upload URL</span></div>
|
||||
<span id="tunnelUploadUrlDisplay" class="set-copy" title="Click to copy"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1760,17 +1760,16 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
},
|
||||
|
||||
_updateTunnelUrlRow(rowId, displayId, url, suffix = '') {
|
||||
const row = document.getElementById(rowId);
|
||||
const display = document.getElementById(displayId);
|
||||
_updateTunnelUrlDisplay(url) {
|
||||
const row = document.getElementById('tunnelUrlRow');
|
||||
const display = document.getElementById('tunnelUrlDisplay');
|
||||
if (!row || !display) return;
|
||||
if (url) {
|
||||
const fullUrl = url + suffix;
|
||||
row.style.display = '';
|
||||
display.textContent = fullUrl;
|
||||
display.textContent = url;
|
||||
display.onclick = () => {
|
||||
navigator.clipboard.writeText(fullUrl).then(() => {
|
||||
this.showToast(`${suffix ? 'Upload' : 'Tunnel'} URL copied`, 'success');
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
this.showToast('Tunnel URL copied', 'success');
|
||||
});
|
||||
};
|
||||
} else {
|
||||
@@ -1780,11 +1779,6 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
},
|
||||
|
||||
_updateTunnelUrlDisplay(url) {
|
||||
this._updateTunnelUrlRow('tunnelUrlRow', 'tunnelUrlDisplay', url);
|
||||
this._updateTunnelUrlRow('tunnelUploadUrlRow', 'tunnelUploadUrlDisplay', url, '/upload.html');
|
||||
},
|
||||
|
||||
showTunnelQR() {
|
||||
// Close existing popup if open
|
||||
this.closeTunnelQR();
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<title>Upload Screenshot - Codeman</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
background: #0d1117; color: #c9d1d9;
|
||||
min-height: 100vh; min-height: 100dvh;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
.container { max-width: 480px; width: 100%; padding: 24px; }
|
||||
h1 { font-size: 1.4em; margin-bottom: 16px; color: #58a6ff; }
|
||||
.drop-zone {
|
||||
border: 2px dashed #30363d; border-radius: 12px;
|
||||
padding: 48px 20px; text-align: center;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
.drop-zone.active { border-color: #58a6ff; background: #161b22; }
|
||||
.drop-zone p { margin-bottom: 12px; font-size: 1.1em; }
|
||||
.drop-zone small { color: #8b949e; }
|
||||
input[type=file] { display: none; }
|
||||
.preview { margin-top: 16px; text-align: center; display: none; }
|
||||
.preview img { max-width: 100%; max-height: 300px; border-radius: 8px; border: 1px solid #30363d; }
|
||||
.preview .name { margin-top: 6px; font-size: 0.85em; color: #8b949e; word-break: break-all; }
|
||||
button {
|
||||
width: 100%; padding: 14px; margin-top: 16px;
|
||||
background: #238636; color: #fff; border: none;
|
||||
border-radius: 8px; font-size: 1.05em;
|
||||
font-weight: 600; -webkit-appearance: none;
|
||||
opacity: 0.4; pointer-events: none;
|
||||
}
|
||||
button.ready { opacity: 1; pointer-events: auto; }
|
||||
button:active { background: #2ea043; }
|
||||
.status { margin-top: 12px; padding: 10px; border-radius: 6px; text-align: center; display: none; }
|
||||
.status.ok { display: block; background: #1a3a2a; color: #3fb950; }
|
||||
.status.err { display: block; background: #3a1a1a; color: #f85149; }
|
||||
.files { margin-top: 24px; }
|
||||
.files h2 { font-size: 0.95em; color: #8b949e; margin-bottom: 8px; }
|
||||
.files a { display: block; color: #58a6ff; text-decoration: none; padding: 6px 0; font-size: 0.9em; word-break: break-all; }
|
||||
.files a:active { color: #79c0ff; }
|
||||
.back { display: inline-block; margin-bottom: 12px; color: #8b949e; text-decoration: none; font-size: 0.9em; }
|
||||
.back:active { color: #c9d1d9; }
|
||||
</style>
|
||||
</head><body>
|
||||
<div class="container">
|
||||
<a class="back" href="/">← Back to Codeman</a>
|
||||
<h1>Upload Screenshot</h1>
|
||||
<div class="drop-zone" id="drop">
|
||||
<p>Tap to select image</p>
|
||||
<small>PNG, JPG, WebP — up to 10 MB</small>
|
||||
</div>
|
||||
<input type="file" id="file" accept="image/*">
|
||||
<div class="preview" id="preview">
|
||||
<img id="previewImg" alt="Preview">
|
||||
<div class="name" id="previewName"></div>
|
||||
</div>
|
||||
<button id="btn">Upload</button>
|
||||
<div class="status" id="status"></div>
|
||||
<div class="files" id="files"></div>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
var drop = document.getElementById('drop');
|
||||
var fileInput = document.getElementById('file');
|
||||
var preview = document.getElementById('preview');
|
||||
var previewImg = document.getElementById('previewImg');
|
||||
var previewName = document.getElementById('previewName');
|
||||
var btn = document.getElementById('btn');
|
||||
var status = document.getElementById('status');
|
||||
var filesDiv = document.getElementById('files');
|
||||
var selectedFile = null;
|
||||
|
||||
drop.addEventListener('click', function() { fileInput.click(); });
|
||||
|
||||
fileInput.addEventListener('change', function() {
|
||||
if (fileInput.files && fileInput.files[0]) pick(fileInput.files[0]);
|
||||
});
|
||||
|
||||
// Drag-and-drop (desktop fallback)
|
||||
drop.addEventListener('dragover', function(e) { e.preventDefault(); drop.classList.add('active'); });
|
||||
drop.addEventListener('dragleave', function() { drop.classList.remove('active'); });
|
||||
drop.addEventListener('drop', function(e) {
|
||||
e.preventDefault(); drop.classList.remove('active');
|
||||
if (e.dataTransfer && e.dataTransfer.files[0]) pick(e.dataTransfer.files[0]);
|
||||
});
|
||||
|
||||
function pick(f) {
|
||||
selectedFile = f;
|
||||
previewImg.src = URL.createObjectURL(f);
|
||||
previewName.textContent = f.name + ' (' + (f.size / 1024).toFixed(0) + ' KB)';
|
||||
preview.style.display = 'block';
|
||||
btn.classList.add('ready');
|
||||
status.className = 'status';
|
||||
status.style.display = 'none';
|
||||
}
|
||||
|
||||
btn.addEventListener('click', function() {
|
||||
if (!selectedFile) return;
|
||||
btn.classList.remove('ready');
|
||||
btn.textContent = 'Uploading\u2026';
|
||||
var form = new FormData();
|
||||
form.append('file', selectedFile);
|
||||
fetch('/api/screenshots', { method: 'POST', body: form })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(j) {
|
||||
if (j.success) {
|
||||
status.className = 'status ok';
|
||||
status.textContent = 'Saved: ' + j.filename;
|
||||
status.style.display = 'block';
|
||||
selectedFile = null;
|
||||
preview.style.display = 'none';
|
||||
btn.textContent = 'Upload';
|
||||
loadFiles();
|
||||
} else {
|
||||
status.className = 'status err';
|
||||
status.textContent = j.error || 'Upload failed';
|
||||
status.style.display = 'block';
|
||||
btn.classList.add('ready');
|
||||
btn.textContent = 'Upload';
|
||||
}
|
||||
})
|
||||
.catch(function(e) {
|
||||
status.className = 'status err';
|
||||
status.textContent = e.message;
|
||||
status.style.display = 'block';
|
||||
btn.classList.add('ready');
|
||||
btn.textContent = 'Upload';
|
||||
});
|
||||
});
|
||||
|
||||
function loadFiles() {
|
||||
fetch('/api/screenshots')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(j) {
|
||||
if (j.files && j.files.length) {
|
||||
var html = '<h2>Recent uploads</h2>';
|
||||
j.files.forEach(function(f) {
|
||||
html += '<a href="/api/screenshots/' + encodeURIComponent(f.name) + '" target="_blank">' + f.name + '</a>';
|
||||
});
|
||||
filesDiv.innerHTML = html;
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
}
|
||||
|
||||
loadFiles();
|
||||
})();
|
||||
</script>
|
||||
</body></html>
|
||||
@@ -1295,8 +1295,20 @@ export function registerSystemRoutes(
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
// ========== Screenshots ==========
|
||||
// Deprecated (the upload page is gone): removed in a later MAJOR per
|
||||
// docs/versioning-policy.md. Warns once per process on first use.
|
||||
let screenshotsDeprecationWarned = false;
|
||||
const warnScreenshotsDeprecated = (): void => {
|
||||
if (screenshotsDeprecationWarned) return;
|
||||
screenshotsDeprecationWarned = true;
|
||||
console.warn(
|
||||
'[deprecated] /api/screenshots is deprecated and will be removed in a future major release. ' +
|
||||
'Use POST /api/sessions/:id/paste-image to hand a file to a session.'
|
||||
);
|
||||
};
|
||||
|
||||
app.post('/api/screenshots', async (req, reply) => {
|
||||
warnScreenshotsDeprecated();
|
||||
const contentType = req.headers['content-type'] ?? '';
|
||||
if (!contentType.includes('multipart/form-data')) {
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Expected multipart/form-data');
|
||||
@@ -1383,6 +1395,7 @@ export function registerSystemRoutes(
|
||||
});
|
||||
|
||||
app.get('/api/screenshots', async () => {
|
||||
warnScreenshotsDeprecated();
|
||||
if (!existsSync(SCREENSHOTS_DIR)) {
|
||||
return { files: [] };
|
||||
}
|
||||
@@ -1396,6 +1409,7 @@ export function registerSystemRoutes(
|
||||
});
|
||||
|
||||
app.get('/api/screenshots/:name', async (req, reply) => {
|
||||
warnScreenshotsDeprecated();
|
||||
const { name } = req.params as { name: string };
|
||||
// Prevent path traversal
|
||||
if (name.includes('/') || name.includes('\\') || name.includes('..')) {
|
||||
|
||||
+5
-8
@@ -932,7 +932,9 @@ export class WebServer extends EventEmitter {
|
||||
});
|
||||
|
||||
// Serve static files — content-hashed assets (e.g. app.a3f8c2e1.js) are immutable, cache aggressively.
|
||||
// HTML must revalidate every time so browsers pick up new hashed filenames after deploys.
|
||||
// HTML must revalidate every time so browsers pick up new hashed filenames after deploys, and every
|
||||
// HTML page has its own route that says so (/, /index.html, /session/:id). ⚠️ A new static .html
|
||||
// needs such a route too: this plugin would hand it a year of `immutable`.
|
||||
// cacheControl disabled so setHeaders owns Cache-Control for plain static assets.
|
||||
// preCompressed: serve pre-built .br/.gz files (from build step) to avoid per-request CPU compression
|
||||
await this.app.register(fastifyStatic, {
|
||||
@@ -944,7 +946,7 @@ export class WebServer extends EventEmitter {
|
||||
// `ServerResponse` to a `FastifyReply`, so it is `reply.header()` here and
|
||||
// NOT `res.setHeader()`. A v9-style body throws TypeError on every static
|
||||
// request, which is every page load. See the v10.0.0 release notes.
|
||||
setHeaders: (reply, path) => {
|
||||
setHeaders: (reply) => {
|
||||
// ⚠️ That same change ALSO flipped precedence, and silently. Under v9 this
|
||||
// callback wrote to the raw response and Fastify's staged reply headers then
|
||||
// overwrote it, so a route that set its own Cache-Control before .sendFile()
|
||||
@@ -953,12 +955,7 @@ export class WebServer extends EventEmitter {
|
||||
// no-store` its route asks for — a service worker that can never update.
|
||||
// So: a route that already decided keeps its answer.
|
||||
if (reply.getHeader('Cache-Control') !== undefined) return;
|
||||
// Use .includes() not .endsWith() — preCompressed serves .html.br/.html.gz
|
||||
if (path.includes('.html')) {
|
||||
reply.header('Cache-Control', 'no-cache');
|
||||
} else {
|
||||
reply.header('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
}
|
||||
reply.header('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user