diff --git a/public/neu/diag-hoehe.js b/public/neu/diag-hoehe.js deleted file mode 100644 index dbb6e8b..0000000 --- a/public/neu/diag-hoehe.js +++ /dev/null @@ -1,76 +0,0 @@ -/* VORÜBERGEHENDE MESSUNG (2026-10-29), wird nach der Klärung wieder entfernt: Am iPhone (App vom Home-Bildschirm) steht die Leiste zu hoch. - Schickt Bildschirmwerte (Fenster, visualViewport, Bildschirm, Safe-Area, vh-Einheiten, Lage der Leiste) an - /api/diag/hoehe; der Server schreibt sie ins Log (server/routes/diagHoehe.js). Keine Inhalte, keine Eingaben. Höchstens MAX Meldungen je Seite. */ - -const MAX = 80 -const ZIEL = '/api/diag/hoehe' -const rund = (x) => (Number.isFinite(x) ? Math.round(x * 10) / 10 : null) - -export function starteDiagHoehe({ win = window, doc = document, zoom = null }) { - const t0 = Date.now() - let n = 0 - const probe = (stil) => { - const el = doc.createElement('div') - el.setAttribute('aria-hidden', 'true') - el.style.cssText = `position:fixed;left:0;top:0;width:0;visibility:hidden;pointer-events:none;${stil}` - doc.body.append(el) - return el - } - const sa = probe('height:0;padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)') - const vh = probe('height:100vh'); const dvh = probe('height:100dvh'); const svh = probe('height:100svh'); const lvh = probe('height:100lvh') - const unten = probe('top:auto;bottom:0;height:1px') // wo „bottom: 0“ eines festen Elements wirklich landet - - function messe(art, extra) { - const vv = win.visualViewport - const cs = win.getComputedStyle(sa) - const leiste = doc.getElementById('kn-leiste')?.getBoundingClientRect() - const huelle = doc.querySelector('.kn-huelle')?.getBoundingClientRect() - const aktiv = doc.activeElement - return { - art, ms: Date.now() - t0, hash: win.location.hash, - iw: win.innerWidth, ih: win.innerHeight, ow: win.outerWidth, oh: win.outerHeight, - cw: doc.documentElement.clientWidth, ch: doc.documentElement.clientHeight, dh: doc.documentElement.scrollHeight, - sw: win.screen?.width, sh: win.screen?.height, sah: win.screen?.availHeight, dpr: win.devicePixelRatio, - vv: vv ? [rund(vv.width), rund(vv.height), rund(vv.offsetTop), rund(vv.pageTop), Math.round(vv.scale * 1000) / 1000] : null, - zf: zoom?.faktor?.() ?? null, swd: doc.documentElement.scrollWidth, - fs: aktiv && aktiv !== doc.body ? win.getComputedStyle(aktiv).fontSize : null, - sy: rund(win.scrollY), - sa: [cs.paddingTop, cs.paddingRight, cs.paddingBottom, cs.paddingLeft].map((p) => rund(parseFloat(p))), - vh: [vh.offsetHeight, dvh.offsetHeight, svh.offsetHeight, lvh.offsetHeight], - unten: rund(unten.getBoundingClientRect().bottom), - leiste: leiste ? [rund(leiste.top), rund(leiste.bottom), rund(leiste.height)] : null, - huelle: huelle ? [rund(huelle.top), rund(huelle.height)] : null, - app: Boolean(win.matchMedia?.('(display-mode: standalone)').matches), ns: win.navigator?.standalone ?? null, - ori: win.screen?.orientation?.type ?? null, - fokus: aktiv && aktiv !== doc.body ? aktiv.tagName : null, - tast: doc.body.classList.contains('kn-tastatur'), - ...(n === 0 ? { ua: String(win.navigator?.userAgent ?? '').slice(0, 200) } : {}), - ...(extra ?? {}), - } - } - - function sende(art, extra) { - if (n >= MAX) return - let daten - try { daten = messe(art, extra) } catch (fehler) { daten = { art, fehler: String(fehler?.message ?? fehler).slice(0, 200) } } - n += 1 - try { - win.fetch(ZIEL, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(daten), keepalive: true }).catch(() => {}) - } catch { /* Messung darf nie stören */ } - } - - const spaeter = (fn, ms) => win.setTimeout(fn, ms) - const entprellt = (art) => { let id = null; return () => { if (id) win.clearTimeout(id); id = spaeter(() => { id = null; sende(art) }, 250) } } - sende('start') - spaeter(() => sende('start+1s'), 1000) - spaeter(() => sende('start+4s'), 4000) - win.addEventListener('resize', entprellt('resize')) - win.visualViewport?.addEventListener('resize', entprellt('vv-resize')) - win.visualViewport?.addEventListener('scroll', entprellt('vv-scroll')) - doc.addEventListener('focusin', (ev) => { if (ev.target?.matches?.('textarea, input')) sende('fokus') }) - doc.addEventListener('focusout', (ev) => { if (ev.target?.matches?.('textarea, input')) { spaeter(() => sende('unfokus+300ms'), 300); spaeter(() => sende('unfokus+1s'), 1000) } }) - doc.addEventListener('visibilitychange', () => { if (doc.visibilityState === 'visible') { sende('sichtbar'); spaeter(() => sende('sichtbar+1s'), 1000) } }) - win.addEventListener('hashchange', () => spaeter(() => sende('route'), 600)) - win.addEventListener('pageshow', (ev) => { if (ev.persisted) sende('pageshow') }) - return { sende } -} diff --git a/public/neu/huelle.js b/public/neu/huelle.js index ed29e48..3d6a280 100644 --- a/public/neu/huelle.js +++ b/public/neu/huelle.js @@ -9,7 +9,6 @@ import * as routerStandard from './router.js' import { BEREICHE, FUNKTIONS_ETAGEN, hashFuerAnsicht, hashFuerRoute, elternVon, titelFuer, zeigtWiderrufsstreifen } from './routen.js' import { HANDY_ABFRAGE, baueNavigation, beobachteTastatur, istTextfeld, leisteSichtbar } from './leiste.js' import { beobachteZoom } from './zoom-ausgleich.js' -import { starteDiagHoehe } from './diag-hoehe.js' import { baueEtage } from './etage.js' import { erzeugeZaehler } from './zaehler.js' import { baueStreifen, ladeFristen, ladeFristenNeu } from './fristen-streifen.js' @@ -280,9 +279,7 @@ export async function starteHuelle({ win = window, doc = document, api = apiStan else handyMq.addListener?.(beiMedien) beobachteTastatur({ win, doc, onChange: (z) => doc.body.classList.toggle('kn-tastatur', !leisteSichtbar(z)) }) // iOS: bei verkleinerter Seite die Feldschrift so vergrößern, dass iOS beim Tippen nicht hineinzoomt (zoom-ausgleich.js) - const zoom = istIos() ? beobachteZoom({ win, doc, istTextfeld }) : null - // VORÜBERGEHENDE MESSUNG (2026-10-29, diag-hoehe.js): Bildschirmwerte am iPhone ins Server-Log, wird nach der Klärung entfernt - if (istIos() && istHandy() && !viewAs) starteDiagHoehe({ win, doc, zoom }) + if (istIos()) beobachteZoom({ win, doc, istTextfeld }) // 7. Fristen und Zähler laden parallel und brechen den Start nie ab ladeFristen(api).then((daten) => { fristen = daten; streifen.setze(daten); aktualisiereKonto() }) diff --git a/server/index.js b/server/index.js index e8417ac..3196ab5 100644 --- a/server/index.js +++ b/server/index.js @@ -27,7 +27,6 @@ import zeitenRoutes from './routes/zeiten.js' import publicAuftragRoutes from './routes/auftrag.js' import kundeAuftraegeRoutes from './routes/auftraegeKunde.js' import takeoverCreditRoutes from './routes/takeoverCredits.js' -import diagHoeheRoutes from './routes/diagHoehe.js' import { purgeExpiredUploads } from './services/takeoverCredits.js' import { purgePendingChatUploads } from './services/chatAttachments.js' import { purgeViewLog } from './services/customerViewLog.js' @@ -96,8 +95,6 @@ app.use(express.json({ limit: '2mb', verify: (req, _res, buf) => { req.rawBody = app.use(corsMiddleware(config.cors.allowedOrigins)) app.use('/api/auth', authRoutes) -// VORÜBERGEHENDE MESSUNG (2026-10-29): Bildschirmwerte der iPhone-App ins Log (routes/diagHoehe.js), wird nach der Klärung entfernt -app.use('/api/diag/hoehe', diagHoeheRoutes) app.use('/api/projects', projectsRoutes) app.use('/api/features', featuresRoutes) app.use('/api/public/news', publicNewsRoutes) diff --git a/server/routes/diagHoehe.js b/server/routes/diagHoehe.js deleted file mode 100644 index b25a679..0000000 --- a/server/routes/diagHoehe.js +++ /dev/null @@ -1,31 +0,0 @@ -import express from 'express' -import { parsePortalSession } from '../middleware/session.js' -import { neueAnsichtFuerSession } from '../services/kundenansicht/schalter.js' - -/* VORÜBERGEHENDE MESSUNG (2026-10-29), wird nach der Klärung wieder entfernt: Am iPhone (App vom Home-Bildschirm) steht die Leiste der neuen - Kundenansicht zu hoch. Die Seite (public/neu/diag-hoehe.js) schickt Bildschirmwerte, keine Inhalte; hier wird je Meldung eine Zeile - „[diag-hoehe] …“ ins Log geschrieben. Nur für Kunden-Sitzungen, die die neue Ansicht sehen (Pilot), nicht für die Mitarbeiter-Kundenansicht. - Ohne Appwrite-Abfrage (der signierte Cookie reicht), höchstens MAX_JE_STUNDE Zeilen je Kunde und Stunde, höchstens MAX_BYTES je Zeile. */ - -export const MAX_BYTES = 4000 -export const MAX_JE_STUNDE = 400 -const zaehler = new Map() // customerId -> { stunde, n } - -const router = express.Router() - -router.post('/', (req, res) => { - const session = parsePortalSession(req) - if (!session || session.viewAs || !neueAnsichtFuerSession(session)) return res.status(204).end() - const stunde = Math.floor(Date.now() / 3600000) - const stand = zaehler.get(session.customerId) - const z = stand && stand.stunde === stunde ? stand : { stunde, n: 0 } - z.n += 1 - zaehler.set(session.customerId, z) - if (z.n > MAX_JE_STUNDE) return res.status(204).end() - const zeile = JSON.stringify(req.body && typeof req.body === 'object' ? req.body : {}) - if (zeile.length > MAX_BYTES) return res.status(204).end() - console.log(`[diag-hoehe] ${String(session.customerId).slice(0, 6)} ${zeile}`) - return res.status(204).end() -}) - -export default router diff --git a/test/diagHoehe.test.js b/test/diagHoehe.test.js deleted file mode 100644 index 09f616e..0000000 --- a/test/diagHoehe.test.js +++ /dev/null @@ -1,57 +0,0 @@ -import { test, after } from 'node:test' -import assert from 'node:assert/strict' -import { startAppwriteMock } from './support/appwriteMock.js' -import { startFakeStripe } from './support/fakeStripe.js' -import { useTestEnv } from './support/testEnv.js' - -// Vorübergehende Messung der iPhone-Höhe (routes/diagHoehe.js): nur Pilot-Kunden schreiben ins Log, alle bekommen 204 -const aw = await startAppwriteMock({ schema: {} }) -const st = await startFakeStripe() -useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) -const { config } = await import('../server/config.js') -const { startTestApp, send } = await import('./support/testApp.js') -const { customerCookie, kundenansichtCookie } = await import('./support/fixtures.js') -const { default: diagHoeheRoutes, MAX_BYTES, MAX_JE_STUNDE } = await import('../server/routes/diagHoehe.js') -const app = await startTestApp({ '/api/diag/hoehe': diagHoeheRoutes }) -after(() => Promise.all([app.close(), aw.close(), st.close()])) - -const stelle = (mode, pilotEmails = []) => Object.assign(config.kundenansichtNeu, { mode, pilotEmails }) -async function mitLog(fn) { - const zeilen = [] - const alt = console.log - console.log = (...a) => { if (String(a[0]).startsWith('[diag-hoehe]')) zeilen.push(a.join(' ')); else alt(...a) } - try { await fn() } finally { console.log = alt } - return zeilen -} -const melde = (body, cookie) => send('POST', '/api/diag/hoehe', { body, cookie }) - -test('diag-hoehe: Pilot-Kunde schreibt eine Zeile, alle anderen nichts; Antwort immer 204', async () => { - stelle('pilot', ['k@example.invalid']) - const zeilen = await mitLog(async () => { - assert.equal((await melde({ art: 'start', ih: 873 }, customerCookie('c1'))).status, 204) - assert.equal((await melde({ art: 'ohne' })).status, 204) - assert.equal((await melde({ art: 'mitarbeiter' }, kundenansichtCookie())).status, 204) - stelle('pilot', ['jemand@example.invalid']) - assert.equal((await melde({ art: 'kein-pilot' }, customerCookie('c1'))).status, 204) - stelle('off') - assert.equal((await melde({ art: 'aus' }, customerCookie('c1'))).status, 204) - }) - assert.deepEqual(zeilen, ['[diag-hoehe] c1 {"art":"start","ih":873}']) -}) - -test('diag-hoehe: zu große Meldungen und mehr als MAX_JE_STUNDE je Stunde landen nicht im Log', async () => { - stelle('on') - const zeilen = await mitLog(async () => { - assert.equal((await melde({ x: 'a'.repeat(MAX_BYTES) }, customerCookie('c1'))).status, 204) - for (let i = 0; i < MAX_JE_STUNDE + 3; i++) await melde({ i }, customerCookie('c1')) - }) - assert.ok(zeilen.length <= MAX_JE_STUNDE && zeilen.length >= MAX_JE_STUNDE - 2, `Zeilen: ${zeilen.length}`) - assert.ok(zeilen.every((z) => z.length < MAX_BYTES)) - stelle('off') -}) - -test('server/index.js: die Messung hängt hinter dem JSON-Parser unter /api/diag/hoehe', async () => { - const { readFileSync } = await import('node:fs') - const js = readFileSync(new URL('../server/index.js', import.meta.url), 'utf8') - assert.ok(js.indexOf("app.use('/api/diag/hoehe', diagHoeheRoutes)") > js.indexOf("app.use(express.json({ limit: '2mb'")) -}) diff --git a/test/knChatHoehe.test.js b/test/knChatHoehe.test.js index e2062ca..c4b8f38 100644 --- a/test/knChatHoehe.test.js +++ b/test/knChatHoehe.test.js @@ -13,8 +13,7 @@ test('huelle.css: am Handy hat die Spalte im Chat mindestens volle Höhe (100vh, assert.match(css, /\.kn-spalte \{ position: relative; min-height: 100vh;/) }) -test('huelle.js: der alte Höhen-Wächter (Hülle aus- und einblenden) ist weg, die Messung läuft nur am iPhone und nicht in der Mitarbeiter-Ansicht', () => { +test('huelle.js: der alte Höhen-Wächter und die vorübergehende Messung sind wieder weg', () => { const js = lies('huelle.js') - assert.doesNotMatch(js, /beobachteHoehe|'\.\/hoehe\.js'/) - assert.match(js, /if \(istIos\(\) && istHandy\(\) && !viewAs\) starteDiagHoehe\(\{ win, doc, zoom \}\)/) + assert.doesNotMatch(js, /beobachteHoehe|'\.\/hoehe\.js'|starteDiagHoehe|diag-hoehe/) }) diff --git a/test/knZoomAusgleich.test.js b/test/knZoomAusgleich.test.js index 628d6e8..521e450 100644 --- a/test/knZoomAusgleich.test.js +++ b/test/knZoomAusgleich.test.js @@ -42,5 +42,5 @@ test('huelle.css und huelle.js: Ausgleich nur über die Klasse an , für a const css = lies('huelle.css') assert.match(css, /\.kn-zoom-ausgleich \.kn :is\(input:not\(\[type="checkbox"\]\)[^{]*, textarea, select\) \{ font-size: calc\(16px \* var\(--kn-zoom-ausgleich, 1\)\) !important; \}/) const js = lies('huelle.js') - assert.match(js, /const zoom = istIos\(\) \? beobachteZoom\(\{ win, doc, istTextfeld \}\) : null/) + assert.match(js, /if \(istIos\(\)\) beobachteZoom\(\{ win, doc, istTextfeld \}\)/) })