mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 14:09:42 +02:00
chore: version packages
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -855,6 +855,17 @@ html.mobile-init .file-browser-panel {
|
||||
-webkit-tap-highlight-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Per-URL edit/delete in the Web / URL list need a real touch target, and they
|
||||
sit next to the row's own tap area, so they get sized up rather than relying
|
||||
on the 24px desktop hit box. */
|
||||
.run-mode-row-btn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
font-size: 1rem;
|
||||
-webkit-tap-highlight-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.run-mode-webview-delete { font-size: 1.2rem; }
|
||||
|
||||
.run-mode-history {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: manipulation;
|
||||
|
||||
@@ -12561,6 +12561,44 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
|
||||
|
||||
.run-mode-dot.web { background: #38bdf8; }
|
||||
.run-mode-webviews { max-height: 180px; overflow-y: auto; }
|
||||
|
||||
/* A saved URL is a ROW: open on the left, edit + delete on the right, so a URL can
|
||||
be changed or removed without first opening it as a tab. The side buttons stay
|
||||
permanently visible rather than hover-revealed, because this menu is used on
|
||||
touch devices where there is no hover to reveal them with. */
|
||||
.run-mode-row--web {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
.run-mode-row--web .run-mode-option--web {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.run-mode-row-btn {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: var(--btn-radius);
|
||||
color: var(--text-dim);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.run-mode-row-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
color: var(--text);
|
||||
}
|
||||
.run-mode-webview-delete { font-size: 1.05rem; }
|
||||
.run-mode-webview-delete:hover { color: var(--danger, #ef4444); }
|
||||
.run-mode-empty {
|
||||
padding: 4px 10px 6px;
|
||||
font-size: 0.75em;
|
||||
|
||||
@@ -290,7 +290,13 @@ Object.assign(CodemanApp.prototype, {
|
||||
|
||||
// ── Run-menu entries ──────────────────────────────────────────────────────
|
||||
|
||||
/** Saved dashboards listed inside the Run dropdown, under "Web / URL". */
|
||||
/**
|
||||
* Saved dashboards listed inside the Run dropdown, under "Web / URL".
|
||||
*
|
||||
* Each row carries its own edit and delete buttons. Without them the only way to
|
||||
* change or remove a saved URL was to open it as a tab first and go through the
|
||||
* tab's gear, which is a dead end for a URL you no longer want open at all.
|
||||
*/
|
||||
renderWebviewMenuItems() {
|
||||
const container = document.getElementById('runModeWebviews');
|
||||
if (!container) return;
|
||||
@@ -300,15 +306,20 @@ Object.assign(CodemanApp.prototype, {
|
||||
return;
|
||||
}
|
||||
container.innerHTML = list
|
||||
.map(
|
||||
(w) => `<button class="run-mode-option run-mode-option--web" onclick="app.openWebviewFromMenu(${escapeHtml(
|
||||
JSON.stringify(w.id)
|
||||
)})" title="${escapeHtml(w.url)}">
|
||||
<span class="run-mode-menu-icon">${w.icon ? escapeHtml(w.icon) : '<span class="run-mode-dot web"></span>'}</span>${escapeHtml(
|
||||
w.name
|
||||
)}
|
||||
</button>`
|
||||
)
|
||||
.map((w) => {
|
||||
const jsonId = escapeHtml(JSON.stringify(w.id));
|
||||
const name = escapeHtml(w.name);
|
||||
const icon = w.icon ? escapeHtml(w.icon) : '<span class="run-mode-dot web"></span>';
|
||||
return `<div class="run-mode-row run-mode-row--web">
|
||||
<button class="run-mode-option run-mode-option--web" onclick="app.openWebviewFromMenu(${jsonId})" title="${escapeHtml(w.url)}">
|
||||
<span class="run-mode-menu-icon">${icon}</span>${name}
|
||||
</button>
|
||||
<button class="run-mode-row-btn run-mode-webview-edit" onclick="event.stopPropagation(); app.showWebviewModal(${jsonId})"
|
||||
title="Edit URL" aria-label="Edit ${name}">⚙</button>
|
||||
<button class="run-mode-row-btn run-mode-webview-delete" onclick="event.stopPropagation(); app.deleteWebviewById(${jsonId})"
|
||||
title="Delete URL" aria-label="Delete ${name}">×</button>
|
||||
</div>`;
|
||||
})
|
||||
.join('');
|
||||
},
|
||||
|
||||
@@ -429,17 +440,36 @@ Object.assign(CodemanApp.prototype, {
|
||||
async deleteWebview() {
|
||||
const id = this._editingWebviewId;
|
||||
if (!id) return;
|
||||
if (await this._confirmAndDeleteWebview(id)) this.closeWebviewModal();
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete straight from a Run-dropdown row, without opening the editor first.
|
||||
*
|
||||
* The dropdown's outside-click handler closes the menu when the click target is
|
||||
* not inside it, and by the time the delete resolves this row is gone, so the
|
||||
* menu is re-asserted open: deleting one of several saved URLs should leave you
|
||||
* looking at the rest of the list.
|
||||
*/
|
||||
async deleteWebviewById(id) {
|
||||
if (!id) return;
|
||||
if (!(await this._confirmAndDeleteWebview(id))) return;
|
||||
document.getElementById('runModeMenu')?.classList.add('active');
|
||||
},
|
||||
|
||||
/** Shared by the row button and the editor modal. @returns true when deleted. */
|
||||
async _confirmAndDeleteWebview(id) {
|
||||
const webview = this.webviews.get(id);
|
||||
if (!confirm(`Delete "${webview?.name || id}"?`)) return;
|
||||
if (!confirm(`Delete "${webview?.name || id}"?`)) return false;
|
||||
const res = await this._apiDelete(`/api/webviews/${encodeURIComponent(id)}`);
|
||||
if (!res || !res.ok) {
|
||||
this.showToast?.('Could not delete URL', 'error');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
this._removeWebviewTab(id);
|
||||
this.webviews.delete(id);
|
||||
this.closeWebviewModal();
|
||||
this.renderWebviewMenuItems();
|
||||
this.renderSessionTabs();
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
+129
-1
@@ -372,9 +372,26 @@ export function buildDownstreamResponseHeaders(
|
||||
* whole class instead of trading security for it: the page never emits a
|
||||
* root-absolute request in the first place.
|
||||
*
|
||||
* ## Why the DOM sinks are patched too, not just fetch/XHR
|
||||
*
|
||||
* A dashboard that renders `container.innerHTML = '<img src="/api/hero?slug=x">'`
|
||||
* or `img.src = '/api/slide?n=01'` produces exactly the same root-absolute request,
|
||||
* and NONE of the other layers can reach it: `<base>` does not apply to
|
||||
* root-absolute URLs at all, and `rewriteHtml()` only ever sees the initial
|
||||
* document, not markup built later by page script. The visible symptom is very
|
||||
* specific and easy to misread: the dashboard's DATA loads (reads go through
|
||||
* `fetch`, which was already patched) while every IMAGE stays broken. So the same
|
||||
* `rw()` is applied to `innerHTML`/`outerHTML`/`insertAdjacentHTML`, to
|
||||
* `setAttribute`, and to the `src`/`href`/`srcset`/... property setters, with a
|
||||
* `MutationObserver` as a last net for any sink not patched above (that one costs a
|
||||
* wasted 404 per node, since the browser starts fetching on insert, so it is a net
|
||||
* and not the mechanism).
|
||||
*
|
||||
* Runs before any page script because it is injected immediately after `<base>`.
|
||||
* Only same-origin, non-prefixed, root-absolute URLs are touched; relative URLs
|
||||
* (already handled by `<base>`) and cross-origin URLs are passed through.
|
||||
* (already handled by `<base>`) and cross-origin URLs are passed through. Every
|
||||
* rewrite is idempotent, so a value that passes through two layers is unchanged by
|
||||
* the second.
|
||||
*/
|
||||
export function runtimeUrlShim(prefix: string): string {
|
||||
// Kept dependency-free and defensive: it runs inside a page we do not control,
|
||||
@@ -420,6 +437,117 @@ if(window.XMLHttpRequest&&XMLHttpRequest.prototype.open){
|
||||
['CONNECTING','OPEN','CLOSING','CLOSED'].forEach(function(s){if(s in C)W[s]=C[s];});
|
||||
window[k]=W;
|
||||
});
|
||||
var A=['src','href','action','poster','data','formaction','srcset'];
|
||||
function rwSet(v){
|
||||
try{
|
||||
return String(v).split(',').map(function(p){
|
||||
var t=p.trim();if(!t)return t;
|
||||
var i=t.search(/\\s/);
|
||||
return i===-1?rw(t):rw(t.slice(0,i))+t.slice(i);
|
||||
}).join(', ');
|
||||
}catch(e){return v;}
|
||||
}
|
||||
function rwAttr(n,v){
|
||||
try{
|
||||
if(v==null)return v;
|
||||
var k=String(n).toLowerCase();
|
||||
if(k==='srcset')return rwSet(v);
|
||||
return A.indexOf(k)===-1?v:rw(v);
|
||||
}catch(e){return v;}
|
||||
}
|
||||
// Each value goes through rw() rather than a blind prefix concat, because unlike
|
||||
// the server-side rewriteHtml() this runs on markup that may ALREADY be proxied
|
||||
// (a page re-injecting its own outerHTML), and rw() is the idempotent one.
|
||||
function rwHtml(s){
|
||||
try{
|
||||
if(typeof s!=='string')return s;
|
||||
return s
|
||||
.replace(/(\\s(?:src|href|action|poster|formaction|data)\\s*=\\s*")([^"]*)(")/gi,function(m,a,v,q){return a+rw(v)+q;})
|
||||
.replace(/(\\s(?:src|href|action|poster|formaction|data)\\s*=\\s*')([^']*)(')/gi,function(m,a,v,q){return a+rw(v)+q;})
|
||||
.replace(/(\\ssrcset\\s*=\\s*")([^"]*)(")/gi,function(m,a,v,q){return a+rwSet(v)+q;})
|
||||
.replace(/(\\ssrcset\\s*=\\s*')([^']*)(')/gi,function(m,a,v,q){return a+rwSet(v)+q;});
|
||||
}catch(e){return s;}
|
||||
}
|
||||
// Marked with __cmrw so a double injection (a page that re-runs the shim) cannot
|
||||
// wrap an already-wrapped setter and rewrite twice.
|
||||
function patchProp(C,prop,conv){
|
||||
try{
|
||||
if(!C||!C.prototype)return;
|
||||
var d=Object.getOwnPropertyDescriptor(C.prototype,prop);
|
||||
if(!d||!d.set||d.set.__cmrw)return;
|
||||
var s=d.set;
|
||||
var ns=function(v){var w=v;try{w=conv(v);}catch(e){}return s.call(this,w);};
|
||||
ns.__cmrw=1;
|
||||
Object.defineProperty(C.prototype,prop,{get:d.get,set:ns,configurable:true,enumerable:d.enumerable});
|
||||
}catch(e){}
|
||||
}
|
||||
function patchHtmlProp(O,prop){
|
||||
try{
|
||||
if(!O)return;
|
||||
var d=Object.getOwnPropertyDescriptor(O,prop);
|
||||
if(!d||!d.set||d.set.__cmrw)return;
|
||||
var s=d.set;
|
||||
var ns=function(v){return s.call(this,rwHtml(v));};
|
||||
ns.__cmrw=1;
|
||||
Object.defineProperty(O,prop,{get:d.get,set:ns,configurable:true,enumerable:d.enumerable});
|
||||
}catch(e){}
|
||||
}
|
||||
function patchFn(O,name,wrap){
|
||||
try{
|
||||
var f=O&&O[name];
|
||||
if(typeof f!=='function'||f.__cmrw)return;
|
||||
var nf=wrap(f);nf.__cmrw=1;O[name]=nf;
|
||||
}catch(e){}
|
||||
}
|
||||
[['HTMLImageElement','src'],['HTMLImageElement','srcset'],['HTMLSourceElement','src'],
|
||||
['HTMLSourceElement','srcset'],['HTMLMediaElement','src'],['HTMLVideoElement','poster'],
|
||||
['HTMLScriptElement','src'],['HTMLIFrameElement','src'],['HTMLEmbedElement','src'],
|
||||
['HTMLTrackElement','src'],['HTMLLinkElement','href'],['HTMLAnchorElement','href'],
|
||||
['HTMLAreaElement','href'],['HTMLObjectElement','data'],['HTMLFormElement','action']
|
||||
].forEach(function(p){patchProp(window[p[0]],p[1],p[1]==='srcset'?rwSet:rw);});
|
||||
var EP=window.Element&&window.Element.prototype;
|
||||
patchHtmlProp(EP,'innerHTML');
|
||||
patchHtmlProp(EP,'outerHTML');
|
||||
patchHtmlProp(window.ShadowRoot&&window.ShadowRoot.prototype,'innerHTML');
|
||||
patchFn(EP,'insertAdjacentHTML',function(f){return function(p,h){return f.call(this,p,rwHtml(h));};});
|
||||
patchFn(EP,'setAttribute',function(f){return function(n,v){return f.call(this,n,rwAttr(n,v));};});
|
||||
patchFn(EP,'setAttributeNS',function(f){return function(ns,n,v){
|
||||
var k=String(n==null?'':n),i=k.indexOf(':');
|
||||
return f.call(this,ns,n,rwAttr(i===-1?k:k.slice(i+1),v));
|
||||
};});
|
||||
// Last net: anything inserted by a sink not patched above still gets corrected.
|
||||
// setAttribute below is the patched one, so this stays idempotent and terminates.
|
||||
try{
|
||||
var doc=window.document,MO=window.MutationObserver;
|
||||
if(MO&&doc&&doc.documentElement){
|
||||
var fix=function(el){
|
||||
try{
|
||||
if(!el||el.nodeType!==1||!el.hasAttribute)return;
|
||||
for(var i=0;i<A.length;i++){
|
||||
var n=A[i];if(!el.hasAttribute(n))continue;
|
||||
var c=el.getAttribute(n),x=rwAttr(n,c);
|
||||
if(x!=null&&x!==c)el.setAttribute(n,x);
|
||||
}
|
||||
}catch(e){}
|
||||
};
|
||||
var scan=function(node){
|
||||
try{
|
||||
fix(node);
|
||||
if(node&&node.querySelectorAll){
|
||||
var l=node.querySelectorAll('[src],[href],[action],[poster],[data],[srcset],[formaction]');
|
||||
for(var i=0;i<l.length;i++)fix(l[i]);
|
||||
}
|
||||
}catch(e){}
|
||||
};
|
||||
new MO(function(ms){
|
||||
for(var i=0;i<ms.length;i++){
|
||||
var m=ms[i];
|
||||
if(m.type==='attributes')fix(m.target);
|
||||
else for(var j=0;j<m.addedNodes.length;j++)scan(m.addedNodes[j]);
|
||||
}
|
||||
}).observe(doc.documentElement,{subtree:true,childList:true,attributes:true,attributeFilter:A});
|
||||
}
|
||||
}catch(e){}
|
||||
}catch(e){}})();</script>`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user