Files
dogfather-universe/workspace/assets/js/installieren.js
T
DogFatherGitandClaude Opus 5 95c4599baa Anfuehrungszeichen: 199 falsche Schlusszeichen im sichtbaren Text
Deutsch oeffnet mit „ und schliesst mit “. An 199 Stellen, die ein
Mensch liest, stand als Schlusszeichen ein GERADES " -- „Nächstes"
statt „Nächstes“. Auf sechzehn oeffentlichen Seiten, in vierzig
Dateien des Workspace und in zwoelf Servermodulen, die Texte
verschicken.

Auf dem Bildschirm sieht man den Unterschied sofort. Beim Schreiben
nicht: Das gerade " liegt auf der Tastatur, die anderen nicht.

WARUM EIN ERSTER ANLAUF ZURUECKGENOMMEN WURDE

Ein gerades " ist an vielen Stellen SYNTAX und kein Schriftzeichen --
Grenze einer Zeichenkette, Grenze eines HTML-Attributs, Zeichen in
einem regulaeren Ausdruck. Wer stumpf ersetzt, macht aus

    „<a href="https://…                  ein kaputtes Attribut
    /^["'„»\s]+|["'“«.\s]+$/              einen kaputten Ausdruck
    "… nichts „mal " + "eben …"          eine kaputte Zeichenkette

DIE UNTERSCHEIDUNG LAEUFT AN MERKMALEN, NICHT AN EINER LISTE

  < > = dazwischen        -> HTML-Marke oder Attribut
  endet auf Leerzeichen   -> die Zeichenkette hoert hier auf, der
                             Satz geht in der naechsten Zeile weiter.
                             Ein deutsches Schlusszeichen steht NIE
                             hinter einem Leerzeichen.
  Rueckstrich mittendrin  -> regulaerer Ausdruck
  ${ ohne }               -> mitten in einem Ausdruck

Eine Liste erlaubter Ausnahmen waere die naechste, die niemand
pflegt. Zwoelf Stellen bleiben dadurch stehen, alle zwoelf einzeln
angesehen und alle zwoelf zu Recht -- dort steht das richtige
Schlusszeichen ohnehin weiter unten im Satz.

Fuenf davon waren allerdings ECHTE Fehler HINTER dem Link
(`…>HasiDog</a>".`) -- die erste Regel hatte nur das Attribut
gesehen, nicht den Satz danach. Gezielt nachgezogen.

Ein maskiertes `\"` in workspace-vorlagen.js (28 Hooks) wird zu “ --
ohne Rueckstrich, denn “ begrenzt nichts.

KOMMENTARE BLEIBEN, WIE SIE SIND. Dort liest es niemand ausser mir;
eine Wache, die auch Kosmetik anmahnt, wird weggeklickt. Beim ersten
Messen fielen ausserdem acht Stellen aus buehne.html faelschlich an,
weil `/* */` in HTML (in <style> und <script>) nicht ausgeblendet
war -- jetzt schon.

DIE WACHE DAZU

pruef-struktur prueft es ab sofort mit derselben Regel: 322 Dateien
mit sichtbarem Text, 0 Funde, und die zwoelf bewussten Ausnahmen
werden GEZAEHLT und genannt (erlaubt: 12). Eine Ausnahme, die niemand
sieht, waechst -- und irgendwann steht der echte Fall darin.

GEPRUEFT

  pruef-struktur   59 -> 68 Pruefungen, 0 Fehler
  node --check auf allen geaenderten JS-Dateien
  nachgemessen: 199 geaendert, 12 mit Grund stehen geblieben

  gruen geblieben: bewerbung-aufgaben 163, nachwuchs 262,
  reaktion 421, support 63, content 45, terminregel 35, treff 85

  Und nachgesehen, ob eine Pruefung noch die alte Schreibweise
  ERWARTET: 13 Fundstellen, alle dreizehn nur Text in ihrer eigenen
  Ausgabe, keine einzige ein Vergleich mit dem Seitentext.

GEGENPROBE: In reaktion.html ein Schlusszeichen zurueckgedreht ->
„workspace/reaktion.html:546 „Nächstes"", mit Datei und Zeile. Und
die Erkennung einzeln gegen HTML-Attribut, fortgesetzte
Zeichenkette, regulaeren Ausdruck und eingesetzten Wert geprueft.

Stempel gesetzt: workspace 670 Verweise in 45 Dateien, oeffentlich
554 in 48 Seiten.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-01 01:56:15 +02:00

274 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
installieren.js — die Seite als App aufs Gerät holen
=====================================================================
ZWEI DINGE IN EINER DATEI, und sie gehoeren zusammen:
1. Der Service Worker wird angemeldet -- IMMER, auf jeder Seite.
2. Der Knopf, der das Installieren anbietet.
WARUM BEIDES HIER STEHT (22.09.2026, nach Filipes drittem Anlauf:
"ich will einen installieren button ... das hab ich auch schon paar
mal gefragt. mach das es ist seeeeeeeehr wichtig."):
Der Knopf gab es seit dem 20.09. Er war nur NIE ZU SEHEN. Chrome
zeigt sein Installationsangebot (`beforeinstallprompt`) erst, wenn
ein Service Worker laeuft -- und der wurde ausschliesslich in
`glocke.js` angemeldet, also erst, wenn jemand Benachrichtigungen
ERLAUBT. Auf Filipes Rechner stehen sie auf "Vom Browser blockiert".
Damit lief nie einer, damit kam das Ereignis nie, damit blieb der
Knopf `hidden`. Fuer immer.
Die Anmeldung gehoert also nicht an die Glocke, sondern an das
Installieren -- und beides an jede Seite. Deshalb eine Datei, die
ueberall geladen wird, statt einer Abhaengigkeit, die kaum jemand
erfuellt.
UND DESHALB LAEDT AUCH DIE ANMELDEWAND SIE. Sie hat keine
Kopfleiste und deshalb frueher kein Angebot bekommen -- dabei ist
sie die Seite, auf der jeder zuerst landet.
===================================================================== */
(() => {
'use strict';
/* ---------------------------------------------------------------------
DER SERVICE WORKER -- ohne Bedingung, aber auch ohne Laerm
---------------------------------------------------------------------
Er scheitert an genug Stellen, an denen niemand etwas falsch
gemacht hat: Privates Fenster, abgeschaltete Worker,
Unternehmens-Richtlinie, veralteter Browser. Nichts davon ist ein
Fehler, den ein Besucher sehen soll -- die Seite funktioniert auch
ohne, sie laesst sich dann nur nicht installieren.
`load` abwarten: Die Anmeldung zieht Bandbreite, und zwar genau in
dem Moment, in dem die Seite noch laedt. */
if ('serviceWorker' in navigator) {
const anmelden = () => {
navigator.serviceWorker.register('/workspace/sw.js', { scope: '/workspace/' })
.catch(() => { /* geht hier nicht -- die Seite laeuft trotzdem */ });
};
if (document.readyState === 'complete') anmelden();
else window.addEventListener('load', anmelden, { once: true });
}
/* =====================================================================
DIE SEITE ALS APP INSTALLIEREN (20.09.2026)
Filipe: „kannst du da dazwischen in der starseite oben, ein button
reinsetzen damit die leute die seite installieren koennen auf dem
handy oder pc. das soll auch verschwinden wenn die leute es
installiert haben."
WARUM ES DEN KNOPF BRAUCHT: Das Manifest ist seit Langem
vollstaendig -- die App LIESS sich installieren. Nur bot das
ausschliesslich der Browser selbst an, versteckt in seinem
Dreipunktemenue. Wer es nicht sucht, findet es nie. Und ohne
installierte App gibt es auf dem Handy keine verlaesslichen
Benachrichtigungen; genau daran hing im September das Telefonieren.
DREI ZUSTAENDE, UND JEDER BRAUCHT EINE ANDERE ANTWORT:
1. SCHON INSTALLIERT -> gar kein Knopf. Ein Angebot fuer etwas,
das man hat, ist Rauschen. Erkannt an `display-mode:
standalone` (und an Safaris eigenem `navigator.standalone`).
2. DER BROWSER BIETET ES AN (Chrome, Edge, Android) -> der Knopf
fragt ihn. `beforeinstallprompt` kommt EINMAL und muss
aufgehoben werden; wer ihn nicht festhaelt, kann spaeter
nicht mehr fragen.
3. SAFARI AUF DEM iPHONE -> dort gibt es `beforeinstallprompt`
NICHT, und es wird ihn nicht geben. Apple laesst nur den Weg
ueber „Teilen -> Zum Home-Bildschirm". Ein Knopf, der dort
nichts tut, waere schlimmer als keiner -- also sagt er in
diesem Fall, was zu tun ist, statt es zu versuchen.
WARUM AUF JEDER SEITE UND NICHT NUR AUF DER STARTSEITE: Wer den
Knopf einmal uebersieht, sieht ihn beim naechsten Mal wieder -- und
er verschwindet ohnehin, sobald er ueberfluessig ist.
(Hier stand "auf allen 32 Seiten". Es sind inzwischen mehr, und
seit dem 22.09. auch die Anmeldewand. Eine gezaehlte Zahl in einem
Kommentar wird still falsch, sobald jemand eine Seite anlegt --
also steht jetzt keine mehr da.)
===================================================================== */
function knopfBauen(wohin) {
/* ZWEIMAL AUFGERUFEN HEISST NICHT ZWEI KNOEPFE. Seit der Aufruf
nicht mehr an der Reihenfolge der Skriptzeilen haengt, kann er
aus zwei Richtungen kommen -- aus kopf.js und aus dem Platz
unten in dieser Datei. Zwei Angebote fuer dieselbe Sache sind
eines zu viel. */
if (!wohin || wohin.querySelector('.install-knopf')) return;
const el = (tag, klasse, text) => {
const k = document.createElement(tag);
if (klasse) k.className = klasse;
if (text !== undefined) k.textContent = text;
return k;
};
/* ---- Zustand 1: schon installiert -> nichts tun --------------- */
const schonDrin = () =>
(window.matchMedia && window.matchMedia('(display-mode: standalone)').matches)
|| window.navigator.standalone === true;
if (schonDrin()) return;
/* Apple: kein `beforeinstallprompt`, aber ein Weg. */
const istApple = /iphone|ipad|ipod/i.test(navigator.userAgent)
|| (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
let angebot = null; // der aufgehobene Browser-Vorschlag
const knopf = el('button', 'install-knopf');
knopf.type = 'button';
knopf.hidden = true;
knopf.title = 'Diese Seite als App installieren';
/* DAS WORT VERSCHWINDET AM HANDY (siehe CSS) -- der Satz fuer
Vorleseprogramme nicht. Ein Knopf, der dort nur aus einem
Pfeil besteht, heisst sonst "Schaltflaeche". */
knopf.setAttribute('aria-label', 'Diese Seite als App installieren');
const zeichen = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
zeichen.setAttribute('viewBox', '0 0 24 24');
zeichen.setAttribute('class', 'install-knopf__zeichen');
zeichen.setAttribute('aria-hidden', 'true');
zeichen.innerHTML = '<path d="M12 3.5v11M8 11l4 4 4-4"/><path d="M5 18.5h14"/>';
knopf.append(zeichen, el('span', 'install-knopf__text', 'Installieren'));
knopf.addEventListener('click', async () => {
if (angebot) {
/* Zustand 2: den Browser fragen. Die Antwort interessiert uns
nur insofern, als ein angenommener Vorschlag den Knopf
ueberfluessig macht -- ein abgelehnter nicht: Vielleicht
ueberlegt man es sich morgen anders. */
knopf.disabled = true;
try {
angebot.prompt();
const { outcome } = await angebot.userChoice;
if (outcome === 'accepted') knopf.remove();
} catch { /* dann eben nicht */ }
angebot = null;
knopf.disabled = false;
return;
}
/* Zustand 3: erklaeren statt versuchen. */
erklaeren();
});
/* WARUM DAS NICHT EINFACH `frageNach` AUFRUFT (22.09.2026).
Der erste Entwurf tat genau das -- und haette auf der
Anmeldewand gar nichts getan: Sie laedt `nachfrage.js` nicht,
also ist `window.frageNach` dort `undefined`, und `?.()`
verschluckt das lautlos. Auf dem iPhone steht der Knopf aber von
Anfang an da. Ein Knopf, der beim Antippen nichts tut, ist
schlimmer als keiner.
Die Datei nachzuladen waere die falsche Antwort: 22 KB und die
halbe Stilvorlage `module.css` fuer eine Seite, die eine einzige
Aufgabe hat. Und ein Kasten, der sich ueber das Codefeld legt,
waere dort ohnehin im Weg.
Also: Wo es den Baustein gibt, wird er benutzt -- die Seite soll
eine Sprache sprechen. Wo nicht, steht die Erklaerung als
schlichter Absatz unter dem Knopf. Beides sagt dasselbe. */
let hinweis = null;
function erklaeren() {
const titel = 'Als App auf dem Home-Bildschirm';
const weg = istApple
? 'Tippe unten in Safari auf das Teilen-Zeichen (Viereck mit Pfeil nach oben), '
+ 'dann auf „Zum Home-Bildschirm“.'
: 'Öffne das Menü deines Browsers (die drei Punkte) und wähle dort '
+ '„App installieren“ oder „Zum Startbildschirm hinzufügen“.';
const dazu = 'Danach liegt die Seite wie eine App auf deinem Gerät – und '
+ 'Anrufe und Nachrichten kommen zuverlässiger an.';
if (typeof window.frageNach === 'function') {
window.frageNach({ titel, was: weg, bleibt: dazu, ja: 'Alles klar', nein: null });
return;
}
/* Ohne Dialogbaustein: ein Absatz, der beim zweiten Antippen
wieder verschwindet. Kein zweiter, wenn schon einer dasteht. */
if (hinweis) { hinweis.remove(); hinweis = null; knopf.setAttribute('aria-expanded', 'false'); return; }
hinweis = el('p', 'install-hinweis');
hinweis.id = 'install-hinweis';
hinweis.append(el('strong', 'install-hinweis__titel', titel));
hinweis.append(el('span', 'install-hinweis__weg', weg));
hinweis.append(el('span', 'install-hinweis__dazu', dazu));
knopf.setAttribute('aria-expanded', 'true');
knopf.setAttribute('aria-controls', 'install-hinweis');
knopf.insertAdjacentElement('afterend', hinweis);
}
wohin.prepend(knopf);
/* Der Browser meldet sich, wenn er es anbieten kann. */
window.addEventListener('beforeinstallprompt', (e) => {
/* Ohne `preventDefault` zeigt der Browser seine eigene Leiste --
zwei Angebote fuer dieselbe Sache sind eines zu viel. */
e.preventDefault();
angebot = e;
knopf.hidden = false;
});
/* WANN DER KNOPF ZU SEHEN IST -- und warum nicht erst nach dem
Angebot des Browsers (geaendert 22.09.2026).
Bis heute wurde er ausschliesslich durch `beforeinstallprompt`
sichtbar. Das hat zwei Loecher:
* Apple schickt das Ereignis nie. (Das war bekannt und ist
unten schon abgefangen.)
* Chrome schickt es zwar, aber nicht verlaesslich sofort -- und
gar nicht, wenn es kurz vorher weggeklickt wurde. Dann steht
der Knopf da, wo er gebraucht wird, und ist unsichtbar.
Filipe hat dreimal danach gefragt. Ein Angebot, das man nur unter
guenstigen Umstaenden sieht, ist fuer ihn keins.
GEPRUEFT WIRD DIE FAEHIGKEIT, NICHT DER NAME DES BROWSERS:
`'onbeforeinstallprompt' in window` ist bei allen Chromium-
Browsern wahr -- Chrome, Edge, Brave, Samsung Internet, Android.
Eine Liste von Browsernamen waere die, die beim naechsten
veraltet.
Wo BEIDES nicht zutrifft -- Firefox am Rechner etwa, der
Webanwendungen schlicht nicht installiert --, bleibt der Knopf
weg. Ein Knopf, der eine Anleitung fuer etwas zeigt, das dieser
Browser nicht kann, ist schlimmer als keiner. */
const kannVielleicht = 'onbeforeinstallprompt' in window;
if (istApple || kannVielleicht) knopf.hidden = false;
/* Und wenn es geklappt hat, ist er fertig. */
window.addEventListener('appinstalled', () => knopf.remove());
/* Manche Browser melden `appinstalled` nicht -- dann faellt der
Knopf beim naechsten Oeffnen ueber `schonDrin()` weg. Und wer die
App waehrend der Sitzung installiert und zurueckwechselt, merkt
es hier: */
try {
window.matchMedia('(display-mode: standalone)')
.addEventListener('change', (m) => { if (m.matches) knopf.remove(); });
} catch { /* aeltere Browser koennen das nicht -- kein Grund zu scheitern */ }
}
/* ---------------------------------------------------------------------
WOHIN DER KNOPF GEHOERT
---------------------------------------------------------------------
Zwei Faelle, und sie schliessen sich aus:
* Es gibt eine Kopfleiste -> kopf.js sagt, wohin (es kennt die
Reihenfolge der Knoepfe darin).
* Es gibt keine -> dann steht auf der Seite ein Platz mit
`data-install-platz`, und wir setzen uns selbst hinein.
Der zweite Fall wird NICHT geraten: Ohne diesen Platz passiert
nichts. Ein Knopf, der sich irgendwohin haengt, weil er sonst
nirgends hinpasst, landet frueher oder spaeter mitten im Text.
--------------------------------------------------------------------- */
window.installKnopfBauen = knopfBauen;
const platz = document.querySelector('[data-install-platz]');
if (platz) knopfBauen(platz);
})();