feat(frontend): "Auf den Server legen" im Dokumenten-Menue (D76-Nachtrag 8)

Bis hierher kam ein Plan nur per curl auf den Server — der Menueintrag war im
Konzept vorgesehen und fehlte. Jetzt legt der Knopf das aktive Dokument an,
schaltet dorthin um und schreibt den Link in die Adresszeile und in die
Zwischenablage. Die Adresszeile IST der Link: dort sucht man ihn, und ein
Neuladen fuehrt ins selbe Dokument zurueck.

Die Basis-Adresse ist die eigene Herkunft — produktiv liegt das Backend hinter
derselben Domain, wer nichts konfiguriert bekommt also das Richtige. Darueber
liegen ?server= (Entwicklung) und die Adresse des offenen Server-Dokuments.
Traegt nichts, wird gefragt.

Das lokale Dokument bleibt: Wer sein einziges Exemplar einem Server
anvertraut, soll es nicht im selben Zug verlieren. Dafuer nennt der Waehler
jetzt den Host neben Server-Dokumenten — im Test standen sonst zwei Eintraege
"Nur lokal" da, unterscheidbar nur am Tooltip. Und der Knopf verschwindet bei
Dokumenten, die schon auf einem Server liegen.

Geprueft im Browser gegen ein echtes Backend: Knopf sichtbar bei einem lokalen
Dokument, nach dem Klick steht ?live=... in der Adresszeile, das lokale
Dokument ist noch da, der Knopf verborgen — und eine getippte Zeile erreicht
den Server als Version 2.

523 Frontend-Tests (5 neue fuer serverBase/documentsUrl).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-26 19:39:28 +02:00
co-authored by Claude Opus 5
parent dce196254c
commit ea1979d567
9 changed files with 256 additions and 3 deletions
+2
View File
@@ -23,6 +23,8 @@ reverse.
- When two people change the same lines, a bar asks whose version should win — everything else the server merges by itself
- The caret stays where you put it when someone else inserts lines above you
- The backend can be deployed to a server: its own JDK, a systemd service, and Apache passing `/api/` on
- "Put on the server" in the document menu turns a local plan into a shared one and puts the link in the address bar
- A service endpoint `/api/v1/info` says which build is running — for deploys and monitoring
## 2026-08-25
+34
View File
@@ -6280,6 +6280,40 @@ alles Übrige zu sehen.
**Offen bleibt** `PATCH /title` (und damit das Ereignis `RENAMED`), ein
Eingabefeld für den Anzeigenamen und die Präsenz-Anzeige.
**Nachtrag 8 — „Auf den Server legen" im Dokumenten-Menü (2026-08-26).** Der
Haupttext sah den Menüeintrag vor („analog zu Als Datei speichern'", D72), er
fehlte aber: Auf den Server kam ein Plan nur per `curl`. Jetzt legt der Knopf
das aktive Dokument an, schaltet dorthin um, schreibt den Link in die
Adresszeile (`history.replaceState`) und in die Zwischenablage. Vier
Festlegungen:
**Die Basis-Adresse ist die eigene Herkunft.** Bei der produktiven
Installation liegt das Backend hinter derselben Domain (`/api/…` per
Proxy-Regel, D77) — wer nichts konfiguriert, bekommt also das Richtige.
Darüber liegen zwei stärkere Quellen: der `?server=`-Parameter (für die
Entwicklung, Editor auf 8137 und Backend auf 8080) und die Adresse des
gerade offenen Server-Dokuments (wer dort sitzt und ein neues anlegt, meint
denselben Server). Trägt nichts davon — auf `file://` gibt es keine
brauchbare Herkunft —, wird gefragt und die Antwort gemerkt. Die Reihenfolge
steht als reine Funktion in `live.js`, der Dialog nicht: Fragen ist keine
entscheidbare Regel.
**Das lokale Dokument bleibt.** Es zu löschen wäre die aufgeräumtere Geste
und die riskantere: Wer sein einziges Exemplar einem Server anvertraut, soll
es nicht im selben Zug verlieren.
**Dafür braucht der Wähler eine Unterscheidung.** Das hochgeladene Dokument
trägt denselben Namen wie das lokale — im Test standen prompt zwei Einträge
„Nur lokal" da, unterscheidbar nur am Tooltip. Server-Dokumente nennen
deshalb ihren **Host** neben dem Namen, zurückgenommen gesetzt; der Name
bleibt die Hauptaussage.
**Und der Knopf verschwindet, wo er nichts mehr zu tun hat.** Bei einem
Dokument, das schon auf einem Server liegt, entstünde sonst ein zweites,
gleichnamiges daneben. Erkannt an der id (`live:…`), nicht am laufenden
Feed: Auch ein Server-Dokument, das gerade nicht das aktive ist, liegt
bereits dort.
## D77 — Backend-Deploy: JDK im Home, systemd-User-Unit, Proxy in der `.htaccess`
Das Frontend geht seit D16 per rsync auf die stabile Instanz. Das Backend
braucht mehr als Dateien: eine Java-Laufzeit, einen dauerhaft laufenden Dienst
+7
View File
@@ -169,6 +169,7 @@
| [?] #col.live.crdt: Text CRDT — the plan is plain text, so it fits (L)
| [?] #col.live.ot: Operational transformation (XL)
- [^] #col.live.ask: On a real overlap, ask whose version wins (S)
- [x] #col.live.upload: Put a local plan on the server, from the menu (S)
- [?] #col.live.cursors: Cursors and who else is here (S)
- [-] #col.live.ws: Transport over a websocket (S) %% plain HTTP turned out to be enough
- [-] #col.live.own: A merge algorithm of our own (XL) %% solved problem, do not reinvent
@@ -986,6 +987,12 @@
The other established answer, and the one Google Docs uses. More machinery
for the same result.
#col.live.upload
Without it the only way onto the server was a curl command. The local plan
stays where it is: whoever entrusts their only copy to a server should not
lose it in the act. The picker names the host beside a server document, so
the two entries are told apart.
#col.live.cursors
Seeing where the others are. A by-product of the merge layer rather than a
feature in its own right.
+5 -1
View File
@@ -118,7 +118,11 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
(Adressen, Zeilen-Diff, Rebasen, Cursor-Rechnung, Feed-Regel), `app.js` die
Verdrahtung: `loadLive()` holt Text und Version und merkt beides als
**Schattenkopie**, `scheduleLivePush()` schickt nach 1,5 s Ruhe das Diff,
`runFeed()` hält einen Abruf offen (nur im sichtbaren Tab). Zwei Fallen, beide
`runFeed()` hält einen Abruf offen (nur im sichtbaren Tab), `putOnServer()`
legt das aktive Dokument an („Auf den Server legen"). Die Basis-Adresse
bestimmt `serverBase()` (live.js): `?server=` vor dem offenen Dokument vor
der eigenen Herkunft — die Vorgabe stimmt damit ohne Konfiguration, weil das
Backend produktiv hinter derselben Domain liegt. Zwei Fallen, beide
im Live-Test gefunden (D76-Nachtrag 7): `clientId` **und** `seq` gehören in den
`sessionStorage` (je Tab, überlebt Neuladen — sonst hält der Server die erste
Änderung nach einem Reload für eine Wiederholung und tut nichts), und der
+1
View File
@@ -83,6 +83,7 @@
<button type="button" class="docact" id="docNew" data-i18n="docNew">Neues Dokument</button>
<button type="button" class="docact" id="docOpenFile" data-i18n="docOpenFile">Datei öffnen…</button>
<button type="button" class="docact" id="docSaveFile" data-i18n="docSaveFile">Als Datei speichern</button>
<button type="button" class="docact" id="docToServer" data-i18n="docToServer">Auf den Server legen</button>
<button type="button" class="docact" id="docRename" data-i18n="docRename">Umbenennen</button>
<button type="button" class="docact" id="docRestore" data-i18n="docRestore" hidden>Original wiederherstellen</button>
<button type="button" class="docact docdel" id="docDelete" data-i18n="docDelete">Löschen</button>
+124 -1
View File
@@ -2262,6 +2262,10 @@ const I18N = {
docRestore:"Original wiederherstellen",
docRestoreConfirm:"„{name}“ auf den mitgelieferten Stand zurücksetzen? Eigene Änderungen gehen verloren.",
docOpenFile:"Datei öffnen…", docSaveFile:"Als Datei speichern",
docToServer:"Auf den Server legen",
docToServerAsk:"Adresse des Werkbaum-Servers (z. B. https://werkbaum.example):",
docToServerDone:"Auf dem Server — der Link steht in der Adresszeile und in der Zwischenablage.",
docToServerFailed:"Nicht auf den Server gelegt: {error}",
fsNotice:"Dieser Browser kann lokale Dateien nicht direkt beschreiben: Eine geöffnete Datei wird als Kopie geladen, und Speichern legt eine neue Datei in den Downloads ab. Browser mit der File-System-Access-Schnittstelle (z. B. Chrome oder Edge) schreiben direkt in die geöffnete Datei zurück.", fsNoticeOk:"Verstanden", fsNoticeBrave:"In Brave lässt sich die Schnittstelle von Hand einschalten:",
copy:"kopieren", copyDone:"kopiert ✓", copyTooltip:"Text in die Zwischenablage kopieren",
copyDiagramTooltip:"Diagramm als PNG-Bild in die Zwischenablage kopieren",
@@ -2379,6 +2383,10 @@ const I18N = {
docRestore:"Restore original",
docRestoreConfirm:"Reset “{name}” to the shipped version? Your changes will be lost.",
docOpenFile:"Open file…", docSaveFile:"Save as file",
docToServer:"Put on the server",
docToServerAsk:"Address of the Werkbaum server (e.g. https://werkbaum.example):",
docToServerDone:"On the server — the link is in the address bar and on the clipboard.",
docToServerFailed:"Not put on the server: {error}",
fsNotice:"This browser cannot write to local files directly: an opened file is loaded as a copy, and saving puts a new file into your downloads. Browsers with the File System Access API (such as Chrome or Edge) write straight back into the opened file.", fsNoticeOk:"Got it", fsNoticeBrave:"In Brave you can enable the API yourself:",
copy:"copy", copyDone:"copied ✓", copyTooltip:"Copy text to clipboard",
copyDiagramTooltip:"Copy diagram as a PNG image to the clipboard",
@@ -2496,6 +2504,10 @@ const I18N = {
docRestore:"Restaurar original",
docRestoreConfirm:"¿Restablecer «{name}» a la versión incluida? Tus cambios se perderán.",
docOpenFile:"Abrir archivo…", docSaveFile:"Guardar como archivo",
docToServer:"Poner en el servidor",
docToServerAsk:"Dirección del servidor Werkbaum (p. ej. https://werkbaum.example):",
docToServerDone:"En el servidor: el enlace está en la barra de direcciones y en el portapapeles.",
docToServerFailed:"No se pudo poner en el servidor: {error}",
fsNotice:"Este navegador no puede escribir directamente en archivos locales: un archivo abierto se carga como copia, y al guardar se crea un archivo nuevo en las descargas. Los navegadores con la API File System Access (como Chrome o Edge) escriben directamente en el archivo abierto.", fsNoticeOk:"Entendido", fsNoticeBrave:"En Brave puedes activar la API manualmente:",
copy:"copiar", copyDone:"copiado ✓", copyTooltip:"Copiar el texto al portapapeles",
copyDiagramTooltip:"Copiar el diagrama como imagen PNG al portapapeles",
@@ -2613,6 +2625,10 @@ const I18N = {
docRestore:"Restaurer loriginal",
docRestoreConfirm:"Réinitialiser « {name} » à la version livrée ? Vos modifications seront perdues.",
docOpenFile:"Ouvrir un fichier…", docSaveFile:"Enregistrer comme fichier",
docToServer:"Mettre sur le serveur",
docToServerAsk:"Adresse du serveur Werkbaum (p. ex. https://werkbaum.example) :",
docToServerDone:"Sur le serveur — le lien est dans la barre d'adresse et dans le presse-papiers.",
docToServerFailed:"Non déposé sur le serveur : {error}",
fsNotice:"Ce navigateur ne peut pas écrire directement dans les fichiers locaux : un fichier ouvert est chargé comme copie, et lenregistrement dépose un nouveau fichier dans les téléchargements. Les navigateurs dotés de lAPI File System Access (comme Chrome ou Edge) réécrivent directement dans le fichier ouvert.", fsNoticeOk:"Compris", fsNoticeBrave:"Dans Brave, lAPI peut être activée manuellement :",
copy:"copier", copyDone:"copié ✓", copyTooltip:"Copier le texte dans le presse-papiers",
copyDiagramTooltip:"Copier le diagramme comme image PNG dans le presse-papiers",
@@ -2730,6 +2746,10 @@ const I18N = {
docRestore:"Przywróć oryginał",
docRestoreConfirm:"Przywrócić „{name}” do dostarczonej wersji? Twoje zmiany zostaną utracone.",
docOpenFile:"Otwórz plik…", docSaveFile:"Zapisz jako plik",
docToServer:"Umieść na serwerze",
docToServerAsk:"Adres serwera Werkbaum (np. https://werkbaum.example):",
docToServerDone:"Na serwerze — link jest w pasku adresu i w schowku.",
docToServerFailed:"Nie umieszczono na serwerze: {error}",
fsNotice:"Ta przeglądarka nie może bezpośrednio zapisywać plików lokalnych: otwarty plik jest wczytywany jako kopia, a zapis tworzy nowy plik w pobranych. Przeglądarki z interfejsem File System Access (np. Chrome lub Edge) zapisują bezpośrednio do otwartego pliku.", fsNoticeOk:"Rozumiem", fsNoticeBrave:"W Brave interfejs można włączyć ręcznie:",
copy:"kopiuj", copyDone:"skopiowano ✓", copyTooltip:"Kopiuj tekst do schowka",
copyDiagramTooltip:"Kopiuj diagram jako obraz PNG do schowka",
@@ -2847,6 +2867,10 @@ const I18N = {
docRestore:"Восстановить оригинал",
docRestoreConfirm:"Вернуть «{name}» к поставляемой версии? Ваши изменения будут потеряны.",
docOpenFile:"Открыть файл…", docSaveFile:"Сохранить как файл",
docToServer:"Положить на сервер",
docToServerAsk:"Адрес сервера Werkbaum (например, https://werkbaum.example):",
docToServerDone:"На сервере — ссылка в адресной строке и в буфере обмена.",
docToServerFailed:"Не удалось положить на сервер: {error}",
fsNotice:"Этот браузер не может напрямую записывать локальные файлы: открытый файл загружается как копия, а сохранение создаёт новый файл в загрузках. Браузеры с API File System Access (например, Chrome или Edge) записывают прямо в открытый файл.", fsNoticeOk:"Понятно", fsNoticeBrave:"В Brave интерфейс можно включить вручную:",
copy:"копировать", copyDone:"скопировано ✓", copyTooltip:"Скопировать текст в буфер обмена",
copyDiagramTooltip:"Скопировать диаграмму как изображение PNG в буфер обмена",
@@ -2964,6 +2988,10 @@ const I18N = {
docRestore:"मूल पुनर्स्थापित करें",
docRestoreConfirm:"„{name}“ को मूल संस्करण पर लौटाएँ? आपके परिवर्तन खो जाएँगे।",
docOpenFile:"फ़ाइल खोलें…", docSaveFile:"फ़ाइल के रूप में सहेजें",
docToServer:"सर्वर पर रखें",
docToServerAsk:"Werkbaum सर्वर का पता (जैसे https://werkbaum.example):",
docToServerDone:"सर्वर पर है — लिंक पता-पट्टी में और क्लिपबोर्ड में है।",
docToServerFailed:"सर्वर पर नहीं रखा जा सका: {error}",
fsNotice:"यह ब्राउज़र स्थानीय फ़ाइलों में सीधे नहीं लिख सकता: खोली गई फ़ाइल एक प्रति के रूप में लोड होती है, और सहेजने पर डाउनलोड में एक नई फ़ाइल बनती है। File System Access API वाले ब्राउज़र (जैसे Chrome या Edge) सीधे खोली गई फ़ाइल में लिखते हैं।", fsNoticeOk:"समझ गया", fsNoticeBrave:"Brave में आप API को स्वयं चालू कर सकते हैं:",
copy:"कॉपी करें", copyDone:"कॉपी हो गया ✓", copyTooltip:"टेक्स्ट को क्लिपबोर्ड पर कॉपी करें",
copyDiagramTooltip:"आरेख को PNG छवि के रूप में क्लिपबोर्ड पर कॉपी करें",
@@ -3081,6 +3109,10 @@ const I18N = {
docRestore:"恢复原始版本",
docRestoreConfirm:"将“{name}”重置为随附版本?您的更改将丢失。",
docOpenFile:"打开文件…", docSaveFile:"另存为文件",
docToServer:"放到服务器上",
docToServerAsk:"Werkbaum 服务器地址(例如 https://werkbaum.example):",
docToServerDone:"已在服务器上——链接在地址栏和剪贴板中。",
docToServerFailed:"未能放到服务器上:{error}",
fsNotice:"此浏览器无法直接写入本地文件:打开的文件会作为副本载入,保存时会在下载目录生成一个新文件。支持 File System Access API 的浏览器(如 Chrome、Edge)则会直接写回打开的文件。", fsNoticeOk:"知道了", fsNoticeBrave:"在 Brave 中可以手动启用该接口:",
copy:"复制", copyDone:"已复制 ✓", copyTooltip:"将文本复制到剪贴板",
copyDiagramTooltip:"将图表作为 PNG 图片复制到剪贴板",
@@ -3198,6 +3230,10 @@ const I18N = {
docRestore:"オリジナルを復元",
docRestoreConfirm:"「{name}」を同梱版に戻しますか?変更内容は失われます。",
docOpenFile:"ファイルを開く…", docSaveFile:"ファイルとして保存",
docToServer:"サーバーに置く",
docToServerAsk:"Werkbaum サーバーのアドレス(例: https://werkbaum.example:",
docToServerDone:"サーバーにあります — リンクはアドレス欄とクリップボードにあります。",
docToServerFailed:"サーバーに置けませんでした: {error}",
fsNotice:"このブラウザーはローカルファイルへ直接書き込めません。開いたファイルはコピーとして読み込まれ、保存するとダウンロードに新しいファイルが作られます。File System Access API に対応したブラウザー(Chrome や Edge など)は、開いたファイルへ直接書き戻します。", fsNoticeOk:"わかりました", fsNoticeBrave:"Brave では、この API を手動で有効にできます:",
copy:"コピー", copyDone:"コピーしました ✓", copyTooltip:"テキストをクリップボードにコピー",
copyDiagramTooltip:"ダイアグラムを PNG 画像としてクリップボードにコピー",
@@ -3603,6 +3639,12 @@ let renamingId = null; /* id des gerade inline umbenannten Dokuments (oder nul
/* Kam das Umbenennen von einem frisch angelegten Dokument? Dann geht es danach
im Textfeld weiter Anlegen heißt schreiben wollen (D51). */
let renameIsNew = false;
/* Host eines Server-Dokuments (`live:<url>`), sonst null. */
function serverHostOf(id){
const roh = String(id || '');
if(!roh.startsWith('live:')) return null;
try{ return new URL(roh.slice(5)).host; }catch(_){ return null; }
}
function renderDocMenu(){
docList.innerHTML = docs.map(d => {
if(d.id === renamingId){
@@ -3611,10 +3653,16 @@ function renderDocMenu(){
return `<div class="docitem editing"><span class="doccheck" aria-hidden="true"></span>` +
`<input type="text" class="docrename"></div>`;
}
/* Bei einem Server-Dokument steht der Host daneben. Ein hochgeladenes
Dokument trägt denselben Namen wie das lokale, aus dem es entstand
ohne den Zusatz stünden zwei gleiche Einträge da, und nur der Tooltip
verriete den Unterschied. */
const host = serverHostOf(d.id);
const zusatz = host ? `<span class="docitem-where">${esc(host)}</span>` : '';
return `<button type="button" class="docitem" role="menuitemradio" data-id="${d.id}" ` +
`aria-checked="${d.id === activeId ? 'true' : 'false'}">` +
`<span class="doccheck" aria-hidden="true">✓</span>` +
`<span class="docitem-name">${esc(d.name)}</span></button>`;
`<span class="docitem-name">${esc(d.name)}</span>${zusatz}</button>`;
}).join('');
if(renamingId){
const inp = docList.querySelector('.docrename');
@@ -3650,6 +3698,14 @@ function updateRestoreBtn(){
Eintrag nicht auffindbar (Nutzer: ich sehe keinen Reset-Button"). */
btn.hidden = !shipped;
btn.disabled = !!shipped && d.text === shipped.text && d.name === shipped.name;
/* Auf den Server legen" entfällt, wo es schon liegt. Sonst entstünde ein
zweites, gleichnamiges Dokument auf demselben Server im Wähler nicht zu
unterscheiden. Erkennbar an der id (`live:…`), nicht am laufenden
`liveState`: Auch ein Server-Dokument, das gerade nicht das aktive ist,
liegt bereits dort. */
const zumServer = document.getElementById('docToServer');
if(zumServer) zumServer.hidden = !!d && String(d.id).startsWith('live:');
}
function restoreDoc(){
const d = activeDoc();
@@ -4752,6 +4808,72 @@ function displayName(){
try{ return localStorage.getItem(LS_NAME) || undefined; }catch(_){ return undefined; }
}
/* ---------- Ein Dokument auf den Server legen (D76) ----------
Der Weg vom lokalen Dokument zum gemeinsam bearbeitbaren: anlegen, dorthin
umschalten, Link in die Adresszeile und in die Zwischenablage.
Das lokale Dokument bleibt bestehen. Es zu löschen wäre die aufgeräumtere
Geste und die riskantere: Wer sein einziges Exemplar einem Server anvertraut,
soll es nicht dabei verlieren. Der Server-Stand ist ein neues Dokument im
Wähler, erkennbar am Namen und an der Adresse im Tooltip. */
const SERVER_PARAM = 'server';
const LS_SERVER = 'werkbaum-server';
/* Wohin? Reihenfolge und Begründung stehen in live.js. Was dort nicht
hingehört, ist der letzte Ausweg: fragen und die Antwort merken ein
Dialog ist keine entscheidbare Regel. */
function serverBaseOrAsk(){
const offen = liveState ? liveState.urls.doc : null;
let gemerkt = null;
try{ gemerkt = localStorage.getItem(LS_SERVER); }catch(_){}
const basis = live.serverBase(urlParam(SERVER_PARAM) || gemerkt, offen, location.href);
if(basis) return basis;
const eingabe = window.prompt(t('docToServerAsk'), 'https://');
if(!eingabe) return null;
const geprueft = live.serverBase(eingabe, null, location.href);
if(geprueft){ try{ localStorage.setItem(LS_SERVER, geprueft); }catch(_){} }
return geprueft;
}
async function putOnServer(){
const d = activeDoc();
if(!d) return;
const basis = serverBaseOrAsk();
if(!basis) return;
closeDocMenu();
try{
const doc = await fetchJson(live.documentsUrl(basis), {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({title: d.name, content: live.normalize(src.value)}),
});
const urls = live.liveUrls(basis + '/api/v1/documents/' + doc.id, location.href);
if(!urls) throw new Error('unerwartete Antwort des Servers');
stopLive();
liveState = {urls, id: 'live:' + urls.doc, version: 0, shadow: [''],
pushTimer: null, feedAbort: null, busy: false};
adoptLive(doc);
runFeed();
/* Die Adresszeile IST der Link dort sucht man ihn, und ein Neuladen
führt zurück ins selbe Dokument. Zusätzlich in die Zwischenablage,
weil Weitergeben der eigentliche Zweck ist. */
const teilen = location.origin + location.pathname + '?live=' + urls.doc;
try{ history.replaceState(null, '', teilen); }catch(_){}
try{ await navigator.clipboard.writeText(teilen); }catch(_){}
flashBtn(document.getElementById('docTrigger'));
sourceWarning = null;
render();
}catch(err){
sourceWarning = {type: 'liveLoad', url: live.documentsUrl(basis),
error: (err && err.message) || String(err)};
render();
}
}
/* ---------- Pad: auf Knopfdruck neu holen (D31) ----------
Kein Hintergrund-Takt. Der erste Entwurf holte alle 2,5 s selbsttätig und
lief damit in Etherpads **Drosselung**: `importExportRateLimiting` ist
@@ -4973,6 +5095,7 @@ docList.addEventListener('click', e => {
document.getElementById('docNew').addEventListener('click', e => { e.stopPropagation(); newDoc(); });
document.getElementById('docOpenFile').addEventListener('click', e => { e.stopPropagation(); openLocalFile(); });
document.getElementById('docSaveFile').addEventListener('click', e => { e.stopPropagation(); saveLocalFile(); });
document.getElementById('docToServer').addEventListener('click', e => { e.stopPropagation(); putOnServer(); });
document.getElementById('docRename').addEventListener('click', e => { e.stopPropagation(); renameDoc(); });
document.getElementById('docDelete').addEventListener('click', e => { e.stopPropagation(); deleteDoc(); });
document.getElementById('docRestore').addEventListener('click', e => { e.stopPropagation(); restoreDoc(); });
+40
View File
@@ -37,6 +37,46 @@ export function liveUrls(raw, base){
return {doc, content: doc + '/content', changes: doc + '/changes', id: m[1].toLowerCase()};
}
/* Basis-Adresse des Backends bestimmen — für „auf den Server legen" (D76).
Der Editor läuft je nach Installation überall, das Backend liegt bei der
produktiven Installation hinter **derselben** Herkunft (`/api/…` per
Proxy-Regel, D77). Das ist deshalb die Vorgabe: Wer nichts konfiguriert,
bekommt das Richtige.
Vorrang, von stark nach schwach:
1. [explicit] — der `?server=`-Parameter, für Entwicklung (Editor auf 8137,
Backend auf 8080) oder ein Backend auf fremdem Host.
2. [openDoc] — die Adresse des gerade offenen Server-Dokuments. Wer in einem
`?live=`-Dokument sitzt und ein neues anlegt, meint denselben Server.
3. Die Herkunft der Seite selbst.
`null`, wenn nichts davon trägt — auf `file://` gibt es keine brauchbare
Herkunft, und Raten wäre schlechter als Fragen. */
export function serverBase(explicit, openDoc, pageUrl){
for(const kandidat of [explicit, openDoc, pageUrl]){
if(!kandidat) continue;
let u;
try{ u = new URL(kandidat, pageUrl || undefined); }catch(_){ continue; }
if(u.protocol !== 'http:' && u.protocol !== 'https:') continue;
return u.origin + basisPfad(u.pathname);
}
return null;
}
/* Der Pfad **vor** `/api/v1/…`, damit ein Backend auch unter einem
Unterverzeichnis liegen darf. Alles andere fällt weg: Aus der Adresse eines
Dokuments wird die des Dienstes. */
function basisPfad(pfad){
const i = pfad.indexOf('/api/v1/');
return i >= 0 ? pfad.slice(0, i) : '';
}
/* Sammel-Adresse: hier wird angelegt (POST) und aufgelistet (GET). */
export function documentsUrl(base){
return String(base).replace(/\/+$/, '') + '/api/v1/documents';
}
/* ------------------------------------------------------------------ Zeilen */
/* Zeilenenden auf LF (SPEC §12). Der Server normalisiert beim Speichern
+10
View File
@@ -1568,3 +1568,13 @@
.live-conflict button.mine{ background: #fff; color: var(--warn, #B45309); border-color: #fff; }
.live-conflict button:focus-visible{ outline: 2px solid #fff; outline-offset: 2px; }
@media print{ .live-conflict{ display: none; } }
/* Server-Dokumente im Wähler: der Host neben dem Namen (D76). Ein
hochgeladenes Dokument heißt wie das lokale, aus dem es entstand — ohne
diesen Zusatz stünden zwei gleiche Einträge da. Zurückgenommen, denn der
Name bleibt die Hauptaussage. */
.docitem-where{
margin-left: auto; padding-left: 10px;
font-size: .82em; color: var(--muted); white-space: nowrap;
}
.docitem[aria-checked="true"] .docitem-where{ color: inherit; opacity: .75; }
+33 -1
View File
@@ -1,7 +1,7 @@
import {describe, it, expect} from 'vitest';
import {
liveUrls, normalize, lines, text, computeOps, applyOps,
mapLine, caretToLineCol, lineColToCaret, feedAction,
mapLine, caretToLineCol, lineColToCaret, feedAction, serverBase, documentsUrl,
} from '../src/live.js';
describe('Adressen', () => {
@@ -185,3 +185,35 @@ describe('Feed-Antwort anwenden oder nicht', () => {
expect(feedAction({currentVersion: 9}, 4)).toBe('skip');
});
});
describe('Basis-Adresse des Backends', () => {
const seite = 'https://werkbaum.example/editor/';
it('nimmt die Herkunft der Seite, wenn nichts anderes da ist', () => {
expect(serverBase(null, null, seite)).toBe('https://werkbaum.example');
expect(documentsUrl(serverBase(null, null, seite)))
.toBe('https://werkbaum.example/api/v1/documents');
});
it('der ?server=-Parameter hat Vorrang', () => {
expect(serverBase('http://localhost:8080', 'https://fremd.example/api/v1/documents/x', seite))
.toBe('http://localhost:8080');
});
it('sonst gilt der Server des offenen Dokuments', () => {
// Wer in einem ?live=-Dokument sitzt und ein neues anlegt, meint denselben.
expect(serverBase(null, 'https://anderer.example/api/v1/documents/3f2a', seite))
.toBe('https://anderer.example');
});
it('ein Backend unter einem Unterpfad bleibt erhalten', () => {
expect(serverBase(null, 'https://h.example/werkbaum/api/v1/documents/3f2a', seite))
.toBe('https://h.example/werkbaum');
});
it('ohne brauchbare Herkunft lieber nichts', () => {
// Auf file:// gibt es keine; raten waere schlechter als fragen.
expect(serverBase(null, null, 'file:///home/x/index.html')).toBe(null);
expect(serverBase('javascript:alert(1)', null, null)).toBe(null);
});
})