Compare commits
980
Commits
d5bfd1be3d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
594aa9196a | ||
|
|
3b5e1d559a | ||
|
|
c6e3c47be7 | ||
|
|
b73cd340bc | ||
|
|
53ae593860 | ||
|
|
fd5994c243 | ||
|
|
5c49f63616 | ||
|
|
23ab43f9fe | ||
|
|
b3ca84a8e5 | ||
|
|
6a25df22fb | ||
|
|
e554071739 | ||
|
|
0fbeadd0c1 | ||
|
|
dd062b1f15 | ||
|
|
07a4e153bf | ||
|
|
9ed295946c | ||
|
|
f3f8ac3777 | ||
|
|
ace4c85e72 | ||
|
|
14dc2ad1bf | ||
|
|
6bfcaa892c | ||
|
|
d7724afc02 | ||
|
|
5f4e158660 | ||
|
|
85808fc115 | ||
|
|
1703dabaaf | ||
|
|
d62f8b805e | ||
|
|
ec79defb2a | ||
|
|
670ead3091 | ||
|
|
9aeb9872be | ||
|
|
db62e320ff | ||
|
|
53a191219e | ||
|
|
2064bddba6 | ||
|
|
f231f4b887 | ||
|
|
5cd49d6a4a | ||
|
|
c5f1d9545c | ||
|
|
0ee5c86809 | ||
|
|
be34f1be1d | ||
|
|
fa79e6e10c | ||
|
|
c92ddd9363 | ||
|
|
6ff63a7a11 | ||
|
|
f6fc291dfe | ||
|
|
3e32ddfe86 | ||
|
|
29ca12a409 | ||
|
|
140275022d | ||
|
|
8bb02f3949 | ||
|
|
f8962ffa50 | ||
|
|
4c64edc057 | ||
|
|
ddaf2879c4 | ||
|
|
306c53871d | ||
|
|
4cac03a2bd | ||
|
|
72b5ac5265 | ||
|
|
9c04f806eb | ||
|
|
3962074af6 | ||
|
|
8e349f6eca | ||
|
|
92d7d247e9 | ||
|
|
112475e873 | ||
|
|
d4050f870a | ||
|
|
779320c080 | ||
|
|
036bf4b0b9 | ||
|
|
9166e023c5 | ||
|
|
d17520c4f5 | ||
|
|
9b00771d11 | ||
|
|
e7ac625f26 | ||
|
|
c6ecb5242c | ||
|
|
fcf357c0b7 | ||
|
|
841bc76bbb | ||
|
|
da2fe83d47 | ||
|
|
528ca0875e | ||
|
|
4e0e42f633 | ||
|
|
99e1987bc4 | ||
|
|
bf1e968a83 | ||
|
|
4a3b0030d2 | ||
|
|
71b2543e95 | ||
|
|
bfe3895a50 | ||
|
|
447129d4e2 | ||
|
|
524ac64d20 | ||
|
|
0df96611dc | ||
|
|
f76749133f | ||
|
|
a9b50a5542 | ||
|
|
b20ac71750 | ||
|
|
cbd36c0ae6 | ||
|
|
a6a656f18a | ||
|
|
5534655671 | ||
|
|
c37faabcd3 | ||
|
|
5f3d8f3e96 | ||
|
|
28da4180bd | ||
|
|
1cabab2826 | ||
|
|
b6f70a502a | ||
|
|
364ded1662 | ||
|
|
a50c5ef6c9 | ||
|
|
b91ba1d5cb | ||
|
|
c87c87e76b | ||
|
|
9ca7900213 | ||
|
|
36890c6734 | ||
|
|
f30e72ebbb | ||
|
|
57112cc51f | ||
|
|
906c9ec1f0 | ||
|
|
c72c9d9f5a | ||
|
|
437947cdf6 | ||
|
|
1cd16ff293 | ||
|
|
01c4f22a84 | ||
|
|
04a81a1d46 | ||
|
|
eb7c44ac11 | ||
|
|
2ac7d3f64c | ||
|
|
3c2450dd2e | ||
|
|
ade7e872d9 | ||
|
|
861b74f70e | ||
|
|
d99c29b6c6 | ||
|
|
1b66f6aa81 | ||
|
|
27e6c8fe4f | ||
|
|
4473a746ee | ||
|
|
f9c4fbe9bb | ||
|
|
8a23652e07 | ||
|
|
989e7be640 | ||
|
|
cf391eb117 | ||
|
|
8c8d29657d | ||
|
|
72c5fa9fea | ||
|
|
9a3ce7c8bf | ||
|
|
a3831040ba | ||
|
|
bb1efa5c9e | ||
|
|
e036d24ae5 | ||
|
|
df53cd039b | ||
|
|
166188c069 | ||
|
|
3c44aaea77 | ||
|
|
b77c3291c5 | ||
|
|
d5a7312896 | ||
|
|
91e5bca1c5 | ||
|
|
db47f13510 | ||
|
|
7a03f073bc | ||
|
|
d8c952ac2a | ||
|
|
385844192d | ||
|
|
5297a05125 | ||
|
|
92ac61dffd | ||
|
|
de8b0c0fca | ||
|
|
408f7d0cc2 | ||
|
|
cdc8f5c424 | ||
|
|
9489a70f0c | ||
|
|
664043e8e9 | ||
|
|
142aa6e9d6 | ||
|
|
3d4d49de82 | ||
|
|
7595844170 | ||
|
|
f016d7ae6c | ||
|
|
710c57cf87 | ||
|
|
57d5db2344 | ||
|
|
0432b7e01d | ||
|
|
23252df359 | ||
|
|
99d7e2bdca | ||
|
|
b6f2df0d4a | ||
|
|
4953c8fd0f | ||
|
|
b4b18792c3 | ||
|
|
e30318a470 | ||
|
|
3241aedc3c | ||
|
|
a8b8838d9e | ||
|
|
e144a9611d | ||
|
|
f25b7608b4 | ||
|
|
748226d631 | ||
|
|
9e7db59326 | ||
|
|
f5b43c55a9 | ||
|
|
d112fe2326 | ||
|
|
a51643c4a5 | ||
|
|
1d149f50b1 | ||
|
|
819725a3c1 | ||
|
|
b000dba698 | ||
|
|
33b7a4ce0f | ||
|
|
4bf1b67e67 | ||
|
|
90b7f10f1a | ||
|
|
8e0d2b2736 | ||
|
|
9b177a15a3 | ||
|
|
882438df38 | ||
|
|
7f4e84e0b1 | ||
|
|
6f0808647f | ||
|
|
5c21023e28 | ||
|
|
46edb7c418 | ||
|
|
214d698d6f | ||
|
|
8d3eb73db7 | ||
|
|
2a8db582cb | ||
|
|
65b1389b55 | ||
|
|
c5b1333953 | ||
|
|
c2b7073d99 | ||
|
|
c6a9a2c4a6 | ||
|
|
f2c9eb107a | ||
|
|
7c4a4a567d | ||
|
|
f573d49f0a | ||
|
|
08dd766e9c | ||
|
|
d3d28492c0 | ||
|
|
d33a483fd8 | ||
|
|
a45f436124 | ||
|
|
1e0eb9f569 | ||
|
|
02a3732e4a | ||
|
|
a4ade5f649 | ||
|
|
b264d7f734 | ||
|
|
69df9e7046 | ||
|
|
a1400ac315 | ||
|
|
3526909d61 | ||
|
|
fd971309a8 | ||
|
|
59783ce685 | ||
|
|
1c6b6bf8d6 | ||
|
|
a6c6dabaa3 | ||
|
|
3f5b030a50 | ||
|
|
592ac20503 | ||
|
|
f4bc752553 | ||
|
|
1d9515823c | ||
|
|
6d35674c1e | ||
|
|
4f9fb6b109 | ||
|
|
17400cac1f | ||
|
|
1cbec5b32d | ||
|
|
ab838d07bc | ||
|
|
1fe1ae31bf | ||
|
|
e2fc4eb676 | ||
|
|
5bf1504ce8 | ||
|
|
54310d9bae | ||
|
|
727613d0e2 | ||
|
|
392b85c1f0 | ||
|
|
a071e57fb7 | ||
|
|
b6c1d81342 | ||
|
|
435dd0b746 | ||
|
|
c2245b6905 | ||
|
|
4177b76d82 | ||
|
|
1bb172ad91 | ||
|
|
e3ae152f12 | ||
|
|
57d1ab82c6 | ||
|
|
d96d09f589 | ||
|
|
2df56eccc5 | ||
|
|
838bc96d1b | ||
|
|
f5e304ad7a | ||
|
|
196921fdf0 | ||
|
|
03ae8c77cd | ||
|
|
cb0799a35c | ||
|
|
e5793fd151 | ||
|
|
74543b75c3 | ||
|
|
c3eef4747d | ||
|
|
fbd07b3d8c | ||
|
|
95cbd473d0 | ||
|
|
f01e6def59 | ||
|
|
54442a4682 | ||
|
|
b992dd2388 | ||
|
|
2eb38032d3 | ||
|
|
d27f6d5ddb | ||
|
|
83cdab405b | ||
|
|
cebde0e070 | ||
|
|
97dd84fbb8 | ||
|
|
7b978ad2e4 | ||
|
|
35b0438d00 | ||
|
|
9d01d38a45 | ||
|
|
827ee54875 | ||
|
|
c019d3c3b0 | ||
|
|
f57a3d477e | ||
|
|
e3b399e003 | ||
|
|
32cffb3518 | ||
|
|
5541e0cb60 | ||
|
|
d0ab92658a | ||
|
|
05006f1480 | ||
|
|
aad0a8b807 | ||
|
|
d9058ef1a6 | ||
|
|
b25689a015 | ||
|
|
efeb99e214 | ||
|
|
a94b50fbc1 | ||
|
|
f3b6e00bf7 | ||
|
|
5d5f6333a4 | ||
|
|
8f53b560d5 | ||
|
|
bd12892ed2 | ||
|
|
095694bc44 | ||
|
|
e5fc5e2c74 | ||
|
|
743af802c1 | ||
|
|
d2c48ab99b | ||
|
|
d990155df9 | ||
|
|
88a7305e41 | ||
|
|
ab3e6938da | ||
|
|
d264f2af5a | ||
|
|
3beba876d6 | ||
|
|
60a51c0f68 | ||
|
|
10fe9a2ae0 | ||
|
|
ca0f762272 | ||
|
|
e680ea3266 | ||
|
|
4553bc8e92 | ||
|
|
7d3114d7a3 | ||
|
|
c72b252e63 | ||
|
|
73c5446cf8 | ||
|
|
c116c3cb4f | ||
|
|
66ec8e0a44 | ||
|
|
22a7c8123d | ||
|
|
b53f9f67ed | ||
|
|
4f8e956574 | ||
|
|
503337d7ae | ||
|
|
c791146af4 | ||
|
|
cf87a7e1e0 | ||
|
|
6cb5bfa6c7 | ||
|
|
11531cacb6 | ||
|
|
0da5b30119 | ||
|
|
aca393d564 | ||
|
|
cad9ba7e40 | ||
|
|
8c8fa05591 | ||
|
|
c74f7eeea7 | ||
|
|
576a17bfa4 | ||
|
|
335ac60e54 | ||
|
|
ed7567397d | ||
|
|
38734e99aa | ||
|
|
b78ca971af | ||
|
|
3464808a40 | ||
|
|
19abac0373 | ||
|
|
3a46210e9c | ||
|
|
51a36be36a | ||
|
|
6f213dca25 | ||
|
|
0fc2eb4516 | ||
|
|
bd87bb115c | ||
|
|
fd189b085d | ||
|
|
01406c82e5 | ||
|
|
f0402a7ea9 | ||
|
|
f71e3e8345 | ||
|
|
8e643160ec | ||
|
|
2d91a54e4a | ||
|
|
4706f232b5 | ||
|
|
e15486263f | ||
|
|
ef646f12de | ||
|
|
9fa965521a | ||
|
|
846059b455 | ||
|
|
37cd312549 | ||
|
|
e6a0fc9acf | ||
|
|
731bc8c05e | ||
|
|
6c1eaf697b | ||
|
|
32726fa1c2 | ||
|
|
aeae17b491 | ||
|
|
7cec85f888 | ||
|
|
7ad9c12bca | ||
|
|
7553253746 | ||
|
|
8032aa67c6 | ||
|
|
edccd4d8af | ||
|
|
b097fedf5a | ||
|
|
e3df5383d1 | ||
|
|
db9fc61894 | ||
|
|
ae6d84b2a4 | ||
|
|
445d234695 | ||
|
|
cf3e0fd52a | ||
|
|
10679ae726 | ||
|
|
68ffac24c4 | ||
|
|
3305371111 | ||
|
|
987268e2f0 | ||
|
|
0b0d8da335 | ||
|
|
40a366b8ae | ||
|
|
bc9b4d9576 | ||
|
|
0c5ca76a4d | ||
|
|
8627ed1124 | ||
|
|
4f467fa9c5 | ||
|
|
e9e198e53c | ||
|
|
98eceaf14c | ||
|
|
9b509fefbc | ||
|
|
47a92811a3 | ||
|
|
3ecea05b52 | ||
|
|
035967f8eb | ||
|
|
9cd5ea3038 | ||
|
|
118be44847 | ||
|
|
a3db7f310a | ||
|
|
4e4feba4c9 | ||
|
|
004e564479 | ||
|
|
ddffc7a92d | ||
|
|
0ab3cbbaa3 | ||
|
|
16f08b5ac3 | ||
|
|
1b5b5b7094 | ||
|
|
d7b5b98187 | ||
|
|
a68baf10d9 | ||
|
|
1d2ca2791d | ||
|
|
f7650fe883 | ||
|
|
947e9da2e3 | ||
|
|
802b5d3ad4 | ||
|
|
bdf191d945 | ||
|
|
c38c8e7c13 | ||
|
|
84e7a99f41 | ||
|
|
5717a5cdd0 | ||
|
|
18369a348c | ||
|
|
668bc62c55 | ||
|
|
5a05bd27e9 | ||
|
|
9d82fc579f | ||
|
|
0bb463eb7d | ||
|
|
e00b4ae572 | ||
|
|
8814c4aef1 | ||
|
|
50f854ef1c | ||
|
|
d0691236d5 | ||
|
|
3984d468bf | ||
|
|
3e66dfa91c | ||
|
|
e87140e5ba | ||
|
|
c7cd436ec2 | ||
|
|
cab9c33db6 | ||
|
|
a4a18a1c89 | ||
|
|
348ddc927c | ||
|
|
9350fccd77 | ||
|
|
2c7cd26532 | ||
|
|
ce84fc849a | ||
|
|
bf308a75ae | ||
|
|
43473c0879 | ||
|
|
4ab84e31ac | ||
|
|
72d79ddba7 | ||
|
|
52543a99b1 | ||
|
|
578a64782b | ||
|
|
c36cb9b4a3 | ||
|
|
b2072e4e7c | ||
|
|
08d837d690 | ||
|
|
2f4f360a21 | ||
|
|
1f56b86791 | ||
|
|
7104f97feb | ||
|
|
72fb37edcc | ||
|
|
77fb8c8f44 | ||
|
|
1b7aa9ebe3 | ||
|
|
a129bdb0ec | ||
|
|
309c98d531 | ||
|
|
45487d6c7e | ||
|
|
c6706f88ff | ||
|
|
21805c9a93 | ||
|
|
d4b9e6a5b9 | ||
|
|
cb6291410d | ||
|
|
55fe1c567f | ||
|
|
9c2897a0b0 | ||
|
|
42a6bb5a4a | ||
|
|
329b8cc949 | ||
|
|
c259e45412 | ||
|
|
dbb2042983 | ||
|
|
9283ff42c1 | ||
|
|
172d9f1778 | ||
|
|
5693f37d5f | ||
|
|
16a7f05aa2 | ||
|
|
ea4457e6a4 | ||
|
|
17ad5cbcdc | ||
|
|
b8055b336c | ||
|
|
73820fb1e6 | ||
|
|
cc668d3765 | ||
|
|
648d0e07fe | ||
|
|
3ed96a5838 | ||
|
|
aa9ee35ab6 | ||
|
|
6d8487633b | ||
|
|
eb616ddbe8 | ||
|
|
583a2d33c2 | ||
|
|
c13bc94a0f | ||
|
|
738afa2d0e | ||
|
|
7464a4ef55 | ||
|
|
ecf67d6978 | ||
|
|
987b184aa1 | ||
|
|
bf99f659eb | ||
|
|
9de23c7fe2 | ||
|
|
809da80c9b | ||
|
|
0ebd2e6663 | ||
|
|
38bd56fe8b | ||
|
|
98d1e6613d | ||
|
|
9722f0a938 | ||
|
|
9d19a2cbad | ||
|
|
da43ee2b41 | ||
|
|
03a7f1d877 | ||
|
|
0fb84021f8 | ||
|
|
608ff61d3d | ||
|
|
1febdfaf53 | ||
|
|
83605975b0 | ||
|
|
0063c04d8a | ||
|
|
bea9f3cfba | ||
|
|
129f8de278 | ||
|
|
0e74f1669c | ||
|
|
c10e50d0b3 | ||
|
|
6782d99960 | ||
|
|
308268e901 | ||
|
|
e9ab15bdb5 | ||
|
|
b168b10485 | ||
|
|
90e8607e26 | ||
|
|
af63b69ec1 | ||
|
|
be007af7d0 | ||
|
|
bea07e73b1 | ||
|
|
6a9daa0583 | ||
|
|
b3e96d4a56 | ||
|
|
5f99f08a39 | ||
|
|
2fc48df915 | ||
|
|
242fbb01e5 | ||
|
|
b974b3a3ec | ||
|
|
f01ed15247 | ||
|
|
04a3525f79 | ||
|
|
4dc447ed33 | ||
|
|
cee44d6852 | ||
|
|
7050b63a37 | ||
|
|
7bb918390a | ||
|
|
6765b14feb | ||
|
|
f20f056c60 | ||
|
|
ec320853ce | ||
|
|
9a2a70b491 | ||
|
|
811e054ca4 | ||
|
|
bade4edb74 | ||
|
|
019833d385 | ||
|
|
1d5a565e9f | ||
|
|
5b5b4b90eb | ||
|
|
a629b2519b | ||
|
|
6ec8885bfd | ||
|
|
ed99234ae4 | ||
|
|
722c686690 | ||
|
|
495d5ad9ca | ||
|
|
c1266fcce7 | ||
|
|
a6a4eae294 | ||
|
|
1a91a331b4 | ||
|
|
01bf05cc35 | ||
|
|
a2d11ad6c4 | ||
|
|
34384c7a39 | ||
|
|
90098726b9 | ||
|
|
a198c472bd | ||
|
|
9a14e874c0 | ||
|
|
2c059cb3d6 | ||
|
|
efc6dcf332 | ||
|
|
03bdeb4461 | ||
|
|
49acd13f84 | ||
|
|
24cb549e78 | ||
|
|
53253f0451 | ||
|
|
97e6971276 | ||
|
|
6c474c46c3 | ||
|
|
6b4cbb3204 | ||
|
|
3073fffcf5 | ||
|
|
11dc4b2c9f | ||
|
|
b20d0464c4 | ||
|
|
47ae572fff | ||
|
|
66793d06d5 | ||
|
|
8d670ca5dc | ||
|
|
302b58cf72 | ||
|
|
0042d8dd8f | ||
|
|
d1f9176c99 | ||
|
|
938b7099dd | ||
|
|
d4c1be91c7 | ||
|
|
1c9f773e97 | ||
|
|
f830698dc3 | ||
|
|
7bf2ad2665 | ||
|
|
8fe14c2fe6 | ||
|
|
097036c9a5 | ||
|
|
2db3eca357 | ||
|
|
88fb9263f9 | ||
|
|
90692b646b | ||
|
|
5f52ef54e8 | ||
|
|
61c2f72631 | ||
|
|
13bbadc729 | ||
|
|
59b0ea799d | ||
|
|
ee454017b0 | ||
|
|
06ebcd1b0a | ||
|
|
0f25f7cb52 | ||
|
|
2636ce60be | ||
|
|
1ac142b92f | ||
|
|
db9f7df747 | ||
|
|
2314f7d940 | ||
|
|
4b4cd1058f | ||
|
|
385f768d76 | ||
|
|
87d267e51a | ||
|
|
360d218d4e | ||
|
|
52456dac76 | ||
|
|
6ae0f67457 | ||
|
|
8d628ef906 | ||
|
|
9bf5ec09f8 | ||
|
|
f1b43ca2bf | ||
|
|
fea58fc621 | ||
|
|
0db59d38e9 | ||
|
|
eadb768d1a | ||
|
|
a9f0711e97 | ||
|
|
1964e5fcca | ||
|
|
55e98b038c | ||
|
|
f119a0cacc | ||
|
|
69c94d83d7 | ||
|
|
9d1691cf6b | ||
|
|
97bff91a99 | ||
|
|
37bec17d73 | ||
|
|
c1dada3189 | ||
|
|
2f9dfd8ad5 | ||
|
|
20583424a3 | ||
|
|
747be432c5 | ||
|
|
0090332c70 | ||
|
|
edaefd4237 | ||
|
|
91b1a3bcd8 | ||
|
|
29a38a9592 | ||
|
|
1cd74226cb | ||
|
|
75794f14d5 | ||
|
|
42a3befc5f | ||
|
|
250fc0c4df | ||
|
|
31aa80affd | ||
|
|
008ad4fb1e | ||
|
|
ab47fabcb2 | ||
|
|
36b36bcc4d | ||
|
|
d66dd60a76 | ||
|
|
c5009da570 | ||
|
|
6bd6aed7d5 | ||
|
|
30701ba277 | ||
|
|
91e7f7d8a3 | ||
|
|
1c74c89feb | ||
|
|
4fc4c70b7d | ||
|
|
3156bb790a | ||
|
|
13523ca5e2 | ||
|
|
addb88d5fd | ||
|
|
17f933dbc7 | ||
|
|
973135d2de | ||
|
|
eae6eee03c | ||
|
|
35c9800856 | ||
|
|
144b053851 | ||
|
|
cf9f9b30a0 | ||
|
|
800630ea4b | ||
|
|
f57dad73ca | ||
|
|
017a9c22f2 | ||
|
|
f4754f573a | ||
|
|
d878df96e0 | ||
|
|
e43cdb667b | ||
|
|
468df49812 | ||
|
|
992d7358fa | ||
|
|
e200efb7e5 | ||
|
|
9dd0244537 | ||
|
|
7fcb268644 | ||
|
|
1f6ae0f7e0 | ||
|
|
ded6bb2a77 | ||
|
|
d568a3798d | ||
|
|
786df1230e | ||
|
|
d8e4d79891 | ||
|
|
98fa03aad7 | ||
|
|
c584d220c8 | ||
|
|
e2f50407a0 | ||
|
|
aaed273b62 | ||
|
|
39225594e5 | ||
|
|
4601c5ef3b | ||
|
|
689a21f9ac | ||
|
|
3a3319ec14 | ||
|
|
43b3caddde | ||
|
|
36b94c3035 | ||
|
|
4d96565240 | ||
|
|
c35711caad | ||
|
|
978722023f | ||
|
|
d040a55f89 | ||
|
|
a1732613c9 | ||
|
|
9b4305e5de | ||
|
|
5c95dfc322 | ||
|
|
2dd9c0dec7 | ||
|
|
8c1055e8a0 | ||
|
|
a72380cc8b | ||
|
|
6ed0e3bcaf | ||
|
|
3a0b0089dd | ||
|
|
5077dd5a20 | ||
|
|
10b393687c | ||
|
|
2659385205 | ||
|
|
be7f9b1aa3 | ||
|
|
5bd8ee8457 | ||
|
|
f134ed0032 | ||
|
|
794a63c0d7 | ||
|
|
bd65c9dc58 | ||
|
|
4be886a4de | ||
|
|
3ad056f261 | ||
|
|
0b75b31009 | ||
|
|
650919b022 | ||
|
|
9c07f6a8c5 | ||
|
|
43e4826ae3 | ||
|
|
70940e02c8 | ||
|
|
734e397e73 | ||
|
|
781eccb2d6 | ||
|
|
4465ac7b94 | ||
|
|
5fef7e47f0 | ||
|
|
bb45d35ec7 | ||
|
|
e3e198fb1a | ||
|
|
9e8fd33713 | ||
|
|
ae1886c030 | ||
|
|
2b0c05b8e9 | ||
|
|
43ac05aa06 | ||
|
|
485ff3bc15 | ||
|
|
efe17551c2 | ||
|
|
589f5088a6 | ||
|
|
be49d38e0c | ||
|
|
c4ec24448e | ||
|
|
2d9b75b3bf | ||
|
|
1289b5697b | ||
|
|
d1470b894a | ||
|
|
8e25cd2d13 | ||
|
|
30d40ad6ce | ||
|
|
af4029af81 | ||
|
|
70b904260a | ||
|
|
6c25a76e28 | ||
|
|
274192e88e | ||
|
|
171b5086b1 | ||
|
|
181445a69d | ||
|
|
0c033ab1d2 | ||
|
|
459efff10a | ||
|
|
9ad8515d60 | ||
|
|
e281db1b2b | ||
|
|
4def5c735d | ||
|
|
cce6e64dd1 | ||
|
|
759be69250 | ||
|
|
9cacd5bd3b | ||
|
|
310f8434d6 | ||
|
|
a5e12d7f34 | ||
|
|
334dc1e669 | ||
|
|
f63165c158 | ||
|
|
f28060198c | ||
|
|
e3ff05c804 | ||
|
|
275dbcd690 | ||
|
|
d8328722b3 | ||
|
|
0a113c2983 | ||
|
|
ff7fe35c06 | ||
|
|
df8b952b81 | ||
|
|
95e2807dc7 | ||
|
|
7c00633db0 | ||
|
|
3a4fa581c0 | ||
|
|
d0ca3c41cd | ||
|
|
40f0a04908 | ||
|
|
45203284ee | ||
|
|
0fdd13246c | ||
|
|
7d93ae0cd9 | ||
|
|
a06c3bc8ef | ||
|
|
a6ac4510cd | ||
|
|
48dd19b75a | ||
|
|
5f5df7ba1a | ||
|
|
acf6ff9ee1 | ||
|
|
6718018211 | ||
|
|
a64a996291 | ||
|
|
0ccdde1c28 | ||
|
|
c1fda8a717 | ||
|
|
c5afd42a5f | ||
|
|
7d8ba56088 | ||
|
|
25f3c1df4d | ||
|
|
1cd776a2ee | ||
|
|
fdf594706d | ||
|
|
772095ab19 | ||
|
|
be65baff01 | ||
|
|
5fb39f1a1b | ||
|
|
546d3c84d0 | ||
|
|
495c3a2c40 | ||
|
|
78170567b4 | ||
|
|
fbc40252a3 | ||
|
|
3bac4093af | ||
|
|
ddb91a9950 | ||
|
|
f401dc14d7 | ||
|
|
3d05472b25 | ||
|
|
c6824f83a6 | ||
|
|
97890a8eee | ||
|
|
a73e0be952 | ||
|
|
96409c6306 | ||
|
|
8d64a18d35 | ||
|
|
c2cbc4156f | ||
|
|
9449542686 | ||
|
|
c0c1a3d7a9 | ||
|
|
f0b57ebe41 | ||
|
|
73811e0300 | ||
|
|
5ed4fbd188 | ||
|
|
039fdcaf27 | ||
|
|
83ee9d5a25 | ||
|
|
daa451094a | ||
|
|
ef2ce49669 | ||
|
|
3c5e06723f | ||
|
|
7dbf2ce75f | ||
|
|
34dfe181e4 | ||
|
|
9361000a70 | ||
|
|
34ff97e924 | ||
|
|
6e43d54fe8 | ||
|
|
2bf67cb48d | ||
|
|
f14a6b7b57 | ||
|
|
3cedb5feef | ||
|
|
05e3478229 | ||
|
|
db243f50bb | ||
|
|
6af9834293 | ||
|
|
7b35ed67a5 | ||
|
|
cba14ed0f0 | ||
|
|
75ca8b78c0 | ||
|
|
588cc95f5d | ||
|
|
3dd2fac50b | ||
|
|
9329318df5 | ||
|
|
53b50011e8 | ||
|
|
30412cb0ea | ||
|
|
ab4c53d6df | ||
|
|
b193b327ab | ||
|
|
d416a7aa09 | ||
|
|
4bf3726c00 | ||
|
|
466fcffa21 | ||
|
|
714687fe3b | ||
|
|
7ead59ff73 | ||
|
|
2ca18f9c65 | ||
|
|
0b59dac2be | ||
|
|
821e947275 | ||
|
|
e3fed71c76 | ||
|
|
e33e1cf5ec | ||
|
|
7fc5ba8528 | ||
|
|
5a6fbc7328 | ||
|
|
fc937b0d1e | ||
|
|
f4c8b291d7 | ||
|
|
696f17b470 | ||
|
|
62f196f705 | ||
|
|
813c56e6a4 | ||
|
|
37203f00c4 | ||
|
|
f0eee196ed | ||
|
|
9e66f55c2c | ||
|
|
9fa05e23d1 | ||
|
|
18d37d1e01 | ||
|
|
7de9f1aaa3 | ||
|
|
add7fa4add | ||
|
|
28c7a90d8b | ||
|
|
da8bba2a1c | ||
|
|
5930f052d7 | ||
|
|
305939fdbd | ||
|
|
6233850bb3 | ||
|
|
8b107019c3 | ||
|
|
3a48e6e677 | ||
|
|
cd9aebe4b1 | ||
|
|
f7da79e4b3 | ||
|
|
a1ce46672a | ||
|
|
29986cf697 | ||
|
|
c37dd49b05 | ||
|
|
04b45e4207 | ||
|
|
a5cb668853 | ||
|
|
56637f2005 | ||
|
|
c256b78dae | ||
|
|
8a028ce75a | ||
|
|
54f307d12a | ||
|
|
9488efc97a | ||
|
|
126d556bd7 | ||
|
|
b7cbe3aa38 | ||
|
|
19cd8f90ce | ||
|
|
96c26fb9b8 | ||
|
|
cd9a6e0cf1 | ||
|
|
b3cf78c82a | ||
|
|
07bef6971c | ||
|
|
3316b562af | ||
|
|
cf2841adf5 | ||
|
|
d788dba937 | ||
|
|
4258b5b9f1 | ||
|
|
40a8aad3f9 | ||
|
|
def490ceca | ||
|
|
20ca1de694 | ||
|
|
e358514240 | ||
|
|
4afe7eb5cd | ||
|
|
6cd50bba9f | ||
|
|
71f9243bf2 | ||
|
|
5b902519a9 | ||
|
|
4fffd8b218 | ||
|
|
4ed7e5d7a5 | ||
|
|
12513b73c3 | ||
|
|
bcc0e42374 | ||
|
|
10ef2c8085 | ||
|
|
b8174e296a | ||
|
|
e2c517a015 | ||
|
|
bc5fddf13f | ||
|
|
8588e63124 | ||
|
|
aeb08580bb | ||
|
|
c402837df6 | ||
|
|
651745eccc | ||
|
|
edef85c7b3 | ||
|
|
d4241d20c4 | ||
|
|
00ea4baf65 | ||
|
|
37489aa866 | ||
|
|
4e8b8f1698 | ||
|
|
43c168ab54 | ||
|
|
c1f8ace348 | ||
|
|
d3c6858ecc | ||
|
|
1f1ffb5275 | ||
|
|
de4b6f3a35 | ||
|
|
b8a530958f | ||
|
|
08958dee77 | ||
|
|
3e6f35f836 | ||
|
|
36c7822783 | ||
|
|
df0c7a05e8 | ||
|
|
c93cb93a36 | ||
|
|
85b972f9dc | ||
|
|
a2e308711e | ||
|
|
b98562bb4f | ||
|
|
7a4af8e3c8 | ||
|
|
b45108c1de | ||
|
|
4b7f73abba | ||
|
|
0dc57fe5e8 | ||
|
|
0e0776b406 | ||
|
|
035d1d5517 | ||
|
|
5a09d060a1 | ||
|
|
6dac6aa073 | ||
|
|
a421308019 | ||
|
|
319f8937aa | ||
|
|
d320730251 | ||
|
|
434bab0b7a | ||
|
|
ad0b08f32a | ||
|
|
ac7375e889 | ||
|
|
8374a570aa | ||
|
|
0cc35bf1a5 | ||
|
|
770cbe32b7 | ||
|
|
4b1df8fbfa | ||
|
|
16fbb03c13 | ||
|
|
85bee3190c | ||
|
|
c55e6f2dfe | ||
|
|
586e3cc85d | ||
|
|
7ae0ffdd3a | ||
|
|
8acc743cf7 | ||
|
|
88854aa2b4 | ||
|
|
7da4452bbd | ||
|
|
7c6b0c906b | ||
|
|
c4fab31603 | ||
|
|
6643283c6f | ||
|
|
7d4767a137 | ||
|
|
a28ae21389 | ||
|
|
1a1005d099 | ||
|
|
ec3f22d98e | ||
|
|
5b7365b99e | ||
|
|
cdc814ae09 | ||
|
|
af7ecce78d | ||
|
|
6f4be30c18 | ||
|
|
d7c2027d3f | ||
|
|
acc86c267b | ||
|
|
0555a2274d | ||
|
|
9b14a74fcc | ||
|
|
25f15c0ecb | ||
|
|
23d78815f9 | ||
|
|
d5c70a5bd6 | ||
|
|
febac3c2a3 | ||
|
|
ed8c0f82d5 | ||
|
|
a5da4849e5 | ||
|
|
5c39640224 | ||
|
|
5ce8aa6fbb | ||
|
|
a960701d48 | ||
|
|
405b2a0485 | ||
|
|
6e6c9a4c24 | ||
|
|
51c4852488 | ||
|
|
e409c11d5c | ||
|
|
8d5de87ead | ||
|
|
531ee2a02b | ||
|
|
ea71b3f1c3 | ||
|
|
679f996597 | ||
|
|
a3faa1aff1 | ||
|
|
3edb5f8bb2 | ||
|
|
8c9d28c30f | ||
|
|
e4efdac226 | ||
|
|
78b4d7b7fe | ||
|
|
46f229804d | ||
|
|
09fc940619 | ||
|
|
05a59c4d61 | ||
|
|
f7eeda8c19 | ||
|
|
c95695c175 | ||
|
|
90a3c57aa3 | ||
|
|
19af93338b | ||
|
|
0129ac48e7 | ||
|
|
36b54f6dab | ||
|
|
6c8e7703ae | ||
|
|
9342ba10c8 | ||
|
|
cc42ab16c1 | ||
|
|
12a24514a4 | ||
|
|
63d3bb11bf | ||
|
|
19f9c747a2 | ||
|
|
0d2254ca20 | ||
|
|
e5cc2f3c82 | ||
|
|
7ffe95f24a | ||
|
|
f66a713531 | ||
|
|
662f8d8144 | ||
|
|
bc20ce0673 | ||
|
|
188db4b3c1 | ||
|
|
379cc26dcd | ||
|
|
887e1e54a0 | ||
|
|
7df0b72019 | ||
|
|
c4607e9c43 | ||
|
|
05363d06ad | ||
|
|
e5cc57cefd | ||
|
|
7799d7c4de | ||
|
|
7e869280f3 | ||
|
|
f856869747 | ||
|
|
71fa0fc8e2 | ||
|
|
478fbae3aa | ||
|
|
91d9360dd2 | ||
|
|
e456c3de69 | ||
|
|
0c2e50fc20 | ||
|
|
6f7daf757f | ||
|
|
a2c1f0d81c | ||
|
|
0098899ac6 | ||
|
|
fead8fdd5a | ||
|
|
f19187cc16 | ||
|
|
f016ad0326 | ||
|
|
47c1af7015 | ||
|
|
2694ef18d6 | ||
|
|
dc61649228 | ||
|
|
51fc2434e5 | ||
|
|
d097704d49 | ||
|
|
54defb75c4 | ||
|
|
877fde7a6f | ||
|
|
afec0b82fd | ||
|
|
405408d076 | ||
|
|
c76ea0bf9e | ||
|
|
09870a3b83 | ||
|
|
65ab5013cb | ||
|
|
18c3cdf3e5 | ||
|
|
e3daffffa3 | ||
|
|
8fa7cc3db7 | ||
|
|
60baeb3271 | ||
|
|
32e441c8bc | ||
|
|
81119c19bc | ||
|
|
63ddbf55cc | ||
|
|
054b04d5b6 | ||
|
|
c600118374 | ||
|
|
d5eb908cb6 | ||
|
|
ba9916cfe6 | ||
|
|
e54f97144c | ||
|
|
93381a81c5 | ||
|
|
d72d30dd2a | ||
|
|
57ba2c06f9 | ||
|
|
bd7b1fc671 |
No files matched your search
+7
-1
@@ -6,7 +6,8 @@ APPWRITE_ENDPOINT=https://ticket.webklar.com/v1
|
||||
APPWRITE_PROJECT_ID=6a1058610003c5a13a05
|
||||
# Fest: nur woms-database (Ticketsystem). Wird bei abweichendem Wert ignoriert.
|
||||
# APPWRITE_DATABASE_ID=woms-database
|
||||
# API-Key Scopes (Appwrite 1.8): databases.read/write, collections.read/write, documents.read/write
|
||||
# API-Key Scopes (Appwrite 1.8): databases.read/write, collections.read/write, documents.read/write,
|
||||
# users.read/write (Kundenzugänge anlegen, Passwort ändern, Abmelden anderer Geräte)
|
||||
APPWRITE_API_KEY=
|
||||
|
||||
APPWRITE_COLLECTION_CUSTOMERS=customers
|
||||
@@ -21,3 +22,8 @@ GITEA_API_TOKEN=
|
||||
PREVIEW_DEPLOY_ROOT=/var/www/previews
|
||||
PREVIEW_BASE_HOST=project.webklar.com
|
||||
ALLOWED_CUSTOMER_STATUSES=active
|
||||
|
||||
# Finanzen (Bankabruf Finom über Enable Banking): beides setzen, sonst ist der Bankabruf aus.
|
||||
# APP_ID = Anwendungs-ID bei Enable Banking, KEY_PATH = Pfad zum privaten Schlüssel (PEM) auf dem Server, nie im Repo.
|
||||
# ENABLE_BANKING_APP_ID=
|
||||
# ENABLE_BANKING_KEY_PATH=
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
node_modules/
|
||||
node_modules
|
||||
.env
|
||||
.DS_Store
|
||||
preview-data/
|
||||
|
||||
@@ -56,6 +56,15 @@ Database: woms-database
|
||||
|
||||
`customerId` wird **im Ticketsystem** am `websiteProjects`-Eintrag gesetzt, nicht im Webhook.
|
||||
|
||||
## E-Mails
|
||||
|
||||
Alle automatischen Mails sind HTML-Vorlagen mit Textfassung in `server/mail-templates/`
|
||||
(K01–K11 an Kunden, T01–T11 ans Team; Design aus Claude Design). Jede Vorlage nennt im
|
||||
Kopfkommentar Betreff, Platzhalter (`{{name}}`) und optionale Blöcke
|
||||
(`<!-- WENN x --> … <!-- ENDE x -->`). Eine neue Fassung aus dem Design kann die Datei direkt
|
||||
ersetzen, solange Platzhalter und Blöcke gleich heißen; `npm test` prüft das. Das Logo in den
|
||||
Mails liegt unter `public/webklar-logo.png`.
|
||||
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Erzeugt dist/w.js und dist/h.js: core.js + Einstieg als IIFE, ohne import/export (kein Build-Tool).
|
||||
// Aufruf: node client/build.js
|
||||
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const DIR = dirname(fileURLToPath(import.meta.url));
|
||||
const read = (f) => readFileSync(join(DIR, f), 'utf8');
|
||||
|
||||
function wrap(name, entry) {
|
||||
const core = read('core.js').replace(/^export\s+/gm, '');
|
||||
const body = read(entry).replace(/^import\s[^\n]*\n/gm, '');
|
||||
return `/* WEBklar Analyse ${name} - generiert aus client/core.js + client/${entry} (node client/build.js), nicht von Hand aendern */\n(function () {\n'use strict';\n${core}\n${body}\n})();\n`;
|
||||
}
|
||||
|
||||
export function buildAll() {
|
||||
return { w: wrap('w.js', 'w.js'), h: wrap('h.js', 'h.js') };
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const b = buildAll();
|
||||
mkdirSync(join(DIR, 'dist'), { recursive: true });
|
||||
writeFileSync(join(DIR, 'dist', 'w.js'), b.w);
|
||||
writeFileSync(join(DIR, 'dist', 'h.js'), b.h);
|
||||
console.log('dist/w.js', b.w.length, 'Bytes; dist/h.js', b.h.length, 'Bytes');
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// Reine Funktionen des Analyse-Skripts (ohne DOM-Zugriff ausser an uebergebenen Objekten).
|
||||
// Wird von w.js/h.js importiert; build.js fuegt alles zu je einer Datei ohne Import zusammen.
|
||||
|
||||
export const CONSENT_KEY = 'wk-consent';
|
||||
export const CONSENT_MAX_AGE = 183 * 86400000;
|
||||
export const BANNER_VERSION = '2026-10-02';
|
||||
export const BANNER_TEXT = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
|
||||
// Eigene Websites der WEBklar GbR (webklar.com, Skript-Attribut data-variant="eigen"): Sie-Form, WEBklar misst selbst
|
||||
// (keine Auftragsverarbeitung). Eigene Fassung, damit eine Textaenderung nur dort neu fragt.
|
||||
export const BANNER_VERSION_EIGEN = '2026-10-17';
|
||||
export const BANNER_TEXT_EIGEN = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
|
||||
export const BANNER_LINK_LABEL = 'Datenschutz';
|
||||
export const BANNER_ARIA = 'Einwilligung zur Reichweitenmessung';
|
||||
export const SETTINGS_LABEL = 'Datenschutz-Einstellungen';
|
||||
|
||||
export function vpClass(width) {
|
||||
const w = Number(width);
|
||||
if (w < 768) return 'm';
|
||||
if (w < 1024) return 't';
|
||||
return 'd';
|
||||
}
|
||||
|
||||
export function cleanPath(href) {
|
||||
let p = '/';
|
||||
const h = String(href == null ? '' : href);
|
||||
if (h.charAt(0) === '/') {
|
||||
p = h.replace(/[?#].*$/, '') || '/'; // wie location.pathname, auch '//foo'
|
||||
} else {
|
||||
try { p = new URL(h, 'http://x.invalid/').pathname || '/'; } catch (e) { /* bleibt '/' */ }
|
||||
}
|
||||
try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ }
|
||||
if (p.charAt(0) !== '/') p = '/' + p;
|
||||
return p.slice(0, 300);
|
||||
}
|
||||
|
||||
const stripWww = (h) => String(h).toLowerCase().replace(/^www\./, '');
|
||||
|
||||
export function refHost(referrer, ownHost) {
|
||||
if (!referrer) return '';
|
||||
try {
|
||||
const u = new URL(String(referrer));
|
||||
if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';
|
||||
const h = u.hostname.toLowerCase();
|
||||
if (!h || stripWww(h) === stripWww(ownHost || '')) return '';
|
||||
return h;
|
||||
} catch (e) { return ''; }
|
||||
}
|
||||
|
||||
// Nur Tag-Namen mit :nth-of-type, keine IDs/Klassen (koennten personenbezogene Texte enthalten).
|
||||
export function cssPath(el) {
|
||||
const parts = [];
|
||||
let n = el;
|
||||
while (n && n.tagName && parts.length < 6) {
|
||||
const tag = String(n.tagName).toLowerCase().replace(/[^a-z0-9-]/g, '');
|
||||
if (!tag) break;
|
||||
let idx = 1;
|
||||
for (let s = n.previousElementSibling; s; s = s.previousElementSibling) {
|
||||
if (String(s.tagName).toLowerCase() === String(n.tagName).toLowerCase()) idx++;
|
||||
}
|
||||
parts.unshift(tag + ':nth-of-type(' + idx + ')');
|
||||
if (tag === 'body' || tag === 'html') break;
|
||||
n = n.parentNode;
|
||||
}
|
||||
let out = parts.join(' > ');
|
||||
while (out.length > 200 && parts.length > 1) { parts.shift(); out = parts.join(' > '); }
|
||||
return out.slice(0, 200);
|
||||
}
|
||||
|
||||
function parseHref(a) {
|
||||
const href = a && (typeof a.href === 'string' && a.href ? a.href : (a.getAttribute && a.getAttribute('href'))) || '';
|
||||
return String(href).trim();
|
||||
}
|
||||
|
||||
export function linkHost(a) {
|
||||
try { return new URL(parseHref(a)).hostname.toLowerCase(); } catch (e) { return ''; }
|
||||
}
|
||||
|
||||
export function contactKind(a, ownHost) {
|
||||
const href = parseHref(a);
|
||||
if (!href) return null;
|
||||
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(href);
|
||||
if (!scheme) return null; // relativ, Anker
|
||||
const s = scheme[1].toLowerCase();
|
||||
if (s === 'tel') return 'tel';
|
||||
if (s === 'mailto') return 'mail';
|
||||
if (s === 'whatsapp') return 'wa';
|
||||
if (s !== 'http' && s !== 'https') return null;
|
||||
const host = linkHost(a);
|
||||
if (!host) return null;
|
||||
if (host === 'wa.me' || host === 'api.whatsapp.com') return 'wa';
|
||||
const own = ownHost == null ? (typeof location !== 'undefined' ? location.hostname : '') : ownHost;
|
||||
return stripWww(host) === stripWww(own || '') ? null : 'out';
|
||||
}
|
||||
|
||||
// Banner-Fassung je Variante: 'eigen' fuer WEBklars eigene Websites, alles andere ist die Kunden-Fassung.
|
||||
export function bannerFor(variant) {
|
||||
return variant === 'eigen'
|
||||
? { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN }
|
||||
: { version: BANNER_VERSION, text: BANNER_TEXT };
|
||||
}
|
||||
|
||||
export function readConsent(storage, now, version = BANNER_VERSION) {
|
||||
try {
|
||||
const raw = storage.getItem(CONSENT_KEY);
|
||||
if (!raw) return null;
|
||||
const c = JSON.parse(raw);
|
||||
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
|
||||
if (c.bv !== version) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
|
||||
const age = now - c.t;
|
||||
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
|
||||
return { v: 1, a: c.a, t: c.t, bv: c.bv };
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
export function contrastOk(hex) {
|
||||
if (typeof hex !== 'string' || !/^#[0-9a-f]{6}$/i.test(hex)) return false;
|
||||
const lin = (i) => {
|
||||
const c = parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16) / 255;
|
||||
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
||||
};
|
||||
const L = 0.2126 * lin(0) + 0.7152 * lin(1) + 0.0722 * lin(2);
|
||||
return 1.05 / (L + 0.05) >= 4.5;
|
||||
}
|
||||
|
||||
export function escapeHtml(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
}
|
||||
Vendored
+262
@@ -0,0 +1,262 @@
|
||||
/* WEBklar Analyse h.js - generiert aus client/core.js + client/h.js (node client/build.js), nicht von Hand aendern */
|
||||
(function () {
|
||||
'use strict';
|
||||
// Reine Funktionen des Analyse-Skripts (ohne DOM-Zugriff ausser an uebergebenen Objekten).
|
||||
// Wird von w.js/h.js importiert; build.js fuegt alles zu je einer Datei ohne Import zusammen.
|
||||
|
||||
const CONSENT_KEY = 'wk-consent';
|
||||
const CONSENT_MAX_AGE = 183 * 86400000;
|
||||
const BANNER_VERSION = '2026-10-02';
|
||||
const BANNER_TEXT = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
|
||||
// Eigene Websites der WEBklar GbR (webklar.com, Skript-Attribut data-variant="eigen"): Sie-Form, WEBklar misst selbst
|
||||
// (keine Auftragsverarbeitung). Eigene Fassung, damit eine Textaenderung nur dort neu fragt.
|
||||
const BANNER_VERSION_EIGEN = '2026-10-17';
|
||||
const BANNER_TEXT_EIGEN = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
|
||||
const BANNER_LINK_LABEL = 'Datenschutz';
|
||||
const BANNER_ARIA = 'Einwilligung zur Reichweitenmessung';
|
||||
const SETTINGS_LABEL = 'Datenschutz-Einstellungen';
|
||||
|
||||
function vpClass(width) {
|
||||
const w = Number(width);
|
||||
if (w < 768) return 'm';
|
||||
if (w < 1024) return 't';
|
||||
return 'd';
|
||||
}
|
||||
|
||||
function cleanPath(href) {
|
||||
let p = '/';
|
||||
const h = String(href == null ? '' : href);
|
||||
if (h.charAt(0) === '/') {
|
||||
p = h.replace(/[?#].*$/, '') || '/'; // wie location.pathname, auch '//foo'
|
||||
} else {
|
||||
try { p = new URL(h, 'http://x.invalid/').pathname || '/'; } catch (e) { /* bleibt '/' */ }
|
||||
}
|
||||
try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ }
|
||||
if (p.charAt(0) !== '/') p = '/' + p;
|
||||
return p.slice(0, 300);
|
||||
}
|
||||
|
||||
const stripWww = (h) => String(h).toLowerCase().replace(/^www\./, '');
|
||||
|
||||
function refHost(referrer, ownHost) {
|
||||
if (!referrer) return '';
|
||||
try {
|
||||
const u = new URL(String(referrer));
|
||||
if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';
|
||||
const h = u.hostname.toLowerCase();
|
||||
if (!h || stripWww(h) === stripWww(ownHost || '')) return '';
|
||||
return h;
|
||||
} catch (e) { return ''; }
|
||||
}
|
||||
|
||||
// Nur Tag-Namen mit :nth-of-type, keine IDs/Klassen (koennten personenbezogene Texte enthalten).
|
||||
function cssPath(el) {
|
||||
const parts = [];
|
||||
let n = el;
|
||||
while (n && n.tagName && parts.length < 6) {
|
||||
const tag = String(n.tagName).toLowerCase().replace(/[^a-z0-9-]/g, '');
|
||||
if (!tag) break;
|
||||
let idx = 1;
|
||||
for (let s = n.previousElementSibling; s; s = s.previousElementSibling) {
|
||||
if (String(s.tagName).toLowerCase() === String(n.tagName).toLowerCase()) idx++;
|
||||
}
|
||||
parts.unshift(tag + ':nth-of-type(' + idx + ')');
|
||||
if (tag === 'body' || tag === 'html') break;
|
||||
n = n.parentNode;
|
||||
}
|
||||
let out = parts.join(' > ');
|
||||
while (out.length > 200 && parts.length > 1) { parts.shift(); out = parts.join(' > '); }
|
||||
return out.slice(0, 200);
|
||||
}
|
||||
|
||||
function parseHref(a) {
|
||||
const href = a && (typeof a.href === 'string' && a.href ? a.href : (a.getAttribute && a.getAttribute('href'))) || '';
|
||||
return String(href).trim();
|
||||
}
|
||||
|
||||
function linkHost(a) {
|
||||
try { return new URL(parseHref(a)).hostname.toLowerCase(); } catch (e) { return ''; }
|
||||
}
|
||||
|
||||
function contactKind(a, ownHost) {
|
||||
const href = parseHref(a);
|
||||
if (!href) return null;
|
||||
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(href);
|
||||
if (!scheme) return null; // relativ, Anker
|
||||
const s = scheme[1].toLowerCase();
|
||||
if (s === 'tel') return 'tel';
|
||||
if (s === 'mailto') return 'mail';
|
||||
if (s === 'whatsapp') return 'wa';
|
||||
if (s !== 'http' && s !== 'https') return null;
|
||||
const host = linkHost(a);
|
||||
if (!host) return null;
|
||||
if (host === 'wa.me' || host === 'api.whatsapp.com') return 'wa';
|
||||
const own = ownHost == null ? (typeof location !== 'undefined' ? location.hostname : '') : ownHost;
|
||||
return stripWww(host) === stripWww(own || '') ? null : 'out';
|
||||
}
|
||||
|
||||
// Banner-Fassung je Variante: 'eigen' fuer WEBklars eigene Websites, alles andere ist die Kunden-Fassung.
|
||||
function bannerFor(variant) {
|
||||
return variant === 'eigen'
|
||||
? { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN }
|
||||
: { version: BANNER_VERSION, text: BANNER_TEXT };
|
||||
}
|
||||
|
||||
function readConsent(storage, now, version = BANNER_VERSION) {
|
||||
try {
|
||||
const raw = storage.getItem(CONSENT_KEY);
|
||||
if (!raw) return null;
|
||||
const c = JSON.parse(raw);
|
||||
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
|
||||
if (c.bv !== version) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
|
||||
const age = now - c.t;
|
||||
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
|
||||
return { v: 1, a: c.a, t: c.t, bv: c.bv };
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
function contrastOk(hex) {
|
||||
if (typeof hex !== 'string' || !/^#[0-9a-f]{6}$/i.test(hex)) return false;
|
||||
const lin = (i) => {
|
||||
const c = parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16) / 255;
|
||||
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
||||
};
|
||||
const L = 0.2126 * lin(0) + 0.7152 * lin(1) + 0.0722 * lin(2);
|
||||
return 1.05 / (L + 0.05) >= 4.5;
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
}
|
||||
|
||||
// WEBklar Analyse: Heatmap-Overlay (wird von w.js nur mit gueltigem ?__wk_heat=Token geladen).
|
||||
// data-embed="1" (nur in einem Rahmen): Ansicht im Geraeterahmen des Kundenportals. Dann ohne Leiste,
|
||||
// Links und Formulare gesperrt, und die Klickzahl geht per postMessage nur an das Portal (Herkunft von h.js).
|
||||
|
||||
function main() {
|
||||
const me = document.currentScript;
|
||||
const token = me && me.getAttribute('data-token');
|
||||
const src = String((me && (me.src || me.getAttribute('src'))) || '').replace(/[?#].*$/, '');
|
||||
const base = src.replace(/[^/]*$/, '');
|
||||
if (!token || !base) return;
|
||||
|
||||
const de = document.documentElement;
|
||||
let portal = '';
|
||||
try { portal = new URL(base).origin; } catch (e) { /* ohne Portal keine Einbettung */ }
|
||||
const embed = !!portal && me.getAttribute('data-embed') === '1' && window.parent && window.parent !== window;
|
||||
function tell(state, count) {
|
||||
if (!embed) return;
|
||||
try { window.parent.postMessage({ wk: 'heat', state: state, count: count }, portal); } catch (e) { /* egal */ }
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const cs = canvas.style;
|
||||
cs.position = 'absolute'; cs.left = '0'; cs.top = '0'; cs.pointerEvents = 'none'; cs.zIndex = '2147483646';
|
||||
document.body.appendChild(canvas);
|
||||
|
||||
let points = [];
|
||||
let lastW = 0, lastH = 0, timer = 0;
|
||||
|
||||
// Farbverlauf: transparent -> blau -> gruen -> gelb -> rot
|
||||
const ramp = (function () {
|
||||
const c = document.createElement('canvas'); c.width = 256; c.height = 1;
|
||||
const g = c.getContext('2d');
|
||||
const lg = g.createLinearGradient(0, 0, 256, 0);
|
||||
lg.addColorStop(0.25, 'rgb(0,0,255)'); lg.addColorStop(0.55, 'rgb(0,255,0)'); lg.addColorStop(0.85, 'rgb(255,255,0)'); lg.addColorStop(1, 'rgb(255,0,0)');
|
||||
g.fillStyle = lg; g.fillRect(0, 0, 256, 1);
|
||||
return g.getImageData(0, 0, 256, 1).data;
|
||||
})();
|
||||
|
||||
if (embed) {
|
||||
// Nur ansehen: kein Seitenwechsel (er wuerde zaehlen und das Banner zeigen)
|
||||
const block = function (e) {
|
||||
const t = e.target;
|
||||
if (e.type === 'submit' || (t && t.closest && t.closest('a,area'))) { e.preventDefault(); e.stopPropagation(); }
|
||||
};
|
||||
window.addEventListener('click', block, true);
|
||||
window.addEventListener('submit', block, true);
|
||||
// Handy und Tablet haben ueberlagernde Scrollleisten; so stimmt die Breite wie auf dem Geraet
|
||||
const st = document.createElement('style');
|
||||
st.textContent = 'html{scrollbar-width:none}html::-webkit-scrollbar{display:none}';
|
||||
(document.head || de).appendChild(st);
|
||||
start(function (n) { tell('ready', n); }, function () { tell('error', 0); });
|
||||
return;
|
||||
}
|
||||
|
||||
const host = document.createElement('div');
|
||||
host.style.position = 'fixed'; host.style.left = '0'; host.style.right = '0'; host.style.top = '0'; host.style.zIndex = '2147483647';
|
||||
const root = host.attachShadow({ mode: 'open' });
|
||||
const style = document.createElement('style');
|
||||
style.textContent = ':host{all:initial}.bar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;background:#111;color:#fff;font:14px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}button{font:600 14px/1 system-ui,sans-serif;padding:8px 14px;border:2px solid #fff;border-radius:6px;background:#fff;color:#111;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}';
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'bar';
|
||||
const msg = document.createElement('span');
|
||||
msg.textContent = 'Heatmap wird geladen ...';
|
||||
const close = document.createElement('button');
|
||||
close.setAttribute('type', 'button');
|
||||
close.textContent = 'Schließen';
|
||||
bar.appendChild(msg); bar.appendChild(close);
|
||||
root.appendChild(style); root.appendChild(bar);
|
||||
document.body.appendChild(host);
|
||||
|
||||
function draw() {
|
||||
const w = Math.max(de.scrollWidth, de.clientWidth, 1);
|
||||
const h = Math.max(de.scrollHeight, de.clientHeight, 1);
|
||||
lastW = w; lastH = h;
|
||||
// Zeichenflaeche begrenzen (Speicher): hoechstens ca. 4 Mio. Pixel
|
||||
const scale = Math.min(1, Math.sqrt(4e6 / (w * h)));
|
||||
const cw = Math.max(1, Math.floor(w * scale)), ch = Math.max(1, Math.floor(h * scale));
|
||||
canvas.width = cw; canvas.height = ch;
|
||||
cs.width = w + 'px'; cs.height = h + 'px';
|
||||
const g = canvas.getContext('2d');
|
||||
g.clearRect(0, 0, cw, ch);
|
||||
const r = Math.max(6, 30 * scale);
|
||||
for (const p of points) {
|
||||
const x = p.x * cw, y = p.y * ch;
|
||||
const rg = g.createRadialGradient(x, y, 0, x, y, r);
|
||||
rg.addColorStop(0, 'rgba(0,0,0,0.25)'); rg.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
g.fillStyle = rg; g.fillRect(x - r, y - r, r * 2, r * 2);
|
||||
}
|
||||
// Deckkraft-Stufen in Farben umsetzen
|
||||
const img = g.getImageData(0, 0, cw, ch);
|
||||
const d = img.data;
|
||||
for (let i = 3; i < d.length; i += 4) {
|
||||
const a = d[i];
|
||||
if (!a) continue;
|
||||
const k = Math.min(255, a * 3) * 4;
|
||||
d[i - 3] = ramp[k]; d[i - 2] = ramp[k + 1]; d[i - 1] = ramp[k + 2];
|
||||
d[i] = Math.min(200, 60 + a);
|
||||
}
|
||||
g.putImageData(img, 0, 0);
|
||||
}
|
||||
|
||||
function redrawIfChanged() {
|
||||
if (Math.max(de.scrollWidth, de.clientWidth) !== lastW || Math.max(de.scrollHeight, de.clientHeight) !== lastH) draw();
|
||||
}
|
||||
function stop() {
|
||||
clearInterval(timer);
|
||||
window.removeEventListener('resize', draw);
|
||||
host.remove(); canvas.remove();
|
||||
}
|
||||
close.addEventListener('click', stop);
|
||||
start(function (n) { msg.textContent = 'Heatmap – ' + n + ' Klicks (letzte 30 Tage)'; },
|
||||
function () { msg.textContent = 'Heatmap-Link abgelaufen oder ungültig'; });
|
||||
|
||||
// Punkte laden, zeichnen und bei Groessenaenderung neu zeichnen
|
||||
function start(onReady, onError) {
|
||||
window.addEventListener('resize', draw);
|
||||
fetch(base + 'heat?token=' + encodeURIComponent(token), { credentials: 'omit' })
|
||||
.then(function (r) { if (!r.ok) throw new Error('heat'); return r.json(); })
|
||||
.then(function (j) {
|
||||
points = (j && Array.isArray(j.points) ? j.points : []).filter(function (p) { return p && p.x >= 0 && p.x <= 1 && p.y >= 0 && p.y <= 1; });
|
||||
draw();
|
||||
timer = setInterval(redrawIfChanged, 1000);
|
||||
onReady(points.length);
|
||||
})
|
||||
.catch(function () { onError(); });
|
||||
}
|
||||
}
|
||||
|
||||
try { main(); } catch (e) { /* nie die Seite stoeren */ }
|
||||
|
||||
})();
|
||||
Vendored
+467
@@ -0,0 +1,467 @@
|
||||
/* WEBklar Analyse w.js - generiert aus client/core.js + client/w.js (node client/build.js), nicht von Hand aendern */
|
||||
(function () {
|
||||
'use strict';
|
||||
// Reine Funktionen des Analyse-Skripts (ohne DOM-Zugriff ausser an uebergebenen Objekten).
|
||||
// Wird von w.js/h.js importiert; build.js fuegt alles zu je einer Datei ohne Import zusammen.
|
||||
|
||||
const CONSENT_KEY = 'wk-consent';
|
||||
const CONSENT_MAX_AGE = 183 * 86400000;
|
||||
const BANNER_VERSION = '2026-10-02';
|
||||
const BANNER_TEXT = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
|
||||
// Eigene Websites der WEBklar GbR (webklar.com, Skript-Attribut data-variant="eigen"): Sie-Form, WEBklar misst selbst
|
||||
// (keine Auftragsverarbeitung). Eigene Fassung, damit eine Textaenderung nur dort neu fragt.
|
||||
const BANNER_VERSION_EIGEN = '2026-10-17';
|
||||
const BANNER_TEXT_EIGEN = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
|
||||
const BANNER_LINK_LABEL = 'Datenschutz';
|
||||
const BANNER_ARIA = 'Einwilligung zur Reichweitenmessung';
|
||||
const SETTINGS_LABEL = 'Datenschutz-Einstellungen';
|
||||
|
||||
function vpClass(width) {
|
||||
const w = Number(width);
|
||||
if (w < 768) return 'm';
|
||||
if (w < 1024) return 't';
|
||||
return 'd';
|
||||
}
|
||||
|
||||
function cleanPath(href) {
|
||||
let p = '/';
|
||||
const h = String(href == null ? '' : href);
|
||||
if (h.charAt(0) === '/') {
|
||||
p = h.replace(/[?#].*$/, '') || '/'; // wie location.pathname, auch '//foo'
|
||||
} else {
|
||||
try { p = new URL(h, 'http://x.invalid/').pathname || '/'; } catch (e) { /* bleibt '/' */ }
|
||||
}
|
||||
try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ }
|
||||
if (p.charAt(0) !== '/') p = '/' + p;
|
||||
return p.slice(0, 300);
|
||||
}
|
||||
|
||||
const stripWww = (h) => String(h).toLowerCase().replace(/^www\./, '');
|
||||
|
||||
function refHost(referrer, ownHost) {
|
||||
if (!referrer) return '';
|
||||
try {
|
||||
const u = new URL(String(referrer));
|
||||
if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';
|
||||
const h = u.hostname.toLowerCase();
|
||||
if (!h || stripWww(h) === stripWww(ownHost || '')) return '';
|
||||
return h;
|
||||
} catch (e) { return ''; }
|
||||
}
|
||||
|
||||
// Nur Tag-Namen mit :nth-of-type, keine IDs/Klassen (koennten personenbezogene Texte enthalten).
|
||||
function cssPath(el) {
|
||||
const parts = [];
|
||||
let n = el;
|
||||
while (n && n.tagName && parts.length < 6) {
|
||||
const tag = String(n.tagName).toLowerCase().replace(/[^a-z0-9-]/g, '');
|
||||
if (!tag) break;
|
||||
let idx = 1;
|
||||
for (let s = n.previousElementSibling; s; s = s.previousElementSibling) {
|
||||
if (String(s.tagName).toLowerCase() === String(n.tagName).toLowerCase()) idx++;
|
||||
}
|
||||
parts.unshift(tag + ':nth-of-type(' + idx + ')');
|
||||
if (tag === 'body' || tag === 'html') break;
|
||||
n = n.parentNode;
|
||||
}
|
||||
let out = parts.join(' > ');
|
||||
while (out.length > 200 && parts.length > 1) { parts.shift(); out = parts.join(' > '); }
|
||||
return out.slice(0, 200);
|
||||
}
|
||||
|
||||
function parseHref(a) {
|
||||
const href = a && (typeof a.href === 'string' && a.href ? a.href : (a.getAttribute && a.getAttribute('href'))) || '';
|
||||
return String(href).trim();
|
||||
}
|
||||
|
||||
function linkHost(a) {
|
||||
try { return new URL(parseHref(a)).hostname.toLowerCase(); } catch (e) { return ''; }
|
||||
}
|
||||
|
||||
function contactKind(a, ownHost) {
|
||||
const href = parseHref(a);
|
||||
if (!href) return null;
|
||||
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(href);
|
||||
if (!scheme) return null; // relativ, Anker
|
||||
const s = scheme[1].toLowerCase();
|
||||
if (s === 'tel') return 'tel';
|
||||
if (s === 'mailto') return 'mail';
|
||||
if (s === 'whatsapp') return 'wa';
|
||||
if (s !== 'http' && s !== 'https') return null;
|
||||
const host = linkHost(a);
|
||||
if (!host) return null;
|
||||
if (host === 'wa.me' || host === 'api.whatsapp.com') return 'wa';
|
||||
const own = ownHost == null ? (typeof location !== 'undefined' ? location.hostname : '') : ownHost;
|
||||
return stripWww(host) === stripWww(own || '') ? null : 'out';
|
||||
}
|
||||
|
||||
// Banner-Fassung je Variante: 'eigen' fuer WEBklars eigene Websites, alles andere ist die Kunden-Fassung.
|
||||
function bannerFor(variant) {
|
||||
return variant === 'eigen'
|
||||
? { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN }
|
||||
: { version: BANNER_VERSION, text: BANNER_TEXT };
|
||||
}
|
||||
|
||||
function readConsent(storage, now, version = BANNER_VERSION) {
|
||||
try {
|
||||
const raw = storage.getItem(CONSENT_KEY);
|
||||
if (!raw) return null;
|
||||
const c = JSON.parse(raw);
|
||||
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
|
||||
if (c.bv !== version) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
|
||||
const age = now - c.t;
|
||||
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
|
||||
return { v: 1, a: c.a, t: c.t, bv: c.bv };
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
function contrastOk(hex) {
|
||||
if (typeof hex !== 'string' || !/^#[0-9a-f]{6}$/i.test(hex)) return false;
|
||||
const lin = (i) => {
|
||||
const c = parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16) / 255;
|
||||
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
||||
};
|
||||
const L = 0.2126 * lin(0) + 0.7152 * lin(1) + 0.0722 * lin(2);
|
||||
return 1.05 / (L + 0.05) >= 4.5;
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
}
|
||||
|
||||
// WEBklar Analyse: Einwilligungs-Banner + Ereignisse. Laeuft auf oeffentlichen Kunden-Websites.
|
||||
// Keine Cookies, keine Kennung im Browser. Ohne Wahl nur pv {t,site,path}; ref/vp/cx/ck/ct/en nur nach Annehmen;
|
||||
// nach Ablehnen oder bei GPC/DNT geht nichts mehr raus.
|
||||
|
||||
function main() {
|
||||
const DOC = document;
|
||||
const WIN = window;
|
||||
if (WIN.__wkAnalytics) return; // doppelt eingebunden
|
||||
WIN.__wkAnalytics = true;
|
||||
const SELF = DOC.currentScript && DOC.currentScript.getAttribute && DOC.currentScript.getAttribute('data-site')
|
||||
? DOC.currentScript : DOC.querySelector('script[src*="/a/w.js"]');
|
||||
if (!SELF) return;
|
||||
const site = String(SELF.getAttribute('data-site') || '');
|
||||
if (!/^[a-z0-9]{16,40}$/.test(site)) return;
|
||||
const colorAttr = String(SELF.getAttribute('data-color') || '');
|
||||
const accent = contrastOk(colorAttr) ? colorAttr : '#111111';
|
||||
// data-variant="eigen": Banner-Fassung fuer WEBklars eigene Websites (Sie-Form, ohne Auftragsverarbeiter)
|
||||
const banner = bannerFor(String(SELF.getAttribute('data-variant') || ''));
|
||||
const src = String(SELF.src || SELF.getAttribute('src') || '').replace(/[?#].*$/, '');
|
||||
const base = src.replace(/[^/]*$/, '');
|
||||
const endpoint = base + 'e';
|
||||
|
||||
// ---- Heatmap-Modus: nichts senden, kein Banner ----
|
||||
const qs = new URLSearchParams(WIN.location.search);
|
||||
const heatToken = qs.get('__wk_heat');
|
||||
if (heatToken) {
|
||||
// __wk_embed=1: Ansicht im Geraeterahmen des Kundenportals (h.js ohne Leiste, Links gesperrt)
|
||||
const embed = qs.get('__wk_embed') === '1';
|
||||
qs.delete('__wk_heat');
|
||||
qs.delete('__wk_embed');
|
||||
const rest = qs.toString();
|
||||
try { WIN.history.replaceState(null, '', WIN.location.pathname + (rest ? '?' + rest : '') + (WIN.location.hash || '')); } catch (e) { /* egal */ }
|
||||
const s = DOC.createElement('script');
|
||||
s.setAttribute('src', base + 'h.js');
|
||||
s.setAttribute('data-token', heatToken);
|
||||
if (embed) s.setAttribute('data-embed', '1');
|
||||
(DOC.head || DOC.documentElement).appendChild(s);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Senden: Fehler werden verschluckt ----
|
||||
function send(obj) {
|
||||
try {
|
||||
const body = JSON.stringify(obj);
|
||||
fetch(endpoint, { method: 'POST', body, keepalive: true, credentials: 'omit', headers: { 'Content-Type': 'text/plain' } }).catch(function () {});
|
||||
} catch (e) { /* egal */ }
|
||||
}
|
||||
const safe = (fn) => function () { try { return fn.apply(this, arguments); } catch (e) { /* nie die Seite stoeren */ } };
|
||||
const vp = () => vpClass(WIN.innerWidth);
|
||||
const ownHost = () => WIN.location.hostname;
|
||||
|
||||
// ---- Widerspruch (GPC/DNT) und Wahl: vor jedem Senden und vor jedem Hook ----
|
||||
let storage = null;
|
||||
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
|
||||
const dnt = (v) => v === '1' || v === 'yes';
|
||||
const optOut = navigator.globalPrivacyControl === true || dnt(navigator.doNotTrack) || dnt(WIN.doNotTrack) || dnt(navigator.msDoNotTrack);
|
||||
if (optOut) return; // wie Ablehnen: nichts senden, kein Banner, keine Hooks
|
||||
const stored = readConsent(storage, Date.now(), banner.version);
|
||||
let memChoice = null;
|
||||
let tracking = false;
|
||||
// Wahl bei jedem Aufruf frisch lesen (Speicher, sonst nur fuer diese Seite gemerkt)
|
||||
function choiceNow() {
|
||||
const c = readConsent(storage, Date.now(), banner.version);
|
||||
return c ? c.a : (memChoice ? memChoice.a : null);
|
||||
}
|
||||
const counting = () => choiceNow() !== 'reject';
|
||||
|
||||
// ---- Seitenaufruf (pv): ohne Wahl nur Seitenadresse, ref/vp nur nach Annehmen ----
|
||||
let lastPath = null;
|
||||
let pvStart = Date.now();
|
||||
let maxScroll = 0;
|
||||
let enSent = false;
|
||||
let pvSent = false;
|
||||
let cxSent = false;
|
||||
function sendPv(first) {
|
||||
if (!counting()) return;
|
||||
lastPath = cleanPath(WIN.location.pathname);
|
||||
pvStart = Date.now(); maxScroll = 0; enSent = false;
|
||||
const ev = { t: 'pv', site, path: lastPath };
|
||||
if (choiceNow() === 'accept') {
|
||||
if (first) { ev.ref = refHost(DOC.referrer, ownHost()); cxSent = true; }
|
||||
ev.vp = vp();
|
||||
}
|
||||
pvSent = true;
|
||||
send(ev);
|
||||
}
|
||||
let navTimer = null;
|
||||
function scheduleNav() {
|
||||
if (navTimer || !counting()) return;
|
||||
navTimer = setTimeout(safe(function () {
|
||||
navTimer = null;
|
||||
if (!counting() || cleanPath(WIN.location.pathname) === lastPath) return;
|
||||
// SPA-Seitenwechsel (Router mit preventDefault, onLinkClick greift dann nicht): en der vorigen Seite zuerst
|
||||
sendEn();
|
||||
sendPv(false);
|
||||
}), 300);
|
||||
}
|
||||
let hooksOn = false;
|
||||
function ensureHooks() {
|
||||
if (hooksOn) return;
|
||||
hooksOn = true;
|
||||
WIN.addEventListener('pageshow', safe(function (e) { if (e && e.persisted) sendPv(false); }));
|
||||
try {
|
||||
const orig = WIN.history.pushState;
|
||||
WIN.history.pushState = function () { const r = orig.apply(this, arguments); try { scheduleNav(); } catch (e) { /* egal */ } return r; };
|
||||
WIN.addEventListener('popstate', safe(scheduleNav));
|
||||
} catch (e) { /* egal */ }
|
||||
}
|
||||
// Nach "Annehmen": Herkunft und Fenstergroesse einmal nachreichen, oder (noch kein pv) pv mit Daten senden
|
||||
function onAccept() {
|
||||
ensureHooks();
|
||||
if (!pvSent) { sendPv(true); return; }
|
||||
if (cxSent) return;
|
||||
cxSent = true;
|
||||
send({ t: 'cx', site, path: lastPath, ref: refHost(DOC.referrer, ownHost()), vp: vp() });
|
||||
}
|
||||
if (choiceNow() !== 'reject') { sendPv(true); ensureHooks(); }
|
||||
|
||||
// ---- Tracking (nur nach Annehmen erreichbar) ----
|
||||
const hosts = [];
|
||||
const scrollPct = () => {
|
||||
const h = DOC.documentElement.scrollHeight || 1;
|
||||
return Math.max(0, Math.min(100, Math.round(((WIN.scrollY || 0) + WIN.innerHeight) / h * 100)));
|
||||
};
|
||||
const rel = (v, total) => Math.max(0, Math.min(1, Math.round(v / (total || 1) * 10000) / 10000));
|
||||
|
||||
const onClick = safe(function (e) {
|
||||
if (!allowed()) return;
|
||||
const t = e.target;
|
||||
if (!t || hosts.indexOf(t) !== -1) return;
|
||||
const dw = DOC.documentElement.scrollWidth, dh = DOC.documentElement.scrollHeight;
|
||||
const px = typeof e.pageX === 'number' ? e.pageX : (e.clientX || 0) + (WIN.scrollX || 0);
|
||||
const py = typeof e.pageY === 'number' ? e.pageY : (e.clientY || 0) + (WIN.scrollY || 0);
|
||||
const path = cleanPath(WIN.location.pathname);
|
||||
send({ t: 'ck', site, path, vp: vp(), x: rel(px, dw), y: rel(py, dh), sel: cssPath(t) });
|
||||
let a = t;
|
||||
while (a && String(a.tagName).toUpperCase() !== 'A') a = a.parentNode;
|
||||
const kind = a ? contactKind(a, ownHost()) : null;
|
||||
if (kind) {
|
||||
const ev = { t: 'ct', site, path, kind };
|
||||
if (kind === 'out') ev.host = linkHost(a);
|
||||
send(ev);
|
||||
}
|
||||
});
|
||||
const onScroll = safe(function () { maxScroll = Math.max(maxScroll, scrollPct()); });
|
||||
function sendEn() {
|
||||
if (enSent || !allowed()) return;
|
||||
enSent = true;
|
||||
send({ t: 'en', site, path: lastPath || cleanPath(WIN.location.pathname), vp: vp(),
|
||||
sec: Math.max(0, Math.min(3600, Math.round((Date.now() - pvStart) / 1000))), scroll: Math.max(maxScroll, scrollPct()) });
|
||||
}
|
||||
const onLeave = safe(sendEn);
|
||||
// Interner Seitenwechsel per Link: en sofort senden, der Beacon beim pagehide geht in Chromium oft verloren.
|
||||
// Bubbling-Listener am Dokument: laeuft nach den Handlern der Seite, defaultPrevented ist dann bekannt.
|
||||
const onLinkClick = safe(function (e) {
|
||||
if (e.defaultPrevented || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || (typeof e.button === 'number' && e.button !== 0)) return;
|
||||
let a = e.target;
|
||||
while (a && String(a.tagName).toUpperCase() !== 'A') a = a.parentNode;
|
||||
if (!a || hosts.indexOf(e.target) !== -1) return;
|
||||
const tg = String(a.getAttribute('target') || '').toLowerCase();
|
||||
if ((tg && tg !== '_self') || a.getAttribute('download') !== null) return;
|
||||
const u = new URL(String(a.href || a.getAttribute('href') || ''), WIN.location.href);
|
||||
if (u.protocol !== 'http:' && u.protocol !== 'https:') return;
|
||||
if (u.origin !== WIN.location.origin || u.pathname === WIN.location.pathname) return;
|
||||
sendEn();
|
||||
});
|
||||
const onVis = safe(function () { if (DOC.visibilityState === 'hidden') onLeave(); });
|
||||
|
||||
// Einwilligung unmittelbar vor jedem Senden pruefen (z.B. in einem anderen Tab widerrufen)
|
||||
function allowed() {
|
||||
const ok = choiceNow() === 'accept';
|
||||
if (!ok && tracking) stopTracking();
|
||||
return ok;
|
||||
}
|
||||
|
||||
function startTracking() {
|
||||
if (tracking) return;
|
||||
tracking = true;
|
||||
DOC.addEventListener('click', onClick, { capture: true, passive: true });
|
||||
DOC.addEventListener('click', onLinkClick, { passive: true });
|
||||
DOC.addEventListener('visibilitychange', onVis);
|
||||
WIN.addEventListener('pagehide', onLeave);
|
||||
WIN.addEventListener('scroll', onScroll, { passive: true });
|
||||
}
|
||||
function stopTracking() {
|
||||
tracking = false;
|
||||
DOC.removeEventListener('click', onClick, { capture: true });
|
||||
DOC.removeEventListener('click', onLinkClick);
|
||||
DOC.removeEventListener('visibilitychange', onVis);
|
||||
WIN.removeEventListener('pagehide', onLeave);
|
||||
WIN.removeEventListener('scroll', onScroll);
|
||||
}
|
||||
|
||||
// ---- Einwilligung ----
|
||||
let bannerHost = null;
|
||||
let linkHostEl = null;
|
||||
|
||||
function choose(a) {
|
||||
memChoice = null;
|
||||
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now(), bv: banner.version })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
|
||||
send({ t: 'bn', site, a });
|
||||
if (a === 'accept') { startTracking(); onAccept(); } else stopTracking();
|
||||
closeBanner();
|
||||
}
|
||||
|
||||
function el(tag, attrs, text) {
|
||||
const n = DOC.createElement(tag);
|
||||
for (const k in attrs || {}) n.setAttribute(k, attrs[k]);
|
||||
if (text != null) n.textContent = text;
|
||||
return n;
|
||||
}
|
||||
|
||||
const CSS = ':host{all:initial}' +
|
||||
'*{box-sizing:border-box}' +
|
||||
'.bar{display:flex;align-items:center;justify-content:center;gap:16px;padding:14px 20px;background:#fff;color:#111;border-top:1px solid #d0d0d0;box-shadow:0 -2px 12px rgba(0,0,0,.12);font:15px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}' +
|
||||
'.t{margin:0;max-width:720px}' +
|
||||
'.t a{color:#111;text-decoration:underline}' +
|
||||
'.r{display:flex;gap:10px;flex:none}' +
|
||||
'button{font:600 15px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;width:150px;height:44px;padding:0 20px;border:2px solid ' + accent + ';border-radius:8px;cursor:pointer;background:' + accent + ';color:#fff}' +
|
||||
'button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' +
|
||||
'@media (max-width:767px){.bar{display:block;margin:8px;padding:16px;border:1px solid #d0d0d0;border-radius:12px}.r{flex-direction:column;margin-top:12px}button{width:100%}}';
|
||||
|
||||
let prevPad = '';
|
||||
function applyPad() {
|
||||
if (!bannerHost) return;
|
||||
const h = Math.ceil(bannerHost.getBoundingClientRect().height || bannerHost.offsetHeight || 0);
|
||||
DOC.body.style.paddingBottom = h + 'px';
|
||||
}
|
||||
const onResize = safe(applyPad);
|
||||
function openBanner(focus) {
|
||||
if (bannerHost) return;
|
||||
const prev = DOC.activeElement;
|
||||
bannerHost = DOC.createElement('div');
|
||||
const st = bannerHost.style;
|
||||
st.position = 'fixed'; st.left = '0'; st.right = '0'; st.bottom = '0'; st.zIndex = '2147483647';
|
||||
hosts.push(bannerHost);
|
||||
const root = bannerHost.attachShadow({ mode: 'open' });
|
||||
root.appendChild(el('style', {}, CSS));
|
||||
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': BANNER_ARIA });
|
||||
const p = el('p', { class: 't' }, banner.text);
|
||||
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, BANNER_LINK_LABEL));
|
||||
const row = el('div', { class: 'r' });
|
||||
const no = el('button', { type: 'button' }, 'Ablehnen');
|
||||
const yes = el('button', { type: 'button' }, 'Annehmen');
|
||||
no.addEventListener('click', safe(function () { choose('reject'); if (prev && prev.focus) prev.focus(); }));
|
||||
yes.addEventListener('click', safe(function () { choose('accept'); if (prev && prev.focus) prev.focus(); }));
|
||||
row.appendChild(no); row.appendChild(yes);
|
||||
bar.appendChild(p); bar.appendChild(row);
|
||||
root.appendChild(bar);
|
||||
DOC.body.appendChild(bannerHost);
|
||||
// Platz unten schaffen, damit Footer-Links (Impressum, Datenschutz) erreichbar bleiben
|
||||
prevPad = DOC.body.style.paddingBottom;
|
||||
applyPad();
|
||||
WIN.addEventListener('resize', onResize);
|
||||
if (linkHostEl) linkHostEl.style.display = 'none';
|
||||
if (focus === true) no.focus();
|
||||
}
|
||||
function closeBanner() {
|
||||
if (!bannerHost) return;
|
||||
const i = hosts.indexOf(bannerHost);
|
||||
if (i !== -1) hosts.splice(i, 1);
|
||||
bannerHost.remove();
|
||||
bannerHost = null;
|
||||
WIN.removeEventListener('resize', onResize);
|
||||
DOC.body.style.paddingBottom = prevPad;
|
||||
if (linkHostEl) linkHostEl.style.display = '';
|
||||
}
|
||||
|
||||
// Einstellungsknopf: angeknabberter Keks unten rechts, dreht sich langsam (nicht bei reduzierter Bewegung).
|
||||
// Beim Darueberfahren haelt er an und zeigt "Datenschutz-Einstellungen"; das ist auch sein Name fuer Screenreader.
|
||||
// Inline-SVG statt data:-Grafik, damit eine strenge CSP der Website (img-src) ihn nicht blockiert.
|
||||
const SETTINGS_CSS = ':host{all:initial}' +
|
||||
'.b{position:relative;display:block;width:44px;height:44px;padding:2px;margin:0;border:0;border-radius:50%;background:transparent;cursor:pointer;-webkit-tap-highlight-color:transparent}' +
|
||||
'.b:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' +
|
||||
'.c{display:block;width:40px;height:40px;filter:drop-shadow(0 2px 3px rgba(0,0,0,.35));animation:wk-spin 9s linear infinite}' +
|
||||
'.b:hover .c,.b:focus-visible .c{animation-play-state:paused}' +
|
||||
'.l{position:absolute;right:52px;top:50%;transform:translate(6px,-50%);padding:7px 12px;border-radius:999px;background:#fff;color:#222;box-shadow:0 2px 10px rgba(0,0,0,.18);font:600 13px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s,transform .15s}' +
|
||||
'.b:hover .l,.b:focus-visible .l{opacity:1;transform:translate(0,-50%)}' +
|
||||
'@keyframes wk-spin{to{transform:rotate(360deg)}}' +
|
||||
'@media (prefers-reduced-motion:reduce){.c{animation:none}.l{transition:none}}';
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
function svgEl(tag, attrs) {
|
||||
const n = DOC.createElementNS(SVG_NS, tag);
|
||||
for (const k in attrs) n.setAttribute(k, attrs[k]);
|
||||
return n;
|
||||
}
|
||||
function cookieSvg() {
|
||||
const svg = svgEl('svg', { class: 'c', viewBox: '0 0 64 64', 'aria-hidden': 'true', focusable: 'false' });
|
||||
// Umriss mit Bissspur oben rechts (zwei Bissboegen, dann der grosse Kreis zurueck)
|
||||
svg.appendChild(svgEl('path', { d: 'M44.84 6A10 10 0 0 0 57.5 19.37A7.5 7.5 0 0 0 60.96 33.5A29 29 0 1 1 44.84 6Z', fill: '#dfa25b', stroke: '#a5652c', 'stroke-width': '3', 'stroke-linejoin': 'round' }));
|
||||
const chips = [[19, 22, 4.2, 3.3, -20], [31, 14, 3.2, 2.6, 15], [33, 33, 4, 3.2, 30], [46, 41, 3.6, 2.8, -35], [21, 45, 4, 3, 10], [36, 52, 3, 2.4, -10], [11, 34, 2.6, 2.1, 40], [47, 31, 2.4, 2, 0]];
|
||||
for (const c of chips) svg.appendChild(svgEl('ellipse', { cx: c[0], cy: c[1], rx: c[2], ry: c[3], transform: 'rotate(' + c[4] + ' ' + c[0] + ' ' + c[1] + ')', fill: '#5a3216' }));
|
||||
const crumbs = [[26, 28, 1.1], [40, 22, 1], [28, 40, 1.2], [14, 26, 0.9], [40, 46, 1]];
|
||||
for (const c of crumbs) svg.appendChild(svgEl('circle', { cx: c[0], cy: c[1], r: c[2], fill: '#f3c98c' }));
|
||||
return svg;
|
||||
}
|
||||
function addSettingsLink() {
|
||||
linkHostEl = DOC.createElement('div');
|
||||
const st = linkHostEl.style;
|
||||
st.position = 'fixed'; st.right = '12px'; st.bottom = '12px'; st.zIndex = '2147483646';
|
||||
hosts.push(linkHostEl);
|
||||
const root = linkHostEl.attachShadow({ mode: 'open' });
|
||||
root.appendChild(el('style', {}, SETTINGS_CSS));
|
||||
const b = el('button', { type: 'button', class: 'b', 'aria-label': SETTINGS_LABEL });
|
||||
b.appendChild(el('span', { class: 'l', 'aria-hidden': 'true' }, SETTINGS_LABEL));
|
||||
b.appendChild(cookieSvg());
|
||||
b.addEventListener('click', safe(function () { openBanner(true); }));
|
||||
root.appendChild(b);
|
||||
DOC.body.appendChild(linkHostEl);
|
||||
}
|
||||
|
||||
DOC.addEventListener('click', safe(function (e) {
|
||||
const t = e.target;
|
||||
if (t && t.closest && t.closest('[data-webklar-consent]')) { if (e.preventDefault) e.preventDefault(); openBanner(true); }
|
||||
}));
|
||||
if (!DOC.querySelector('[data-webklar-consent]')) addSettingsLink();
|
||||
|
||||
// Einwilligung in anderem Tab geaendert
|
||||
WIN.addEventListener('storage', safe(function (e) {
|
||||
if (e.key !== null && e.key !== CONSENT_KEY) return;
|
||||
const c = readConsent(storage, Date.now(), banner.version);
|
||||
if (choiceNow() === 'accept') { startTracking(); onAccept(); } else stopTracking();
|
||||
if (c) closeBanner();
|
||||
}));
|
||||
|
||||
if (stored) {
|
||||
if (stored.a === 'accept') startTracking();
|
||||
} else {
|
||||
openBanner();
|
||||
send({ t: 'bn', site, a: 'shown' });
|
||||
}
|
||||
}
|
||||
|
||||
try { main(); } catch (e) { /* nie die Seite stoeren */ }
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,129 @@
|
||||
// WEBklar Analyse: Heatmap-Overlay (wird von w.js nur mit gueltigem ?__wk_heat=Token geladen).
|
||||
// data-embed="1" (nur in einem Rahmen): Ansicht im Geraeterahmen des Kundenportals. Dann ohne Leiste,
|
||||
// Links und Formulare gesperrt, und die Klickzahl geht per postMessage nur an das Portal (Herkunft von h.js).
|
||||
|
||||
function main() {
|
||||
const me = document.currentScript;
|
||||
const token = me && me.getAttribute('data-token');
|
||||
const src = String((me && (me.src || me.getAttribute('src'))) || '').replace(/[?#].*$/, '');
|
||||
const base = src.replace(/[^/]*$/, '');
|
||||
if (!token || !base) return;
|
||||
|
||||
const de = document.documentElement;
|
||||
let portal = '';
|
||||
try { portal = new URL(base).origin; } catch (e) { /* ohne Portal keine Einbettung */ }
|
||||
const embed = !!portal && me.getAttribute('data-embed') === '1' && window.parent && window.parent !== window;
|
||||
function tell(state, count) {
|
||||
if (!embed) return;
|
||||
try { window.parent.postMessage({ wk: 'heat', state: state, count: count }, portal); } catch (e) { /* egal */ }
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const cs = canvas.style;
|
||||
cs.position = 'absolute'; cs.left = '0'; cs.top = '0'; cs.pointerEvents = 'none'; cs.zIndex = '2147483646';
|
||||
document.body.appendChild(canvas);
|
||||
|
||||
let points = [];
|
||||
let lastW = 0, lastH = 0, timer = 0;
|
||||
|
||||
// Farbverlauf: transparent -> blau -> gruen -> gelb -> rot
|
||||
const ramp = (function () {
|
||||
const c = document.createElement('canvas'); c.width = 256; c.height = 1;
|
||||
const g = c.getContext('2d');
|
||||
const lg = g.createLinearGradient(0, 0, 256, 0);
|
||||
lg.addColorStop(0.25, 'rgb(0,0,255)'); lg.addColorStop(0.55, 'rgb(0,255,0)'); lg.addColorStop(0.85, 'rgb(255,255,0)'); lg.addColorStop(1, 'rgb(255,0,0)');
|
||||
g.fillStyle = lg; g.fillRect(0, 0, 256, 1);
|
||||
return g.getImageData(0, 0, 256, 1).data;
|
||||
})();
|
||||
|
||||
if (embed) {
|
||||
// Nur ansehen: kein Seitenwechsel (er wuerde zaehlen und das Banner zeigen)
|
||||
const block = function (e) {
|
||||
const t = e.target;
|
||||
if (e.type === 'submit' || (t && t.closest && t.closest('a,area'))) { e.preventDefault(); e.stopPropagation(); }
|
||||
};
|
||||
window.addEventListener('click', block, true);
|
||||
window.addEventListener('submit', block, true);
|
||||
// Handy und Tablet haben ueberlagernde Scrollleisten; so stimmt die Breite wie auf dem Geraet
|
||||
const st = document.createElement('style');
|
||||
st.textContent = 'html{scrollbar-width:none}html::-webkit-scrollbar{display:none}';
|
||||
(document.head || de).appendChild(st);
|
||||
start(function (n) { tell('ready', n); }, function () { tell('error', 0); });
|
||||
return;
|
||||
}
|
||||
|
||||
const host = document.createElement('div');
|
||||
host.style.position = 'fixed'; host.style.left = '0'; host.style.right = '0'; host.style.top = '0'; host.style.zIndex = '2147483647';
|
||||
const root = host.attachShadow({ mode: 'open' });
|
||||
const style = document.createElement('style');
|
||||
style.textContent = ':host{all:initial}.bar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;background:#111;color:#fff;font:14px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}button{font:600 14px/1 system-ui,sans-serif;padding:8px 14px;border:2px solid #fff;border-radius:6px;background:#fff;color:#111;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}';
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'bar';
|
||||
const msg = document.createElement('span');
|
||||
msg.textContent = 'Heatmap wird geladen ...';
|
||||
const close = document.createElement('button');
|
||||
close.setAttribute('type', 'button');
|
||||
close.textContent = 'Schließen';
|
||||
bar.appendChild(msg); bar.appendChild(close);
|
||||
root.appendChild(style); root.appendChild(bar);
|
||||
document.body.appendChild(host);
|
||||
|
||||
function draw() {
|
||||
const w = Math.max(de.scrollWidth, de.clientWidth, 1);
|
||||
const h = Math.max(de.scrollHeight, de.clientHeight, 1);
|
||||
lastW = w; lastH = h;
|
||||
// Zeichenflaeche begrenzen (Speicher): hoechstens ca. 4 Mio. Pixel
|
||||
const scale = Math.min(1, Math.sqrt(4e6 / (w * h)));
|
||||
const cw = Math.max(1, Math.floor(w * scale)), ch = Math.max(1, Math.floor(h * scale));
|
||||
canvas.width = cw; canvas.height = ch;
|
||||
cs.width = w + 'px'; cs.height = h + 'px';
|
||||
const g = canvas.getContext('2d');
|
||||
g.clearRect(0, 0, cw, ch);
|
||||
const r = Math.max(6, 30 * scale);
|
||||
for (const p of points) {
|
||||
const x = p.x * cw, y = p.y * ch;
|
||||
const rg = g.createRadialGradient(x, y, 0, x, y, r);
|
||||
rg.addColorStop(0, 'rgba(0,0,0,0.25)'); rg.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
g.fillStyle = rg; g.fillRect(x - r, y - r, r * 2, r * 2);
|
||||
}
|
||||
// Deckkraft-Stufen in Farben umsetzen
|
||||
const img = g.getImageData(0, 0, cw, ch);
|
||||
const d = img.data;
|
||||
for (let i = 3; i < d.length; i += 4) {
|
||||
const a = d[i];
|
||||
if (!a) continue;
|
||||
const k = Math.min(255, a * 3) * 4;
|
||||
d[i - 3] = ramp[k]; d[i - 2] = ramp[k + 1]; d[i - 1] = ramp[k + 2];
|
||||
d[i] = Math.min(200, 60 + a);
|
||||
}
|
||||
g.putImageData(img, 0, 0);
|
||||
}
|
||||
|
||||
function redrawIfChanged() {
|
||||
if (Math.max(de.scrollWidth, de.clientWidth) !== lastW || Math.max(de.scrollHeight, de.clientHeight) !== lastH) draw();
|
||||
}
|
||||
function stop() {
|
||||
clearInterval(timer);
|
||||
window.removeEventListener('resize', draw);
|
||||
host.remove(); canvas.remove();
|
||||
}
|
||||
close.addEventListener('click', stop);
|
||||
start(function (n) { msg.textContent = 'Heatmap – ' + n + ' Klicks (letzte 30 Tage)'; },
|
||||
function () { msg.textContent = 'Heatmap-Link abgelaufen oder ungültig'; });
|
||||
|
||||
// Punkte laden, zeichnen und bei Groessenaenderung neu zeichnen
|
||||
function start(onReady, onError) {
|
||||
window.addEventListener('resize', draw);
|
||||
fetch(base + 'heat?token=' + encodeURIComponent(token), { credentials: 'omit' })
|
||||
.then(function (r) { if (!r.ok) throw new Error('heat'); return r.json(); })
|
||||
.then(function (j) {
|
||||
points = (j && Array.isArray(j.points) ? j.points : []).filter(function (p) { return p && p.x >= 0 && p.x <= 1 && p.y >= 0 && p.y <= 1; });
|
||||
draw();
|
||||
timer = setInterval(redrawIfChanged, 1000);
|
||||
onReady(points.length);
|
||||
})
|
||||
.catch(function () { onError(); });
|
||||
}
|
||||
}
|
||||
|
||||
try { main(); } catch (e) { /* nie die Seite stoeren */ }
|
||||
@@ -0,0 +1,335 @@
|
||||
// WEBklar Analyse: Einwilligungs-Banner + Ereignisse. Laeuft auf oeffentlichen Kunden-Websites.
|
||||
// Keine Cookies, keine Kennung im Browser. Ohne Wahl nur pv {t,site,path}; ref/vp/cx/ck/ct/en nur nach Annehmen;
|
||||
// nach Ablehnen oder bei GPC/DNT geht nichts mehr raus.
|
||||
import { cleanPath, vpClass, refHost, cssPath, contactKind, linkHost, readConsent, contrastOk, bannerFor, CONSENT_KEY, BANNER_LINK_LABEL, BANNER_ARIA, SETTINGS_LABEL } from './core.js';
|
||||
|
||||
function main() {
|
||||
const DOC = document;
|
||||
const WIN = window;
|
||||
if (WIN.__wkAnalytics) return; // doppelt eingebunden
|
||||
WIN.__wkAnalytics = true;
|
||||
const SELF = DOC.currentScript && DOC.currentScript.getAttribute && DOC.currentScript.getAttribute('data-site')
|
||||
? DOC.currentScript : DOC.querySelector('script[src*="/a/w.js"]');
|
||||
if (!SELF) return;
|
||||
const site = String(SELF.getAttribute('data-site') || '');
|
||||
if (!/^[a-z0-9]{16,40}$/.test(site)) return;
|
||||
const colorAttr = String(SELF.getAttribute('data-color') || '');
|
||||
const accent = contrastOk(colorAttr) ? colorAttr : '#111111';
|
||||
// data-variant="eigen": Banner-Fassung fuer WEBklars eigene Websites (Sie-Form, ohne Auftragsverarbeiter)
|
||||
const banner = bannerFor(String(SELF.getAttribute('data-variant') || ''));
|
||||
const src = String(SELF.src || SELF.getAttribute('src') || '').replace(/[?#].*$/, '');
|
||||
const base = src.replace(/[^/]*$/, '');
|
||||
const endpoint = base + 'e';
|
||||
|
||||
// ---- Heatmap-Modus: nichts senden, kein Banner ----
|
||||
const qs = new URLSearchParams(WIN.location.search);
|
||||
const heatToken = qs.get('__wk_heat');
|
||||
if (heatToken) {
|
||||
// __wk_embed=1: Ansicht im Geraeterahmen des Kundenportals (h.js ohne Leiste, Links gesperrt)
|
||||
const embed = qs.get('__wk_embed') === '1';
|
||||
qs.delete('__wk_heat');
|
||||
qs.delete('__wk_embed');
|
||||
const rest = qs.toString();
|
||||
try { WIN.history.replaceState(null, '', WIN.location.pathname + (rest ? '?' + rest : '') + (WIN.location.hash || '')); } catch (e) { /* egal */ }
|
||||
const s = DOC.createElement('script');
|
||||
s.setAttribute('src', base + 'h.js');
|
||||
s.setAttribute('data-token', heatToken);
|
||||
if (embed) s.setAttribute('data-embed', '1');
|
||||
(DOC.head || DOC.documentElement).appendChild(s);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Senden: Fehler werden verschluckt ----
|
||||
function send(obj) {
|
||||
try {
|
||||
const body = JSON.stringify(obj);
|
||||
fetch(endpoint, { method: 'POST', body, keepalive: true, credentials: 'omit', headers: { 'Content-Type': 'text/plain' } }).catch(function () {});
|
||||
} catch (e) { /* egal */ }
|
||||
}
|
||||
const safe = (fn) => function () { try { return fn.apply(this, arguments); } catch (e) { /* nie die Seite stoeren */ } };
|
||||
const vp = () => vpClass(WIN.innerWidth);
|
||||
const ownHost = () => WIN.location.hostname;
|
||||
|
||||
// ---- Widerspruch (GPC/DNT) und Wahl: vor jedem Senden und vor jedem Hook ----
|
||||
let storage = null;
|
||||
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
|
||||
const dnt = (v) => v === '1' || v === 'yes';
|
||||
const optOut = navigator.globalPrivacyControl === true || dnt(navigator.doNotTrack) || dnt(WIN.doNotTrack) || dnt(navigator.msDoNotTrack);
|
||||
if (optOut) return; // wie Ablehnen: nichts senden, kein Banner, keine Hooks
|
||||
const stored = readConsent(storage, Date.now(), banner.version);
|
||||
let memChoice = null;
|
||||
let tracking = false;
|
||||
// Wahl bei jedem Aufruf frisch lesen (Speicher, sonst nur fuer diese Seite gemerkt)
|
||||
function choiceNow() {
|
||||
const c = readConsent(storage, Date.now(), banner.version);
|
||||
return c ? c.a : (memChoice ? memChoice.a : null);
|
||||
}
|
||||
const counting = () => choiceNow() !== 'reject';
|
||||
|
||||
// ---- Seitenaufruf (pv): ohne Wahl nur Seitenadresse, ref/vp nur nach Annehmen ----
|
||||
let lastPath = null;
|
||||
let pvStart = Date.now();
|
||||
let maxScroll = 0;
|
||||
let enSent = false;
|
||||
let pvSent = false;
|
||||
let cxSent = false;
|
||||
function sendPv(first) {
|
||||
if (!counting()) return;
|
||||
lastPath = cleanPath(WIN.location.pathname);
|
||||
pvStart = Date.now(); maxScroll = 0; enSent = false;
|
||||
const ev = { t: 'pv', site, path: lastPath };
|
||||
if (choiceNow() === 'accept') {
|
||||
if (first) { ev.ref = refHost(DOC.referrer, ownHost()); cxSent = true; }
|
||||
ev.vp = vp();
|
||||
}
|
||||
pvSent = true;
|
||||
send(ev);
|
||||
}
|
||||
let navTimer = null;
|
||||
function scheduleNav() {
|
||||
if (navTimer || !counting()) return;
|
||||
navTimer = setTimeout(safe(function () {
|
||||
navTimer = null;
|
||||
if (!counting() || cleanPath(WIN.location.pathname) === lastPath) return;
|
||||
// SPA-Seitenwechsel (Router mit preventDefault, onLinkClick greift dann nicht): en der vorigen Seite zuerst
|
||||
sendEn();
|
||||
sendPv(false);
|
||||
}), 300);
|
||||
}
|
||||
let hooksOn = false;
|
||||
function ensureHooks() {
|
||||
if (hooksOn) return;
|
||||
hooksOn = true;
|
||||
WIN.addEventListener('pageshow', safe(function (e) { if (e && e.persisted) sendPv(false); }));
|
||||
try {
|
||||
const orig = WIN.history.pushState;
|
||||
WIN.history.pushState = function () { const r = orig.apply(this, arguments); try { scheduleNav(); } catch (e) { /* egal */ } return r; };
|
||||
WIN.addEventListener('popstate', safe(scheduleNav));
|
||||
} catch (e) { /* egal */ }
|
||||
}
|
||||
// Nach "Annehmen": Herkunft und Fenstergroesse einmal nachreichen, oder (noch kein pv) pv mit Daten senden
|
||||
function onAccept() {
|
||||
ensureHooks();
|
||||
if (!pvSent) { sendPv(true); return; }
|
||||
if (cxSent) return;
|
||||
cxSent = true;
|
||||
send({ t: 'cx', site, path: lastPath, ref: refHost(DOC.referrer, ownHost()), vp: vp() });
|
||||
}
|
||||
if (choiceNow() !== 'reject') { sendPv(true); ensureHooks(); }
|
||||
|
||||
// ---- Tracking (nur nach Annehmen erreichbar) ----
|
||||
const hosts = [];
|
||||
const scrollPct = () => {
|
||||
const h = DOC.documentElement.scrollHeight || 1;
|
||||
return Math.max(0, Math.min(100, Math.round(((WIN.scrollY || 0) + WIN.innerHeight) / h * 100)));
|
||||
};
|
||||
const rel = (v, total) => Math.max(0, Math.min(1, Math.round(v / (total || 1) * 10000) / 10000));
|
||||
|
||||
const onClick = safe(function (e) {
|
||||
if (!allowed()) return;
|
||||
const t = e.target;
|
||||
if (!t || hosts.indexOf(t) !== -1) return;
|
||||
const dw = DOC.documentElement.scrollWidth, dh = DOC.documentElement.scrollHeight;
|
||||
const px = typeof e.pageX === 'number' ? e.pageX : (e.clientX || 0) + (WIN.scrollX || 0);
|
||||
const py = typeof e.pageY === 'number' ? e.pageY : (e.clientY || 0) + (WIN.scrollY || 0);
|
||||
const path = cleanPath(WIN.location.pathname);
|
||||
send({ t: 'ck', site, path, vp: vp(), x: rel(px, dw), y: rel(py, dh), sel: cssPath(t) });
|
||||
let a = t;
|
||||
while (a && String(a.tagName).toUpperCase() !== 'A') a = a.parentNode;
|
||||
const kind = a ? contactKind(a, ownHost()) : null;
|
||||
if (kind) {
|
||||
const ev = { t: 'ct', site, path, kind };
|
||||
if (kind === 'out') ev.host = linkHost(a);
|
||||
send(ev);
|
||||
}
|
||||
});
|
||||
const onScroll = safe(function () { maxScroll = Math.max(maxScroll, scrollPct()); });
|
||||
function sendEn() {
|
||||
if (enSent || !allowed()) return;
|
||||
enSent = true;
|
||||
send({ t: 'en', site, path: lastPath || cleanPath(WIN.location.pathname), vp: vp(),
|
||||
sec: Math.max(0, Math.min(3600, Math.round((Date.now() - pvStart) / 1000))), scroll: Math.max(maxScroll, scrollPct()) });
|
||||
}
|
||||
const onLeave = safe(sendEn);
|
||||
// Interner Seitenwechsel per Link: en sofort senden, der Beacon beim pagehide geht in Chromium oft verloren.
|
||||
// Bubbling-Listener am Dokument: laeuft nach den Handlern der Seite, defaultPrevented ist dann bekannt.
|
||||
const onLinkClick = safe(function (e) {
|
||||
if (e.defaultPrevented || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || (typeof e.button === 'number' && e.button !== 0)) return;
|
||||
let a = e.target;
|
||||
while (a && String(a.tagName).toUpperCase() !== 'A') a = a.parentNode;
|
||||
if (!a || hosts.indexOf(e.target) !== -1) return;
|
||||
const tg = String(a.getAttribute('target') || '').toLowerCase();
|
||||
if ((tg && tg !== '_self') || a.getAttribute('download') !== null) return;
|
||||
const u = new URL(String(a.href || a.getAttribute('href') || ''), WIN.location.href);
|
||||
if (u.protocol !== 'http:' && u.protocol !== 'https:') return;
|
||||
if (u.origin !== WIN.location.origin || u.pathname === WIN.location.pathname) return;
|
||||
sendEn();
|
||||
});
|
||||
const onVis = safe(function () { if (DOC.visibilityState === 'hidden') onLeave(); });
|
||||
|
||||
// Einwilligung unmittelbar vor jedem Senden pruefen (z.B. in einem anderen Tab widerrufen)
|
||||
function allowed() {
|
||||
const ok = choiceNow() === 'accept';
|
||||
if (!ok && tracking) stopTracking();
|
||||
return ok;
|
||||
}
|
||||
|
||||
function startTracking() {
|
||||
if (tracking) return;
|
||||
tracking = true;
|
||||
DOC.addEventListener('click', onClick, { capture: true, passive: true });
|
||||
DOC.addEventListener('click', onLinkClick, { passive: true });
|
||||
DOC.addEventListener('visibilitychange', onVis);
|
||||
WIN.addEventListener('pagehide', onLeave);
|
||||
WIN.addEventListener('scroll', onScroll, { passive: true });
|
||||
}
|
||||
function stopTracking() {
|
||||
tracking = false;
|
||||
DOC.removeEventListener('click', onClick, { capture: true });
|
||||
DOC.removeEventListener('click', onLinkClick);
|
||||
DOC.removeEventListener('visibilitychange', onVis);
|
||||
WIN.removeEventListener('pagehide', onLeave);
|
||||
WIN.removeEventListener('scroll', onScroll);
|
||||
}
|
||||
|
||||
// ---- Einwilligung ----
|
||||
let bannerHost = null;
|
||||
let linkHostEl = null;
|
||||
|
||||
function choose(a) {
|
||||
memChoice = null;
|
||||
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now(), bv: banner.version })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
|
||||
send({ t: 'bn', site, a });
|
||||
if (a === 'accept') { startTracking(); onAccept(); } else stopTracking();
|
||||
closeBanner();
|
||||
}
|
||||
|
||||
function el(tag, attrs, text) {
|
||||
const n = DOC.createElement(tag);
|
||||
for (const k in attrs || {}) n.setAttribute(k, attrs[k]);
|
||||
if (text != null) n.textContent = text;
|
||||
return n;
|
||||
}
|
||||
|
||||
const CSS = ':host{all:initial}' +
|
||||
'*{box-sizing:border-box}' +
|
||||
'.bar{display:flex;align-items:center;justify-content:center;gap:16px;padding:14px 20px;background:#fff;color:#111;border-top:1px solid #d0d0d0;box-shadow:0 -2px 12px rgba(0,0,0,.12);font:15px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}' +
|
||||
'.t{margin:0;max-width:720px}' +
|
||||
'.t a{color:#111;text-decoration:underline}' +
|
||||
'.r{display:flex;gap:10px;flex:none}' +
|
||||
'button{font:600 15px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;width:150px;height:44px;padding:0 20px;border:2px solid ' + accent + ';border-radius:8px;cursor:pointer;background:' + accent + ';color:#fff}' +
|
||||
'button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' +
|
||||
'@media (max-width:767px){.bar{display:block;margin:8px;padding:16px;border:1px solid #d0d0d0;border-radius:12px}.r{flex-direction:column;margin-top:12px}button{width:100%}}';
|
||||
|
||||
let prevPad = '';
|
||||
function applyPad() {
|
||||
if (!bannerHost) return;
|
||||
const h = Math.ceil(bannerHost.getBoundingClientRect().height || bannerHost.offsetHeight || 0);
|
||||
DOC.body.style.paddingBottom = h + 'px';
|
||||
}
|
||||
const onResize = safe(applyPad);
|
||||
function openBanner(focus) {
|
||||
if (bannerHost) return;
|
||||
const prev = DOC.activeElement;
|
||||
bannerHost = DOC.createElement('div');
|
||||
const st = bannerHost.style;
|
||||
st.position = 'fixed'; st.left = '0'; st.right = '0'; st.bottom = '0'; st.zIndex = '2147483647';
|
||||
hosts.push(bannerHost);
|
||||
const root = bannerHost.attachShadow({ mode: 'open' });
|
||||
root.appendChild(el('style', {}, CSS));
|
||||
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': BANNER_ARIA });
|
||||
const p = el('p', { class: 't' }, banner.text);
|
||||
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, BANNER_LINK_LABEL));
|
||||
const row = el('div', { class: 'r' });
|
||||
const no = el('button', { type: 'button' }, 'Ablehnen');
|
||||
const yes = el('button', { type: 'button' }, 'Annehmen');
|
||||
no.addEventListener('click', safe(function () { choose('reject'); if (prev && prev.focus) prev.focus(); }));
|
||||
yes.addEventListener('click', safe(function () { choose('accept'); if (prev && prev.focus) prev.focus(); }));
|
||||
row.appendChild(no); row.appendChild(yes);
|
||||
bar.appendChild(p); bar.appendChild(row);
|
||||
root.appendChild(bar);
|
||||
DOC.body.appendChild(bannerHost);
|
||||
// Platz unten schaffen, damit Footer-Links (Impressum, Datenschutz) erreichbar bleiben
|
||||
prevPad = DOC.body.style.paddingBottom;
|
||||
applyPad();
|
||||
WIN.addEventListener('resize', onResize);
|
||||
if (linkHostEl) linkHostEl.style.display = 'none';
|
||||
if (focus === true) no.focus();
|
||||
}
|
||||
function closeBanner() {
|
||||
if (!bannerHost) return;
|
||||
const i = hosts.indexOf(bannerHost);
|
||||
if (i !== -1) hosts.splice(i, 1);
|
||||
bannerHost.remove();
|
||||
bannerHost = null;
|
||||
WIN.removeEventListener('resize', onResize);
|
||||
DOC.body.style.paddingBottom = prevPad;
|
||||
if (linkHostEl) linkHostEl.style.display = '';
|
||||
}
|
||||
|
||||
// Einstellungsknopf: angeknabberter Keks unten rechts, dreht sich langsam (nicht bei reduzierter Bewegung).
|
||||
// Beim Darueberfahren haelt er an und zeigt "Datenschutz-Einstellungen"; das ist auch sein Name fuer Screenreader.
|
||||
// Inline-SVG statt data:-Grafik, damit eine strenge CSP der Website (img-src) ihn nicht blockiert.
|
||||
const SETTINGS_CSS = ':host{all:initial}' +
|
||||
'.b{position:relative;display:block;width:44px;height:44px;padding:2px;margin:0;border:0;border-radius:50%;background:transparent;cursor:pointer;-webkit-tap-highlight-color:transparent}' +
|
||||
'.b:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' +
|
||||
'.c{display:block;width:40px;height:40px;filter:drop-shadow(0 2px 3px rgba(0,0,0,.35));animation:wk-spin 9s linear infinite}' +
|
||||
'.b:hover .c,.b:focus-visible .c{animation-play-state:paused}' +
|
||||
'.l{position:absolute;right:52px;top:50%;transform:translate(6px,-50%);padding:7px 12px;border-radius:999px;background:#fff;color:#222;box-shadow:0 2px 10px rgba(0,0,0,.18);font:600 13px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s,transform .15s}' +
|
||||
'.b:hover .l,.b:focus-visible .l{opacity:1;transform:translate(0,-50%)}' +
|
||||
'@keyframes wk-spin{to{transform:rotate(360deg)}}' +
|
||||
'@media (prefers-reduced-motion:reduce){.c{animation:none}.l{transition:none}}';
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
function svgEl(tag, attrs) {
|
||||
const n = DOC.createElementNS(SVG_NS, tag);
|
||||
for (const k in attrs) n.setAttribute(k, attrs[k]);
|
||||
return n;
|
||||
}
|
||||
function cookieSvg() {
|
||||
const svg = svgEl('svg', { class: 'c', viewBox: '0 0 64 64', 'aria-hidden': 'true', focusable: 'false' });
|
||||
// Umriss mit Bissspur oben rechts (zwei Bissboegen, dann der grosse Kreis zurueck)
|
||||
svg.appendChild(svgEl('path', { d: 'M44.84 6A10 10 0 0 0 57.5 19.37A7.5 7.5 0 0 0 60.96 33.5A29 29 0 1 1 44.84 6Z', fill: '#dfa25b', stroke: '#a5652c', 'stroke-width': '3', 'stroke-linejoin': 'round' }));
|
||||
const chips = [[19, 22, 4.2, 3.3, -20], [31, 14, 3.2, 2.6, 15], [33, 33, 4, 3.2, 30], [46, 41, 3.6, 2.8, -35], [21, 45, 4, 3, 10], [36, 52, 3, 2.4, -10], [11, 34, 2.6, 2.1, 40], [47, 31, 2.4, 2, 0]];
|
||||
for (const c of chips) svg.appendChild(svgEl('ellipse', { cx: c[0], cy: c[1], rx: c[2], ry: c[3], transform: 'rotate(' + c[4] + ' ' + c[0] + ' ' + c[1] + ')', fill: '#5a3216' }));
|
||||
const crumbs = [[26, 28, 1.1], [40, 22, 1], [28, 40, 1.2], [14, 26, 0.9], [40, 46, 1]];
|
||||
for (const c of crumbs) svg.appendChild(svgEl('circle', { cx: c[0], cy: c[1], r: c[2], fill: '#f3c98c' }));
|
||||
return svg;
|
||||
}
|
||||
function addSettingsLink() {
|
||||
linkHostEl = DOC.createElement('div');
|
||||
const st = linkHostEl.style;
|
||||
st.position = 'fixed'; st.right = '12px'; st.bottom = '12px'; st.zIndex = '2147483646';
|
||||
hosts.push(linkHostEl);
|
||||
const root = linkHostEl.attachShadow({ mode: 'open' });
|
||||
root.appendChild(el('style', {}, SETTINGS_CSS));
|
||||
const b = el('button', { type: 'button', class: 'b', 'aria-label': SETTINGS_LABEL });
|
||||
b.appendChild(el('span', { class: 'l', 'aria-hidden': 'true' }, SETTINGS_LABEL));
|
||||
b.appendChild(cookieSvg());
|
||||
b.addEventListener('click', safe(function () { openBanner(true); }));
|
||||
root.appendChild(b);
|
||||
DOC.body.appendChild(linkHostEl);
|
||||
}
|
||||
|
||||
DOC.addEventListener('click', safe(function (e) {
|
||||
const t = e.target;
|
||||
if (t && t.closest && t.closest('[data-webklar-consent]')) { if (e.preventDefault) e.preventDefault(); openBanner(true); }
|
||||
}));
|
||||
if (!DOC.querySelector('[data-webklar-consent]')) addSettingsLink();
|
||||
|
||||
// Einwilligung in anderem Tab geaendert
|
||||
WIN.addEventListener('storage', safe(function (e) {
|
||||
if (e.key !== null && e.key !== CONSENT_KEY) return;
|
||||
const c = readConsent(storage, Date.now(), banner.version);
|
||||
if (choiceNow() === 'accept') { startTracking(); onAccept(); } else stopTracking();
|
||||
if (c) closeBanner();
|
||||
}));
|
||||
|
||||
if (stored) {
|
||||
if (stored.a === 'accept') startTracking();
|
||||
} else {
|
||||
openBanner();
|
||||
send({ t: 'bn', site, a: 'shown' });
|
||||
}
|
||||
}
|
||||
|
||||
try { main(); } catch (e) { /* nie die Seite stoeren */ }
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "webklar-analytics",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"test": "node --test test/*.test.js"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env node
|
||||
// CSV-Export einer Website (Tageswerte, Einzelereignisse, Klicks). Öffnet die Datenbank nur lesend.
|
||||
//
|
||||
// Ablauf (Runbook): Im Container webklar-analytics ausführen (Code liegt dort unter /analytics, schreibbar ist nur /srv/webklar-analytics):
|
||||
// docker exec webklar-analytics node /analytics/scripts/export-site.mjs <siteKey> --out /srv/webklar-analytics/exports/<siteKey>
|
||||
// Die CSV-Dateien enthalten Besucher-Hashes (Rechte 0600) und müssen dem Kunden als Kopie innerhalb von zwei Wochen
|
||||
// übergeben werden (AVV Nr. 9); danach den Export-Ordner wieder löschen.
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const RE_KEY = /^[a-z0-9]{16,40}$/;
|
||||
const iso = (ts) => new Date(ts).toISOString();
|
||||
|
||||
function cell(v) {
|
||||
if (v === null || v === undefined) return '';
|
||||
let s = String(v);
|
||||
if (typeof v === 'string' && /^[=+\-@\t\r]/.test(s)) s = `'${s}`;
|
||||
return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
||||
}
|
||||
const csv = (head, rows) => [head, ...rows].map((r) => r.map(cell).join(',')).join('\r\n') + '\r\n';
|
||||
|
||||
export function exportSite({ dbPath, siteKey, outDir }) {
|
||||
if (!RE_KEY.test(siteKey || '')) throw new Error('Ungültiger Website-Schlüssel');
|
||||
const db = new DatabaseSync(dbPath, { readOnly: true });
|
||||
try {
|
||||
if (!db.prepare('SELECT 1 FROM sites WHERE siteKey = ?').get(siteKey)) throw new Error('Unbekannte Website');
|
||||
const daily = db.prepare('SELECT day, dim, key, count FROM daily WHERE site = ? ORDER BY day, dim, key').all(siteKey);
|
||||
const ev = db.prepare('SELECT ts, type, path, vp, ref, country, device, visitor, sec, scroll, kind, host FROM events_raw WHERE site = ? ORDER BY ts, id').all(siteKey);
|
||||
const cl = db.prepare('SELECT ts, path, vp, x, y, sel FROM clicks WHERE site = ? ORDER BY ts, id').all(siteKey);
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const files = {
|
||||
[`${siteKey}-tageswerte.csv`]: csv(['day', 'dim', 'key', 'count'], daily.map((r) => [r.day, r.dim, r.key, r.count])),
|
||||
[`${siteKey}-ereignisse.csv`]: csv(['time', 'type', 'path', 'vp', 'ref', 'country', 'device', 'visitor', 'sec', 'scroll', 'kind', 'host'],
|
||||
ev.map((r) => [iso(r.ts), r.type, r.path, r.vp, r.ref, r.country, r.device, r.visitor, r.sec, r.scroll, r.kind, r.host])),
|
||||
[`${siteKey}-klicks.csv`]: csv(['time', 'path', 'vp', 'x', 'y', 'sel'], cl.map((r) => [iso(r.ts), r.path, r.vp, r.x, r.y, r.sel])),
|
||||
};
|
||||
for (const [n, c] of Object.entries(files)) writeFileSync(join(outDir, n), c, { encoding: 'utf8', mode: 0o600 });
|
||||
return Object.keys(files).map((n) => join(outDir, n));
|
||||
} finally { db.close(); }
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const args = process.argv.slice(2);
|
||||
const opt = (n, d) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : d; };
|
||||
const siteKey = args.find((a, i) => !a.startsWith('--') && !(i > 0 && args[i - 1].startsWith('--')));
|
||||
try {
|
||||
const files = exportSite({ dbPath: opt('--db', '/srv/webklar-analytics/analytics.db'), siteKey, outDir: opt('--out', '.') });
|
||||
console.log(files.join('\n'));
|
||||
} catch (e) {
|
||||
console.error(`Export fehlgeschlagen: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/bin/sh
|
||||
# Lädt die aktuelle DB-IP-Lite-Länderdatei (monatlich) nach $ANALYTICS_DATA_DIR/geo.csv und startet
|
||||
# den Dienst nur neu, wenn sich die Datei geändert hat. Läuft auf dem Host (z. B. per Cron, 1x pro Woche).
|
||||
# Die alte geo.csv wird nie gelöscht, bevor die neue geprüft und umbenannt ist.
|
||||
set -eu
|
||||
|
||||
DIR="${ANALYTICS_DATA_DIR:-/srv/webklar-analytics}"
|
||||
CONTAINER="${ANALYTICS_CONTAINER:-webklar-analytics}"
|
||||
NEU="$DIR/geo.csv.new"
|
||||
ZIEL="$DIR/geo.csv"
|
||||
MARKER="$DIR/geo.restart-pending"
|
||||
|
||||
fehler() { echo "Fehler (geo-update): $*" >&2; rm -f "$NEU" "$NEU.gz"; exit 1; }
|
||||
|
||||
command -v curl >/dev/null 2>&1 || fehler "curl fehlt."
|
||||
command -v gunzip >/dev/null 2>&1 || fehler "gunzip fehlt."
|
||||
[ -d "$DIR" ] || fehler "Datenverzeichnis $DIR existiert nicht."
|
||||
|
||||
JETZT="$(date -u +%Y-%m)"
|
||||
JAHR="${JETZT%-*}"
|
||||
MONAT="${JETZT#*-}"
|
||||
m="$(printf '%s' "$MONAT" | sed 's/^0//')"
|
||||
if [ "$m" -eq 1 ]; then VORMONAT="$((JAHR - 1))-12"; else VORMONAT="$JAHR-$(printf '%02d' "$((m - 1))")"; fi
|
||||
|
||||
geladen=""
|
||||
for monat in "$JETZT" "$VORMONAT"; do
|
||||
url="https://download.db-ip.com/free/dbip-country-lite-$monat.csv.gz"
|
||||
if curl -fsSL --retry 2 --max-time 300 -o "$NEU.gz" "$url" 2>/dev/null && gunzip -c "$NEU.gz" > "$NEU" 2>/dev/null; then
|
||||
geladen="$monat"
|
||||
break
|
||||
fi
|
||||
done
|
||||
rm -f "$NEU.gz"
|
||||
[ -n "$geladen" ] || fehler "Download von DB-IP fehlgeschlagen (weder $JETZT noch $VORMONAT). Bestehende geo.csv bleibt unverändert."
|
||||
|
||||
zeilen="$(wc -l < "$NEU" | tr -d ' ')"
|
||||
[ "$zeilen" -gt 100000 ] || fehler "Neue Datei hat nur $zeilen Zeilen (erwartet > 100000). Bestehende geo.csv bleibt unverändert."
|
||||
grep -Eq ',"?DE"?\r?$' "$NEU" || fehler "Neue Datei enthält kein 'DE'. Bestehende geo.csv bleibt unverändert."
|
||||
|
||||
neustart() {
|
||||
docker restart "$CONTAINER" >/dev/null || fehler "Neustart von $CONTAINER fehlgeschlagen (geo.csv ist aber aktualisiert, nächster Lauf versucht den Neustart erneut)."
|
||||
rm -f "$MARKER"
|
||||
echo "$CONTAINER neu gestartet."
|
||||
}
|
||||
|
||||
if [ -f "$ZIEL" ] && cmp -s "$NEU" "$ZIEL"; then
|
||||
rm -f "$NEU"
|
||||
if [ -f "$MARKER" ]; then
|
||||
echo "geo.csv ($geladen) unverändert, aber ein früherer Neustart steht aus."
|
||||
neustart
|
||||
else
|
||||
echo "geo.csv ($geladen) unverändert, kein Neustart nötig."
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mv -f "$NEU" "$ZIEL"
|
||||
: > "$MARKER" # bleibt liegen, bis der Neustart gelungen ist
|
||||
echo "geo.csv aktualisiert ($geladen, $zeilen Zeilen)."
|
||||
neustart
|
||||
@@ -0,0 +1,82 @@
|
||||
// GET/HEAD /a/w.js und /a/h.js (öffentlicher Listener): generierte Client-Skripte aus client/dist/.
|
||||
// Einmal beim Start in den Speicher geladen (roh + gzip); fehlt eine Datei, antwortet der Pfad mit 503.
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { gzipSync } from 'node:zlib';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const DEFAULT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'client', 'dist');
|
||||
const FILES = { '/a/w.js': 'w.js', '/a/h.js': 'h.js' };
|
||||
|
||||
const RE_KEY = /^[a-z0-9]{16,40}$/;
|
||||
const EMPTY = Buffer.from('/* WEBklar Analyse: aus */\n');
|
||||
|
||||
export function createAssets({ distDir = DEFAULT_DIR, log = console, isActive = () => true } = {}) {
|
||||
const assets = new Map();
|
||||
for (const [path, file] of Object.entries(FILES)) {
|
||||
try {
|
||||
const raw = readFileSync(join(distDir, file));
|
||||
assets.set(path, { raw, gz: gzipSync(raw, { level: 9 }), etag: `"${crypto.createHash('sha256').update(raw).digest('hex')}"` });
|
||||
} catch (e) {
|
||||
log.error(`Analytics: Skript ${file} fehlt in ${distDir} (${e.code || e.message}); ${path} antwortet mit 503`);
|
||||
}
|
||||
}
|
||||
|
||||
// true, wenn die Anfrage ein Skript betraf.
|
||||
return function handle(req, res, pathname) {
|
||||
if (!(pathname in FILES)) return false;
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
||||
res.writeHead(405, { Allow: 'GET, HEAD' });
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
if (pathname === '/a/w.js') {
|
||||
let key = '';
|
||||
try { key = new URL(req.url, 'http://x').searchParams.get('s') || ''; } catch { /* leer */ }
|
||||
let on = false;
|
||||
try { on = RE_KEY.test(key) && !!isActive(key); } catch (e) { log.error('Analytics: isActive:', e.message); }
|
||||
if (!on) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/javascript; charset=utf-8',
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
Vary: 'Accept-Encoding',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'Cross-Origin-Resource-Policy': 'cross-origin',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Length': EMPTY.length,
|
||||
});
|
||||
res.end(req.method === 'HEAD' ? undefined : EMPTY);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const a = assets.get(pathname);
|
||||
if (!a) {
|
||||
res.writeHead(503, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
||||
res.end(req.method === 'HEAD' ? '' : JSON.stringify({ error: 'Skript nicht verfügbar' }));
|
||||
return true;
|
||||
}
|
||||
const headers = {
|
||||
'Content-Type': 'text/javascript; charset=utf-8',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
ETag: a.etag,
|
||||
Vary: 'Accept-Encoding',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'Cross-Origin-Resource-Policy': 'cross-origin',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
};
|
||||
const inm = req.headers['if-none-match'];
|
||||
if (inm && inm.split(',').some((t) => t.trim().replace(/^W\//, '') === a.etag)) {
|
||||
res.writeHead(304, headers);
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
const gzip = /\bgzip\b/i.test(req.headers['accept-encoding'] || '');
|
||||
const body = gzip ? a.gz : a.raw;
|
||||
if (gzip) headers['Content-Encoding'] = 'gzip';
|
||||
headers['Content-Length'] = body.length;
|
||||
res.writeHead(200, headers);
|
||||
res.end(req.method === 'HEAD' ? undefined : body);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Konfiguration aus der Umgebung; leerer adminToken sperrt alle Admin-Routen.
|
||||
export function loadConfig(env = process.env) {
|
||||
return {
|
||||
port: Number(env.ANALYTICS_PORT) || 8090,
|
||||
adminPort: Number(env.ANALYTICS_ADMIN_PORT) || 8091,
|
||||
dataDir: env.ANALYTICS_DATA_DIR || '/srv/webklar-analytics',
|
||||
adminToken: env.ANALYTICS_ADMIN_TOKEN || '',
|
||||
heatSecret: env.ANALYTICS_HEAT_SECRET || '',
|
||||
geoFile: env.ANALYTICS_GEO_FILE || '',
|
||||
baseHost: env.ANALYTICS_BASE_HOST || 'project.webklar.com',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
|
||||
const SCHEMA = `
|
||||
CREATE TABLE IF NOT EXISTS sites (
|
||||
siteKey TEXT PRIMARY KEY,
|
||||
projectId TEXT NOT NULL,
|
||||
domains TEXT NOT NULL,
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
offSince TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS events_raw (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
site TEXT NOT NULL,
|
||||
ts INTEGER NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
path TEXT,
|
||||
vp TEXT,
|
||||
ref TEXT,
|
||||
country TEXT,
|
||||
device TEXT,
|
||||
visitor TEXT,
|
||||
sec INTEGER,
|
||||
scroll INTEGER,
|
||||
kind TEXT,
|
||||
host TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_site_ts ON events_raw(site, ts);
|
||||
CREATE TABLE IF NOT EXISTS clicks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
site TEXT NOT NULL,
|
||||
ts INTEGER NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
vp TEXT NOT NULL,
|
||||
x REAL NOT NULL,
|
||||
y REAL NOT NULL,
|
||||
sel TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_clicks_site_path_vp_ts ON clicks(site, path, vp, ts);
|
||||
CREATE TABLE IF NOT EXISTS daily (
|
||||
site TEXT NOT NULL,
|
||||
day TEXT NOT NULL,
|
||||
dim TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
count INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (site, day, dim, key)
|
||||
);
|
||||
-- Merker für die Nachtverdichtung: Tage, die schon in daily stehen (macht runNightly idempotent).
|
||||
CREATE TABLE IF NOT EXISTS agg_state (
|
||||
site TEXT NOT NULL,
|
||||
day TEXT NOT NULL,
|
||||
PRIMARY KEY (site, day)
|
||||
);
|
||||
`;
|
||||
|
||||
export function openDb(path) {
|
||||
const db = new DatabaseSync(path);
|
||||
if (path !== ':memory:') db.exec('PRAGMA journal_mode = WAL');
|
||||
db.exec('PRAGMA secure_delete = ON');
|
||||
db.exec(SCHEMA);
|
||||
return db;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// Länderzuordnung per DB-IP Lite (country, CSV: start_ip,end_ip,land). Die Datei wird einmal beim
|
||||
// Start geladen (IPv4 als Uint32, IPv6 als 2×64 Bit in BigUint64Array) und per Binärsuche abgefragt.
|
||||
import { readFileSync } from 'node:fs';
|
||||
import net from 'node:net';
|
||||
|
||||
const MASK64 = (1n << 64n) - 1n;
|
||||
|
||||
function parseV4(s) {
|
||||
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(s);
|
||||
if (!m) return -1;
|
||||
let n = 0;
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const o = Number(m[i]);
|
||||
if (o > 255) return -1;
|
||||
n = n * 256 + o;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// IPv6 ⇒ 128-Bit-BigInt oder null (auch mit angehängter IPv4-Schreibweise).
|
||||
function parseV6(s) {
|
||||
if (net.isIPv6(s) === false) return null;
|
||||
s = s.split('%')[0];
|
||||
const dot = s.lastIndexOf('.');
|
||||
if (dot !== -1) {
|
||||
const v4 = parseV4(s.slice(s.lastIndexOf(':') + 1));
|
||||
if (v4 < 0) return null;
|
||||
s = s.slice(0, s.lastIndexOf(':') + 1) + Math.floor(v4 / 65536).toString(16) + ':' + (v4 % 65536).toString(16);
|
||||
}
|
||||
const [head, tail] = s.split('::');
|
||||
const h = head ? head.split(':') : [];
|
||||
const t = tail === undefined ? [] : (tail ? tail.split(':') : []);
|
||||
const fill = tail === undefined ? 0 : 8 - h.length - t.length;
|
||||
const groups = [...h, ...Array(Math.max(fill, 0)).fill('0'), ...t];
|
||||
if (groups.length !== 8) return null;
|
||||
let n = 0n;
|
||||
for (const g of groups) n = (n << 16n) | BigInt(parseInt(g, 16));
|
||||
return n;
|
||||
}
|
||||
|
||||
const pack = (c) => (c.length === 2 && c !== 'ZZ' && /^[A-Za-z]{2}$/.test(c) ? (c.toUpperCase().charCodeAt(0) << 8) | c.toUpperCase().charCodeAt(1) : 0);
|
||||
const unpack = (n) => (n ? String.fromCharCode(n >> 8, n & 255) : '');
|
||||
|
||||
function sortedBy(n, cmp) {
|
||||
for (let i = 1; i < n; i++) if (cmp(i - 1, i) > 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function loadGeo(file) {
|
||||
const none = { lookup: () => '', size: 0 };
|
||||
let text;
|
||||
try { text = readFileSync(file, 'utf8'); } catch (e) {
|
||||
console.warn(`Analytics: Länderdatei ${file} nicht lesbar (${e.code || e.message}); Länder bleiben leer.`);
|
||||
return none;
|
||||
}
|
||||
// Obergrenze der Zeilen für die Vorab-Reservierung
|
||||
let cap = 1;
|
||||
for (let i = text.indexOf('\n'); i !== -1; i = text.indexOf('\n', i + 1)) cap++;
|
||||
let n4 = 0;
|
||||
let n6 = 0;
|
||||
const s4 = new Uint32Array(cap); const e4 = new Uint32Array(cap); const c4 = new Uint16Array(cap);
|
||||
const sh = new BigUint64Array(cap); const sl = new BigUint64Array(cap);
|
||||
const eh = new BigUint64Array(cap); const el = new BigUint64Array(cap); const c6 = new Uint16Array(cap);
|
||||
let pos = 0;
|
||||
while (pos < text.length) {
|
||||
let nl = text.indexOf('\n', pos);
|
||||
if (nl === -1) nl = text.length;
|
||||
const line = text.slice(pos, nl).replace(/["\r]/g, '').trim();
|
||||
pos = nl + 1;
|
||||
if (!line) continue;
|
||||
const p = line.split(',');
|
||||
if (p.length < 3) continue;
|
||||
const a = p[0].trim(); const b = p[1].trim(); const c = pack(p[2].trim());
|
||||
if (a.includes(':')) {
|
||||
const x = parseV6(a); const y = parseV6(b);
|
||||
if (x === null || y === null || y < x) continue;
|
||||
sh[n6] = x >> 64n; sl[n6] = x & MASK64; eh[n6] = y >> 64n; el[n6] = y & MASK64; c6[n6++] = c;
|
||||
} else {
|
||||
const x = parseV4(a); const y = parseV4(b);
|
||||
if (x < 0 || y < x) continue;
|
||||
s4[n4] = x; e4[n4] = y; c4[n4++] = c;
|
||||
}
|
||||
}
|
||||
text = null;
|
||||
|
||||
// Falls die Datei nicht sortiert ist: Indexsortierung
|
||||
let S4 = s4.slice(0, n4); let E4 = e4.slice(0, n4); let C4 = c4.slice(0, n4);
|
||||
if (!sortedBy(n4, (i, j) => S4[i] - S4[j])) {
|
||||
const idx = Array.from({ length: n4 }, (_, i) => i).sort((i, j) => S4[i] - S4[j]);
|
||||
S4 = Uint32Array.from(idx, (i) => s4[i]); E4 = Uint32Array.from(idx, (i) => e4[i]); C4 = Uint16Array.from(idx, (i) => c4[i]);
|
||||
}
|
||||
let SH = sh.slice(0, n6); let SL = sl.slice(0, n6); let EH = eh.slice(0, n6); let EL = el.slice(0, n6); let C6 = c6.slice(0, n6);
|
||||
const cmp6 = (i, j) => (SH[i] !== SH[j] ? (SH[i] < SH[j] ? -1 : 1) : (SL[i] === SL[j] ? 0 : (SL[i] < SL[j] ? -1 : 1)));
|
||||
if (!sortedBy(n6, cmp6)) {
|
||||
const idx = Array.from({ length: n6 }, (_, i) => i).sort(cmp6);
|
||||
const pick = (A, T) => T.from(idx, (i) => A[i]);
|
||||
const [a, b, c, d, e] = [pick(SH, BigUint64Array), pick(SL, BigUint64Array), pick(EH, BigUint64Array), pick(EL, BigUint64Array), pick(C6, Uint16Array)];
|
||||
SH = a; SL = b; EH = c; EL = d; C6 = e;
|
||||
}
|
||||
|
||||
const look4 = (v) => {
|
||||
let lo = 0; let hi = n4 - 1; let best = -1;
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >>> 1;
|
||||
if (S4[mid] <= v) { best = mid; lo = mid + 1; } else hi = mid - 1;
|
||||
}
|
||||
return best >= 0 && v <= E4[best] ? unpack(C4[best]) : '';
|
||||
};
|
||||
const look6 = (h, l) => {
|
||||
let lo = 0; let hi = n6 - 1; let best = -1;
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >>> 1;
|
||||
if (SH[mid] < h || (SH[mid] === h && SL[mid] <= l)) { best = mid; lo = mid + 1; } else hi = mid - 1;
|
||||
}
|
||||
if (best < 0) return '';
|
||||
const inside = h < EH[best] || (h === EH[best] && l <= EL[best]);
|
||||
return inside ? unpack(C6[best]) : '';
|
||||
};
|
||||
|
||||
return {
|
||||
size: n4 + n6,
|
||||
lookup(ip) {
|
||||
if (typeof ip !== 'string') return '';
|
||||
ip = ip.trim();
|
||||
if (!ip.includes(':')) {
|
||||
const v = parseV4(ip);
|
||||
return v < 0 ? '' : look4(v);
|
||||
}
|
||||
const v = parseV6(ip);
|
||||
if (v === null) return '';
|
||||
const h = v >> 64n; const l = v & MASK64;
|
||||
if (h === 0n && (l >> 32n) === 0xffffn) return look4(Number(l & 0xffffffffn)); // ::ffff:a.b.c.d
|
||||
return look6(h, l);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// GET /a/heat?token= (öffentlicher Listener): Klickpunkte für das Heatmap-Overlay.
|
||||
// Token (R3): <b64url(JSON {s,p,v,e})>.<hex HMAC-SHA256(heatSecret, b64part)>, e = Ablauf in Unix-Sekunden.
|
||||
import crypto from 'node:crypto';
|
||||
import { hostOf, cleanPath } from './ingest.js';
|
||||
|
||||
const SITE_KEY = /^[a-z0-9]{16,40}$/;
|
||||
const MAX_AHEAD = 30 * 60; // Sekunden
|
||||
const POINTS = 5000;
|
||||
const CLICK_DAYS = 30;
|
||||
|
||||
export function signHeatToken({ siteKey, path, vp, exp }, secret) {
|
||||
const part = Buffer.from(JSON.stringify({ s: siteKey, p: path, v: vp, e: exp })).toString('base64url');
|
||||
return `${part}.${crypto.createHmac('sha256', secret).update(part).digest('hex')}`;
|
||||
}
|
||||
|
||||
// Liefert { siteKey, path, vp, exp } oder null (Signatur, Format, abgelaufen, > 30 Min voraus).
|
||||
// ignoreExpiry: nur Signatur und Format prüfen (um die Website eines abgelaufenen Links zu erkennen).
|
||||
export function verifyHeatToken(token, secret, nowMs, { ignoreExpiry = false } = {}) {
|
||||
if (!secret || typeof token !== 'string') return null;
|
||||
const m = /^([A-Za-z0-9_-]+)\.([0-9a-f]{64})$/.exec(token);
|
||||
if (!m) return null;
|
||||
const expect = crypto.createHmac('sha256', secret).update(m[1]).digest();
|
||||
const got = Buffer.from(m[2], 'hex');
|
||||
if (got.length !== expect.length || !crypto.timingSafeEqual(got, expect)) return null;
|
||||
let o;
|
||||
try { o = JSON.parse(Buffer.from(m[1], 'base64url').toString('utf8')); } catch { return null; }
|
||||
if (!o || typeof o !== 'object' || typeof o.s !== 'string' || !SITE_KEY.test(o.s)
|
||||
|| typeof o.p !== 'string' || !o.p.startsWith('/') || !['m', 't', 'd'].includes(o.v)
|
||||
|| !Number.isInteger(o.e)) return null;
|
||||
const nowS = nowMs / 1000;
|
||||
if (!ignoreExpiry && (o.e <= nowS || o.e > nowS + MAX_AHEAD)) return null;
|
||||
return { siteKey: o.s, path: o.p, vp: o.v, exp: o.e };
|
||||
}
|
||||
|
||||
export function createHeat({ db, config = {}, now }) {
|
||||
const clock = now || Date.now;
|
||||
const getSite = db.prepare('SELECT domains, active FROM sites WHERE siteKey = ?');
|
||||
const getClicks = db.prepare('SELECT x, y FROM clicks WHERE site = ? AND path = ? AND vp = ? AND ts >= ? ORDER BY ts DESC, id DESC LIMIT ?');
|
||||
|
||||
// CORS: nur die eigenen https-Domains der Website (das Overlay läuft auf der Kundenseite).
|
||||
function cors(res, req, domains) {
|
||||
const origin = req.headers.origin;
|
||||
const host = hostOf(origin);
|
||||
res.setHeader('Vary', 'Origin');
|
||||
if (host && domains.includes(host)) res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
}
|
||||
const domainsOf = (site) => { try { return JSON.parse(site.domains); } catch { return []; } };
|
||||
|
||||
// true, wenn die Anfrage /a/heat betraf.
|
||||
return function handle(req, res, url) {
|
||||
if (url.pathname !== '/a/heat') return false;
|
||||
const end = (status, body, extra = {}) => {
|
||||
const b = body ? JSON.stringify(body) : '';
|
||||
res.writeHead(status, { 'Cache-Control': 'no-store', ...(body ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(b) } : {}), ...extra });
|
||||
res.end(b);
|
||||
return true;
|
||||
};
|
||||
const token = url.searchParams.get('token');
|
||||
// Website zum Token (auch bei abgelaufenem, aber echt signiertem Link), nur für die CORS-Köpfe der Fehlerantworten.
|
||||
const knownSite = () => {
|
||||
const t = verifyHeatToken(token, config.heatSecret, clock(), { ignoreExpiry: true });
|
||||
return t && getSite.get(t.siteKey);
|
||||
};
|
||||
if (req.method === 'OPTIONS') {
|
||||
// Preflight: CORS-Köpfe nur, wenn das Token zu einer Website gehört und der Origin deren https-Domain ist.
|
||||
const site = knownSite();
|
||||
if (site) cors(res, req, domainsOf(site));
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||
return end(204);
|
||||
}
|
||||
if (req.method !== 'GET') return end(405, null, { Allow: 'GET, OPTIONS' });
|
||||
const t = verifyHeatToken(token, config.heatSecret, clock());
|
||||
const site = t && getSite.get(t.siteKey);
|
||||
if (!t || !site || !site.active) {
|
||||
// Bekannte Website: CORS-Kopf auch bei 403, damit das Overlay "Link abgelaufen oder ungültig" zeigen kann.
|
||||
const known = site || knownSite();
|
||||
if (known) cors(res, req, domainsOf(known));
|
||||
return end(403, { error: 'Ungültiger oder abgelaufener Link' });
|
||||
}
|
||||
cors(res, req, domainsOf(site));
|
||||
const since = clock() - CLICK_DAYS * 86400 * 1000;
|
||||
const points = getClicks.all(t.siteKey, cleanPath(t.path) || t.path, t.vp, since, POINTS).map((r) => ({ x: r.x, y: r.y }));
|
||||
return end(200, { points });
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { loadConfig } from './config.js';
|
||||
import { openDb } from './db.js';
|
||||
import { createSalt } from './salt.js';
|
||||
import { loadGeo } from './geo.js';
|
||||
import { scheduleNightly } from './jobs.js';
|
||||
import { createPublicServer, createAdminServer } from './server.js';
|
||||
|
||||
const config = loadConfig(process.env);
|
||||
mkdirSync(config.dataDir, { recursive: true });
|
||||
const db = openDb(join(config.dataDir, 'analytics.db'));
|
||||
const geo = loadGeo(config.geoFile || join(config.dataDir, 'geo.csv'));
|
||||
if (geo.size) console.log(`Analytics: ${geo.size} Länder-Bereiche geladen`);
|
||||
|
||||
const publicServer = createPublicServer({ config, db, geo, salt: createSalt() });
|
||||
const servers = [
|
||||
['öffentlich', publicServer, config.port],
|
||||
['intern (Admin)', createAdminServer({ config, db, onSitesChanged: () => publicServer.invalidateSites() }), config.adminPort],
|
||||
];
|
||||
for (const [name, server, port] of servers) {
|
||||
server.on('error', (e) => { console.error(`Analytics-Listener ${name} (Port ${port}):`, e.message); process.exit(1); });
|
||||
server.listen(port, () => console.log(`Analytics-Dienst ${name} auf Port ${port}`));
|
||||
}
|
||||
|
||||
// Nachtjob: täglich 02:30 Europe/Berlin und einmal 60 s nach dem Start
|
||||
const nightly = scheduleNightly(db);
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
nightly.stop();
|
||||
let open = servers.length;
|
||||
const done = () => { if (--open === 0) { db.close(); process.exit(0); } };
|
||||
for (const [, server] of servers) { server.close(done); server.closeIdleConnections?.(); }
|
||||
setTimeout(() => process.exit(0), 5000).unref();
|
||||
});
|
||||
@@ -0,0 +1,231 @@
|
||||
// Annahme von POST /a/e. Datenschutz-Kern: IP und User-Agent leben nur in lokalen
|
||||
// Variablen (Geo, Limit-Schlüssel, Besucher-Hash) und werden nie gespeichert oder geloggt.
|
||||
import { classifyUa } from './ua.js';
|
||||
|
||||
const MAX_BODY = 4096;
|
||||
const SITE_KEY = /^[a-z0-9]{16,40}$/;
|
||||
const HOST = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/;
|
||||
const LIMIT = 120;
|
||||
const HOUR = 3600 * 1000;
|
||||
const MINUTE = 60 * 1000;
|
||||
const FIELDS = Object.assign(Object.create(null), {
|
||||
pv: ['t', 'site', 'path', 'ref', 'vp'],
|
||||
cx: ['t', 'site', 'path', 'ref', 'vp'],
|
||||
bn: ['t', 'site', 'a'],
|
||||
en: ['t', 'site', 'path', 'vp', 'sec', 'scroll'],
|
||||
ck: ['t', 'site', 'path', 'vp', 'x', 'y', 'sel'],
|
||||
ct: ['t', 'site', 'path', 'kind', 'host'],
|
||||
});
|
||||
const SITES_TTL = 30 * 1000;
|
||||
|
||||
function tooBig() { return Object.assign(new Error('zu groß'), { status: 413 }); }
|
||||
|
||||
function readBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (Number(req.headers['content-length']) > MAX_BODY) return reject(tooBig());
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
req.on('data', (c) => {
|
||||
size += c.length;
|
||||
if (size > MAX_BODY) { chunks.length = 0; reject(tooBig()); return; }
|
||||
chunks.push(c);
|
||||
});
|
||||
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Rechtester X-Forwarded-For-Eintrag (von Traefik angehängt), sonst Socket-Adresse.
|
||||
function clientIp(req) {
|
||||
const xff = String(req.headers['x-forwarded-for'] || '').split(',').map((s) => s.trim()).filter(Boolean);
|
||||
const ip = xff.length ? xff[xff.length - 1] : (req.socket.remoteAddress || '');
|
||||
return ip.replace(/^::ffff:/i, '');
|
||||
}
|
||||
|
||||
// Nur https ohne expliziten Port zählt; alles andere ergibt '' (kein Treffer).
|
||||
export function hostOf(value) {
|
||||
if (typeof value !== 'string' || !value) return '';
|
||||
try {
|
||||
const u = new URL(value);
|
||||
return u.protocol === 'https:' && !u.port ? u.hostname.toLowerCase() : '';
|
||||
} catch { return ''; }
|
||||
}
|
||||
|
||||
// Für cleanRef: Host einer beliebigen Referrer-URL (auch http, mit Port).
|
||||
function anyHostOf(value) {
|
||||
try { return new URL(value).hostname.toLowerCase(); } catch { return ''; }
|
||||
}
|
||||
|
||||
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
// Kennungsartige Segmente (lange Ziffernfolge, UUID, E-Mail) werden nie gespeichert.
|
||||
function looksLikeId(seg) {
|
||||
let d = seg;
|
||||
try { d = decodeURIComponent(seg); } catch { /* Rohwert */ }
|
||||
return /\d{6,}/.test(d) || UUID.test(d) || d.includes('@');
|
||||
}
|
||||
|
||||
export function cleanPath(p) {
|
||||
if (typeof p !== 'string') return null;
|
||||
const s = p.split(/[?#]/)[0];
|
||||
if (!s.startsWith('/') || s.length > 300 || /[\u0000-\u001f\u007f]/.test(s)) return null;
|
||||
return s.split('/').map((seg) => (looksLikeId(seg) ? ':id' : seg)).join('/');
|
||||
}
|
||||
|
||||
function cleanHost(h) {
|
||||
if (typeof h !== 'string') return null;
|
||||
const s = h.toLowerCase();
|
||||
return s.length <= 253 && HOST.test(s) ? s : null;
|
||||
}
|
||||
|
||||
function cleanRef(ref, domains) {
|
||||
if (typeof ref !== 'string' || !ref) return null;
|
||||
const host = cleanHost(ref.includes('://') ? anyHostOf(ref) : ref);
|
||||
return host && !domains.includes(host) ? host : null;
|
||||
}
|
||||
|
||||
const cleanVp = (v) => (v === 'm' || v === 't' || v === 'd' ? v : null);
|
||||
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
||||
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
||||
|
||||
export function createIngest({ db, geo = { lookup: () => '' }, salt, now = Date.now }) {
|
||||
const getSite = db.prepare('SELECT domains, active FROM sites WHERE siteKey = ?');
|
||||
const activeSites = db.prepare('SELECT domains FROM sites WHERE active = 1');
|
||||
let hostCache = null; // { at, hosts: Set } für Preflight-Prüfungen
|
||||
let hostCacheStale = true;
|
||||
const insEvent = db.prepare(`INSERT INTO events_raw (site, ts, type, path, vp, ref, country, device, visitor, sec, scroll, kind, host)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
||||
const insClick = db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y, sel) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
||||
const windows = new Map(); // rateKey -> { w, n }; rateKey ist ein HMAC, keine IP
|
||||
let lastPrune = 0;
|
||||
|
||||
const originHost = (req) => hostOf(req.headers.origin) || (req.headers.origin ? '' : hostOf(req.headers.referer));
|
||||
const parseDomains = (s) => { try { return JSON.parse(s); } catch { return []; } };
|
||||
|
||||
function activeHosts() {
|
||||
const t = now();
|
||||
if (hostCacheStale || !hostCache || t - hostCache.at >= SITES_TTL || t < hostCache.at) {
|
||||
const hosts = new Set();
|
||||
for (const s of activeSites.all()) for (const d of parseDomains(s.domains)) hosts.add(d);
|
||||
hostCache = { at: t, hosts };
|
||||
hostCacheStale = false;
|
||||
}
|
||||
return hostCache.hosts;
|
||||
}
|
||||
|
||||
function limited(key) {
|
||||
const t = now();
|
||||
const w = Math.floor(t / MINUTE);
|
||||
if (w !== lastPrune) { // alte Fenster entfernen
|
||||
for (const [k, v] of windows) if (v.w !== w) windows.delete(k);
|
||||
lastPrune = w;
|
||||
}
|
||||
let e = windows.get(key);
|
||||
if (!e || e.w !== w) { e = { w, n: 0 }; windows.set(key, e); }
|
||||
return ++e.n > LIMIT;
|
||||
}
|
||||
|
||||
function cors(res, origin) {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
res.setHeader('Vary', 'Origin');
|
||||
}
|
||||
|
||||
function store(ev, site, domains, ip, ua) {
|
||||
if (typeof ev.t !== 'string') return;
|
||||
const spec = FIELDS[ev.t];
|
||||
if (!spec) return;
|
||||
const e = {};
|
||||
for (const k of spec) if (k in ev) e[k] = ev[k]; // unbekannte Felder werden nie übernommen
|
||||
const t = now();
|
||||
if (e.t === 'bn') {
|
||||
if (!['shown', 'accept', 'reject'].includes(e.a)) return;
|
||||
insEvent.run(site, Math.floor(t / HOUR) * HOUR, 'bn', null, null, null, null, null, null, null, null, e.a, null);
|
||||
return;
|
||||
}
|
||||
const path = cleanPath(e.path);
|
||||
if (!path) return;
|
||||
const hour = Math.floor(t / HOUR) * HOUR;
|
||||
if (e.t === 'cx') {
|
||||
const vp = cleanVp(e.vp);
|
||||
if (!vp) return;
|
||||
insEvent.run(site, hour, 'cx', path, vp, cleanRef(e.ref, domains), null, null, null, null, null, null, null);
|
||||
return;
|
||||
}
|
||||
if (e.t === 'pv') {
|
||||
const vp = e.vp === undefined ? null : cleanVp(e.vp);
|
||||
if (e.vp !== undefined && !vp) return;
|
||||
const c = String(geo.lookup(ip) || '');
|
||||
const country = /^[A-Za-z]{2}$/.test(c) ? c.toUpperCase() : '';
|
||||
insEvent.run(site, hour, 'pv', path, vp, vp ? cleanRef(e.ref, domains) : null, country, classifyUa(ua).device, null, null, null, null, null);
|
||||
return;
|
||||
}
|
||||
if (e.t === 'ck') {
|
||||
const vp = cleanVp(e.vp);
|
||||
const x = num(e.x);
|
||||
const y = num(e.y);
|
||||
if (!vp || x === null || y === null) return;
|
||||
const sel = typeof e.sel === 'string' ? e.sel.slice(0, 200) : null;
|
||||
insClick.run(site, hour, path, vp, Math.round(clamp(x, 0, 1) * 1000) / 1000, Math.round(clamp(y, 0, 1) * 1000) / 1000, sel);
|
||||
return;
|
||||
}
|
||||
const visitor = salt.visitorId(site, ip, ua);
|
||||
if (e.t === 'en') {
|
||||
const vp = cleanVp(e.vp);
|
||||
const sec = num(e.sec);
|
||||
const scroll = num(e.scroll);
|
||||
if (!vp || sec === null || scroll === null) return;
|
||||
insEvent.run(site, hour, 'en', path, vp, null, null, null, visitor, Math.floor(clamp(sec, 0, 3600)), Math.floor(clamp(scroll, 0, 100)), null, null);
|
||||
return;
|
||||
}
|
||||
if (e.t === 'ct') {
|
||||
if (!['tel', 'mail', 'wa', 'out'].includes(e.kind)) return;
|
||||
let host = null;
|
||||
if (e.kind === 'out') { host = cleanHost(e.host); if (!host) return; }
|
||||
insEvent.run(site, hour, 'ct', path, null, null, null, null, visitor, null, null, e.kind, host);
|
||||
}
|
||||
}
|
||||
|
||||
// Liefert true, wenn die Anfrage /a/e betraf.
|
||||
async function handle(req, res, pathname) {
|
||||
if (pathname !== '/a/e') return false;
|
||||
const done = (status) => {
|
||||
if (status === 413) { // Rest des Bodys nicht abwarten: Antwort senden, danach Verbindung kappen
|
||||
res.writeHead(413, { Connection: 'close' });
|
||||
res.end(() => req.destroy());
|
||||
return true;
|
||||
}
|
||||
res.writeHead(status); res.end(); return true;
|
||||
};
|
||||
if (req.method === 'OPTIONS') {
|
||||
const host = originHost(req);
|
||||
const origin = req.headers.origin;
|
||||
if (host && origin && activeHosts().has(host)) {
|
||||
cors(res, origin);
|
||||
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
res.setHeader('Access-Control-Max-Age', '86400');
|
||||
}
|
||||
return done(204);
|
||||
}
|
||||
if (req.method !== 'POST') { res.setHeader('Allow', 'POST, OPTIONS'); return done(405); }
|
||||
let raw;
|
||||
try { raw = await readBody(req); } catch (e) { return done(e.status === 413 ? 413 : 204); }
|
||||
let ev;
|
||||
try { ev = JSON.parse(raw); } catch { return done(204); }
|
||||
if (!ev || typeof ev !== 'object' || Array.isArray(ev) || typeof ev.site !== 'string' || !SITE_KEY.test(ev.site)) return done(204);
|
||||
const site = getSite.get(ev.site);
|
||||
if (!site || !site.active) return done(204);
|
||||
const domains = parseDomains(site.domains);
|
||||
const host = originHost(req);
|
||||
if (!host || !domains.includes(host)) return done(204);
|
||||
if (req.headers.origin) cors(res, req.headers.origin);
|
||||
const ua = String(req.headers['user-agent'] || '');
|
||||
if (classifyUa(ua).bot) return done(204);
|
||||
const ip = clientIp(req);
|
||||
if (limited(salt.rateKey(ev.site, ip))) return done(204);
|
||||
try { store(ev, ev.site, domains, ip, ua); } catch (e) { console.error('Analytics-Annahme:', e.message); }
|
||||
return done(204);
|
||||
}
|
||||
|
||||
handle.invalidateSites = () => { hostCacheStale = true; };
|
||||
return handle;
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Nachtjob: Rohdaten tageweise in `daily` verdichten, Fristen durchsetzen.
|
||||
// Idempotenz: agg_state(site, day) merkt sich verdichtete Tage; ein Tag wird nie zweimal gezählt.
|
||||
// Rohzeilen bleiben bis zur 30-Tage-Löschung bestehen (Statistik für heute/gestern liest sie live).
|
||||
const DAY_FMT = new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Berlin', year: 'numeric', month: '2-digit', day: '2-digit' });
|
||||
const WALL_FMT = new Intl.DateTimeFormat('en-GB', {
|
||||
timeZone: 'Europe/Berlin', hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
});
|
||||
const DAY_MS = 86400 * 1000;
|
||||
export const RAW_DAYS = 30;
|
||||
export const OFF_DAYS = 60;
|
||||
export const DAILY_MONTHS = 25;
|
||||
|
||||
// Berliner Tag von `now` minus DAILY_MONTHS Kalendermonate (Monatsende auf den letzten Tag des Zielmonats begrenzt).
|
||||
export function dailyCutoffDay(now) {
|
||||
const [y, m, d] = DAY_FMT.format(now).split('-').map(Number);
|
||||
const idx = y * 12 + (m - 1) - DAILY_MONTHS;
|
||||
const ty = Math.floor(idx / 12);
|
||||
const tm = idx % 12;
|
||||
const last = new Date(Date.UTC(ty, tm + 1, 0)).getUTCDate();
|
||||
return `${String(ty).padStart(4, '0')}-${String(tm + 1).padStart(2, '0')}-${String(Math.min(d, last)).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// Kalendertag (YYYY-MM-DD) in Europe/Berlin.
|
||||
export const dayOf = (ts) => DAY_FMT.format(ts);
|
||||
|
||||
// Verdichtet Rohzeilen EINES Tages zu [dim, key, count]-Tripeln.
|
||||
export function aggregateRows(rows) {
|
||||
const m = new Map();
|
||||
const add = (dim, key, n = 1) => { const k = `${dim}\u0000${key}`; m.set(k, (m.get(k) || 0) + n); };
|
||||
const visitors = new Set();
|
||||
for (const r of rows) {
|
||||
if (r.type === 'pv') {
|
||||
add('pv_total', ''); add('pv_path', r.path || '');
|
||||
if (r.vp != null) add('pv_ref', r.ref || ''); // nur Besucher mit Einwilligung; sonst kommt pv_ref aus cx
|
||||
add('pv_country', r.country || ''); add('pv_device', r.device || '');
|
||||
} else if (r.type === 'cx') {
|
||||
add('pv_ref', r.ref || '');
|
||||
} else if (r.type === 'en') {
|
||||
add('en_count', ''); add('en_sec_sum', '', r.sec || 0); add('scroll_sum', '', r.scroll || 0);
|
||||
if (r.visitor) visitors.add(r.visitor);
|
||||
} else if (r.type === 'ct') {
|
||||
add('ct_kind', r.kind || '');
|
||||
if (r.visitor) visitors.add(r.visitor);
|
||||
} else if (r.type === 'bn' && ['shown', 'accept', 'reject'].includes(r.kind)) {
|
||||
add(`bn_${r.kind}`, '');
|
||||
}
|
||||
}
|
||||
if (visitors.size) add('visitors', '', visitors.size);
|
||||
return [...m].filter(([, n]) => n > 0).map(([k, n]) => { const [dim, key] = k.split('\u0000'); return [dim, key, n]; });
|
||||
}
|
||||
|
||||
export function runNightly(db, { now = Date.now(), log = console } = {}) {
|
||||
const today = dayOf(now);
|
||||
const rawCut = now - RAW_DAYS * DAY_MS;
|
||||
const offCut = new Date(now - OFF_DAYS * DAY_MS).toISOString();
|
||||
const siteKeys = db.prepare(`SELECT DISTINCT site FROM events_raw UNION SELECT siteKey FROM sites`).all().map((r) => r.site);
|
||||
const selRaw = db.prepare('SELECT ts, type, path, vp, ref, country, device, visitor, sec, scroll, kind FROM events_raw WHERE site = ? AND ts < ? ORDER BY ts');
|
||||
const selDone = db.prepare('SELECT day FROM agg_state WHERE site = ?');
|
||||
const insDaily = db.prepare(`INSERT INTO daily (site, day, dim, key, count) VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(site, day, dim, key) DO UPDATE SET count = count + excluded.count`);
|
||||
const insDone = db.prepare('INSERT OR IGNORE INTO agg_state (site, day) VALUES (?, ?)');
|
||||
const stats = { aggregated: 0, sitesDeleted: 0, dailyDeleted: 0, orphansMarked: 0, orphanRowsDeleted: 0 };
|
||||
|
||||
const tx = (fn) => {
|
||||
db.exec('BEGIN');
|
||||
try { fn(); db.exec('COMMIT'); } catch (e) { db.exec('ROLLBACK'); throw e; }
|
||||
};
|
||||
|
||||
for (const site of siteKeys) {
|
||||
tx(() => {
|
||||
const done = new Set(selDone.all(site).map((r) => r.day));
|
||||
const byDay = new Map();
|
||||
for (const r of selRaw.all(site, now)) {
|
||||
const d = dayOf(r.ts);
|
||||
if (d >= today || done.has(d)) continue;
|
||||
if (!byDay.has(d)) byDay.set(d, []);
|
||||
byDay.get(d).push(r);
|
||||
}
|
||||
for (const [day, rows] of byDay) {
|
||||
for (const [dim, key, n] of aggregateRows(rows)) insDaily.run(site, day, dim, key, n);
|
||||
insDone.run(site, day);
|
||||
stats.aggregated++;
|
||||
}
|
||||
// Fristen erst NACH der Verdichtung (Rohdaten und Klicks > 30 Tage)
|
||||
db.prepare('DELETE FROM events_raw WHERE site = ? AND ts < ?').run(site, rawCut);
|
||||
db.prepare('DELETE FROM clicks WHERE site = ? AND ts < ?').run(site, rawCut);
|
||||
});
|
||||
}
|
||||
// Klicks ganz ohne Website-Zeile / Ereignisse
|
||||
db.prepare('DELETE FROM clicks WHERE ts < ?').run(rawCut);
|
||||
|
||||
for (const { siteKey } of db.prepare('SELECT siteKey FROM sites WHERE offSince IS NOT NULL AND offSince < ?').all(offCut)) {
|
||||
tx(() => {
|
||||
for (const t of ['events_raw', 'clicks', 'daily', 'agg_state']) db.prepare(`DELETE FROM ${t} WHERE site = ?`).run(siteKey);
|
||||
db.prepare('DELETE FROM sites WHERE siteKey = ?').run(siteKey);
|
||||
stats.sitesDeleted++;
|
||||
});
|
||||
}
|
||||
|
||||
// Tageswerte älter als 25 Monate (alle Websites)
|
||||
tx(() => {
|
||||
const cut = dailyCutoffDay(now);
|
||||
stats.dailyDeleted = Number(db.prepare('DELETE FROM daily WHERE day < ?').run(cut).changes);
|
||||
db.prepare('DELETE FROM agg_state WHERE day < ?').run(cut);
|
||||
});
|
||||
|
||||
// Ausgeschaltete Websites ohne offSince: Frist beginnt bei Entdeckung.
|
||||
stats.orphansMarked = Number(db.prepare('UPDATE sites SET offSince = ? WHERE active = 0 AND offSince IS NULL').run(new Date(now).toISOString()).changes);
|
||||
// Zeilen ohne Website-Eintrag
|
||||
tx(() => {
|
||||
for (const t of ['events_raw', 'clicks', 'daily', 'agg_state']) {
|
||||
stats.orphanRowsDeleted += Number(db.prepare(`DELETE FROM ${t} WHERE site NOT IN (SELECT siteKey FROM sites)`).run().changes);
|
||||
}
|
||||
});
|
||||
|
||||
try { db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } catch (e) { log.error('Analytics-Nachtjob: wal_checkpoint:', e.message); }
|
||||
return stats;
|
||||
}
|
||||
|
||||
// Offset (ms) von Europe/Berlin gegenüber UTC zum Zeitpunkt ts.
|
||||
function berlinOffset(ts) {
|
||||
const p = Object.fromEntries(WALL_FMT.formatToParts(ts).map((x) => [x.type, x.value]));
|
||||
return Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second) - Math.floor(ts / 1000) * 1000;
|
||||
}
|
||||
|
||||
// Nächster Lauf um hour:minute Europe/Berlin (strikt nach now), als UTC-ms.
|
||||
export function nextRunAt(now, hour = 2, minute = 30) {
|
||||
const [y, m, d] = dayOf(now).split('-').map(Number);
|
||||
const at = (dd) => {
|
||||
const guess = Date.UTC(y, m - 1, dd, hour, minute);
|
||||
return guess - berlinOffset(guess - berlinOffset(guess));
|
||||
};
|
||||
const t = at(d);
|
||||
return t > now ? t : at(d + 1);
|
||||
}
|
||||
|
||||
// Plant runNightly täglich 02:30 Berlin und einmal kurz nach dem Start; stop() beendet alle Timer.
|
||||
// Test-Hooks: now, timers ({ setTimeout, clearTimeout }) und job (statt runNightly).
|
||||
export function scheduleNightly(db, { startDelayMs = 60000, log = console, now = Date.now, timers = { setTimeout, clearTimeout }, job = runNightly } = {}) {
|
||||
let timer = null;
|
||||
let stopped = false;
|
||||
const run = () => {
|
||||
try {
|
||||
const r = job(db, { now: now() });
|
||||
log.log(`Analytics-Nachtjob: ${r.aggregated} Tage verdichtet, ${r.sitesDeleted} Websites gelöscht, ${r.dailyDeleted} Tageswerte > ${DAILY_MONTHS} Monate gelöscht, ${r.orphansMarked} Websites markiert, ${r.orphanRowsDeleted} verwaiste Zeilen gelöscht`);
|
||||
} catch (e) { log.error('Analytics-Nachtjob:', e.message); }
|
||||
};
|
||||
const arm = (delay, fn) => { if (!stopped) timer = timers.setTimeout(fn, delay); };
|
||||
// Ein Timer kann früh feuern (Uhrsprung, Timer-Ungenauigkeit): dann nur neu auf den Zielzeitpunkt setzen,
|
||||
// nie vorzeitig laufen. Nach dem Lauf strikt hinter das Ziel planen, damit 02:30 nie doppelt läuft.
|
||||
const planDaily = (target) => arm(Math.max(0, target - now()), () => {
|
||||
if (now() < target) return planDaily(target);
|
||||
run();
|
||||
planDaily(nextRunAt(Math.max(now(), target) + 1000));
|
||||
});
|
||||
arm(startDelayMs, () => { run(); planDaily(nextRunAt(now())); });
|
||||
return { stop: () => { stopped = true; timers.clearTimeout(timer); } };
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const DAY = new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Berlin', year: 'numeric', month: '2-digit', day: '2-digit' });
|
||||
|
||||
// Tagessalz nur im RAM; neu erzeugt, sobald sich der Kalendertag in Europe/Berlin ändert.
|
||||
export function createSalt({ now = Date.now, random = () => crypto.randomBytes(32) } = {}) {
|
||||
let day = null;
|
||||
let salt = null;
|
||||
const current = () => {
|
||||
const d = DAY.format(now());
|
||||
if (d !== day) { day = d; salt = random(); }
|
||||
return salt;
|
||||
};
|
||||
const hmac = (s) => crypto.createHmac('sha256', current()).update(s).digest('hex');
|
||||
return {
|
||||
visitorId: (site, ip, ua) => hmac(`${site}|${ip}|${ua}`).slice(0, 16),
|
||||
rateKey: (site, ip) => hmac(`${site}|${ip}`),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import http from 'node:http';
|
||||
import crypto from 'node:crypto';
|
||||
import { createIngest } from './ingest.js';
|
||||
import { createSalt } from './salt.js';
|
||||
import { createHeat } from './heat.js';
|
||||
import { computeStats } from './stats.js';
|
||||
import { createAssets } from './assets.js';
|
||||
|
||||
const MAX_ADMIN_BODY = 64 * 1024;
|
||||
const SITE_KEY = /^[a-z0-9]{16,40}$/;
|
||||
const HOSTNAME = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/;
|
||||
|
||||
function tokenOk(header, token) {
|
||||
if (!token) return false;
|
||||
const m = /^Bearer (.+)$/.exec(header || '');
|
||||
if (!m) return false;
|
||||
const a = crypto.createHash('sha256').update(m[1]).digest();
|
||||
const b = crypto.createHash('sha256').update(token).digest();
|
||||
return crypto.timingSafeEqual(a, b);
|
||||
}
|
||||
|
||||
function send(res, status, obj) {
|
||||
const body = JSON.stringify(obj);
|
||||
res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function readJson(req, max) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
let tooBig = false;
|
||||
req.on('data', (c) => {
|
||||
if (tooBig) return; // Rest verwerfen, damit die Antwort noch ausgeliefert wird
|
||||
size += c.length;
|
||||
if (size > max) { tooBig = true; chunks.length = 0; return; }
|
||||
chunks.push(c);
|
||||
});
|
||||
req.on('end', () => {
|
||||
if (tooBig) return reject(Object.assign(new Error('Anfrage zu groß'), { status: 413 }));
|
||||
const raw = Buffer.concat(chunks).toString('utf8');
|
||||
if (!raw) return resolve({});
|
||||
try { resolve(JSON.parse(raw)); } catch { reject(Object.assign(new Error('Ungültiges JSON'), { status: 400 })); }
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function validateSite(b) {
|
||||
if (!b || typeof b !== 'object' || Array.isArray(b)) return { error: 'Ungültige Anfrage' };
|
||||
if (typeof b.projectId !== 'string' || !b.projectId || b.projectId.length > 100) return { error: 'Ungültige Projekt-ID' };
|
||||
if (!Array.isArray(b.domains) || b.domains.length < 1 || b.domains.length > 5
|
||||
|| !b.domains.every((d) => typeof d === 'string' && d.length <= 253 && HOSTNAME.test(d))) {
|
||||
return { error: 'Ungültige Domains (1 bis 5 Hostnamen, klein geschrieben, ohne Port und Pfad)' };
|
||||
}
|
||||
if (new Set(b.domains).size !== b.domains.length) return { error: 'Doppelte Domains' };
|
||||
if (typeof b.active !== 'boolean') return { error: 'Ungültiger Status' };
|
||||
return { site: { projectId: b.projectId, domains: b.domains, active: b.active } };
|
||||
}
|
||||
|
||||
function createHandlerServer({ config, db, role, geo, salt, now, onSitesChanged }) {
|
||||
const ingest = role === 'public' ? createIngest({ db, geo, salt: salt || createSalt({ now }), now }) : null;
|
||||
const heat = role === 'public' ? createHeat({ db, config, now }) : null;
|
||||
const assets = role === 'public' ? createAssets({ distDir: config.distDir, isActive: (k) => !!db.prepare('SELECT 1 FROM sites WHERE siteKey = ? AND active = 1').get(k) }) : null;
|
||||
const clock = now || Date.now;
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const url = new URL(req.url, 'http://x');
|
||||
const parts = url.pathname.split('/').filter(Boolean);
|
||||
const method = req.method;
|
||||
|
||||
if (ingest && await ingest(req, res, url.pathname)) return;
|
||||
if (heat && heat(req, res, url)) return;
|
||||
if (assets && assets(req, res, url.pathname)) return;
|
||||
|
||||
if (method === 'GET' && url.pathname === '/a/health') return send(res, 200, { ok: true });
|
||||
|
||||
if (role === 'admin' && parts[0] === 'a' && parts[1] === 'admin' && parts[2] === 'sites' && parts.length === 4
|
||||
&& (method === 'PUT' || method === 'DELETE')) {
|
||||
if (!tokenOk(req.headers.authorization, config.adminToken)) return send(res, 401, { error: 'Nicht autorisiert' });
|
||||
const key = parts[3];
|
||||
if (!SITE_KEY.test(key)) return send(res, 400, { error: 'Ungültiger Website-Schlüssel' });
|
||||
|
||||
if (method === 'DELETE') {
|
||||
db.exec('BEGIN');
|
||||
try {
|
||||
db.prepare('DELETE FROM sites WHERE siteKey = ?').run(key);
|
||||
db.prepare('DELETE FROM events_raw WHERE site = ?').run(key);
|
||||
db.prepare('DELETE FROM clicks WHERE site = ?').run(key);
|
||||
db.prepare('DELETE FROM daily WHERE site = ?').run(key);
|
||||
db.prepare('DELETE FROM agg_state WHERE site = ?').run(key);
|
||||
db.exec('COMMIT');
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
throw e;
|
||||
}
|
||||
onSitesChanged?.();
|
||||
return send(res, 200, { ok: true });
|
||||
}
|
||||
const v = validateSite(await readJson(req, MAX_ADMIN_BODY));
|
||||
if (v.error) return send(res, 400, { error: v.error });
|
||||
const { projectId, domains, active } = v.site;
|
||||
const old = db.prepare('SELECT offSince FROM sites WHERE siteKey = ?').get(key);
|
||||
const offSince = active ? null : (old && old.offSince) || new Date().toISOString();
|
||||
db.prepare(`INSERT INTO sites (siteKey, projectId, domains, active, offSince) VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(siteKey) DO UPDATE SET projectId = excluded.projectId, domains = excluded.domains,
|
||||
active = excluded.active, offSince = excluded.offSince`)
|
||||
.run(key, projectId, JSON.stringify(domains), active ? 1 : 0, offSince);
|
||||
onSitesChanged?.();
|
||||
return send(res, 200, { ok: true });
|
||||
}
|
||||
|
||||
// Beenden ohne Domainliste: active=0 und offSince einmalig (Portal: stopSite)
|
||||
if (role === 'admin' && method === 'POST' && parts[0] === 'a' && parts[1] === 'admin' && parts[2] === 'sites' && parts.length === 5 && parts[4] === 'off') {
|
||||
if (!tokenOk(req.headers.authorization, config.adminToken)) return send(res, 401, { error: 'Nicht autorisiert' });
|
||||
const key = parts[3];
|
||||
if (!SITE_KEY.test(key)) return send(res, 400, { error: 'Ungültiger Website-Schlüssel' });
|
||||
const old = db.prepare('SELECT offSince FROM sites WHERE siteKey = ?').get(key);
|
||||
if (!old) return send(res, 404, { error: 'Website nicht gefunden' });
|
||||
db.prepare('UPDATE sites SET active = 0, offSince = ? WHERE siteKey = ?').run(old.offSince || new Date().toISOString(), key);
|
||||
onSitesChanged?.();
|
||||
return send(res, 200, { ok: true });
|
||||
}
|
||||
|
||||
if (role === 'admin' && method === 'GET' && parts[0] === 'a' && parts[1] === 'admin' && parts[2] === 'stats' && parts.length === 4) {
|
||||
if (!tokenOk(req.headers.authorization, config.adminToken)) return send(res, 401, { error: 'Nicht autorisiert' });
|
||||
const key = parts[3];
|
||||
if (!SITE_KEY.test(key)) return send(res, 400, { error: 'Ungültiger Website-Schlüssel' });
|
||||
const r = url.searchParams.get('range') ?? '30';
|
||||
if (!['7', '30', '90'].includes(r)) return send(res, 400, { error: 'Ungültiger Zeitraum (7, 30 oder 90)' });
|
||||
if (!db.prepare('SELECT 1 FROM sites WHERE siteKey = ?').get(key)) return send(res, 404, { error: 'Website nicht gefunden' });
|
||||
return send(res, 200, computeStats(db, key, Number(r), clock()));
|
||||
}
|
||||
return send(res, 404, { error: 'Nicht gefunden' });
|
||||
} catch (e) {
|
||||
const status = e.status || 500;
|
||||
if (status >= 500) console.error('Analytics-Fehler:', e);
|
||||
if (!res.headersSent) send(res, status, { error: status === 500 ? 'Interner Fehler' : e.message });
|
||||
}
|
||||
});
|
||||
server.invalidateSites = ingest ? ingest.invalidateSites : () => {};
|
||||
return server;
|
||||
}
|
||||
|
||||
// Öffentlicher Listener: /a/health, /a/e (Erfassung), /a/heat, /a/w.js und /a/h.js; Admin-Routen gibt es hier nicht.
|
||||
export const createPublicServer = ({ config, db, geo, salt, now }) => createHandlerServer({ config, db, role: 'public', geo, salt, now });
|
||||
// Interner Listener für die Website-Verwaltung.
|
||||
// onSitesChanged: wird nach jeder Änderung an Websites gerufen (public.invalidateSites()).
|
||||
export const createAdminServer = ({ config, db, onSitesChanged, now }) => createHandlerServer({ config, db, role: 'admin', onSitesChanged, now });
|
||||
@@ -0,0 +1,137 @@
|
||||
// Statistik für das Portal: Vergangenheit aus `daily`, noch nicht verdichtete Tage live aus den Rohdaten.
|
||||
import { dayOf, aggregateRows } from './jobs.js';
|
||||
|
||||
const TOP = 20;
|
||||
|
||||
// Kalendertag (YYYY-MM-DD) um `n` Tage verschoben, rein kalendarisch (UTC-Datumsrechnung, keine Zeitumstellung).
|
||||
function shiftDay(day, n) {
|
||||
const [y, m, d] = day.split('-').map(Number);
|
||||
return new Date(Date.UTC(y, m - 1, d + n)).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
// Liste der Kalendertage (YYYY-MM-DD), oldest first, endend am Tag `endDay`.
|
||||
function dayList(endDay, range) {
|
||||
const days = [];
|
||||
for (let i = range - 1; i >= 0; i--) days.push(shiftDay(endDay, -i));
|
||||
return days;
|
||||
}
|
||||
|
||||
const top = (map) => [...map].map(([key, count]) => ({ key, count })).filter((e) => e.count > 0)
|
||||
.sort((a, b) => b.count - a.count || (a.key < b.key ? -1 : 1)).slice(0, TOP);
|
||||
|
||||
// Sammelt die Zähler für eine zusammenhängende Tagesliste: verdichtete Tage aus `daily`,
|
||||
// Tage ohne agg_state (heute, ggf. gestern vor 02:30) live aus den Rohdaten. Kein Tag zählt doppelt.
|
||||
function collect(db, site, days, now) {
|
||||
const first = days[0];
|
||||
const last = days[days.length - 1];
|
||||
const inRange = new Set(days);
|
||||
const perDay = new Map(days.map((d) => [d, { pv: 0, visitors: 0, contacts: 0, en: 0, sec: 0, scroll: 0 }]));
|
||||
const dims = {};
|
||||
const apply = (day, dim, key, n) => {
|
||||
const t = perDay.get(day);
|
||||
if (dim === 'pv_total') t.pv += n;
|
||||
else if (dim === 'visitors') t.visitors += n;
|
||||
else if (dim === 'ct_kind') t.contacts += n;
|
||||
else if (dim === 'en_count') t.en += n;
|
||||
else if (dim === 'en_sec_sum') t.sec += n;
|
||||
else if (dim === 'scroll_sum') t.scroll += n;
|
||||
if (!dims[dim]) dims[dim] = new Map();
|
||||
dims[dim].set(key, (dims[dim].get(key) || 0) + n);
|
||||
};
|
||||
|
||||
const done = new Set(db.prepare('SELECT day FROM agg_state WHERE site = ? AND day >= ? AND day <= ?').all(site, first, last).map((r) => r.day));
|
||||
for (const r of db.prepare('SELECT day, dim, key, count FROM daily WHERE site = ? AND day >= ? AND day <= ?').all(site, first, last)) {
|
||||
if (inRange.has(r.day)) apply(r.day, r.dim, r.key, r.count);
|
||||
}
|
||||
// Ein Berliner Tag beginnt 22:00/23:00 UTC des Vortags: zwei Tage Rand reichen auf beiden Seiten, danach filtert dayOf
|
||||
const [fy, fm, fd] = first.split('-').map(Number);
|
||||
const [ly, lm, ld] = last.split('-').map(Number);
|
||||
const lo = Date.UTC(fy, fm - 1, fd - 2);
|
||||
const hi = Math.min(now, Date.UTC(ly, lm - 1, ld + 2));
|
||||
const live = new Map();
|
||||
for (const r of db.prepare('SELECT ts, type, path, vp, ref, country, device, visitor, sec, scroll, kind FROM events_raw WHERE site = ? AND ts >= ? AND ts <= ? ORDER BY ts')
|
||||
.all(site, lo, hi)) {
|
||||
const d = dayOf(r.ts);
|
||||
if (!inRange.has(d) || done.has(d)) continue;
|
||||
if (!live.has(d)) live.set(d, []);
|
||||
live.get(d).push(r);
|
||||
}
|
||||
for (const [day, rows] of live) for (const [dim, key, n] of aggregateRows(rows)) apply(day, dim, key, n);
|
||||
return { days, perDay, dims };
|
||||
}
|
||||
|
||||
const avg = (sum, n) => (n ? Math.round(sum / n) : null);
|
||||
|
||||
// Formt gesammelte Zähler zu banner + totals + series (gleiche Form für aktuellen Zeitraum und Vorzeitraum).
|
||||
function shape({ days, perDay, dims }) {
|
||||
const sum = (dim) => [...(dims[dim] || new Map()).values()].reduce((a, b) => a + b, 0);
|
||||
const banner = { shown: sum('bn_shown'), accept: sum('bn_accept'), reject: sum('bn_reject') };
|
||||
const decided = banner.accept + banner.reject;
|
||||
const en = sum('en_count');
|
||||
return {
|
||||
banner,
|
||||
totals: {
|
||||
pv: sum('pv_total'),
|
||||
// Summe der Tages-Besucher (kein tagesübergreifender Abgleich möglich: Salz wechselt täglich)
|
||||
visitors: sum('visitors'),
|
||||
contacts: sum('ct_kind'),
|
||||
consentRate: decided ? banner.accept / decided : 0,
|
||||
engaged: en,
|
||||
avgSec: avg(sum('en_sec_sum'), en),
|
||||
avgScroll: avg(sum('scroll_sum'), en),
|
||||
},
|
||||
series: days.map((day) => {
|
||||
const t = perDay.get(day);
|
||||
return { day, pv: t.pv, visitors: t.visitors, contacts: t.contacts, avgSec: avg(t.sec, t.en) };
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// Frühester Berliner Tag mit Daten dieser Website ('' = keine): kleinster Tag aus daily und events_raw.
|
||||
function firstDayOf(db, site) {
|
||||
const fromDaily = db.prepare('SELECT MIN(day) AS d FROM daily WHERE site = ?').get(site)?.d || '';
|
||||
const ts = db.prepare('SELECT MIN(ts) AS t FROM events_raw WHERE site = ?').get(site)?.t;
|
||||
const fromRaw = ts == null ? '' : dayOf(ts);
|
||||
if (!fromDaily) return fromRaw;
|
||||
if (!fromRaw) return fromDaily;
|
||||
return fromDaily < fromRaw ? fromDaily : fromRaw;
|
||||
}
|
||||
|
||||
// Der Zeitraum sind die `range` abgeschlossenen Kalendertage, die gestern enden (wie GA4 "Letzte 28 Tage"): Der
|
||||
// angebrochene heutige Tag würde gegen einen vollen Vorzeitraum als Einbruch erscheinen. Heute steht getrennt in `today`.
|
||||
export function computeStats(db, site, range, now = Date.now()) {
|
||||
const todayDay = dayOf(now);
|
||||
const days = dayList(shiftDay(todayDay, -1), range);
|
||||
const cur = collect(db, site, days, now);
|
||||
const { banner, totals, series } = shape(cur);
|
||||
// Heute bis jetzt: live aus den Rohdaten (der Tag ist noch nicht verdichtet)
|
||||
const t = shape(collect(db, site, [todayDay], now)).series[0];
|
||||
// Vorzeitraum: die `range` Kalendertage direkt vor dem aktuellen Zeitraum (kalendarisch, nie now - range * 24 h)
|
||||
const prevDays = dayList(shiftDay(days[0], -1), range);
|
||||
const prevCollected = collect(db, site, prevDays, now);
|
||||
const prev = shape(prevCollected);
|
||||
const firstDay = firstDayOf(db, site);
|
||||
return {
|
||||
range,
|
||||
from: days[0],
|
||||
to: days[days.length - 1],
|
||||
firstDay,
|
||||
totals,
|
||||
series,
|
||||
today: { day: todayDay, pv: t.pv, visitors: t.visitors, contacts: t.contacts },
|
||||
pages: top(cur.dims.pv_path || new Map()),
|
||||
refs: top(cur.dims.pv_ref || new Map()),
|
||||
countries: top(cur.dims.pv_country || new Map()),
|
||||
devices: top(cur.dims.pv_device || new Map()),
|
||||
contacts: top(cur.dims.ct_kind || new Map()),
|
||||
banner,
|
||||
previous: {
|
||||
from: prevDays[0],
|
||||
to: prevDays[prevDays.length - 1],
|
||||
complete: firstDay !== '' && firstDay <= prevDays[0],
|
||||
totals: prev.totals,
|
||||
series: prev.series,
|
||||
contacts: top(prevCollected.dims.ct_kind || new Map()),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Gröbste Bot- und Gerätetyp-Erkennung; der User-Agent wird danach verworfen.
|
||||
const BOT = /bot|crawl|spider|slurp|headless|lighthouse|pingdom|uptime|curl|wget|python|go-http|java\/|facebookexternalhit|preview/i;
|
||||
|
||||
export function classifyUa(ua) {
|
||||
const s = typeof ua === 'string' ? ua : '';
|
||||
if (!s || BOT.test(s)) return { bot: true, device: 'd' };
|
||||
if (/Mobi|Android.*Mobile|iPhone/i.test(s)) return { bot: false, device: 'm' };
|
||||
if (/iPad|Tablet|Android(?!.*Mobile)/i.test(s)) return { bot: false, device: 't' };
|
||||
return { bot: false, device: 'd' };
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import crypto from 'node:crypto';
|
||||
import http from 'node:http';
|
||||
import { readFileSync, mkdtempSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { gunzipSync } from 'node:zlib';
|
||||
import { openDb } from '../src/db.js';
|
||||
import { createPublicServer, createAdminServer } from '../src/server.js';
|
||||
|
||||
const DIST = new URL('../client/dist/', import.meta.url);
|
||||
|
||||
const KEY = 'abcdef0123456789abcd';
|
||||
const OFFKEY = 'offoffoffoffoffoffoff';
|
||||
async function start(config = {}) {
|
||||
const db = openDb(':memory:');
|
||||
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)').run(KEY, 'p', '[]');
|
||||
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,0)').run(OFFKEY, 'p', '[]');
|
||||
const server = createPublicServer({ config, db });
|
||||
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
||||
return { base: `http://127.0.0.1:${server.address().port}`, close: () => server.close() };
|
||||
}
|
||||
|
||||
// fetch dekodiert gzip selbst; für den rohen Body http.request verwenden.
|
||||
function raw(base, path, headers = {}, method = 'GET') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request(base + path, { method, headers }, (res) => {
|
||||
const chunks = [];
|
||||
res.on('data', (c) => chunks.push(c));
|
||||
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) }));
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
for (const file of ['w.js', 'h.js']) {
|
||||
test(`/a/${file}: Header, identity und gzip liefern den dist-Inhalt`, async () => {
|
||||
const s = await start();
|
||||
const want = readFileSync(new URL(file, DIST));
|
||||
const etag = `"${crypto.createHash('sha256').update(want).digest('hex')}"`;
|
||||
const id = await raw(s.base, `/a/${file}${file === 'w.js' ? `?s=${KEY}` : ''}`, { 'Accept-Encoding': 'identity' });
|
||||
assert.equal(id.status, 200);
|
||||
assert.deepEqual(id.body, want);
|
||||
assert.equal(id.headers['content-type'], 'text/javascript; charset=utf-8');
|
||||
assert.equal(id.headers['cache-control'], 'public, max-age=3600');
|
||||
assert.equal(id.headers.etag, etag);
|
||||
assert.equal(id.headers.vary, 'Accept-Encoding');
|
||||
assert.equal(id.headers['x-content-type-options'], 'nosniff');
|
||||
assert.equal(id.headers['cross-origin-resource-policy'], 'cross-origin');
|
||||
assert.equal(id.headers['access-control-allow-origin'], '*');
|
||||
assert.equal(id.headers['content-encoding'], undefined);
|
||||
const gz = await raw(s.base, `/a/${file}${file === 'w.js' ? `?s=${KEY}` : ''}`, { 'Accept-Encoding': 'gzip, deflate' });
|
||||
assert.equal(gz.status, 200);
|
||||
assert.equal(gz.headers['content-encoding'], 'gzip');
|
||||
assert.equal(gz.headers.etag, etag);
|
||||
assert.ok(gz.body.length < want.length);
|
||||
assert.deepEqual(gunzipSync(gz.body), want);
|
||||
s.close();
|
||||
});
|
||||
}
|
||||
|
||||
test('ETag: 304 bei If-None-Match, 200 bei anderem Wert', async () => {
|
||||
const s = await start();
|
||||
const first = await raw(s.base, `/a/w.js?s=${KEY}`);
|
||||
const hit = await raw(s.base, `/a/w.js?s=${KEY}`, { 'If-None-Match': first.headers.etag });
|
||||
assert.equal(hit.status, 304);
|
||||
assert.equal(hit.body.length, 0);
|
||||
assert.equal(hit.headers.etag, first.headers.etag);
|
||||
assert.equal(hit.headers['cache-control'], 'public, max-age=3600');
|
||||
assert.equal((await raw(s.base, `/a/w.js?s=${KEY}`, { 'If-None-Match': '"anders"' })).status, 200);
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('HEAD: Header wie GET, kein Body', async () => {
|
||||
const s = await start();
|
||||
const get = await raw(s.base, '/a/h.js', { 'Accept-Encoding': 'identity' });
|
||||
const head = await raw(s.base, '/a/h.js', { 'Accept-Encoding': 'identity' }, 'HEAD');
|
||||
assert.equal(head.status, 200);
|
||||
assert.equal(head.body.length, 0);
|
||||
assert.equal(head.headers['content-length'], String(get.body.length));
|
||||
assert.equal(head.headers.etag, get.headers.etag);
|
||||
assert.equal((await raw(s.base, '/a/h.js', {}, 'POST')).status, 405);
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('Admin-Listener liefert die Skripte nicht', async () => {
|
||||
const server = createAdminServer({ config: { adminToken: 't' }, db: openDb(':memory:') });
|
||||
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
assert.equal((await fetch(`${base}/a/w.js`)).status, 404);
|
||||
assert.equal((await fetch(`${base}/a/h.js`)).status, 404);
|
||||
server.close();
|
||||
});
|
||||
|
||||
test('fehlende dist-Datei: 503 nur für diesen Pfad, deutscher Fehler im Log', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dist-'));
|
||||
writeFileSync(join(dir, 'w.js'), 'console.log(1)');
|
||||
const orig = console.error;
|
||||
const logged = [];
|
||||
console.error = (...a) => logged.push(a.join(' '));
|
||||
let s;
|
||||
try { s = await start({ distDir: dir }); } finally { console.error = orig; }
|
||||
assert.ok(logged.some((l) => l.includes('h.js') && l.includes('fehlt')));
|
||||
assert.equal((await fetch(`${s.base}/a/w.js`)).status, 200);
|
||||
assert.equal((await fetch(`${s.base}/a/h.js`)).status, 503);
|
||||
s.close();
|
||||
});
|
||||
|
||||
const EMPTY = '/* WEBklar Analyse: aus */\n';
|
||||
for (const [name, q] of [['unbekannter Schlüssel', '?s=unbekannt0123456789ab'], ['ungültiger Schlüssel', '?s=ABC'], ['ohne s', ''], ['ausgeschaltete Website', `?s=${OFFKEY}`]]) {
|
||||
test(`w.js ${name}: leeres Skript`, async () => {
|
||||
const s = await start();
|
||||
const r = await raw(s.base, `/a/w.js${q}`, { 'Accept-Encoding': 'gzip' });
|
||||
assert.equal(r.status, 200);
|
||||
assert.equal(r.body.toString(), EMPTY);
|
||||
assert.equal(r.headers['content-type'], 'text/javascript; charset=utf-8');
|
||||
assert.equal(r.headers['cache-control'], 'public, max-age=60');
|
||||
assert.equal(r.headers['x-content-type-options'], 'nosniff');
|
||||
assert.equal(r.headers['cross-origin-resource-policy'], 'cross-origin');
|
||||
assert.equal(r.headers['access-control-allow-origin'], '*');
|
||||
assert.equal(r.headers.vary, 'Accept-Encoding');
|
||||
assert.equal(r.headers['content-encoding'], undefined);
|
||||
assert.equal(r.headers.etag, undefined);
|
||||
s.close();
|
||||
});
|
||||
}
|
||||
|
||||
test('Ende-zu-Ende: Ausschalten wirkt sofort, Einschalten ebenfalls', async () => {
|
||||
const db = openDb(':memory:');
|
||||
const pub = createPublicServer({ config: {}, db });
|
||||
const adm = createAdminServer({ config: { adminToken: 't' }, db });
|
||||
await Promise.all([pub, adm].map((x) => new Promise((r) => x.listen(0, '127.0.0.1', r))));
|
||||
const pb = `http://127.0.0.1:${pub.address().port}`;
|
||||
const ab = `http://127.0.0.1:${adm.address().port}`;
|
||||
const put = (active) => fetch(`${ab}/a/admin/sites/${KEY}`, { method: 'PUT', headers: { Authorization: 'Bearer t', 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ projectId: 'p', domains: ['abc.project.webklar.com'], active }) });
|
||||
const get = async () => (await raw(pb, `/a/w.js?s=${KEY}`)).body.toString();
|
||||
assert.equal(await get(), EMPTY);
|
||||
assert.equal((await put(true)).status, 200);
|
||||
assert.notEqual(await get(), EMPTY);
|
||||
assert.equal((await put(false)).status, 200);
|
||||
assert.equal(await get(), EMPTY);
|
||||
assert.equal((await put(true)).status, 200);
|
||||
assert.notEqual(await get(), EMPTY);
|
||||
pub.close(); adm.close();
|
||||
});
|
||||
@@ -0,0 +1,709 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import vm from 'node:vm';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import {
|
||||
vpClass, cleanPath, refHost, cssPath, contactKind, readConsent, contrastOk, escapeHtml,
|
||||
} from '../client/core.js';
|
||||
import { BANNER_VERSION, BANNER_TEXT, BANNER_LINK_LABEL, BANNER_ARIA, SETTINGS_LABEL, BANNER_VERSION_EIGEN, BANNER_TEXT_EIGEN, bannerFor } from '../client/core.js';
|
||||
import { buildAll } from '../client/build.js';
|
||||
|
||||
const BV = '2026-10-02';
|
||||
const DIST = join(dirname(fileURLToPath(import.meta.url)), '..', 'client', 'dist');
|
||||
|
||||
// ---------- core ----------
|
||||
test('vpClass', () => {
|
||||
assert.equal(vpClass(0), 'm');
|
||||
assert.equal(vpClass(767), 'm');
|
||||
assert.equal(vpClass(768), 't');
|
||||
assert.equal(vpClass(1023), 't');
|
||||
assert.equal(vpClass(1024), 'd');
|
||||
assert.equal(vpClass(NaN), 'd');
|
||||
});
|
||||
|
||||
test('cleanPath', () => {
|
||||
assert.equal(cleanPath('https://x.de/a/b?q=1#h'), '/a/b');
|
||||
assert.equal(cleanPath('/über%20uns?x=1'), '/über uns');
|
||||
assert.equal(cleanPath('/%E0%A4%A'), '/%E0%A4%A'); // kaputtes Encoding bleibt roh
|
||||
assert.equal(cleanPath('https://x.de'), '/');
|
||||
assert.equal(cleanPath(''), '/');
|
||||
assert.equal(cleanPath('/' + 'a'.repeat(400)).length, 300);
|
||||
assert.equal(cleanPath(null), '/');
|
||||
assert.equal(cleanPath('//foo/bar?x=1'), '//foo/bar');
|
||||
assert.equal(cleanPath('//foo'), '//foo');
|
||||
});
|
||||
|
||||
test('refHost', () => {
|
||||
assert.equal(refHost('https://www.google.com/search?q=x', 'mein.de'), 'www.google.com');
|
||||
assert.equal(refHost('https://mein.de/a', 'mein.de'), '');
|
||||
assert.equal(refHost('https://www.mein.de/a', 'mein.de'), '');
|
||||
assert.equal(refHost('', 'mein.de'), '');
|
||||
assert.equal(refHost('kein url', 'mein.de'), '');
|
||||
assert.equal(refHost('android-app://com.x', 'mein.de'), '');
|
||||
assert.equal(refHost('HTTPS://Foo.DE/x', 'mein.de'), 'foo.de');
|
||||
});
|
||||
|
||||
function el(tag, o = {}) {
|
||||
const e = { tagName: tag.toUpperCase(), id: o.id || '', parentNode: o.parent || null, previousElementSibling: o.prev || null,
|
||||
attrs: { class: o.cls || '' }, getAttribute(n) { return this.attrs[n] ?? null; } };
|
||||
return e;
|
||||
}
|
||||
|
||||
test('cssPath: nur Tags + nth-of-type, keine IDs/Klassen', () => {
|
||||
const body = el('body');
|
||||
const div = el('div', { parent: body, cls: 'hero main' });
|
||||
const p1 = el('p', { parent: div });
|
||||
const btn0 = el('button', { parent: div, prev: p1 });
|
||||
const btn = el('button', { parent: div, prev: btn0, cls: 'cta' });
|
||||
assert.equal(cssPath(btn), 'body:nth-of-type(1) > div:nth-of-type(1) > button:nth-of-type(2)');
|
||||
const pii = el('a', { parent: body, id: 'max@firma.de', cls: 'team-max-mustermann' });
|
||||
const sel = cssPath(pii);
|
||||
assert.equal(sel, 'body:nth-of-type(1) > a:nth-of-type(1)');
|
||||
assert.ok(!/max|firma|mustermann|#|\./.test(sel));
|
||||
// max. 6 Ebenen
|
||||
let n = el('div');
|
||||
for (let i = 0; i < 9; i++) n = el('span', { parent: n });
|
||||
assert.equal(cssPath(n).split(' > ').length, 6);
|
||||
// stoppt bei body
|
||||
const outer = el('html'); body.parentNode = outer;
|
||||
assert.equal(cssPath(div).split(' > ')[0], 'body:nth-of-type(1)');
|
||||
// <= 200
|
||||
let deep = el('div');
|
||||
for (let i = 0; i < 5; i++) deep = el('x'.repeat(40), { parent: deep });
|
||||
assert.ok(cssPath(deep).length <= 200);
|
||||
assert.equal(cssPath(null), '');
|
||||
});
|
||||
|
||||
test('contactKind', () => {
|
||||
const k = (href) => contactKind({ href }, 'mein.de');
|
||||
assert.equal(k('tel:+49123'), 'tel');
|
||||
assert.equal(k('TEL:+49123'), 'tel');
|
||||
assert.equal(k('mailto:a@b.de'), 'mail');
|
||||
assert.equal(k('https://wa.me/49123'), 'wa');
|
||||
assert.equal(k('https://api.whatsapp.com/send?phone=1'), 'wa');
|
||||
assert.equal(k('whatsapp://send?text=x'), 'wa');
|
||||
assert.equal(k('https://anders.de/x'), 'out');
|
||||
assert.equal(k('https://mein.de/x'), null);
|
||||
assert.equal(k('https://www.mein.de/x'), null);
|
||||
assert.equal(k('/kontakt'), null);
|
||||
assert.equal(k('#top'), null);
|
||||
assert.equal(k('javascript:void(0)'), null);
|
||||
assert.equal(k(''), null);
|
||||
assert.equal(contactKind(null, 'mein.de'), null);
|
||||
});
|
||||
|
||||
test('readConsent', () => {
|
||||
const now = 1_700_000_000_000;
|
||||
const day = 86400000;
|
||||
const st = (v) => ({ getItem: () => v });
|
||||
const J = (o) => st(JSON.stringify(o));
|
||||
assert.equal(BANNER_VERSION, '2026-10-02');
|
||||
assert.deepEqual(readConsent(J({ v: 1, a: 'accept', bv: BV, t: now - 10 * day }), now), { v: 1, a: 'accept', t: now - 10 * day, bv: BV });
|
||||
assert.equal(readConsent(J({ v: 1, a: 'reject', bv: BV, t: now - 182 * day }), now).a, 'reject');
|
||||
assert.equal(readConsent(J({ v: 1, a: 'accept', bv: BV, t: now - 183 * day }), now), null);
|
||||
assert.equal(readConsent(J({ v: 1, a: 'accept', t: now }), now), null, 'ohne bv');
|
||||
assert.equal(readConsent(J({ v: 1, a: 'accept', bv: '2026-01-01', t: now }), now), null, 'alte Fassung');
|
||||
assert.equal(readConsent(J({ v: 2, a: 'accept', bv: BV, t: now }), now), null);
|
||||
assert.equal(readConsent(J({ v: 1, a: 'maybe', bv: BV, t: now }), now), null);
|
||||
assert.equal(readConsent(J({ v: 1, a: 'accept', bv: BV, t: 'x' }), now), null);
|
||||
assert.equal(readConsent(J({ v: 1, a: 'accept', bv: BV, t: now + day }), now), null);
|
||||
assert.equal(readConsent(st('kaputt{'), now), null);
|
||||
assert.equal(readConsent(st(null), now), null);
|
||||
assert.equal(readConsent({ getItem() { throw new Error('blocked'); } }, now), null);
|
||||
assert.equal(readConsent(null, now), null);
|
||||
});
|
||||
|
||||
test('contrastOk', () => {
|
||||
assert.equal(contrastOk('#111111'), true);
|
||||
assert.equal(contrastOk('#ffffff'), false);
|
||||
assert.equal(contrastOk('#ffff00'), false);
|
||||
assert.equal(contrastOk('#767676'), true); // 4.54
|
||||
assert.equal(contrastOk('#777777'), false); // 4.48
|
||||
assert.equal(contrastOk('#00F'), false); // ungueltig
|
||||
assert.equal(contrastOk(undefined), false);
|
||||
});
|
||||
|
||||
test('escapeHtml', () => {
|
||||
assert.equal(escapeHtml(`<a href="x">&'</a>`), '<a href="x">&'</a>');
|
||||
assert.equal(escapeHtml(null), '');
|
||||
});
|
||||
|
||||
// ---------- dist ----------
|
||||
test('dist ist aktuell', () => {
|
||||
const b = buildAll();
|
||||
assert.equal(readFileSync(join(DIST, 'w.js'), 'utf8'), b.w);
|
||||
assert.equal(readFileSync(join(DIST, 'h.js'), 'utf8'), b.h);
|
||||
assert.ok(!/^\s*(import|export)\s/m.test(b.w + b.h));
|
||||
});
|
||||
|
||||
// ---------- Verhalten von w.js (Fake-DOM) ----------
|
||||
function makeEl(tag) {
|
||||
const e = {
|
||||
tagName: String(tag).toUpperCase(), attrs: {}, children: [], listeners: {}, style: {}, parentNode: null, id: '',
|
||||
textContent: '', focused: false, removed: false,
|
||||
setAttribute(n, v) { this.attrs[n] = String(v); if (n === 'id') this.id = String(v); },
|
||||
getAttribute(n) { return n in this.attrs ? this.attrs[n] : null; },
|
||||
appendChild(c) { c.parentNode = this; this.children.push(c); return c; },
|
||||
append(...cs) { cs.forEach((c) => this.appendChild(c)); },
|
||||
addEventListener(t, f) { (this.listeners[t] ||= []).push(f); },
|
||||
removeEventListener(t, f) { this.listeners[t] = (this.listeners[t] || []).filter((x) => x !== f); },
|
||||
focus() { this.focused = true; },
|
||||
getBoundingClientRect() { return { height: this.fakeHeight || 0 }; },
|
||||
remove() { this.removed = true; if (this.parentNode) this.parentNode.children = this.parentNode.children.filter((c) => c !== this); },
|
||||
closest(sel) {
|
||||
const attr = /^\[([\w-]+)\]$/.exec(sel);
|
||||
for (let n = this; n; n = n.parentNode) if (attr && n.attrs && attr[1] in n.attrs) return n;
|
||||
return null;
|
||||
},
|
||||
attachShadow() { const r = makeEl('shadow'); r.isShadow = true; this.shadow = r; return r; },
|
||||
click(extra = {}) { fire(this, 'click', { target: this, ...extra }); },
|
||||
};
|
||||
Object.defineProperty(e, 'href', { get() { return this.attrs.href || ''; } });
|
||||
Object.defineProperty(e, 'className', { set(v) { this.attrs.class = v; }, get() { return this.attrs.class || ''; } });
|
||||
return e;
|
||||
}
|
||||
function fire(node, type, ev) { for (const f of [...(node.listeners[type] || [])]) f({ type, preventDefault() {}, ...ev }); }
|
||||
function walk(n, fn) { fn(n); for (const c of n.children || []) walk(c, fn); if (n.shadow) walk(n.shadow, fn); }
|
||||
function findAll(root, pred) { const out = []; walk(root, (n) => { if (pred(n)) out.push(n); }); return out; }
|
||||
const btn = (root, label) => findAll(root, (n) => n.tagName === 'BUTTON' && n.textContent === label)[0];
|
||||
const settingsBtn = (h) => findAll(h.body, (n) => n.tagName === 'BUTTON' && n.attrs['aria-label'] === SETTINGS_LABEL)[0];
|
||||
|
||||
function harness({ gpc, dnt, stored, search = '', src = 'https://project.webklar.com/a/w.js', withConsentLink = false, bannerHeight = 0, bodyPad, variant } = {}) {
|
||||
const sent = []; const reads = { referrer: 0, innerWidth: 0 }; const beacon = []; const fetches = []; const store = {}; let now = 1_700_000_000_000;
|
||||
if (stored) store['wk-consent'] = JSON.stringify(stored);
|
||||
const body = makeEl('body'); if (bodyPad !== undefined) body.style.paddingBottom = bodyPad; const html = makeEl('html');
|
||||
const timers = [];
|
||||
const script = makeEl('script'); script.setAttribute('src', src); script.setAttribute('data-site', 'abcdef0123456789abcd'); script.setAttribute('data-color', '#0a5a8a'); if (variant !== undefined) script.setAttribute('data-variant', variant); script.src = src;
|
||||
const docL = {}; const winL = {};
|
||||
const link = withConsentLink ? makeEl('a') : null; if (link) link.setAttribute('data-webklar-consent', '');
|
||||
const injected = [];
|
||||
if (link) link.click = () => { for (const f of [...(docL.click || [])]) f({ type: 'click', target: link, preventDefault() {} }); };
|
||||
const document = {
|
||||
currentScript: script, get referrer() { reads.referrer++; return 'https://www.google.com/'; }, visibilityState: 'visible',
|
||||
documentElement: Object.assign(html, { scrollWidth: 1000, scrollHeight: 2000 }), body, head: makeEl('head'),
|
||||
createElement(t) { const e = makeEl(t); if (bannerHeight) e.offsetHeight = bannerHeight; return e; }, activeElement: null,
|
||||
createElementNS(ns, t) { const e = makeEl(t); e.ns = ns; return e; },
|
||||
querySelector(s) { return s === '[data-webklar-consent]' ? link : null; },
|
||||
addEventListener(t, f) { (docL[t] ||= []).push(f); },
|
||||
removeEventListener(t, f) { docL[t] = (docL[t] || []).filter((x) => x !== f); },
|
||||
};
|
||||
body.appendChild = function (c) { c.parentNode = this; this.children.push(c); if (c.attrs.src) injected.push(c); return c; };
|
||||
document.head.appendChild = body.appendChild;
|
||||
const origPush = function () {};
|
||||
const history = { pushState: origPush, replaceState(s, t, u) { this.replaced = u; } };
|
||||
const win = {
|
||||
get innerWidth() { reads.innerWidth++; return 1200; }, innerHeight: 800, scrollX: 0, scrollY: 300, document, history,
|
||||
location: { hostname: 'mein.de', origin: 'https://mein.de', pathname: '/start', search, href: 'https://mein.de/start' + search, hash: '' },
|
||||
addEventListener(t, f) { (winL[t] ||= []).push(f); },
|
||||
removeEventListener(t, f) { winL[t] = (winL[t] || []).filter((x) => x !== f); },
|
||||
};
|
||||
const sandbox = {
|
||||
window: win, document, history, location: win.location, URL, URLSearchParams, JSON, Math, Date: { now: () => now }, String, Number, Array, Object, RegExp, Error, parseInt,
|
||||
navigator: { globalPrivacyControl: gpc, doNotTrack: dnt, sendBeacon(u, d) { beacon.push({ u, d }); return true; } },
|
||||
localStorage: { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); } },
|
||||
setTimeout: (f, ms) => { timers.push(f); return timers.length; }, clearTimeout() {},
|
||||
setInterval: () => 0, clearInterval() {}, Blob: class {}, fetch: (u, o) => { fetches.push({ u, o }); sent.push({ u, d: JSON.parse(o.body) }); return Promise.resolve(); },
|
||||
};
|
||||
win.localStorage = sandbox.localStorage;
|
||||
sandbox.globalThis = sandbox;
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(readFileSync(join(DIST, 'w.js'), 'utf8'), sandbox);
|
||||
const types = () => sent.map((s) => s.d.t + (s.d.a ? ':' + s.d.a : ''));
|
||||
const ck = (target, ev = {}) => { for (const f of [...(docL.click || [])]) f({ type: 'click', target, pageX: 500, pageY: 1000, clientX: 500, clientY: 700, preventDefault() {}, ...ev }); };
|
||||
const leave = () => { for (const f of [...(winL.pagehide || [])]) f({}); document.visibilityState = 'hidden'; for (const f of [...(docL.visibilitychange || [])]) f({}); };
|
||||
const rerun = () => vm.runInContext(readFileSync(join(DIST, 'w.js'), 'utf8'), sandbox);
|
||||
const storage = () => (winL.storage || []).forEach((f) => f({ key: 'wk-consent' }));
|
||||
const nav = (p) => { win.location.pathname = p; history.pushState(null, '', p); timers.splice(0).forEach((f) => f()); };
|
||||
return { nav, reads, beacon, fetches, origPush, rerun, storage, sent, store, types, ck, leave, body, docL, winL, win, injected, history, timers, setNow: (n) => { now = n; }, link, document };
|
||||
}
|
||||
const target = () => { const b = makeEl('button'); b.parentNode = makeEl('body'); b.attrs.class = 'cta'; return b; };
|
||||
const hostEl = (h) => h.body.children.find((c) => c.shadow && findAll(c.shadow, (n) => n.attrs && n.attrs.role === 'dialog').length);
|
||||
|
||||
const SITE = 'abcdef0123456789abcd';
|
||||
const ACC0 = { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 };
|
||||
test('w.js: ohne Einwilligung nur pv {t,site,path} + bn shown, kein Lesen von referrer/innerWidth', () => {
|
||||
const h = harness();
|
||||
assert.deepEqual(h.types(), ['pv', 'bn:shown']);
|
||||
assert.equal(h.sent[0].u, 'https://project.webklar.com/a/e');
|
||||
assert.deepEqual(h.sent[0].d, { t: 'pv', site: SITE, path: '/start' });
|
||||
assert.deepEqual(Object.keys(h.sent[0].d).sort(), ['path', 'site', 't']);
|
||||
assert.deepEqual(h.sent[1].d, { t: 'bn', site: SITE, a: 'shown' });
|
||||
assert.equal(h.reads.referrer, 0);
|
||||
assert.equal(h.reads.innerWidth, 0);
|
||||
h.ck(target()); h.leave();
|
||||
assert.deepEqual(h.types(), ['pv', 'bn:shown']);
|
||||
assert.equal((h.docL.click || []).length, 1); // nur die Einstellungs-Delegation
|
||||
});
|
||||
|
||||
test('core: Banner-Konstanten', () => {
|
||||
assert.match(BANNER_TEXT, /^Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung\./);
|
||||
assert.match(BANNER_TEXT, /Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast\./);
|
||||
assert.match(BANNER_TEXT, /über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen\. Details: $/);
|
||||
assert.equal(BANNER_LINK_LABEL, 'Datenschutz');
|
||||
assert.equal(BANNER_ARIA, 'Einwilligung zur Reichweitenmessung');
|
||||
assert.equal(SETTINGS_LABEL, 'Datenschutz-Einstellungen');
|
||||
});
|
||||
|
||||
test('w.js: Banner-Aufbau (Dialog, Texte, gleiche Buttons, Akzent nur Annehmen)', () => {
|
||||
const h = harness();
|
||||
const host = hostEl(h);
|
||||
assert.ok(host, 'Banner im Shadow Root');
|
||||
const dlg = findAll(host.shadow, (n) => n.attrs.role === 'dialog')[0];
|
||||
assert.equal(dlg.attrs['aria-label'], BANNER_ARIA);
|
||||
const all = findAll(host.shadow, () => true);
|
||||
const text = all.map((n) => n.textContent).join(' ');
|
||||
assert.ok(all.find((n) => n.tagName === 'P').textContent.startsWith(BANNER_TEXT), 'Text beginnt mit BANNER_TEXT');
|
||||
const a = all.find((n) => n.tagName === 'A');
|
||||
assert.equal(a.attrs.href, '/datenschutz-analyse.html');
|
||||
assert.equal(a.textContent, 'Datenschutz');
|
||||
const css = all.filter((n) => n.tagName === 'STYLE').map((n) => n.textContent).join('');
|
||||
assert.match(css, /#0a5a8a/);
|
||||
assert.match(css, /max-width:\s*767px/);
|
||||
const buttons = all.filter((n) => n.tagName === 'BUTTON');
|
||||
assert.deepEqual(buttons.map((b) => b.textContent), ['Ablehnen', 'Annehmen']);
|
||||
assert.ok(!buttons[0].focused, 'kein Fokusraub beim Laden');
|
||||
assert.equal(buttons[0].attrs.class, buttons[1].attrs.class);
|
||||
assert.ok(!/button\.|\.p\b/.test(css), 'eine Regel fuer beide Buttons');
|
||||
assert.match(css, /button\{[^}]*background:#0a5a8a[^}]*color:#fff/);
|
||||
assert.match(css, /button\{[^}]*[; ]width:150px/);
|
||||
assert.equal((css.match(/(?<![-\w])width:/g) || []).length, 2, 'eine feste Breite fuer beide + 100% mobil');
|
||||
const portal = readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'public', 'analyse.css'), 'utf8');
|
||||
assert.match(portal, /\.an-bn-btn \{[^}]*[; ]width: 150px/);
|
||||
assert.equal((portal.match(/\.an-bn-btn[^{]*\{/g) || []).length, 2, 'Basisregel + mobile Regel');
|
||||
assert.equal(host.style.zIndex, '2147483647');
|
||||
// Escape schliesst nicht
|
||||
fire(dlg, 'keydown', { key: 'Escape' }); fire(host, 'keydown', { key: 'Escape' });
|
||||
assert.ok(!host.removed);
|
||||
});
|
||||
|
||||
test('w.js: Annehmen => bn accept, Speicher, ck/ct/en; Widerruf stoppt sofort', () => {
|
||||
const h = harness();
|
||||
const bh = hostEl(h);
|
||||
btn(bh.shadow, 'Annehmen').click();
|
||||
assert.deepEqual(h.types(), ['pv', 'bn:shown', 'bn:accept', 'cx']);
|
||||
assert.deepEqual(JSON.parse(h.store['wk-consent']), { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 });
|
||||
assert.ok(bh.removed && !hostEl(h));
|
||||
const t = target();
|
||||
h.ck(t);
|
||||
const ck = h.sent.at(-1).d;
|
||||
assert.deepEqual(ck, { t: 'ck', site: 'abcdef0123456789abcd', path: '/start', vp: 'd', x: 0.5, y: 0.5, sel: 'body:nth-of-type(1) > button:nth-of-type(1)' });
|
||||
// Kontakt-Link: ck + ct (Kind-Element in <a>)
|
||||
const a = makeEl('a'); a.setAttribute('href', 'tel:+4912'); a.parentNode = makeEl('body');
|
||||
const span = makeEl('span'); span.parentNode = a;
|
||||
const n = h.sent.length; h.ck(span);
|
||||
assert.deepEqual(h.sent.slice(n).map((s) => s.d.t), ['ck', 'ct']);
|
||||
assert.deepEqual(h.sent.at(-1).d, { t: 'ct', site: 'abcdef0123456789abcd', path: '/start', kind: 'tel' });
|
||||
const out = makeEl('a'); out.setAttribute('href', 'https://anders.de/x'); out.parentNode = makeEl('body');
|
||||
h.ck(out);
|
||||
assert.deepEqual(h.sent.at(-1).d, { t: 'ct', site: 'abcdef0123456789abcd', path: '/start', kind: 'out', host: 'anders.de' });
|
||||
// en einmal
|
||||
h.leave(); h.leave();
|
||||
const en = h.sent.filter((s) => s.d.t === 'en');
|
||||
assert.equal(en.length, 1);
|
||||
assert.equal(en[0].d.scroll, 55); // (300+800)/2000
|
||||
assert.equal(typeof en[0].d.sec, 'number');
|
||||
// Widerruf ueber Einstellungs-Link
|
||||
const settings = settingsBtn(h);
|
||||
assert.ok(settings, 'fester Link');
|
||||
settings.click();
|
||||
assert.ok(btn(hostEl(h).shadow, 'Ablehnen').focused, 'Fokus beim Wiederoeffnen');
|
||||
btn(hostEl(h).shadow, 'Ablehnen').click();
|
||||
assert.equal(h.types().at(-1), 'bn:reject');
|
||||
const before = h.sent.length;
|
||||
h.ck(target()); h.leave();
|
||||
assert.equal(h.sent.length, before);
|
||||
assert.equal((h.docL.click || []).length, 1); // nur die Einstellungs-Delegation
|
||||
assert.equal((h.docL.visibilitychange || []).length, 0);
|
||||
assert.equal((h.winL.pagehide || []).length, 0);
|
||||
});
|
||||
|
||||
test('w.js: gespeicherte Einwilligung => kein Banner, kein bn, Tracking an', () => {
|
||||
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
|
||||
assert.deepEqual(h.types(), ['pv']);
|
||||
assert.equal(hostEl(h), undefined);
|
||||
h.ck(target());
|
||||
assert.equal(h.sent.at(-1).d.t, 'ck');
|
||||
});
|
||||
|
||||
test('w.js: abgelaufene/abgelehnte Einwilligung', () => {
|
||||
const h = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 - 5000 } });
|
||||
assert.deepEqual(h.types(), []);
|
||||
h.ck(target()); h.leave();
|
||||
assert.deepEqual(h.types(), []);
|
||||
const h2 = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 200 * 86400000 } });
|
||||
assert.deepEqual(h2.types(), ['pv', 'bn:shown']);
|
||||
});
|
||||
|
||||
test('w.js: GPC und DNT => nichts senden, kein Banner, keine Hooks (T1)', () => {
|
||||
for (const o of [{ gpc: true }, { dnt: '1' }, { dnt: 'yes' }, { gpc: true, stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 } }]) {
|
||||
const h = harness(o);
|
||||
assert.equal(h.sent.length, 0);
|
||||
assert.equal(h.winL.pageshow, undefined);
|
||||
assert.equal(h.winL.popstate, undefined);
|
||||
assert.equal(h.history.pushState, h.origPush);
|
||||
assert.equal(h.body.children.length, 0, 'kein Banner, kein Einstellungsknopf');
|
||||
h.ck(target()); h.leave(); h.nav('/neu');
|
||||
assert.equal(h.sent.length, 0);
|
||||
assert.equal(h.reads.referrer + h.reads.innerWidth, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('w.js: gespeichertes Ablehnen => nichts senden, nur Einstellungsknopf; spaeteres Annehmen => bn:accept, pv mit ref+vp', () => {
|
||||
const h = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 } });
|
||||
assert.equal(h.sent.length, 0);
|
||||
assert.equal(h.winL.pageshow, undefined);
|
||||
assert.equal(h.winL.popstate, undefined);
|
||||
assert.equal(h.history.pushState, h.origPush);
|
||||
assert.equal(h.body.children.length, 1);
|
||||
const open = settingsBtn(h);
|
||||
assert.ok(open);
|
||||
open.click();
|
||||
assert.equal(h.sent.length, 0);
|
||||
btn(hostEl(h).shadow, 'Annehmen').click();
|
||||
assert.deepEqual(h.types(), ['bn:accept', 'pv']);
|
||||
assert.deepEqual(h.sent[1].d, { t: 'pv', site: SITE, path: '/start', ref: 'www.google.com', vp: 'd' });
|
||||
assert.ok(h.winL.pageshow && h.winL.popstate, 'Hooks nachtraeglich installiert');
|
||||
h.nav('/zwei');
|
||||
assert.equal(h.types().at(-1), 'pv');
|
||||
assert.equal(h.sent.at(-1).d.path, '/zwei');
|
||||
});
|
||||
|
||||
test('w.js: Ablehnen => genau ein bn:reject, danach nichts mehr (auch nicht Seitenwechsel)', () => {
|
||||
const h2 = harness();
|
||||
btn(hostEl(h2).shadow, 'Ablehnen').click();
|
||||
assert.deepEqual(h2.types(), ['pv', 'bn:shown', 'bn:reject']);
|
||||
const n = h2.sent.length;
|
||||
h2.nav('/zwei'); h2.nav('/drei');
|
||||
for (const f of h2.winL.pageshow || []) f({ persisted: true });
|
||||
for (const f of h2.winL.popstate || []) f({});
|
||||
h2.timers.splice(0).forEach((f) => f());
|
||||
h2.ck(target()); h2.leave();
|
||||
for (const f of h2.docL.scroll || []) f({});
|
||||
assert.equal(h2.sent.length, n);
|
||||
assert.equal(h2.types().filter((t) => t === 'bn:reject').length, 1);
|
||||
});
|
||||
|
||||
test('w.js: Seitenwechsel-Timer vor dem Ablehnen sendet nichts', () => {
|
||||
const h = harness();
|
||||
h.win.location.pathname = '/zwei';
|
||||
h.history.pushState(null, '', '/zwei'); // Timer geplant
|
||||
assert.equal(h.timers.length, 1);
|
||||
btn(hostEl(h).shadow, 'Ablehnen').click();
|
||||
const n = h.sent.length;
|
||||
h.timers.splice(0).forEach((f) => f());
|
||||
assert.equal(h.sent.length, n);
|
||||
});
|
||||
|
||||
test('w.js: Ablehnen in anderem Tab (storage) => danach kein pv beim Seitenwechsel', () => {
|
||||
const h = harness();
|
||||
const n = h.sent.length;
|
||||
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 });
|
||||
h.storage();
|
||||
h.nav('/zwei');
|
||||
for (const f of h.winL.pageshow || []) f({ persisted: true });
|
||||
assert.equal(h.sent.length, n);
|
||||
});
|
||||
|
||||
test('w.js: Annehmen auf laufender Seite => bn:accept, genau ein cx mit ref+vp; kein zweites cx', () => {
|
||||
const h = harness();
|
||||
assert.deepEqual(h.types(), ['pv', 'bn:shown']);
|
||||
btn(hostEl(h).shadow, 'Annehmen').click();
|
||||
assert.deepEqual(h.types(), ['pv', 'bn:shown', 'bn:accept', 'cx']);
|
||||
assert.deepEqual(h.sent.at(-1).d, { t: 'cx', site: SITE, path: '/start', ref: refHost('https://www.google.com/', 'mein.de'), vp: 'd' });
|
||||
// Widerruf, erneut annehmen
|
||||
settingsBtn(h).click();
|
||||
btn(hostEl(h).shadow, 'Ablehnen').click();
|
||||
settingsBtn(h).click();
|
||||
btn(hostEl(h).shadow, 'Annehmen').click();
|
||||
assert.equal(h.sent.filter((s) => s.d.t === 'cx').length, 1);
|
||||
});
|
||||
|
||||
test('w.js: gespeichertes Annehmen => erstes pv mit ref+vp, kein cx', () => {
|
||||
const h = harness({ stored: ACC0 });
|
||||
assert.deepEqual(h.types(), ['pv']);
|
||||
assert.deepEqual(h.sent[0].d, { t: 'pv', site: SITE, path: '/start', ref: 'www.google.com', vp: 'd' });
|
||||
});
|
||||
|
||||
test('w.js: T15 kein sendBeacon, fetch mit keepalive, omit, POST, text/plain', () => {
|
||||
const h = harness();
|
||||
assert.ok(h.fetches.length >= 1);
|
||||
assert.equal(h.beacon.length, 0);
|
||||
for (const f of h.fetches) {
|
||||
assert.equal(f.o.keepalive, true);
|
||||
assert.equal(f.o.credentials, 'omit');
|
||||
assert.equal(f.o.method, 'POST');
|
||||
assert.equal(f.o.headers['Content-Type'], 'text/plain');
|
||||
}
|
||||
});
|
||||
|
||||
test('w.js: T19 Platz unten solange Banner offen; Knopf mindestens 24px', () => {
|
||||
const h = harness({ bannerHeight: 90, bodyPad: '5px' });
|
||||
assert.equal(h.body.style.paddingBottom, '90px');
|
||||
btn(hostEl(h).shadow, 'Ablehnen').click();
|
||||
assert.equal(h.body.style.paddingBottom, '5px');
|
||||
const g = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 } });
|
||||
const css = findAll(g.body, (n) => n.tagName === 'STYLE').map((n) => n.textContent).join('');
|
||||
assert.match(css, /width:44px;height:44px/);
|
||||
});
|
||||
|
||||
test('w.js: Einstellungsknopf ist ein angeknabberter Keks, der sich dreht (ohne Drehung bei reduzierter Bewegung)', () => {
|
||||
const h = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 } });
|
||||
const b = settingsBtn(h);
|
||||
assert.ok(b, 'Knopf da');
|
||||
assert.equal(b.attrs.type, 'button');
|
||||
assert.equal(b.textContent, '', 'kein sichtbarer Text im Knopf selbst');
|
||||
// Etikett "Datenschutz-Einstellungen" erscheint beim Darueberfahren, fuer Screenreader zaehlt aria-label
|
||||
const label = findAll(b, (n) => n.textContent === SETTINGS_LABEL)[0];
|
||||
assert.ok(label);
|
||||
assert.equal(label.attrs['aria-hidden'], 'true');
|
||||
// Keks als Inline-SVG (keine data:-Grafik, die eine CSP der Website blockieren koennte)
|
||||
const svg = findAll(b, (n) => n.tagName === 'SVG')[0];
|
||||
assert.ok(svg);
|
||||
assert.equal(svg.ns, 'http://www.w3.org/2000/svg');
|
||||
assert.equal(svg.attrs['aria-hidden'], 'true');
|
||||
const paths = findAll(svg, (n) => n.tagName === 'PATH');
|
||||
assert.ok(paths.length >= 1, 'Umriss mit Bissspur');
|
||||
assert.match(paths[0].attrs.d, /A/, 'Bogen fuer den Biss');
|
||||
assert.ok(findAll(svg, (n) => n.tagName === 'ELLIPSE' || n.tagName === 'CIRCLE').length >= 5, 'Schokostueckchen');
|
||||
const css = findAll(h.body, (n) => n.tagName === 'STYLE').map((n) => n.textContent).join('');
|
||||
assert.match(css, /@keyframes wk-spin\{to\{transform:rotate\(360deg\)\}\}/);
|
||||
assert.match(css, /animation:wk-spin \d+s linear infinite/);
|
||||
assert.match(css, /@media \(prefers-reduced-motion:reduce\)\{[^}]*animation:none/);
|
||||
assert.match(css, /:hover \.c,[^{]*:focus-visible \.c\{animation-play-state:paused\}/);
|
||||
assert.ok(!/url\(/.test(css), 'keine externen oder data:-Grafiken');
|
||||
b.click();
|
||||
assert.ok(hostEl(h), 'Klick oeffnet das Banner');
|
||||
});
|
||||
|
||||
test('w.js: [data-webklar-consent] oeffnet Banner, kein fester Link', () => {
|
||||
const h = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 }, withConsentLink: true });
|
||||
assert.equal(settingsBtn(h), undefined);
|
||||
assert.equal(hostEl(h), undefined);
|
||||
h.link.click();
|
||||
assert.ok(hostEl(h));
|
||||
});
|
||||
|
||||
test('w.js: Heatmap-Modus sendet nichts, entfernt Parameter, laedt h.js', () => {
|
||||
const h = harness({ search: '?a=1&__wk_heat=TOKEN.sig' });
|
||||
assert.deepEqual(h.sent, []);
|
||||
assert.equal(hostEl(h), undefined);
|
||||
assert.equal(h.history.replaced, '/start?a=1');
|
||||
assert.equal(h.injected.length, 1);
|
||||
assert.equal(h.injected[0].attrs.src, 'https://project.webklar.com/a/h.js');
|
||||
assert.equal(h.injected[0].attrs['data-token'], 'TOKEN.sig');
|
||||
assert.equal(h.injected[0].attrs['data-embed'], undefined);
|
||||
assert.equal((h.docL.click || []).length, 0);
|
||||
});
|
||||
|
||||
test('w.js: Heatmap-Modus mit __wk_embed=1 entfernt beide Parameter und reicht data-embed an h.js', () => {
|
||||
const h = harness({ search: '?__wk_heat=TOKEN.sig&__wk_embed=1&b=2' });
|
||||
assert.deepEqual(h.sent, []);
|
||||
assert.equal(h.history.replaced, '/start?b=2');
|
||||
assert.equal(h.injected.length, 1);
|
||||
assert.equal(h.injected[0].attrs['data-embed'], '1');
|
||||
assert.equal(h.injected[0].attrs['data-token'], 'TOKEN.sig');
|
||||
// __wk_embed ohne Heatmap-Token: normaler Betrieb (Banner, pv)
|
||||
const n = harness({ search: '?__wk_embed=1' });
|
||||
assert.deepEqual(n.types(), ['pv', 'bn:shown']);
|
||||
});
|
||||
|
||||
test('w.js: SPA-Navigation sendet pv nur bei Pfadwechsel', () => {
|
||||
const h = harness();
|
||||
h.nav('/start');
|
||||
assert.equal(h.sent.filter((s) => s.d.t === 'pv').length, 1);
|
||||
h.win.location.pathname = '/neu';
|
||||
h.history.pushState(null, '', '/neu'); h.history.pushState(null, '', '/neu');
|
||||
h.timers.splice(0).forEach((f) => f());
|
||||
assert.deepEqual(h.sent.filter((s) => s.d.t === 'pv').map((s) => s.d.path), ['/start', '/neu']);
|
||||
assert.deepEqual(h.sent.at(-1).d, { t: 'pv', site: SITE, path: '/neu' });
|
||||
});
|
||||
|
||||
test('w.js: wirft nie, auch wenn localStorage/sendBeacon kaputt', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
const sandbox = { window: {}, document: { currentScript: null, querySelector() { throw new Error('x'); } }, navigator: {}, location: {} };
|
||||
sandbox.window = sandbox; vm.createContext(sandbox);
|
||||
vm.runInContext(readFileSync(join(DIST, 'w.js'), 'utf8'), sandbox);
|
||||
});
|
||||
});
|
||||
|
||||
test('w.js: doppelt eingebunden => nur ein pv', () => {
|
||||
const h = harness();
|
||||
h.rerun();
|
||||
assert.deepEqual(h.types(), ['pv', 'bn:shown']);
|
||||
});
|
||||
|
||||
test('w.js: Widerruf in anderem Tab (storage-Ereignis) stoppt Tracking sofort', () => {
|
||||
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
|
||||
h.ck(target());
|
||||
assert.equal(h.sent.at(-1).d.t, 'ck');
|
||||
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 });
|
||||
h.storage();
|
||||
const n = h.sent.length;
|
||||
h.ck(target()); h.leave();
|
||||
assert.equal(h.sent.length, n);
|
||||
assert.equal((h.docL.click || []).length, 1); // nur die Einstellungs-Delegation
|
||||
// Annehmen in anderem Tab startet Tracking
|
||||
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 });
|
||||
h.storage();
|
||||
h.ck(target());
|
||||
assert.equal(h.sent.length, n + 1); // nur ck, kein cx (pv trug ref/vp schon)
|
||||
assert.ok(!h.sent.some((x) => x.d.t === 'cx'));
|
||||
});
|
||||
|
||||
test('w.js: Einwilligung wird vor jedem Senden neu geprueft (ohne storage-Ereignis)', () => {
|
||||
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
|
||||
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 });
|
||||
const n = h.sent.length;
|
||||
h.ck(target()); h.leave();
|
||||
assert.equal(h.sent.length, n);
|
||||
assert.equal((h.docL.click || []).length, 1); // nur die Einstellungs-Delegation
|
||||
});
|
||||
|
||||
test('w.js: SPA-pv bei akzeptierter Einwilligung; nach Widerruf kein en', () => {
|
||||
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
|
||||
h.win.location.pathname = '/zwei';
|
||||
h.history.pushState(null, '', '/zwei'); h.timers.splice(0).forEach((f) => f());
|
||||
// en der vorigen Seite vor dem pv der neuen (Router-Links verhindern den Standard-Klick, onLinkClick greift nicht)
|
||||
assert.deepEqual(h.sent.map((s) => `${s.d.t}:${s.d.path}`), ['pv:/start', 'en:/start', 'pv:/zwei']);
|
||||
assert.equal(h.sent[1].d.scroll, 55);
|
||||
h.ck(target());
|
||||
assert.equal(h.sent.at(-1).d.path, '/zwei');
|
||||
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 });
|
||||
h.storage();
|
||||
const n = h.sent.length;
|
||||
h.leave();
|
||||
assert.equal(h.sent.length, n);
|
||||
});
|
||||
|
||||
test('w.js: bfcache-Rueckkehr sendet neues pv und erlaubt neues en', () => {
|
||||
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
|
||||
h.leave();
|
||||
assert.equal(h.sent.filter((s) => s.d.t === 'en').length, 1);
|
||||
for (const f of h.winL.pageshow || []) f({ persisted: false });
|
||||
assert.equal(h.sent.filter((s) => s.d.t === 'pv').length, 1);
|
||||
for (const f of h.winL.pageshow || []) f({ persisted: true });
|
||||
assert.equal(h.sent.filter((s) => s.d.t === 'pv').length, 2);
|
||||
h.leave();
|
||||
assert.equal(h.sent.filter((s) => s.d.t === 'en').length, 2);
|
||||
});
|
||||
|
||||
test('w.js: [data-webklar-consent] spaeter hinzugefuegt funktioniert per Delegation', () => {
|
||||
const h = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 } });
|
||||
// kein Link beim Laden => fester Link; spaeteres Element oeffnet ebenfalls
|
||||
const late = makeEl('a'); late.setAttribute('data-webklar-consent', '');
|
||||
for (const f of [...(h.docL.click || [])]) f({ type: 'click', target: late, preventDefault() {} });
|
||||
assert.ok(hostEl(h));
|
||||
assert.ok(btn(hostEl(h).shadow, 'Ablehnen').focused);
|
||||
});
|
||||
|
||||
// ---------- Verweildauer bei internem Seitenwechsel ----------
|
||||
const ACC = { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 };
|
||||
const lnk = (href, attrs = {}) => { const a = makeEl('a'); a.setAttribute('href', href); for (const k in attrs) a.setAttribute(k, attrs[k]); a.parentNode = makeEl('body'); return a; };
|
||||
const enCount = (h) => h.sent.filter((s) => s.d.t === 'en').length;
|
||||
|
||||
test('w.js: interner Link-Klick sendet en sofort, pagehide danach nicht doppelt', () => {
|
||||
const h = harness({ stored: ACC });
|
||||
h.ck(lnk('/kontakt'));
|
||||
assert.equal(enCount(h), 1);
|
||||
assert.deepEqual(h.sent.map((s) => s.d.t), ['pv', 'ck', 'en']);
|
||||
const en = h.sent.at(-1).d;
|
||||
assert.equal(en.path, '/start'); assert.equal(en.scroll, 55);
|
||||
h.leave();
|
||||
assert.equal(enCount(h), 1);
|
||||
h.ck(lnk('https://mein.de/andere?x=1#a'));
|
||||
assert.equal(enCount(h), 1);
|
||||
});
|
||||
|
||||
test('w.js: kein fruehes en bei externem Link, Anker, gleicher Seite, Modifier, Ziel/Download, defaultPrevented', () => {
|
||||
const h = harness({ stored: ACC });
|
||||
h.ck(lnk('https://anders.de/x'));
|
||||
h.ck(lnk('#oben'));
|
||||
h.ck(lnk('/start#unten'));
|
||||
h.ck(lnk('/kontakt'), { ctrlKey: true });
|
||||
h.ck(lnk('/kontakt'), { metaKey: true });
|
||||
h.ck(lnk('/kontakt'), { shiftKey: true });
|
||||
h.ck(lnk('/kontakt'), { button: 1 });
|
||||
h.ck(lnk('/kontakt', { target: '_blank' }));
|
||||
h.ck(lnk('/datei.pdf', { download: '' }));
|
||||
h.ck(lnk('/kontakt'), { defaultPrevented: true });
|
||||
h.ck(lnk('mailto:a@b.de'));
|
||||
assert.equal(enCount(h), 0);
|
||||
h.leave();
|
||||
assert.equal(enCount(h), 1);
|
||||
});
|
||||
|
||||
test('w.js: ohne Einwilligung oder nach Widerruf kein fruehes en', () => {
|
||||
const h = harness();
|
||||
h.ck(lnk('/kontakt'));
|
||||
assert.equal(enCount(h), 0);
|
||||
const g = harness({ stored: ACC });
|
||||
g.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 });
|
||||
g.ck(lnk('/kontakt'));
|
||||
assert.equal(enCount(g), 0);
|
||||
});
|
||||
|
||||
// ---------- Variante "eigen" (WEBklars eigene Websites, z. B. webklar.com) ----------
|
||||
test('core: bannerFor und readConsent je Fassung', () => {
|
||||
assert.deepEqual(bannerFor('eigen'), { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN });
|
||||
for (const v of ['', 'kunde', 'EIGEN', 'x', undefined]) assert.deepEqual(bannerFor(v), { version: BANNER_VERSION, text: BANNER_TEXT });
|
||||
assert.equal(BANNER_VERSION_EIGEN, '2026-10-17');
|
||||
assert.notEqual(BANNER_VERSION_EIGEN, BANNER_VERSION);
|
||||
const now = 1_700_000_000_000;
|
||||
const st = (o) => ({ getItem: () => JSON.stringify(o) });
|
||||
assert.equal(readConsent(st({ v: 1, a: 'accept', bv: BV, t: now }), now, BANNER_VERSION_EIGEN), null, 'Kunden-Fassung gilt nicht fuer eigen');
|
||||
assert.equal(readConsent(st({ v: 1, a: 'accept', bv: BANNER_VERSION_EIGEN, t: now }), now, BANNER_VERSION_EIGEN).a, 'accept');
|
||||
assert.equal(readConsent(st({ v: 1, a: 'accept', bv: BANNER_VERSION_EIGEN, t: now }), now), null, 'ohne Fassung gilt die Kunden-Fassung');
|
||||
});
|
||||
|
||||
test('core: Banner-Text eigen (Sie-Form, ohne Auftragsverarbeiter)', () => {
|
||||
assert.match(BANNER_TEXT_EIGEN, /^Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung\./);
|
||||
assert.match(BANNER_TEXT_EIGEN, /Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung\./);
|
||||
assert.match(BANNER_TEXT_EIGEN, /Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben\./);
|
||||
assert.match(BANNER_TEXT_EIGEN, /über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen\. Details: $/);
|
||||
assert.doesNotMatch(BANNER_TEXT_EIGEN, /\b(Du|Dein|Deine|Deiner|Dich|Dir)\b/);
|
||||
assert.doesNotMatch(BANNER_TEXT_EIGEN, /betreibt für uns/);
|
||||
});
|
||||
|
||||
test('w.js: data-variant="eigen" => Sie-Text, eigene Fassung im Speicher', () => {
|
||||
const h = harness({ variant: 'eigen' });
|
||||
const host = hostEl(h);
|
||||
const p = findAll(host.shadow, (n) => n.tagName === 'P')[0];
|
||||
assert.ok(p.textContent.startsWith(BANNER_TEXT_EIGEN));
|
||||
assert.ok(!p.textContent.startsWith(BANNER_TEXT));
|
||||
btn(host.shadow, 'Annehmen').click();
|
||||
assert.deepEqual(JSON.parse(h.store['wk-consent']), { v: 1, a: 'accept', bv: BANNER_VERSION_EIGEN, t: 1_700_000_000_000 });
|
||||
assert.deepEqual(h.types(), ['pv', 'bn:shown', 'bn:accept', 'cx']);
|
||||
});
|
||||
|
||||
test('w.js: eigen fragt neu bei Wahl aus der Kunden-Fassung, gilt mit eigener Fassung', () => {
|
||||
const alt = harness({ variant: 'eigen', stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
|
||||
assert.ok(hostEl(alt), 'Banner erscheint erneut');
|
||||
const ok = harness({ variant: 'eigen', stored: { v: 1, a: 'reject', bv: BANNER_VERSION_EIGEN, t: 1_700_000_000_000 - 1000 } });
|
||||
assert.equal(hostEl(ok), undefined);
|
||||
assert.deepEqual(ok.types(), []);
|
||||
});
|
||||
|
||||
test('w.js: unbekannte Variante => Kunden-Fassung', () => {
|
||||
const h = harness({ variant: 'sonstwas' });
|
||||
const p = findAll(hostEl(h).shadow, (n) => n.tagName === 'P')[0];
|
||||
assert.ok(p.textContent.startsWith(BANNER_TEXT));
|
||||
});
|
||||
|
||||
test('w.js: SPA-Wechsel ohne Einwilligung sendet nur pv, kein en', () => {
|
||||
const h = harness();
|
||||
h.win.location.pathname = '/zwei';
|
||||
h.history.pushState(null, '', '/zwei'); h.timers.splice(0).forEach((f) => f());
|
||||
assert.deepEqual(h.types(), ['pv', 'bn:shown', 'pv']);
|
||||
});
|
||||
|
||||
test('w.js: en nach SPA-Wechsel nur einmal je Seite (Link-Klick und Router)', () => {
|
||||
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
|
||||
h.leave();
|
||||
h.win.location.pathname = '/zwei';
|
||||
h.history.pushState(null, '', '/zwei'); h.timers.splice(0).forEach((f) => f());
|
||||
assert.deepEqual(h.sent.map((s) => `${s.d.t}:${s.d.path}`), ['pv:/start', 'en:/start', 'pv:/zwei']);
|
||||
h.leave();
|
||||
assert.deepEqual(h.sent.map((s) => `${s.d.t}:${s.d.path}`).slice(-1), ['en:/zwei']);
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import crypto from 'node:crypto';
|
||||
import { mkdtempSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import { openDb } from '../src/db.js';
|
||||
import { exportSite } from '../scripts/export-site.mjs';
|
||||
|
||||
const KEY = 'abcdef0123456789abcd';
|
||||
const OTHER = 'zzzzzzzz0123456789zz';
|
||||
const SCRIPT = new URL('../scripts/export-site.mjs', import.meta.url).pathname;
|
||||
|
||||
function setup() {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'exp-'));
|
||||
const dbPath = join(dir, 'a.db');
|
||||
const db = openDb(dbPath);
|
||||
for (const k of [KEY, OTHER]) db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)').run(k, 'p', '[]');
|
||||
const ts = Date.UTC(2026, 8, 1, 10);
|
||||
db.prepare("INSERT INTO daily VALUES (?, '2026-08-31','pv_total','',5)").run(KEY);
|
||||
db.prepare("INSERT INTO daily VALUES (?, '2026-08-31','pv_path','/a',2)").run(KEY);
|
||||
db.prepare("INSERT INTO daily VALUES (?, '2026-08-31','pv_total','',9)").run(OTHER);
|
||||
db.prepare("INSERT INTO events_raw (site, ts, type, path, ref) VALUES (?,?, 'pv', '/x', ?)").run(KEY, ts, 'a,"b"\nc');
|
||||
db.prepare("INSERT INTO events_raw (site, ts, type, path) VALUES (?,?, 'pv', '/o')").run(OTHER, ts);
|
||||
db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y, sel) VALUES (?,?,?,?,?,?,?)').run(KEY, ts, '/', 'd', 1.5, 2, '=1+1');
|
||||
db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y, sel) VALUES (?,?,?,?,?,?,?)').run(OTHER, ts, '/', 'd', 1, 2, 'x');
|
||||
db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
||||
db.close();
|
||||
return { dir, dbPath };
|
||||
}
|
||||
const hash = (p) => crypto.createHash('sha256').update(readFileSync(p)).digest('hex');
|
||||
|
||||
test('Export: drei CSV-Dateien mit Kopfzeilen, nur diese Website, Quotes und Formelschutz', () => {
|
||||
const { dir, dbPath } = setup();
|
||||
const before = hash(dbPath);
|
||||
const out = join(dir, 'out');
|
||||
exportSite({ dbPath, siteKey: KEY, outDir: out });
|
||||
assert.deepEqual(readdirSync(out).sort(), [`${KEY}-ereignisse.csv`, `${KEY}-klicks.csv`, `${KEY}-tageswerte.csv`]);
|
||||
const t = readFileSync(join(out, `${KEY}-tageswerte.csv`), 'utf8');
|
||||
assert.equal(t.split('\r\n')[0], 'day,dim,key,count');
|
||||
assert.equal(t.split('\r\n').filter(Boolean).length, 3);
|
||||
const e = readFileSync(join(out, `${KEY}-ereignisse.csv`), 'utf8');
|
||||
assert.equal(e.split('\r\n')[0], 'time,type,path,vp,ref,country,device,visitor,sec,scroll,kind,host');
|
||||
assert.ok(e.includes('2026-09-01T10:00:00.000Z,pv,/x,,"a,""b""\nc"'));
|
||||
assert.ok(!e.includes('/o'));
|
||||
const c = readFileSync(join(out, `${KEY}-klicks.csv`), 'utf8');
|
||||
assert.equal(c.split('\r\n')[0], 'time,path,vp,x,y,sel');
|
||||
assert.equal(c.split('\r\n').filter(Boolean).length, 2);
|
||||
assert.ok(c.includes(",'=1+1"));
|
||||
assert.equal(hash(dbPath), before);
|
||||
});
|
||||
|
||||
test('Export: Datenbank ist nur lesend geöffnet', () => {
|
||||
const { dbPath } = setup();
|
||||
const ro = new DatabaseSync(dbPath, { readOnly: true });
|
||||
assert.throws(() => ro.exec("INSERT INTO sites (siteKey, projectId, domains) VALUES ('q','p','[]')"));
|
||||
ro.close();
|
||||
});
|
||||
|
||||
test('CLI: unbekannter oder ungültiger Schlüssel => Exit 1 ohne Dateien', () => {
|
||||
const { dir, dbPath } = setup();
|
||||
for (const k of ['unbekannt0123456789ab', 'ABC']) {
|
||||
const out = join(dir, 'o-' + k);
|
||||
const r = spawnSync(process.execPath, [SCRIPT, k, '--db', dbPath, '--out', out]);
|
||||
assert.equal(r.status, 1);
|
||||
assert.throws(() => readdirSync(out));
|
||||
}
|
||||
const ok = spawnSync(process.execPath, [SCRIPT, KEY, '--db', dbPath, '--out', join(dir, 'ok')]);
|
||||
assert.equal(ok.status, 0);
|
||||
assert.equal(readdirSync(join(dir, 'ok')).length, 3);
|
||||
});
|
||||
|
||||
test('Export: Formelschutz nur fuer Text (Tab, CR, Minus), Zahlen bleiben unveraendert, Dateien 0600', () => {
|
||||
const { dir, dbPath } = setup();
|
||||
const db = new DatabaseSync(dbPath);
|
||||
const ts = Date.UTC(2026, 8, 1, 10);
|
||||
const ins = db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y, sel) VALUES (?,?,?,?,?,?,?)');
|
||||
ins.run(KEY, ts, '/', 'd', -3, -4.5, '\tcmd');
|
||||
ins.run(KEY, ts, '/', 'd', 1, 2, '\rcmd');
|
||||
ins.run(KEY, ts, '/', 'd', 1, 2, '-cmd');
|
||||
db.close();
|
||||
const out = join(dir, 'out');
|
||||
const files = exportSite({ dbPath, siteKey: KEY, outDir: out });
|
||||
const c = readFileSync(join(out, `${KEY}-klicks.csv`), 'utf8');
|
||||
assert.ok(c.includes(',-3,-4.5,'), 'negative Zahlen ohne Praefix');
|
||||
assert.ok(!c.includes("'-3"));
|
||||
assert.ok(c.includes(",\"'\tcmd\"") || c.includes(",'\tcmd"), 'Tab gesichert');
|
||||
assert.ok(c.includes(`"'\rcmd"`), 'CR gesichert');
|
||||
assert.ok(c.includes(",'-cmd"));
|
||||
for (const f of files) assert.equal(statSync(f).mode & 0o777, 0o600);
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, readFileSync, chmodSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const SCRIPT = new URL('../scripts/geo-update.sh', import.meta.url).pathname;
|
||||
|
||||
// Fremde Programme (curl, docker) sind Attrappen im PATH; docker schlaegt fehl, solange DOCKER_FAIL=1 gesetzt ist.
|
||||
function setup() {
|
||||
const root = mkdtempSync(join(tmpdir(), 'geoupd-'));
|
||||
const bin = join(root, 'bin');
|
||||
const data = join(root, 'data');
|
||||
mkdirSync(bin);
|
||||
mkdirSync(data);
|
||||
const csv = join(root, 'src.csv');
|
||||
const rows = [];
|
||||
for (let i = 0; i < 100100; i++) rows.push(`1.${i % 250}.${Math.floor(i / 250) % 250}.0,1.${i % 250}.${Math.floor(i / 250) % 250}.255,AU`);
|
||||
rows.push('5.9.0.0,5.9.255.255,DE');
|
||||
writeFileSync(csv, rows.join('\n') + '\n');
|
||||
writeFileSync(join(bin, 'curl'), `#!/bin/sh\nwhile [ "$#" -gt 0 ]; do [ "$1" = "-o" ] && out="$2"; shift; done\ngzip -c "${csv}" > "$out"\n`);
|
||||
writeFileSync(join(bin, 'docker'), `#!/bin/sh\necho "$@" >> "${root}/docker.log"\n[ "\${DOCKER_FAIL:-0}" = "1" ] && exit 1\nexit 0\n`);
|
||||
chmodSync(join(bin, 'curl'), 0o755);
|
||||
chmodSync(join(bin, 'docker'), 0o755);
|
||||
const run = (fail) => spawnSync('sh', [SCRIPT], { env: { PATH: `${bin}:${process.env.PATH}`, ANALYTICS_DATA_DIR: data, DOCKER_FAIL: fail ? '1' : '0' }, encoding: 'utf8' });
|
||||
const restarts = () => (existsSync(join(root, 'docker.log')) ? readFileSync(join(root, 'docker.log'), 'utf8').trim().split('\n').length : 0);
|
||||
return { data, run, restarts, marker: join(data, 'geo.restart-pending') };
|
||||
}
|
||||
|
||||
test('geo-update: Neustart schlägt fehl -> Marker; nächster Lauf startet trotz unveränderter Datei neu und entfernt ihn', () => {
|
||||
const s = setup();
|
||||
const first = s.run(true);
|
||||
assert.notEqual(first.status, 0, first.stderr);
|
||||
assert.ok(existsSync(join(s.data, 'geo.csv')), 'neue Datei ist schon an Ort und Stelle');
|
||||
assert.ok(existsSync(s.marker), 'Marker liegt');
|
||||
assert.equal(s.restarts(), 1);
|
||||
const second = s.run(false);
|
||||
assert.equal(second.status, 0, second.stderr);
|
||||
assert.equal(s.restarts(), 2, 'Neustart nachgeholt');
|
||||
assert.equal(existsSync(s.marker), false);
|
||||
const third = s.run(false);
|
||||
assert.equal(third.status, 0);
|
||||
assert.equal(s.restarts(), 2, 'unverändert und ohne Marker: kein Neustart');
|
||||
});
|
||||
|
||||
test('geo-update: erfolgreicher Neustart hinterlässt keinen Marker', () => {
|
||||
const s = setup();
|
||||
assert.equal(s.run(false).status, 0);
|
||||
assert.equal(s.restarts(), 1);
|
||||
assert.equal(existsSync(s.marker), false);
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { loadGeo } from '../src/geo.js';
|
||||
|
||||
const CSV = [
|
||||
'1.0.0.0,1.0.0.255,AU',
|
||||
'5.9.0.0,5.9.255.255,DE',
|
||||
'5.10.0.0,5.10.0.9,ZZ',
|
||||
'8.8.8.0,8.8.8.255,US',
|
||||
'255.255.255.0,255.255.255.255,FR',
|
||||
'2001:db8::,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff,AT',
|
||||
'2a01:4f8::,2a01:4f8:ffff:ffff:ffff:ffff:ffff:ffff,DE',
|
||||
'2a02:1::1,2a02:1::ff,CH',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
function fixture(content) {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'geo-'));
|
||||
const f = join(dir, 'geo.csv');
|
||||
writeFileSync(f, content);
|
||||
return f;
|
||||
}
|
||||
|
||||
test('IPv4: Treffer und Grenzen', () => {
|
||||
const g = loadGeo(fixture(CSV));
|
||||
assert.equal(g.size, 8);
|
||||
assert.equal(g.lookup('5.9.0.0'), 'DE');
|
||||
assert.equal(g.lookup('5.9.123.4'), 'DE');
|
||||
assert.equal(g.lookup('5.9.255.255'), 'DE');
|
||||
assert.equal(g.lookup('5.8.255.255'), '');
|
||||
assert.equal(g.lookup('5.10.0.0'), '');
|
||||
assert.equal(g.lookup('1.0.0.255'), 'AU');
|
||||
assert.equal(g.lookup('1.0.1.0'), '');
|
||||
assert.equal(g.lookup('0.0.0.1'), '');
|
||||
assert.equal(g.lookup('255.255.255.255'), 'FR');
|
||||
assert.equal(g.lookup('8.8.8.8'), 'US');
|
||||
});
|
||||
|
||||
test('IPv6: Treffer, Grenzen, Kurzschreibweise', () => {
|
||||
const g = loadGeo(fixture(CSV));
|
||||
assert.equal(g.lookup('2a01:4f8::'), 'DE');
|
||||
assert.equal(g.lookup('2a01:4f8:1:2:3:4:5:6'), 'DE');
|
||||
assert.equal(g.lookup('2A01:04F8:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF'), 'DE');
|
||||
assert.equal(g.lookup('2a01:4f9::'), '');
|
||||
assert.equal(g.lookup('2a01:4f7:ffff:ffff:ffff:ffff:ffff:ffff'), '');
|
||||
assert.equal(g.lookup('2001:db8::1'), 'AT');
|
||||
assert.equal(g.lookup('2a02:1::1'), 'CH');
|
||||
assert.equal(g.lookup('2a02:1::ff'), 'CH');
|
||||
assert.equal(g.lookup('2a02:1::100'), '');
|
||||
assert.equal(g.lookup('2a02:1::'), '');
|
||||
assert.equal(g.lookup('::1'), '');
|
||||
});
|
||||
|
||||
test('IPv4-gemappte IPv6-Adressen zählen als IPv4', () => {
|
||||
const g = loadGeo(fixture(CSV));
|
||||
assert.equal(g.lookup('::ffff:5.9.1.1'), 'DE');
|
||||
assert.equal(g.lookup('::FFFF:8.8.8.8'), 'US');
|
||||
assert.equal(g.lookup('::ffff:808:808'), 'US');
|
||||
assert.equal(g.lookup('::ffff:9.9.9.9'), '');
|
||||
});
|
||||
|
||||
test('ZZ, ungültige Eingaben, unsortierte Datei', () => {
|
||||
const g = loadGeo(fixture(CSV));
|
||||
assert.equal(g.lookup('5.10.0.5'), '');
|
||||
for (const bad of ['', 'abc', '1.2.3', '256.1.1.1', '1.2.3.4.5', '2001:db8::zz', ':::', '1::2::3', null, undefined, 42]) {
|
||||
assert.equal(g.lookup(bad), '', String(bad));
|
||||
}
|
||||
const u = loadGeo(fixture('8.8.8.0,8.8.8.255,US\n1.0.0.0,1.0.0.255,AU\n2a01:4f8::,2a01:4f8::ff,DE\n2001:db8::,2001:db8::ff,AT\n'));
|
||||
assert.equal(u.lookup('1.0.0.7'), 'AU');
|
||||
assert.equal(u.lookup('8.8.8.8'), 'US');
|
||||
assert.equal(u.lookup('2001:db8::7'), 'AT');
|
||||
assert.equal(u.lookup('2a01:4f8::7'), 'DE');
|
||||
});
|
||||
|
||||
test('fehlende Datei: alles leer, eine Warnung, kein Absturz', () => {
|
||||
const warns = [];
|
||||
const orig = console.warn;
|
||||
console.warn = (...a) => warns.push(a.join(' '));
|
||||
try {
|
||||
const g = loadGeo('/nicht/vorhanden/geo.csv');
|
||||
assert.equal(g.lookup('5.9.1.1'), '');
|
||||
assert.equal(g.lookup('2a01:4f8::1'), '');
|
||||
assert.equal(g.size, 0);
|
||||
} finally { console.warn = orig; }
|
||||
assert.equal(warns.length, 1);
|
||||
assert.match(warns[0], /nicht lesbar/);
|
||||
});
|
||||
|
||||
test('600k Zeilen: lädt zügig, Heap bleibt klein', () => {
|
||||
const lines = [];
|
||||
for (let i = 0; i < 400000; i++) lines.push(`${(i * 4096) >>> 24}.${((i * 4096) >>> 16) & 255}.${((i * 4096) >>> 8) & 255}.0,${(i * 4096) >>> 24}.${((i * 4096) >>> 16) & 255}.${((i * 4096 + 255) >>> 8) & 255}.255,${i % 7 ? 'DE' : 'AT'}`);
|
||||
for (let i = 0; i < 200000; i++) lines.push(`2a00:${(i >> 8).toString(16)}:${(i & 255).toString(16)}::,2a00:${(i >> 8).toString(16)}:${(i & 255).toString(16)}:ffff:ffff:ffff:ffff:ffff,${i % 5 ? 'DE' : 'FR'}`);
|
||||
const f = fixture(lines.join('\n'));
|
||||
lines.length = 0;
|
||||
globalThis.gc?.();
|
||||
const before = process.memoryUsage().heapUsed; const rss0 = process.memoryUsage().rss;
|
||||
const t0 = Date.now();
|
||||
const g = loadGeo(f);
|
||||
const ms = Date.now() - t0;
|
||||
globalThis.gc?.();
|
||||
const grow = (process.memoryUsage().heapUsed - before) / 1048576;
|
||||
console.log(`# geo 600k: ${ms} ms, Heap +${grow.toFixed(1)} MB, RSS +${((process.memoryUsage().rss - rss0) / 1048576).toFixed(0)} MB, Zeilen ${g.size}`);
|
||||
assert.equal(g.size, 600000);
|
||||
assert.equal(g.lookup('0.0.16.5'), 'DE');
|
||||
assert.equal(g.lookup('2a00:0:1::5'), 'DE');
|
||||
assert.ok(grow < 100, `Heap +${grow} MB`);
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { openDb } from '../src/db.js';
|
||||
import { createPublicServer, createAdminServer } from '../src/server.js';
|
||||
import { signHeatToken, verifyHeatToken } from '../src/heat.js';
|
||||
|
||||
const KEY = 'abcdef0123456789abcd';
|
||||
const OTHER = 'zzzzzzzz0123456789zz';
|
||||
const SECRET = 'geheim';
|
||||
const NOW = Date.UTC(2026, 9, 10, 10, 0, 0);
|
||||
const exp = (s = 600) => Math.floor(NOW / 1000) + s;
|
||||
|
||||
async function setup() {
|
||||
const db = openDb(':memory:');
|
||||
const site = db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)');
|
||||
site.run(KEY, 'p', JSON.stringify(['kunde.de', 'www.kunde.de']));
|
||||
site.run(OTHER, 'q', JSON.stringify(['andere.de']));
|
||||
const click = db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,?,?)');
|
||||
click.run(KEY, NOW - 1000, '/', 'd', 0.1, 0.2);
|
||||
click.run(KEY, NOW - 2000, '/', 'd', 0.3, 0.4);
|
||||
click.run(KEY, NOW - 1000, '/', 'm', 0.9, 0.9);
|
||||
click.run(KEY, NOW - 1000, '/x', 'd', 0.8, 0.8);
|
||||
click.run(OTHER, NOW - 1000, '/', 'd', 0.7, 0.7);
|
||||
click.run(KEY, NOW - 31 * 86400000, '/', 'd', 0.5, 0.5);
|
||||
const server = createPublicServer({ config: { heatSecret: SECRET }, db, now: () => NOW });
|
||||
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
const get = (token, headers = {}) => fetch(`${base}/a/heat?token=${encodeURIComponent(token)}`, { headers });
|
||||
return { db, get, base, close: () => server.close() };
|
||||
}
|
||||
const tok = (o = {}, secret = SECRET) => signHeatToken({ siteKey: KEY, path: '/', vp: 'd', exp: exp(), ...o }, secret);
|
||||
|
||||
test('gültiges Token: neueste Punkte der Seite/Viewport, 30 Tage, no-store, CORS nur eigene Domain', async () => {
|
||||
const s = await setup();
|
||||
const r = await s.get(tok(), { Origin: 'https://www.kunde.de' });
|
||||
assert.equal(r.status, 200);
|
||||
assert.equal(r.headers.get('cache-control'), 'no-store');
|
||||
assert.equal(r.headers.get('access-control-allow-origin'), 'https://www.kunde.de');
|
||||
assert.deepEqual(await r.json(), { points: [{ x: 0.1, y: 0.2 }, { x: 0.3, y: 0.4 }] });
|
||||
const fremd = await s.get(tok(), { Origin: 'https://evil.example' });
|
||||
assert.equal(fremd.status, 200);
|
||||
assert.equal(fremd.headers.get('access-control-allow-origin'), null);
|
||||
const http = await s.get(tok(), { Origin: 'http://kunde.de' });
|
||||
assert.equal(http.headers.get('access-control-allow-origin'), null);
|
||||
assert.equal(r.headers.get('access-control-allow-origin') === '*', false);
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('maximal 5000 neueste Punkte', async () => {
|
||||
const s = await setup();
|
||||
s.db.exec('BEGIN');
|
||||
const ins = s.db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,?,?)');
|
||||
for (let i = 0; i < 5100; i++) ins.run(KEY, NOW - 10000 + i, '/many', 'd', 0.5, i < 100 ? 0.01 : 0.99);
|
||||
s.db.exec('COMMIT');
|
||||
const j = await (await s.get(tok({ path: '/many' }))).json();
|
||||
assert.equal(j.points.length, 5000);
|
||||
assert.ok(j.points.every((p) => p.y === 0.99)); // die 100 ältesten fehlen
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('403: abgelaufen, > 30 Min voraus, falsches Secret, manipuliert, Müll, fremde/unbekannte Website', async () => {
|
||||
const s = await setup();
|
||||
const bad = [
|
||||
tok({ exp: exp(-1) }),
|
||||
tok({ exp: exp(30 * 60 + 5) }),
|
||||
tok({}, 'anderes-secret'),
|
||||
'abc', '', 'a.b', 'x'.repeat(10) + '.' + 'f'.repeat(64),
|
||||
tok({ siteKey: 'nichtvorhanden0123456' }),
|
||||
];
|
||||
// Manipulation: Payload tauschen, Signatur behalten
|
||||
const [part, sig] = tok().split('.');
|
||||
for (const patch of [{ p: '/x' }, { v: 'm' }, { s: OTHER }]) {
|
||||
const o = { ...JSON.parse(Buffer.from(part, 'base64url')), ...patch };
|
||||
bad.push(`${Buffer.from(JSON.stringify(o)).toString('base64url')}.${sig}`);
|
||||
}
|
||||
for (const t of bad) assert.equal((await s.get(t)).status, 403, t.slice(0, 30));
|
||||
assert.equal((await fetch(`${s.base}/a/heat`)).status, 403);
|
||||
// Token der anderen Website liefert nur deren Punkte (nie die von KEY)
|
||||
const j = await (await s.get(tok({ siteKey: OTHER }))).json();
|
||||
assert.deepEqual(j.points, [{ x: 0.7, y: 0.7 }]);
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('Token mit exp genau 30 Min voraus ist gültig; Helfer verify', () => {
|
||||
assert.ok(verifyHeatToken(tok({ exp: exp(1800) }), SECRET, NOW));
|
||||
assert.equal(verifyHeatToken(tok({ exp: exp(1801) }), SECRET, NOW), null);
|
||||
assert.equal(verifyHeatToken(tok(), '', NOW), null); // leeres Secret sperrt
|
||||
assert.equal(verifyHeatToken(tok({ vp: 'x' }), SECRET, NOW), null);
|
||||
assert.equal(verifyHeatToken(tok({ path: 'kein-slash' }), SECRET, NOW), null);
|
||||
const t = tok();
|
||||
assert.match(t, /^[A-Za-z0-9_-]+\.[0-9a-f]{64}$/);
|
||||
assert.deepEqual(JSON.parse(Buffer.from(t.split('.')[0], 'base64url')), { s: KEY, p: '/', v: 'd', e: exp() });
|
||||
});
|
||||
|
||||
test('/a/heat nur auf dem öffentlichen Listener', async () => {
|
||||
const db = openDb(':memory:');
|
||||
const server = createAdminServer({ config: { heatSecret: SECRET, adminToken: 't' }, db });
|
||||
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
||||
const r = await fetch(`http://127.0.0.1:${server.address().port}/a/heat?token=${tok()}`);
|
||||
assert.equal(r.status, 404);
|
||||
server.close();
|
||||
});
|
||||
|
||||
test('403 bei ausgeschalteter Website (active = 0), mit CORS für die eigene Domain', async () => {
|
||||
const s = await setup();
|
||||
s.db.prepare('UPDATE sites SET active = 0 WHERE siteKey = ?').run(KEY);
|
||||
const r = await s.get(tok(), { Origin: 'https://kunde.de' });
|
||||
assert.equal(r.status, 403);
|
||||
assert.equal(r.headers.get('access-control-allow-origin'), 'https://kunde.de');
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('403 mit CORS-Kopf, wenn die Website bekannt ist (abgelaufen, aus); sonst ohne', async () => {
|
||||
const s = await setup();
|
||||
const origin = { Origin: 'https://www.kunde.de' };
|
||||
const abgelaufen = await s.get(tok({ exp: exp(-1) }), origin);
|
||||
assert.equal(abgelaufen.status, 403);
|
||||
assert.equal(abgelaufen.headers.get('access-control-allow-origin'), 'https://www.kunde.de');
|
||||
// fremder Origin, falsche Signatur, unbekannte Website: kein CORS-Kopf
|
||||
assert.equal((await s.get(tok({ exp: exp(-1) }), { Origin: 'https://evil.example' })).headers.get('access-control-allow-origin'), null);
|
||||
assert.equal((await s.get(tok({}, 'anderes-secret'), origin)).headers.get('access-control-allow-origin'), null);
|
||||
assert.equal((await s.get(tok({ siteKey: 'nichtvorhanden0123456' }), origin)).headers.get('access-control-allow-origin'), null);
|
||||
assert.equal((await s.get('abc', origin)).headers.get('access-control-allow-origin'), null);
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('OPTIONS-Preflight: CORS-Köpfe nur für erlaubte Origins bekannter Websites', async () => {
|
||||
const s = await setup();
|
||||
const pre = (token, origin) => fetch(`${s.base}/a/heat?token=${encodeURIComponent(token)}`, { method: 'OPTIONS', headers: { Origin: origin, 'Access-Control-Request-Method': 'GET' } });
|
||||
const ok = await pre(tok(), 'https://kunde.de');
|
||||
assert.equal(ok.status, 204);
|
||||
assert.equal(ok.headers.get('access-control-allow-origin'), 'https://kunde.de');
|
||||
assert.match(ok.headers.get('access-control-allow-methods'), /GET/);
|
||||
assert.equal((await pre(tok({ exp: exp(-1) }), 'https://kunde.de')).headers.get('access-control-allow-origin'), 'https://kunde.de');
|
||||
assert.equal((await pre(tok(), 'https://evil.example')).headers.get('access-control-allow-origin'), null);
|
||||
assert.equal((await pre('abc', 'https://kunde.de')).headers.get('access-control-allow-origin'), null);
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('Heatmap: gleiche Stunde (ts-Gleichstand), Limit schneidet über die niedrigste id ab', async () => {
|
||||
const s = await setup();
|
||||
const ins = s.db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,?,?)');
|
||||
const ts = NOW - 3600000;
|
||||
ins.run(KEY, ts, '/g', 'd', 0.99, 0.99); // niedrigste id
|
||||
s.db.exec('BEGIN');
|
||||
for (let i = 0; i < 5000; i++) ins.run(KEY, ts, '/g', 'd', 0.5, 0.5);
|
||||
s.db.exec('COMMIT');
|
||||
const j = await (await s.get(tok({ path: '/g' }))).json();
|
||||
assert.equal(j.points.length, 5000);
|
||||
assert.ok(!j.points.some((p) => p.x === 0.99));
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('Heatmap: Token-Pfad mit Kennung findet die als :id gespeicherten Klicks', async () => {
|
||||
const s = await setup();
|
||||
s.db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,?,?)').run(KEY, NOW - 1000, '/kunde/:id', 'd', 0.4, 0.6);
|
||||
const j = await (await s.get(tok({ path: '/kunde/12345678' }))).json();
|
||||
assert.deepEqual(j, { points: [{ x: 0.4, y: 0.6 }] });
|
||||
s.close();
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
// Verhalten von dist/h.js (Heatmap-Overlay) im Fake-DOM: eigenes Fenster und Einbettung im Portal.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import vm from 'node:vm';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const H = readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'client', 'dist', 'h.js'), 'utf8');
|
||||
|
||||
function makeEl(tag) {
|
||||
const e = {
|
||||
tagName: String(tag).toUpperCase(), nodeType: 1, attrs: {}, children: [], listeners: {}, style: { setProperty() {} }, parentNode: null,
|
||||
textContent: '', removed: false,
|
||||
setAttribute(n, v) { this.attrs[n] = String(v); },
|
||||
getAttribute(n) { return n in this.attrs ? this.attrs[n] : null; },
|
||||
appendChild(c) { c.parentNode = this; this.children.push(c); return c; },
|
||||
addEventListener(t, f, o) { (this.listeners[t] ||= []).push({ f, o }); },
|
||||
removeEventListener(t, f) { this.listeners[t] = (this.listeners[t] || []).filter((x) => x.f !== f); },
|
||||
remove() { this.removed = true; },
|
||||
attachShadow() { const r = makeEl('shadow'); this.shadow = r; return r; },
|
||||
closest(sel) {
|
||||
const tags = sel.split(',').map((t) => t.trim().toUpperCase());
|
||||
for (let n = this; n && n.tagName; n = n.parentNode) if (tags.includes(n.tagName)) return n;
|
||||
return null;
|
||||
},
|
||||
getContext() {
|
||||
return {
|
||||
createLinearGradient: () => ({ addColorStop() {} }),
|
||||
createRadialGradient: () => ({ addColorStop() {} }),
|
||||
fillRect() {}, clearRect() {},
|
||||
getImageData: (x, y, w, hh) => ({ data: new Uint8ClampedArray(Math.max(4, w * hh * 4)) }),
|
||||
putImageData() {},
|
||||
};
|
||||
},
|
||||
};
|
||||
return e;
|
||||
}
|
||||
|
||||
async function harness({ embed = false, framed = true, fail = false, points = [{ x: 0.5, y: 0.5 }, { x: 0.1, y: 0.2 }, { x: 2, y: 0 }] } = {}) {
|
||||
const body = makeEl('body'); const head = makeEl('head'); const html = makeEl('html');
|
||||
Object.assign(html, { scrollWidth: 402, scrollHeight: 2000, clientWidth: 402, clientHeight: 800 });
|
||||
const script = makeEl('script');
|
||||
script.setAttribute('src', 'https://project.webklar.com/a/h.js');
|
||||
script.setAttribute('data-token', 'TOK.sig');
|
||||
if (embed) script.setAttribute('data-embed', '1');
|
||||
script.src = 'https://project.webklar.com/a/h.js';
|
||||
const fetches = []; const posted = []; const winL = {};
|
||||
const document = {
|
||||
currentScript: script, documentElement: html, body, head,
|
||||
createElement: (t) => makeEl(t),
|
||||
};
|
||||
const win = {
|
||||
document,
|
||||
addEventListener(t, f, o) { (winL[t] ||= []).push({ f, o }); },
|
||||
removeEventListener(t, f) { winL[t] = (winL[t] || []).filter((x) => x.f !== f); },
|
||||
};
|
||||
const parent = framed ? { postMessage(data, origin) { posted.push({ data, origin }); } } : win;
|
||||
win.parent = parent;
|
||||
const sandbox = {
|
||||
window: win, document, URL, JSON, Math, String, Number, Array, Object, RegExp, Error, Uint8ClampedArray, Promise,
|
||||
setInterval: () => 1, clearInterval() {},
|
||||
fetch: (u, o) => {
|
||||
fetches.push({ u, o });
|
||||
if (fail) return Promise.resolve({ ok: false, json: () => Promise.resolve({}) });
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve({ points }) });
|
||||
},
|
||||
};
|
||||
sandbox.globalThis = sandbox;
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(H, sandbox);
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
const fire = (type, target) => {
|
||||
const ev = { type, target, prevented: false, stopped: false, preventDefault() { this.prevented = true; }, stopPropagation() { this.stopped = true; } };
|
||||
for (const { f } of winL[type] || []) f(ev);
|
||||
return ev;
|
||||
};
|
||||
const shadowHosts = body.children.filter((c) => c.shadow);
|
||||
return { body, head, fetches, posted, winL, fire, shadowHosts };
|
||||
}
|
||||
|
||||
const linkIn = () => { const a = makeEl('a'); a.setAttribute('href', '/kontakt'); const span = makeEl('span'); span.parentNode = a; return span; };
|
||||
|
||||
test('h.js eigenes Fenster: schwarze Leiste mit Klickzahl, keine Nachricht, Links bleiben klickbar', async () => {
|
||||
const h = await harness();
|
||||
assert.equal(h.fetches.length, 1);
|
||||
assert.equal(h.fetches[0].u, 'https://project.webklar.com/a/heat?token=TOK.sig');
|
||||
assert.equal(h.fetches[0].o.credentials, 'omit');
|
||||
assert.equal(h.shadowHosts.length, 1, 'Leiste');
|
||||
const msg = h.shadowHosts[0].shadow.children.find((c) => c.className === 'bar').children[0];
|
||||
assert.equal(msg.textContent, 'Heatmap – 2 Klicks (letzte 30 Tage)');
|
||||
assert.deepEqual(h.posted, []);
|
||||
assert.equal(h.fire('click', linkIn()).prevented, false);
|
||||
});
|
||||
|
||||
test('h.js eingebettet: keine Leiste, meldet Klickzahl nur an project.webklar.com', async () => {
|
||||
const h = await harness({ embed: true });
|
||||
assert.equal(h.shadowHosts.length, 0, 'keine Leiste');
|
||||
assert.ok(h.body.children.some((c) => c.tagName === 'CANVAS'), 'Overlay da');
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(h.posted)), [{ data: { wk: 'heat', state: 'ready', count: 2 }, origin: 'https://project.webklar.com' }]);
|
||||
// Scrollleisten ausgeblendet (Handy/Tablet haben ueberlagernde Leisten)
|
||||
assert.ok(h.head.children.some((c) => c.tagName === 'STYLE' && /scrollbar-width:none/.test(c.textContent)));
|
||||
});
|
||||
|
||||
test('h.js eingebettet: Links und Formulare gesperrt, andere Klicks nicht', async () => {
|
||||
const h = await harness({ embed: true });
|
||||
assert.ok((h.winL.click || []).some((l) => l.o === true), 'Klick-Sperre in der Capture-Phase');
|
||||
const ev = h.fire('click', linkIn());
|
||||
assert.equal(ev.prevented, true);
|
||||
assert.equal(ev.stopped, true, 'Router der Seite bekommt den Klick nicht');
|
||||
const area = h.fire('click', makeEl('area'));
|
||||
assert.equal(area.prevented, true);
|
||||
const other = h.fire('click', makeEl('button'));
|
||||
assert.equal(other.prevented, false);
|
||||
const sub = h.fire('submit', makeEl('form'));
|
||||
assert.equal(sub.prevented, true);
|
||||
});
|
||||
|
||||
test('h.js eingebettet: Fehler beim Laden => state error', async () => {
|
||||
const h = await harness({ embed: true, fail: true });
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(h.posted)), [{ data: { wk: 'heat', state: 'error', count: 0 }, origin: 'https://project.webklar.com' }]);
|
||||
});
|
||||
|
||||
test('h.js mit data-embed, aber nicht in einem Rahmen: verhaelt sich wie das eigene Fenster', async () => {
|
||||
const h = await harness({ embed: true, framed: false });
|
||||
assert.equal(h.shadowHosts.length, 1);
|
||||
assert.deepEqual(h.posted, []);
|
||||
assert.equal(h.fire('click', linkIn()).prevented, false);
|
||||
});
|
||||
@@ -0,0 +1,451 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { openDb } from '../src/db.js';
|
||||
import net from 'node:net';
|
||||
import { createPublicServer, createAdminServer } from '../src/server.js';
|
||||
import { classifyUa } from '../src/ua.js';
|
||||
import { createSalt } from '../src/salt.js';
|
||||
|
||||
const KEY = 'abcdef0123456789abcd';
|
||||
const HOST = 'abc.project.webklar.com';
|
||||
const ORIGIN = `https://${HOST}`;
|
||||
const IP = '203.0.113.77';
|
||||
const UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Mobile/15E148 Safari/604.1 GEHEIMERUA';
|
||||
|
||||
async function setup({ now = () => Date.UTC(2026, 9, 1, 10, 30, 45), geo = { lookup: () => 'DE' } } = {}) {
|
||||
const db = openDb(':memory:');
|
||||
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)')
|
||||
.run(KEY, 'p1', JSON.stringify([HOST, 'www.kunde.de']));
|
||||
const server = createPublicServer({ config: {}, db, geo, now });
|
||||
const invalidate = () => server.invalidateSites();
|
||||
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
const post = (ev, { origin = ORIGIN, ua = UA, ip = IP, left = '1.2.3.4', referer, raw, type = 'text/plain' } = {}) => {
|
||||
const headers = { 'Content-Type': type, 'User-Agent': ua, 'X-Forwarded-For': `${left}, ${ip}` };
|
||||
if (origin) headers.Origin = origin;
|
||||
if (referer) headers.Referer = referer;
|
||||
return fetch(base + '/a/e', { method: 'POST', headers, body: raw ?? JSON.stringify(ev) });
|
||||
};
|
||||
const rows = () => db.prepare('SELECT * FROM events_raw ORDER BY id').all();
|
||||
return { db, post, rows, base, server, invalidate, close: () => server.close() };
|
||||
}
|
||||
const pv = (o = {}) => ({ t: 'pv', site: KEY, path: '/', vp: 'd', ...o });
|
||||
|
||||
test('classifyUa: Bots, Geräte, leer', () => {
|
||||
assert.equal(classifyUa('').bot, true);
|
||||
assert.equal(classifyUa('Googlebot/2.1').bot, true);
|
||||
assert.equal(classifyUa('curl/8.0').bot, true);
|
||||
assert.equal(classifyUa('Mozilla/5.0 (Windows NT 10.0) Chrome/120').device, 'd');
|
||||
assert.equal(classifyUa('Mozilla/5.0 (iPhone; CPU) Mobile/15E').device, 'm');
|
||||
assert.equal(classifyUa('Mozilla/5.0 (Linux; Android 13; Pixel) Mobile Safari').device, 'm');
|
||||
assert.equal(classifyUa('Mozilla/5.0 (iPad; CPU OS 17)').device, 't');
|
||||
assert.equal(classifyUa('Mozilla/5.0 (Linux; Android 13; SM-T500) Safari').device, 't');
|
||||
assert.equal(classifyUa('Mozilla/5.0 (X11; Linux) Firefox/120').bot, false);
|
||||
});
|
||||
|
||||
test('Salz: stabil am Tag, wechselt um Mitternacht Berlin', () => {
|
||||
let t = Date.UTC(2026, 9, 1, 10, 0, 0);
|
||||
let n = 0;
|
||||
const s = createSalt({ now: () => t, random: () => Buffer.alloc(32, ++n) });
|
||||
const a = s.visitorId('s', '1.1.1.1', 'ua');
|
||||
assert.match(a, /^[0-9a-f]{16}$/);
|
||||
t = Date.UTC(2026, 9, 1, 21, 59, 0); // 23:59 Berlin (MESZ)
|
||||
assert.equal(s.visitorId('s', '1.1.1.1', 'ua'), a);
|
||||
assert.notEqual(s.visitorId('s', '1.1.1.2', 'ua'), a);
|
||||
t = Date.UTC(2026, 9, 1, 22, 0, 1); // 00:00 Berlin
|
||||
assert.notEqual(s.visitorId('s', '1.1.1.1', 'ua'), a);
|
||||
assert.match(s.rateKey('s', '1.1.1.1'), /^[0-9a-f]{64}$/);
|
||||
assert.ok(!s.rateKey('s', '1.1.1.1').includes('1.1.1.1'));
|
||||
});
|
||||
|
||||
test('pv: gespeichert ohne visitor, mit Land und Gerät, Cookie-frei', async () => {
|
||||
const t = await setup();
|
||||
const res = await t.post(pv({ path: '/preise?x=1#a', ref: 'https://Google.com/search?q=1' }));
|
||||
assert.equal(res.status, 204);
|
||||
assert.equal(res.headers.get('set-cookie'), null);
|
||||
const [r] = t.rows();
|
||||
assert.equal(r.type, 'pv');
|
||||
assert.equal(r.site, KEY);
|
||||
assert.equal(r.path, '/preise');
|
||||
assert.equal(r.ref, 'google.com');
|
||||
assert.equal(r.vp, 'd');
|
||||
assert.equal(r.country, 'DE');
|
||||
assert.equal(r.device, 'm');
|
||||
assert.equal(r.visitor, null);
|
||||
assert.equal(res.headers.get('access-control-allow-origin'), ORIGIN);
|
||||
assert.equal(res.headers.get('access-control-allow-credentials'), null);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('pv: ref eigene Domain wird verworfen, ungültige Felder', async () => {
|
||||
const t = await setup();
|
||||
await t.post(pv({ ref: 'https://www.kunde.de/x' }));
|
||||
await t.post(pv({ ref: 'kein host!' }));
|
||||
assert.equal(t.rows()[0].ref, null);
|
||||
assert.equal(t.rows()[1].ref, null);
|
||||
await t.post(pv({ path: 'kein-slash' }));
|
||||
await t.post(pv({ path: '/' + 'a'.repeat(300) }));
|
||||
await t.post(pv({ path: '/a\u0001b' }));
|
||||
await t.post(pv({ vp: 'x' }));
|
||||
await t.post({ t: 'zz', site: KEY, path: '/', vp: 'd' });
|
||||
await t.post({ site: KEY, path: '/', vp: 'd' });
|
||||
await t.post(null, { raw: 'kein json' });
|
||||
await t.post(null, { raw: '[1]' });
|
||||
assert.equal(t.rows().length, 2);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('Antwort immer 204; 413 bei großem Body; 405 bei falscher Methode', async () => {
|
||||
const t = await setup();
|
||||
assert.equal((await t.post(null, { raw: 'kein json' })).status, 204);
|
||||
assert.equal((await t.post(pv({ pad: 'x'.repeat(5000) }))).status, 413);
|
||||
assert.equal(t.rows().length, 0);
|
||||
assert.equal((await fetch(t.base + '/a/e')).status, 405);
|
||||
assert.equal((await fetch(t.base + '/a/e', { method: 'PUT', body: '{}' })).status, 405);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('application/json wird ebenfalls angenommen; unbekannte Felder werden nicht gespeichert', async () => {
|
||||
const t = await setup();
|
||||
const res = await t.post(pv({ evil: 'GEHEIM', ip: '9.9.9.9' }), { type: 'application/json' });
|
||||
assert.equal(res.status, 204);
|
||||
assert.equal(t.rows().length, 1);
|
||||
assert.ok(!JSON.stringify(t.db.prepare('SELECT * FROM events_raw').all()).includes('GEHEIM'));
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('Origin: fremd, fehlend, Referer-Fallback, inaktiv, unbekannte Site', async () => {
|
||||
const t = await setup();
|
||||
assert.equal((await t.post(pv(), { origin: 'https://evil.example' })).headers.get('access-control-allow-origin'), null);
|
||||
await t.post(pv(), { origin: null });
|
||||
assert.equal(t.rows().length, 0);
|
||||
await t.post(pv(), { origin: null, referer: `${ORIGIN}/seite` });
|
||||
assert.equal(t.rows().length, 1);
|
||||
await t.post(pv(), { origin: 'https://www.kunde.de' });
|
||||
assert.equal(t.rows().length, 2);
|
||||
await t.post(pv({ site: 'zzzzzzzzzzzzzzzzzzzz' }));
|
||||
assert.equal(t.rows().length, 2);
|
||||
t.db.prepare('UPDATE sites SET active = 0').run();
|
||||
await t.post(pv());
|
||||
assert.equal(t.rows().length, 2);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('Bot und leerer User-Agent: nichts gespeichert', async () => {
|
||||
const t = await setup();
|
||||
await t.post(pv(), { ua: 'Googlebot/2.1' });
|
||||
await t.post(pv(), { ua: '' });
|
||||
assert.equal(t.rows().length, 0);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('bn: nur site, ts (Stunde), type, kind', async () => {
|
||||
const t = await setup();
|
||||
await t.post({ t: 'bn', site: KEY, a: 'shown', path: '/x', vp: 'm', visitor: 'v' });
|
||||
await t.post({ t: 'bn', site: KEY, a: 'accept' });
|
||||
await t.post({ t: 'bn', site: KEY, a: 'foo' });
|
||||
const rows = t.rows();
|
||||
assert.equal(rows.length, 2);
|
||||
for (const r of rows) {
|
||||
assert.equal(r.type, 'bn');
|
||||
for (const f of ['path', 'vp', 'country', 'device', 'visitor', 'ref', 'sec', 'scroll', 'host']) assert.equal(r[f], null, f);
|
||||
assert.equal(r.ts, Date.UTC(2026, 9, 1, 10, 0, 0));
|
||||
}
|
||||
assert.deepEqual(rows.map((r) => r.kind), ['shown', 'accept']);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('en: sec/scroll begrenzt, mit visitor', async () => {
|
||||
const t = await setup();
|
||||
await t.post({ t: 'en', site: KEY, path: '/a?b', vp: 't', sec: 99999, scroll: 250 });
|
||||
await t.post({ t: 'en', site: KEY, path: '/a', vp: 't', sec: 12.7, scroll: -5 });
|
||||
await t.post({ t: 'en', site: KEY, path: '/a', vp: 't', sec: 'x', scroll: 1 });
|
||||
const rows = t.rows();
|
||||
assert.equal(rows.length, 2);
|
||||
assert.match(rows[0].visitor, /^[0-9a-f]{16}$/);
|
||||
assert.equal(rows[0].path, '/a');
|
||||
assert.equal(rows[0].sec, 3600);
|
||||
assert.equal(rows[0].scroll, 100);
|
||||
assert.equal(rows[1].sec, 12);
|
||||
assert.equal(rows[1].scroll, 0);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('ck: in clicks, geklemmt, ohne visitor', async () => {
|
||||
const t = await setup();
|
||||
await t.post({ t: 'ck', site: KEY, path: '/a#h', vp: 'd', x: 0.123456, y: 1.7, sel: 'div>a'.repeat(100) });
|
||||
await t.post({ t: 'ck', site: KEY, path: '/a', vp: 'd', x: 'n', y: 0.5 });
|
||||
assert.equal(t.rows().length, 0);
|
||||
const c = t.db.prepare('SELECT * FROM clicks').all();
|
||||
assert.equal(c.length, 1);
|
||||
assert.equal(c[0].path, '/a');
|
||||
assert.equal(c[0].x, 0.123);
|
||||
assert.equal(c[0].y, 1);
|
||||
assert.equal(c[0].sel.length, 200);
|
||||
assert.deepEqual(Object.keys(c[0]).sort(), ['id', 'path', 'sel', 'site', 'ts', 'vp', 'x', 'y']);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('ct: kind und host', async () => {
|
||||
const t = await setup();
|
||||
await t.post({ t: 'ct', site: KEY, path: '/k', kind: 'tel', host: 'ignored.de' });
|
||||
await t.post({ t: 'ct', site: KEY, path: '/k', kind: 'out', host: 'Instagram.COM' });
|
||||
await t.post({ t: 'ct', site: KEY, path: '/k', kind: 'out' });
|
||||
await t.post({ t: 'ct', site: KEY, path: '/k', kind: 'sms' });
|
||||
const rows = t.rows();
|
||||
assert.equal(rows.length, 2);
|
||||
assert.equal(rows[0].kind, 'tel');
|
||||
assert.equal(rows[0].host, null);
|
||||
assert.equal(rows[1].host, 'instagram.com');
|
||||
assert.match(rows[1].visitor, /^[0-9a-f]{16}$/);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('Mengenlimit: 121. Ereignis verworfen, neue Minute frei', async () => {
|
||||
let now = Date.UTC(2026, 9, 1, 10, 30, 0);
|
||||
const t = await setup({ now: () => now });
|
||||
for (let i = 0; i < 125; i++) await t.post(pv());
|
||||
assert.equal(t.rows().length, 120);
|
||||
await t.post(pv(), { ip: '198.51.100.9' });
|
||||
assert.equal(t.rows().length, 121);
|
||||
now += 61000;
|
||||
await t.post(pv());
|
||||
assert.equal(t.rows().length, 122);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('Land: nur 2 Großbuchstaben, sonst leer', async () => {
|
||||
const t = await setup({ geo: { lookup: () => 'xyz' } });
|
||||
await t.post(pv());
|
||||
assert.equal(t.rows()[0].country, '');
|
||||
t.close();
|
||||
const u = await setup({ geo: { lookup: () => 'at' } });
|
||||
await u.post(pv());
|
||||
assert.equal(u.rows()[0].country, 'AT');
|
||||
u.close();
|
||||
});
|
||||
|
||||
test('CORS-Preflight nur für erlaubte Origin', async () => {
|
||||
const t = await setup();
|
||||
const ok = await fetch(t.base + '/a/e', { method: 'OPTIONS', headers: { Origin: ORIGIN, 'Access-Control-Request-Method': 'POST' } });
|
||||
assert.equal(ok.status, 204);
|
||||
assert.equal(ok.headers.get('access-control-allow-origin'), ORIGIN);
|
||||
assert.equal(ok.headers.get('access-control-allow-credentials'), null);
|
||||
const bad = await fetch(t.base + '/a/e', { method: 'OPTIONS', headers: { Origin: 'https://evil.example' } });
|
||||
assert.equal(bad.status, 204);
|
||||
assert.equal(bad.headers.get('access-control-allow-origin'), null);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('Datenschutz: weder IP noch User-Agent irgendwo in der Datenbank', async () => {
|
||||
const t = await setup();
|
||||
await t.post(pv({ ref: 'https://google.com' }));
|
||||
await t.post({ t: 'bn', site: KEY, a: 'accept' });
|
||||
await t.post({ t: 'en', site: KEY, path: '/a', vp: 'd', sec: 5, scroll: 50 });
|
||||
await t.post({ t: 'ck', site: KEY, path: '/a', vp: 'd', x: 0.5, y: 0.5, sel: 'a' });
|
||||
await t.post({ t: 'ct', site: KEY, path: '/a', kind: 'mail' });
|
||||
let dump = '';
|
||||
for (const { name } of t.db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all()) {
|
||||
dump += JSON.stringify(t.db.prepare(`SELECT * FROM ${name}`).all());
|
||||
}
|
||||
for (const needle of [IP, '1.2.3.4', 'GEHEIMERUA', 'Mozilla', 'iPhone']) assert.ok(!dump.includes(needle), needle);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('t: nur eigene Feldlisten, kein Prototyp-Zugriff, keine Logzeile', async () => {
|
||||
const t = await setup();
|
||||
const errs = [];
|
||||
const orig = console.error;
|
||||
console.error = (...a) => errs.push(a.join(' '));
|
||||
try {
|
||||
for (const tt of ['__proto__', 'constructor', 'toString', 'hasOwnProperty', 'valueOf']) {
|
||||
assert.equal((await t.post(null, { raw: JSON.stringify({ t: tt, site: KEY, path: '/', vp: 'd' }) })).status, 204);
|
||||
}
|
||||
assert.equal((await t.post(null, { raw: `{"t":"__proto__","site":"${KEY}","path":"/"}` })).status, 204);
|
||||
assert.equal((await t.post({ t: ['pv'], site: KEY, path: '/', vp: 'd' })).status, 204);
|
||||
assert.equal((await t.post({ t: { toString: 1 }, site: KEY, path: '/', vp: 'd' })).status, 204);
|
||||
} finally { console.error = orig; }
|
||||
assert.equal(t.rows().length, 0);
|
||||
assert.deepEqual(errs, []);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('Origin/Referer: nur https ohne Port zählt', async () => {
|
||||
const t = await setup();
|
||||
for (const origin of [`http://${HOST}`, `https://${HOST}:8080`, `https://evil-${HOST}`, `https://${HOST}.evil.com`, `ftp://${HOST}`]) {
|
||||
const res = await t.post(pv(), { origin });
|
||||
assert.equal(res.headers.get('access-control-allow-origin'), null, origin);
|
||||
const pre = await fetch(t.base + '/a/e', { method: 'OPTIONS', headers: { Origin: origin } });
|
||||
assert.equal(pre.headers.get('access-control-allow-origin'), null, origin);
|
||||
}
|
||||
await t.post(pv(), { origin: null, referer: `http://${HOST}/x` });
|
||||
await t.post(pv(), { origin: null, referer: `https://${HOST}:8080/x` });
|
||||
assert.equal(t.rows().length, 0);
|
||||
await t.post(pv(), { origin: null, referer: `https://${HOST}/x` });
|
||||
assert.equal(t.rows().length, 1);
|
||||
t.close();
|
||||
});
|
||||
|
||||
function rawRequest(port, head, body = '') {
|
||||
return new Promise((resolve) => {
|
||||
const s = net.connect(port, '127.0.0.1');
|
||||
let buf = '';
|
||||
s.on('connect', () => s.write(head + '\r\n\r\n' + body));
|
||||
s.on('data', (d) => { buf += d; });
|
||||
s.on('close', () => resolve(buf));
|
||||
s.on('error', () => resolve(buf));
|
||||
setTimeout(() => { s.destroy(); }, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
test('Body: früh 413 bei Content-Length > 4096, Abbruch bei gestreamtem Überlauf', async () => {
|
||||
const t = await setup();
|
||||
const port = t.server.address().port;
|
||||
const head = `POST /a/e HTTP/1.1\r\nHost: x\r\nOrigin: ${ORIGIN}\r\nContent-Type: text/plain`;
|
||||
// Content-Length 1 MB angekündigt, aber nichts gesendet: Antwort kommt trotzdem sofort
|
||||
const early = await rawRequest(port, `${head}\r\nContent-Length: 1048576`);
|
||||
assert.match(early, /^HTTP\/1\.1 413/);
|
||||
// chunked ohne Content-Length: über 4 KB ⇒ 413 und Verbindung wird beendet
|
||||
const chunk = 'x'.repeat(3000);
|
||||
const body = `${chunk.length.toString(16)}\r\n${chunk}\r\n`.repeat(3);
|
||||
const streamed = await rawRequest(port, `${head}\r\nTransfer-Encoding: chunked`, body);
|
||||
assert.match(streamed, /^HTTP\/1\.1 413/);
|
||||
assert.equal(t.rows().length, 0);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('Preflight: Hostliste gecacht (30 s), invalidateSites() und Admin-Änderung wirken sofort', async () => {
|
||||
let now = Date.UTC(2026, 9, 1, 10, 0, 0);
|
||||
const t = await setup({ now: () => now });
|
||||
const pre = async () => (await fetch(t.base + '/a/e', { method: 'OPTIONS', headers: { Origin: ORIGIN } })).headers.get('access-control-allow-origin');
|
||||
assert.equal(await pre(), ORIGIN);
|
||||
t.db.prepare('UPDATE sites SET active = 0').run();
|
||||
assert.equal(await pre(), ORIGIN); // noch aus dem Cache
|
||||
now += 31000;
|
||||
assert.equal(await pre(), null); // Cache abgelaufen
|
||||
t.db.prepare('UPDATE sites SET active = 1').run();
|
||||
assert.equal(await pre(), null);
|
||||
t.invalidate();
|
||||
assert.equal(await pre(), ORIGIN);
|
||||
// Admin-Listener meldet Änderungen an den öffentlichen Listener
|
||||
const admin = createAdminServer({ config: { adminToken: 'geheim' }, db: t.db, onSitesChanged: () => t.invalidate() });
|
||||
await new Promise((r) => admin.listen(0, '127.0.0.1', r));
|
||||
const res = await fetch(`http://127.0.0.1:${admin.address().port}/a/admin/sites/${KEY}`, {
|
||||
method: 'PUT', headers: { Authorization: 'Bearer geheim', 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ projectId: 'p1', domains: [HOST], active: false }),
|
||||
});
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(await pre(), null);
|
||||
admin.close();
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('X-Forwarded-For: rechtester Eintrag zählt für das Mengenlimit', async () => {
|
||||
const t = await setup();
|
||||
for (let i = 0; i < 120; i++) await t.post(pv(), { left: `10.0.0.${i % 250}` }); // links variiert, rechts gleich
|
||||
assert.equal(t.rows().length, 120);
|
||||
await t.post(pv(), { left: '10.9.9.9' }); // gleicher Bucket ⇒ verworfen
|
||||
assert.equal(t.rows().length, 120);
|
||||
await t.post(pv(), { left: '1.2.3.4', ip: '198.51.100.9' }); // rechts anders ⇒ eigener Bucket
|
||||
assert.equal(t.rows().length, 121);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('T2: pv ohne ref/vp wird gespeichert (vp NULL, ref NULL), Gerät und Land bleiben', async () => {
|
||||
const t = await setup();
|
||||
await t.post({ t: 'pv', site: KEY, path: '/' });
|
||||
const r = t.rows();
|
||||
assert.equal(r.length, 1);
|
||||
assert.equal(r[0].type, 'pv');
|
||||
assert.equal(r[0].vp, null);
|
||||
assert.equal(r[0].ref, null);
|
||||
assert.equal(r[0].device, 'm');
|
||||
assert.equal(r[0].country, 'DE');
|
||||
await t.post({ t: 'pv', site: KEY, path: '/', vp: 'x' });
|
||||
assert.equal(t.rows().length, 1);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('T2: cx braucht gültiges vp, speichert ref, eigene Domain ergibt NULL', async () => {
|
||||
const t = await setup();
|
||||
await t.post({ t: 'cx', site: KEY, path: '/', ref: 'https://www.google.com/x' });
|
||||
await t.post({ t: 'cx', site: KEY, path: '/', ref: 'google.com', vp: 'x' });
|
||||
assert.equal(t.rows().length, 0);
|
||||
await t.post({ t: 'cx', site: KEY, path: '/', ref: 'https://www.google.com/x', vp: 'd' });
|
||||
await t.post({ t: 'cx', site: KEY, path: '/', ref: 'https://www.kunde.de/x', vp: 'm' });
|
||||
const r = t.rows();
|
||||
assert.equal(r.length, 2);
|
||||
assert.equal(r[0].type, 'cx');
|
||||
assert.equal(r[0].ref, 'www.google.com');
|
||||
assert.equal(r[0].vp, 'd');
|
||||
assert.equal(r[0].country, null);
|
||||
assert.equal(r[0].device, null);
|
||||
assert.equal(r[1].ref, null);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('T3: alle Zeitstempel auf die volle Stunde', async () => {
|
||||
const t = await setup();
|
||||
const hour = Date.UTC(2026, 9, 1, 10, 0, 0);
|
||||
await t.post(pv());
|
||||
await t.post({ t: 'cx', site: KEY, path: '/', vp: 'd' });
|
||||
await t.post({ t: 'en', site: KEY, path: '/', vp: 'd', sec: 5, scroll: 10 });
|
||||
await t.post({ t: 'ct', site: KEY, path: '/', kind: 'tel' });
|
||||
await t.post({ t: 'ck', site: KEY, path: '/', vp: 'd', x: 0.5, y: 0.5 });
|
||||
const r = t.rows();
|
||||
assert.deepEqual(r.map((x) => x.type), ['pv', 'cx', 'en', 'ct']);
|
||||
for (const x of r) assert.equal(x.ts, hour);
|
||||
const c = t.db.prepare('SELECT ts FROM clicks').all();
|
||||
assert.equal(c.length, 1);
|
||||
assert.equal(c[0].ts, hour);
|
||||
assert.equal(t.db.prepare('SELECT COUNT(*) n FROM events_raw WHERE ts % 3600000 != 0').get().n, 0);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('T14: kennungsartige Pfadsegmente werden zu :id, alle Typen', async () => {
|
||||
const t = await setup();
|
||||
const cases = [
|
||||
['/kunde/123456789/rechnung', '/kunde/:id/rechnung'],
|
||||
['/u/3f2b8c1e-5d4a-4e0b-9a7c-1b2c3d4e5f60', '/u/:id'],
|
||||
['/mail/max%40example.com', '/mail/:id'],
|
||||
['/mail/max@example.com', '/mail/:id'],
|
||||
['/preise/2026', '/preise/2026'],
|
||||
['/blog/12345', '/blog/12345'],
|
||||
['/', '/'],
|
||||
['/a/123456?x=1#h', '/a/:id'],
|
||||
];
|
||||
for (const [p] of cases) await t.post(pv({ path: p }));
|
||||
assert.deepEqual(t.rows().map((r) => r.path), cases.map((c) => c[1]));
|
||||
await t.post({ t: 'cx', site: KEY, path: '/kunde/987654321', vp: 'd' });
|
||||
await t.post({ t: 'en', site: KEY, path: '/kunde/987654321', vp: 'd', sec: 1, scroll: 1 });
|
||||
await t.post({ t: 'ct', site: KEY, path: '/kunde/987654321', kind: 'tel' });
|
||||
await t.post({ t: 'ck', site: KEY, path: '/kunde/987654321', vp: 'd', x: 0.1, y: 0.1 });
|
||||
const paths = t.rows().slice(cases.length).map((r) => r.path);
|
||||
assert.deepEqual(paths, ['/kunde/:id', '/kunde/:id', '/kunde/:id']);
|
||||
assert.equal(t.db.prepare('SELECT path FROM clicks').get().path, '/kunde/:id');
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('T14: ungültige Prozent-Kodierung fällt auf den Rohwert zurück', async () => {
|
||||
const t = await setup();
|
||||
await t.post(pv({ path: '/a/%E0%A4%A/123456' }));
|
||||
await t.post(pv({ path: '/a/%E0%A4%A/x' }));
|
||||
await t.post(pv({ path: '/a/%E0%A4%A40/x' }));
|
||||
assert.deepEqual(t.rows().map((r) => r.path), ['/a/%E0%A4%A/:id', '/a/%E0%A4%A/x', '/a/%E0%A4%A40/x']);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('T3: jede gespeicherte Zeile jedes Typs liegt auf der vollen Stunde (ungerade Uhrzeit)', async () => {
|
||||
const t = await setup({ now: () => Date.UTC(2026, 9, 1, 10, 37, 12, 345) });
|
||||
await t.post({ t: 'bn', site: KEY, a: 'shown' });
|
||||
await t.post(pv());
|
||||
await t.post({ t: 'cx', site: KEY, path: '/', vp: 'd' });
|
||||
await t.post({ t: 'en', site: KEY, path: '/', vp: 'd', sec: 5, scroll: 10 });
|
||||
await t.post({ t: 'ct', site: KEY, path: '/', kind: 'tel' });
|
||||
await t.post({ t: 'ck', site: KEY, path: '/', vp: 'd', x: 0.5, y: 0.5 });
|
||||
const r = t.rows();
|
||||
assert.deepEqual([...new Set(r.map((x) => x.type))].sort(), ['bn', 'ct', 'cx', 'en', 'pv']);
|
||||
for (const x of r) assert.equal(x.ts % 3600000, 0);
|
||||
for (const x of t.db.prepare('SELECT ts FROM clicks').all()) assert.equal(x.ts % 3600000, 0);
|
||||
t.close();
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, readFileSync, statSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { openDb } from '../src/db.js';
|
||||
import { runNightly, dailyCutoffDay, DAILY_MONTHS } from '../src/jobs.js';
|
||||
|
||||
const KEY = 'abcdef0123456789abcd';
|
||||
const NOW = Date.UTC(2026, 9, 1, 10);
|
||||
const D = 86400 * 1000;
|
||||
|
||||
test('dailyCutoffDay: 25 Kalendermonate zurück, Monatsende begrenzt', () => {
|
||||
assert.equal(DAILY_MONTHS, 25);
|
||||
assert.equal(dailyCutoffDay(Date.UTC(2026, 9, 1, 10)), '2024-09-01');
|
||||
assert.equal(dailyCutoffDay(Date.UTC(2026, 2, 31, 10)), '2024-02-29');
|
||||
});
|
||||
|
||||
function mk() {
|
||||
const db = openDb(':memory:');
|
||||
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)').run(KEY, 'p', '["a.de"]');
|
||||
return db;
|
||||
}
|
||||
|
||||
test('Tageswerte älter als 25 Monate werden gelöscht, Grenztag bleibt', () => {
|
||||
const db = mk();
|
||||
for (const day of ['2024-08-31', '2024-09-01']) {
|
||||
db.prepare("INSERT INTO daily (site, day, dim, key, count) VALUES (?,?, 'pv_total','',3)").run(KEY, day);
|
||||
db.prepare('INSERT INTO agg_state (site, day) VALUES (?,?)').run(KEY, day);
|
||||
}
|
||||
const r = runNightly(db, { now: NOW });
|
||||
assert.deepEqual(db.prepare('SELECT day FROM daily').all().map((x) => x.day), ['2024-09-01']);
|
||||
assert.deepEqual(db.prepare('SELECT day FROM agg_state').all().map((x) => x.day), ['2024-09-01']);
|
||||
assert.equal(r.dailyDeleted, 1);
|
||||
});
|
||||
|
||||
test('ausgeschaltete Website ohne offSince bekommt offSince, zweiter Lauf ändert nichts', () => {
|
||||
const db = mk();
|
||||
db.prepare('UPDATE sites SET active = 0').run();
|
||||
const r = runNightly(db, { now: NOW });
|
||||
assert.equal(db.prepare('SELECT offSince FROM sites').get().offSince, new Date(NOW).toISOString());
|
||||
assert.equal(r.orphansMarked, 1);
|
||||
runNightly(db, { now: NOW + D });
|
||||
assert.equal(db.prepare('SELECT offSince FROM sites').get().offSince, new Date(NOW).toISOString());
|
||||
});
|
||||
|
||||
test('Website mit offSince > 60 Tage wird weiter komplett gelöscht', () => {
|
||||
const db = mk();
|
||||
db.prepare('UPDATE sites SET active = 0, offSince = ?').run(new Date(NOW - 61 * D).toISOString());
|
||||
const r = runNightly(db, { now: NOW });
|
||||
assert.equal(r.sitesDeleted, 1);
|
||||
assert.equal(db.prepare('SELECT COUNT(*) n FROM sites').get().n, 0);
|
||||
});
|
||||
|
||||
test('Zeilen ohne sites-Eintrag werden gelöscht, andere bleiben', () => {
|
||||
const db = mk();
|
||||
const GONE = 'gonegonegonegonegone';
|
||||
for (const s of [KEY, GONE]) {
|
||||
db.prepare("INSERT INTO events_raw (site, ts, type) VALUES (?,?, 'pv')").run(s, NOW - 1000);
|
||||
db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,?,?)').run(s, NOW - 1000, '/', 'd', 1, 1);
|
||||
db.prepare("INSERT INTO daily (site, day, dim, key, count) VALUES (?, '2026-09-01','pv_total','',1)").run(s);
|
||||
db.prepare("INSERT INTO agg_state (site, day) VALUES (?, '2026-09-01')").run(s);
|
||||
}
|
||||
const r = runNightly(db, { now: NOW });
|
||||
for (const t of ['events_raw', 'clicks', 'daily', 'agg_state']) {
|
||||
assert.deepEqual(db.prepare(`SELECT DISTINCT site FROM ${t}`).all().map((x) => x.site), [KEY], t);
|
||||
}
|
||||
assert.ok(r.orphanRowsDeleted >= 4);
|
||||
});
|
||||
|
||||
test('secure_delete: gelöschter Pfad steht nicht mehr in der Datei', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'sd-'));
|
||||
const p = join(dir, 'a.db');
|
||||
const db = openDb(p);
|
||||
assert.equal(db.prepare('PRAGMA secure_delete').get().secure_delete, 1);
|
||||
db.prepare("INSERT INTO events_raw (site, ts, type, path) VALUES ('x', 1, 'pv', 'GEHEIMER-PFAD-4711')").run();
|
||||
db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
||||
db.exec("DELETE FROM events_raw WHERE path = 'GEHEIMER-PFAD-4711'");
|
||||
db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
||||
assert.ok(!readFileSync(p).includes('GEHEIMER-PFAD-4711'));
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('runNightly leert das WAL', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'wal-'));
|
||||
const p = join(dir, 'a.db');
|
||||
const db = openDb(p);
|
||||
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)').run(KEY, 'p', '[]');
|
||||
runNightly(db, { now: NOW });
|
||||
assert.equal(statSync(p + '-wal').size, 0);
|
||||
db.close();
|
||||
});
|
||||
@@ -0,0 +1,209 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { loadConfig } from '../src/config.js';
|
||||
import { openDb } from '../src/db.js';
|
||||
import { createPublicServer, createAdminServer } from '../src/server.js';
|
||||
|
||||
const KEY = 'abcdef0123456789abcd';
|
||||
|
||||
async function setup(adminToken = 'geheim', make = createAdminServer) {
|
||||
const db = openDb(':memory:');
|
||||
const server = make({ config: { adminToken }, db });
|
||||
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
const call = async (method, path, body, token = 'geheim', raw) => {
|
||||
const headers = {};
|
||||
if (token !== null) headers.Authorization = `Bearer ${token}`;
|
||||
if (body !== undefined || raw !== undefined) headers['Content-Type'] = 'application/json';
|
||||
const res = await fetch(base + path, { method, headers, body: raw ?? (body === undefined ? undefined : JSON.stringify(body)) });
|
||||
return { status: res.status, body: await res.json() };
|
||||
};
|
||||
return { db, call, close: () => server.close() };
|
||||
}
|
||||
const row = (db, k = KEY) => db.prepare('SELECT * FROM sites WHERE siteKey = ?').get(k);
|
||||
const valid = { projectId: 'p1', domains: ['abc.project.webklar.com'], active: true };
|
||||
|
||||
test('loadConfig: Vorgaben und Umgebung', () => {
|
||||
const c = loadConfig({});
|
||||
assert.deepEqual(c, { port: 8090, adminPort: 8091, dataDir: '/srv/webklar-analytics', adminToken: '', heatSecret: '', geoFile: '', baseHost: 'project.webklar.com' });
|
||||
const d = loadConfig({ ANALYTICS_PORT: '9000', ANALYTICS_ADMIN_TOKEN: 't', ANALYTICS_HEAT_SECRET: 'h' });
|
||||
assert.equal(d.port, 9000);
|
||||
assert.equal(d.adminToken, 't');
|
||||
assert.equal(d.heatSecret, 'h');
|
||||
});
|
||||
|
||||
test('openDb ist idempotent und legt Tabellen an', () => {
|
||||
const db = openDb(':memory:');
|
||||
const names = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((r) => r.name);
|
||||
for (const n of ['sites', 'events_raw', 'clicks', 'daily']) assert.ok(names.includes(n), n);
|
||||
});
|
||||
|
||||
test('GET /a/health ohne Token', async () => {
|
||||
const t = await setup();
|
||||
const r = await t.call('GET', '/a/health', undefined, null);
|
||||
assert.equal(r.status, 200);
|
||||
assert.deepEqual(r.body, { ok: true });
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('Admin: 401 ohne, mit falschem Token und bei leerem adminToken', async () => {
|
||||
const t = await setup();
|
||||
assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, valid, null)).status, 401);
|
||||
assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, valid, 'falsch')).status, 401);
|
||||
assert.equal((await t.call('DELETE', `/a/admin/sites/${KEY}`, undefined, 'falsch')).status, 401);
|
||||
assert.equal(row(t.db), undefined);
|
||||
t.close();
|
||||
const e = await setup('');
|
||||
assert.equal((await e.call('PUT', `/a/admin/sites/${KEY}`, valid, '')).status, 401);
|
||||
assert.equal((await e.call('PUT', `/a/admin/sites/${KEY}`, valid, 'x')).status, 401);
|
||||
e.close();
|
||||
});
|
||||
|
||||
test('PUT legt an und ändert', async () => {
|
||||
const t = await setup();
|
||||
const r = await t.call('PUT', `/a/admin/sites/${KEY}`, valid);
|
||||
assert.equal(r.status, 200);
|
||||
assert.equal(r.body.ok, true);
|
||||
let s = row(t.db);
|
||||
assert.equal(s.projectId, 'p1');
|
||||
assert.deepEqual(JSON.parse(s.domains), ['abc.project.webklar.com']);
|
||||
assert.equal(s.active, 1);
|
||||
assert.equal(s.offSince, null);
|
||||
await t.call('PUT', `/a/admin/sites/${KEY}`, { projectId: 'p2', domains: ['a.de', 'b.de'], active: true });
|
||||
s = row(t.db);
|
||||
assert.equal(s.projectId, 'p2');
|
||||
assert.deepEqual(JSON.parse(s.domains), ['a.de', 'b.de']);
|
||||
assert.equal(t.db.prepare('SELECT COUNT(*) c FROM sites').get().c, 1);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('active=false setzt offSince einmalig, active=true löscht es', async () => {
|
||||
const t = await setup();
|
||||
await t.call('PUT', `/a/admin/sites/${KEY}`, valid);
|
||||
await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, active: false });
|
||||
const s1 = row(t.db);
|
||||
assert.equal(s1.active, 0);
|
||||
assert.ok(!Number.isNaN(Date.parse(s1.offSince)));
|
||||
assert.match(s1.offSince, /^\d{4}-\d\d-\d\dT/);
|
||||
t.db.prepare('UPDATE sites SET offSince = ? WHERE siteKey = ?').run('2026-01-01T00:00:00.000Z', KEY);
|
||||
await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, active: false });
|
||||
assert.equal(row(t.db).offSince, '2026-01-01T00:00:00.000Z');
|
||||
await t.call('PUT', `/a/admin/sites/${KEY}`, valid);
|
||||
assert.equal(row(t.db).offSince, null);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('PUT: ungültiger siteKey 400', async () => {
|
||||
const t = await setup();
|
||||
for (const k of ['kurz', 'ABCDEF0123456789ABCD', 'abcdef0123456789abc!', 'a'.repeat(41)]) {
|
||||
assert.equal((await t.call('PUT', `/a/admin/sites/${k}`, valid)).status, 400, k);
|
||||
}
|
||||
assert.equal((await t.call('DELETE', '/a/admin/sites/kurz')).status, 400);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('PUT: Domains nur Hostnamen, max 5; projectId/active geprüft', async () => {
|
||||
const t = await setup();
|
||||
const bad = [
|
||||
['a.de:8080'], ['a.de/pfad'], ['https://a.de'], ['A.de'], ['localhost'], [''], ['-a.de'], [1], 'a.de', [],
|
||||
['a.de', 'b.de', 'c.de', 'd.de', 'e.de', 'f.de'],
|
||||
];
|
||||
for (const domains of bad) {
|
||||
const r = await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, domains });
|
||||
assert.equal(r.status, 400, JSON.stringify(domains));
|
||||
assert.equal(typeof r.body.error, 'string');
|
||||
}
|
||||
assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, projectId: '' })).status, 400);
|
||||
assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, active: 'ja' })).status, 400);
|
||||
assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, undefined, 'geheim', '{kaputt')).status, 400);
|
||||
assert.equal(row(t.db), undefined);
|
||||
assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, domains: ['a.de', 'b.de', 'c.de', 'd.de', 'e.de'] })).status, 200);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('PUT: zu großer Body 413', async () => {
|
||||
const t = await setup();
|
||||
const r = await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, projectId: 'x'.repeat(70000) });
|
||||
assert.equal(r.status, 413);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('DELETE entfernt Website, unbekannte Route 404', async () => {
|
||||
const t = await setup();
|
||||
await t.call('PUT', `/a/admin/sites/${KEY}`, valid);
|
||||
assert.equal((await t.call('DELETE', `/a/admin/sites/${KEY}`)).status, 200);
|
||||
assert.equal(row(t.db), undefined);
|
||||
assert.equal((await t.call('GET', '/a/nichts', undefined, null)).status, 404);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('Admin-Route auf dem öffentlichen Listener: 404 auch mit richtigem Token', async () => {
|
||||
const t = await setup('geheim', createPublicServer);
|
||||
assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, valid)).status, 404);
|
||||
assert.equal((await t.call('DELETE', `/a/admin/sites/${KEY}`)).status, 404);
|
||||
assert.equal(row(t.db), undefined);
|
||||
assert.equal((await t.call('GET', '/a/health', undefined, null)).status, 200);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('Doppelte Domains: 400', async () => {
|
||||
const t = await setup();
|
||||
assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, domains: ['a.de', 'a.de'] })).status, 400);
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('Datei-DB: zweimal öffnen (idempotent) und WAL', () => {
|
||||
const f = join(mkdtempSync(join(tmpdir(), 'ana-')), 'a.db');
|
||||
const d1 = openDb(f);
|
||||
d1.prepare("INSERT INTO sites VALUES ('k','p','[]',1,NULL)").run();
|
||||
assert.equal(d1.prepare('PRAGMA journal_mode').get().journal_mode, 'wal');
|
||||
d1.close();
|
||||
const d2 = openDb(f);
|
||||
assert.equal(d2.prepare('SELECT COUNT(*) c FROM sites').get().c, 1);
|
||||
assert.equal(d2.prepare('PRAGMA journal_mode').get().journal_mode, 'wal');
|
||||
d2.close();
|
||||
});
|
||||
|
||||
test('POST …/off: 401 ohne Token, 404 unbekannt, setzt active=0 und offSince einmalig', async () => {
|
||||
const t = await setup();
|
||||
assert.equal((await t.call('POST', `/a/admin/sites/${KEY}/off`, undefined, null)).status, 401);
|
||||
assert.equal((await t.call('POST', `/a/admin/sites/${KEY}/off`, undefined, 'falsch')).status, 401);
|
||||
assert.equal((await t.call('POST', `/a/admin/sites/${KEY}/off`)).status, 404);
|
||||
assert.equal((await t.call('POST', '/a/admin/sites/kurz/off')).status, 400);
|
||||
await t.call('PUT', `/a/admin/sites/${KEY}`, valid);
|
||||
assert.equal(row(t.db).active, 1);
|
||||
const r = await t.call('POST', `/a/admin/sites/${KEY}/off`);
|
||||
assert.equal(r.status, 200);
|
||||
const s1 = row(t.db);
|
||||
assert.equal(s1.active, 0);
|
||||
assert.match(s1.offSince, /^\d{4}-\d\d-\d\dT/);
|
||||
assert.deepEqual(JSON.parse(s1.domains), ['abc.project.webklar.com']);
|
||||
t.db.prepare('UPDATE sites SET offSince = ? WHERE siteKey = ?').run('2026-01-01T00:00:00.000Z', KEY);
|
||||
assert.equal((await t.call('POST', `/a/admin/sites/${KEY}/off`)).status, 200);
|
||||
assert.equal(row(t.db).offSince, '2026-01-01T00:00:00.000Z');
|
||||
t.close();
|
||||
});
|
||||
|
||||
test('POST …/off: ruft onSitesChanged, danach verwirft /a/e Ereignisse und w.js ist leer', async () => {
|
||||
const db = openDb(':memory:');
|
||||
const pub = createPublicServer({ config: {}, db });
|
||||
const adm = createAdminServer({ config: { adminToken: 't' }, db, onSitesChanged: () => pub.invalidateSites() });
|
||||
await Promise.all([pub, adm].map((x) => new Promise((r) => x.listen(0, '127.0.0.1', r))));
|
||||
const pb = `http://127.0.0.1:${pub.address().port}`;
|
||||
const ab = `http://127.0.0.1:${adm.address().port}`;
|
||||
await fetch(`${ab}/a/admin/sites/${KEY}`, { method: 'PUT', headers: { Authorization: 'Bearer t', 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ projectId: 'p', domains: ['abc.project.webklar.com'], active: true }) });
|
||||
const ev = () => fetch(`${pb}/a/e`, { method: 'POST', headers: { 'Content-Type': 'text/plain', Origin: 'https://abc.project.webklar.com', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0) Chrome/120' },
|
||||
body: JSON.stringify({ t: 'pv', site: KEY, path: '/', vp: 'd' }) });
|
||||
await ev();
|
||||
const before = db.prepare('SELECT COUNT(*) c FROM events_raw').get().c;
|
||||
assert.equal(before, 1);
|
||||
assert.equal((await fetch(`${ab}/a/admin/sites/${KEY}/off`, { method: 'POST', headers: { Authorization: 'Bearer t' } })).status, 200);
|
||||
await ev();
|
||||
assert.equal(db.prepare('SELECT COUNT(*) c FROM events_raw').get().c, before);
|
||||
assert.equal((await (await fetch(`${pb}/a/w.js?s=${KEY}`)).text()).length < 200, true);
|
||||
pub.close(); adm.close();
|
||||
});
|
||||
@@ -0,0 +1,393 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { openDb } from '../src/db.js';
|
||||
import { runNightly, nextRunAt, dayOf, scheduleNightly } from '../src/jobs.js';
|
||||
import { createAdminServer } from '../src/server.js';
|
||||
|
||||
const KEY = 'abcdef0123456789abcd';
|
||||
const OTHER = 'zzzzzzzz0123456789zz';
|
||||
const NOW = Date.UTC(2026, 9, 10, 10, 0, 0); // 10.10.2026 12:00 Berlin
|
||||
const H = 3600 * 1000;
|
||||
const D = 24 * H;
|
||||
|
||||
function mk() {
|
||||
const db = openDb(':memory:');
|
||||
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)').run(KEY, 'p', '["a.de"]');
|
||||
const ev = (o) => db.prepare(`INSERT INTO events_raw (site, ts, type, path, vp, ref, country, device, visitor, sec, scroll, kind, host)
|
||||
VALUES (@site,@ts,@type,@path,@vp,@ref,@country,@device,@visitor,@sec,@scroll,@kind,@host)`)
|
||||
.run({ site: KEY, path: null, vp: null, ref: null, country: null, device: null, visitor: null, sec: null, scroll: null, kind: null, host: null, ...o });
|
||||
return { db, ev };
|
||||
}
|
||||
const daily = (db, site = KEY) => db.prepare('SELECT day, dim, key, count FROM daily WHERE site = ? ORDER BY day, dim, key').all(site);
|
||||
const get = (db, day, dim, key = '') => db.prepare('SELECT count FROM daily WHERE site=? AND day=? AND dim=? AND key=?').get(KEY, day, dim, key)?.count;
|
||||
|
||||
test('Verdichtung: Dimensionen, nur Tage < heute, idempotent', () => {
|
||||
const { db, ev } = mk();
|
||||
const t = NOW - D; // 9.10.
|
||||
ev({ ts: t, type: 'pv', path: '/', vp: 'd', ref: 'google.com', country: 'DE', device: 'd' });
|
||||
ev({ ts: t + 1, type: 'pv', path: '/', vp: 'm', ref: null, country: 'DE', device: 'm' });
|
||||
ev({ ts: t + 2, type: 'pv', path: '/x', vp: 'd', ref: 'google.com', country: 'AT', device: 'd' });
|
||||
ev({ ts: t + 3, type: 'en', path: '/', vp: 'd', visitor: 'v1', sec: 10, scroll: 50 });
|
||||
ev({ ts: t + 4, type: 'en', path: '/x', vp: 'd', visitor: 'v1', sec: 20, scroll: 70 });
|
||||
ev({ ts: t + 5, type: 'ct', path: '/', visitor: 'v2', kind: 'tel' });
|
||||
ev({ ts: t + 6, type: 'ct', path: '/', visitor: 'v2', kind: 'out', host: 'x.de' });
|
||||
ev({ ts: t + 7, type: 'bn', kind: 'shown' });
|
||||
ev({ ts: t + 8, type: 'bn', kind: 'accept' });
|
||||
ev({ ts: NOW, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // heute: unberührt
|
||||
runNightly(db, { now: NOW });
|
||||
const d = '2026-10-09';
|
||||
assert.equal(get(db, d, 'pv_total'), 3);
|
||||
assert.equal(get(db, d, 'pv_path', '/'), 2);
|
||||
assert.equal(get(db, d, 'pv_path', '/x'), 1);
|
||||
assert.equal(get(db, d, 'pv_ref', 'google.com'), 2);
|
||||
assert.equal(get(db, d, 'pv_ref', ''), 1);
|
||||
assert.equal(get(db, d, 'pv_country', 'DE'), 2);
|
||||
assert.equal(get(db, d, 'pv_device', 'm'), 1);
|
||||
assert.equal(get(db, d, 'visitors'), 2); // v1, v2
|
||||
assert.equal(get(db, d, 'en_count'), 2);
|
||||
assert.equal(get(db, d, 'en_sec_sum'), 30);
|
||||
assert.equal(get(db, d, 'scroll_sum'), 120);
|
||||
assert.equal(get(db, d, 'ct_kind', 'tel'), 1);
|
||||
assert.equal(get(db, d, 'ct_kind', 'out'), 1);
|
||||
assert.equal(get(db, d, 'bn_shown'), 1);
|
||||
assert.equal(get(db, d, 'bn_accept'), 1);
|
||||
assert.equal(get(db, d, 'bn_reject'), undefined);
|
||||
assert.equal(db.prepare("SELECT COUNT(*) c FROM daily WHERE day = '2026-10-10'").get().c, 0);
|
||||
const before = daily(db);
|
||||
runNightly(db, { now: NOW });
|
||||
runNightly(db, { now: NOW + H });
|
||||
assert.deepEqual(daily(db), before);
|
||||
});
|
||||
|
||||
test('DST: 23:30 UTC im Sommer gehört zum nächsten Berlin-Tag', () => {
|
||||
const { db, ev } = mk();
|
||||
ev({ ts: Date.UTC(2026, 6, 14, 23, 30), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // 15.7. 01:30 Berlin
|
||||
ev({ ts: Date.UTC(2026, 6, 14, 21, 59), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // 14.7. 23:59 Berlin
|
||||
assert.equal(dayOf(Date.UTC(2026, 6, 14, 23, 30)), '2026-07-15');
|
||||
runNightly(db, { now: Date.UTC(2026, 6, 16, 10) });
|
||||
assert.equal(get(db, '2026-07-14', 'pv_total'), 1);
|
||||
assert.equal(get(db, '2026-07-15', 'pv_total'), 1);
|
||||
// Winter: 23:30 UTC = 00:30 Berlin am Folgetag
|
||||
assert.equal(dayOf(Date.UTC(2026, 0, 14, 23, 30)), '2026-01-15');
|
||||
});
|
||||
|
||||
test('Aufbewahrung: Rohdaten und Klicks > 30 Tage weg, vorher verdichtet', () => {
|
||||
const { db, ev } = mk();
|
||||
ev({ ts: NOW - 31 * D, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
ev({ ts: NOW - 29 * D, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
const ins = db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,0.5,0.5)');
|
||||
ins.run(KEY, NOW - 31 * D, '/', 'd'); ins.run(KEY, NOW - 29 * D, '/', 'd');
|
||||
runNightly(db, { now: NOW });
|
||||
assert.equal(db.prepare('SELECT COUNT(*) c FROM events_raw').get().c, 1);
|
||||
assert.equal(db.prepare('SELECT COUNT(*) c FROM clicks').get().c, 1);
|
||||
assert.equal(get(db, dayOf(NOW - 31 * D), 'pv_total'), 1); // in daily erhalten
|
||||
});
|
||||
|
||||
test('Website mit offSince > 60 Tage wird komplett gelöscht', () => {
|
||||
const { db, ev } = mk();
|
||||
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active, offSince) VALUES (?,?,?,0,?)')
|
||||
.run(OTHER, 'q', '["b.de"]', new Date(NOW - 61 * D).toISOString());
|
||||
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active, offSince) VALUES (?,?,?,0,?)')
|
||||
.run('young0123456789abcdef', 'r', '["c.de"]', new Date(NOW - 59 * D).toISOString());
|
||||
ev({ site: OTHER, ts: NOW - D, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
ev({ ts: NOW - D, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,0.1,0.1)').run(OTHER, NOW - D, '/', 'd');
|
||||
runNightly(db, { now: NOW });
|
||||
for (const t of ['sites', 'events_raw', 'clicks', 'daily', 'agg_state']) {
|
||||
const col = t === 'sites' ? 'siteKey' : 'site';
|
||||
assert.equal(db.prepare(`SELECT COUNT(*) c FROM ${t} WHERE ${col} = ?`).get(OTHER).c, 0, t);
|
||||
}
|
||||
assert.equal(db.prepare('SELECT COUNT(*) c FROM sites').get().c, 2); // KEY + junge
|
||||
assert.ok(get(db, dayOf(NOW - D), 'pv_total'));
|
||||
});
|
||||
|
||||
test('nextRunAt: 02:30 Berlin (Sommer/Winter)', () => {
|
||||
assert.equal(new Date(nextRunAt(Date.UTC(2026, 9, 1, 10))).toISOString(), '2026-10-02T00:30:00.000Z');
|
||||
assert.equal(new Date(nextRunAt(Date.UTC(2026, 9, 1, 0, 0))).toISOString(), '2026-10-01T00:30:00.000Z');
|
||||
assert.equal(new Date(nextRunAt(Date.UTC(2026, 0, 1, 12))).toISOString(), '2026-01-02T01:30:00.000Z');
|
||||
});
|
||||
|
||||
async function admin(db, now = () => NOW) {
|
||||
const server = createAdminServer({ config: { adminToken: 'tok' }, db, now });
|
||||
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
const get = (p, auth = 'Bearer tok') => fetch(base + p, { headers: auth ? { Authorization: auth } : {} });
|
||||
return { get, close: () => server.close() };
|
||||
}
|
||||
|
||||
test('Stats-Endpunkt: abgeschlossene Tage bis gestern, heute getrennt in today, Vergangenheit aus daily, consentRate', async () => {
|
||||
const { db, ev } = mk();
|
||||
const y = NOW - D;
|
||||
for (let i = 0; i < 3; i++) ev({ ts: y + i, type: 'pv', path: '/a', vp: 'd', ref: 'g.de', country: 'DE', device: 'd' });
|
||||
ev({ ts: y + 10, type: 'en', path: '/a', vp: 'd', visitor: 'v1', sec: 5, scroll: 5 });
|
||||
ev({ ts: y + 11, type: 'bn', kind: 'accept' });
|
||||
ev({ ts: y + 12, type: 'ct', path: '/a', visitor: 'v1', kind: 'tel' });
|
||||
runNightly(db, { now: NOW }); // gestern -> daily
|
||||
ev({ ts: NOW - H, type: 'pv', path: '/b', vp: 'm', country: 'AT', device: 'm' }); // heute live
|
||||
ev({ ts: NOW - H, type: 'ct', path: '/b', visitor: 'v9', kind: 'mail' });
|
||||
ev({ ts: NOW - H, type: 'bn', kind: 'accept' });
|
||||
ev({ ts: NOW - H, type: 'bn', kind: 'reject' });
|
||||
ev({ ts: NOW - H, type: 'bn', kind: 'reject' });
|
||||
ev({ ts: NOW - H, type: 'bn', kind: 'shown' });
|
||||
const a = await admin(db);
|
||||
const r = await a.get(`/a/admin/stats/${KEY}?range=7`);
|
||||
assert.equal(r.status, 200);
|
||||
const j = await r.json();
|
||||
assert.deepEqual(Object.keys(j).sort(), ['banner', 'contacts', 'countries', 'devices', 'firstDay', 'from', 'pages', 'previous', 'range', 'refs', 'series', 'to', 'today', 'totals']);
|
||||
// Der Zeitraum sind die 7 abgeschlossenen Tage bis gestern (3.–9.10.); heute (10.10.) steht nur in `today`
|
||||
assert.equal(j.series.length, 7);
|
||||
assert.equal(j.series[0].day, '2026-10-03');
|
||||
assert.equal(j.series[6].day, '2026-10-09');
|
||||
assert.deepEqual(j.series.map((s) => s.pv), [0, 0, 0, 0, 0, 0, 3]);
|
||||
assert.deepEqual(j.totals, { pv: 3, visitors: 1, contacts: 1, consentRate: 1, engaged: 1, avgSec: 5, avgScroll: 5 });
|
||||
assert.deepEqual(j.series[6], { day: '2026-10-09', pv: 3, visitors: 1, contacts: 1, avgSec: 5 });
|
||||
assert.deepEqual(j.today, { day: '2026-10-10', pv: 1, visitors: 1, contacts: 1 });
|
||||
assert.equal(j.from, '2026-10-03'); assert.equal(j.to, '2026-10-09'); assert.equal(j.range, 7);
|
||||
assert.deepEqual(j.pages, [{ key: '/a', count: 3 }]);
|
||||
assert.deepEqual(j.refs[0], { key: 'g.de', count: 3 });
|
||||
assert.deepEqual(j.countries, [{ key: 'DE', count: 3 }]);
|
||||
assert.deepEqual(j.devices, [{ key: 'd', count: 3 }]);
|
||||
assert.deepEqual(j.contacts, [{ key: 'tel', count: 1 }]);
|
||||
assert.deepEqual(j.banner, { shown: 0, accept: 1, reject: 0 });
|
||||
// range 90: 90 Tage
|
||||
const j90 = await (await a.get(`/a/admin/stats/${KEY}?range=90`)).json();
|
||||
assert.equal(j90.series.length, 90);
|
||||
assert.equal(j90.series[89].day, '2026-10-09');
|
||||
assert.equal(j90.totals.pv, 3);
|
||||
assert.deepEqual(j90.today, j.today);
|
||||
a.close();
|
||||
});
|
||||
|
||||
test('Stats: today = heutiger Berliner Tag bis jetzt, auch ohne Ereignisse; Mitternacht in Berlin trennt gestern und heute', async () => {
|
||||
const { db, ev } = mk();
|
||||
const a = await admin(db);
|
||||
assert.deepEqual((await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json()).today, { day: '2026-10-10', pv: 0, visitors: 0, contacts: 0 });
|
||||
a.close();
|
||||
// 11.10. 00:10 Berlin (= 10.10. 22:10 UTC, Sommerzeit): 23:00 Berlin am 10.10. gehört zu gestern, 00:05 Berlin am 11.10. zu heute
|
||||
const LATE = Date.UTC(2026, 9, 10, 22, 10);
|
||||
ev({ ts: Date.UTC(2026, 9, 10, 21, 0), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
ev({ ts: Date.UTC(2026, 9, 10, 22, 5), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
ev({ ts: Date.UTC(2026, 9, 10, 22, 6), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
ev({ ts: Date.UTC(2026, 9, 10, 22, 7), type: 'ct', path: '/', visitor: 'v1', kind: 'wa' });
|
||||
ev({ ts: Date.UTC(2026, 9, 10, 23, 0), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // noch in der Zukunft: zählt nicht
|
||||
const b = await admin(db, () => LATE);
|
||||
const j = await (await b.get(`/a/admin/stats/${KEY}?range=7`)).json();
|
||||
assert.equal(j.to, '2026-10-10');
|
||||
assert.equal(j.series[6].day, '2026-10-10'); assert.equal(j.series[6].pv, 1);
|
||||
assert.equal(j.totals.pv, 1);
|
||||
assert.deepEqual(j.today, { day: '2026-10-11', pv: 2, visitors: 1, contacts: 1 });
|
||||
assert.equal(j.contacts.length, 0); // die Kontakt-Aktion von 00:07 ist heute
|
||||
b.close();
|
||||
});
|
||||
|
||||
test('Stats: gestern noch nicht verdichtet wird live gezählt, nie doppelt', async () => {
|
||||
const { db, ev } = mk();
|
||||
ev({ ts: NOW - D, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
ev({ ts: NOW - H, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // heute: nur in today
|
||||
const a = await admin(db);
|
||||
const stats = async () => (await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json());
|
||||
assert.equal((await stats()).totals.pv, 1);
|
||||
runNightly(db, { now: NOW });
|
||||
const j = await stats();
|
||||
assert.equal(j.totals.pv, 1);
|
||||
assert.equal(j.today.pv, 1);
|
||||
a.close();
|
||||
});
|
||||
|
||||
test('Stats: Vorzeitraum = die range Kalendertage vor dem aktuellen Zeitraum, complete nach firstDay', async () => {
|
||||
const { db, ev } = mk();
|
||||
// aktueller Zeitraum zu range 7 am 10.10.: 03.–09.10.; Vorzeitraum: 26.09.–02.10.
|
||||
ev({ ts: Date.UTC(2026, 8, 30, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
ev({ ts: Date.UTC(2026, 9, 1, 10), type: 'en', path: '/', vp: 'd', visitor: 'v1', sec: 20, scroll: 40 });
|
||||
ev({ ts: Date.UTC(2026, 9, 2, 10), type: 'ct', path: '/', visitor: 'v2', kind: 'tel' });
|
||||
const a = await admin(db);
|
||||
const get = async () => (await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json());
|
||||
let j = await get();
|
||||
assert.equal(j.firstDay, '2026-09-30');
|
||||
assert.equal(j.previous.complete, false);
|
||||
// 27.09. liegt noch einen Tag nach dem Beginn des Vorzeitraums (26.09.): weiter unvollständig
|
||||
ev({ ts: Date.UTC(2026, 8, 27, 10), type: 'pv', path: '/x', vp: 'd', country: 'DE', device: 'd' });
|
||||
j = await get();
|
||||
assert.equal(j.firstDay, '2026-09-27');
|
||||
assert.equal(j.previous.complete, false);
|
||||
// genau am ersten Tag des Vorzeitraums: vollständig
|
||||
ev({ ts: Date.UTC(2026, 8, 26, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
j = await get();
|
||||
assert.equal(j.firstDay, '2026-09-26');
|
||||
assert.equal(j.previous.complete, true);
|
||||
// 25.09. liegt vor dem Vorzeitraum und darf nicht mitzählen
|
||||
ev({ ts: Date.UTC(2026, 8, 25, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
j = await get();
|
||||
assert.equal(j.firstDay, '2026-09-25');
|
||||
assert.equal(j.previous.complete, true);
|
||||
assert.equal(j.previous.from, '2026-09-26'); assert.equal(j.previous.to, '2026-10-02');
|
||||
assert.equal(j.previous.series.length, 7); assert.equal(j.previous.series[4].day, '2026-09-30');
|
||||
assert.equal(j.previous.totals.pv, 3); // 26.09., 27.09., 30.09.
|
||||
assert.equal(j.previous.totals.engaged, 1);
|
||||
assert.equal(j.previous.totals.avgSec, 20);
|
||||
assert.equal(j.previous.totals.contacts, 1);
|
||||
assert.deepEqual(j.previous.series[5], { day: '2026-10-01', pv: 0, visitors: 1, contacts: 0, avgSec: 20 });
|
||||
assert.deepEqual(j.previous.series[6], { day: '2026-10-02', pv: 0, visitors: 1, contacts: 1, avgSec: null });
|
||||
assert.deepEqual(j.previous.contacts, [{ key: 'tel', count: 1 }]);
|
||||
assert.equal(j.totals.pv, 0); // aktueller Zeitraum leer
|
||||
assert.equal(j.totals.avgSec, null);
|
||||
a.close();
|
||||
});
|
||||
|
||||
test('Stats: firstDay aus daily und Rohdaten, leer ohne Daten', async () => {
|
||||
const { db, ev } = mk();
|
||||
const a = await admin(db);
|
||||
const first = async () => (await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json());
|
||||
let j = await first();
|
||||
assert.equal(j.firstDay, '');
|
||||
assert.equal(j.previous.complete, false);
|
||||
// nur Rohdaten (heute)
|
||||
ev({ ts: NOW - H, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
assert.equal((await first()).firstDay, '2026-10-10');
|
||||
// 40 Tage alt: nach dem Nachtlauf nur noch in daily (Rohdaten > 30 Tage gelöscht)
|
||||
ev({ ts: NOW - 40 * D, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
assert.equal((await first()).firstDay, '2026-08-31');
|
||||
runNightly(db, { now: NOW });
|
||||
assert.equal(db.prepare('SELECT COUNT(*) c FROM events_raw WHERE ts < ?').get(NOW - 31 * D).c, 0);
|
||||
j = await first();
|
||||
assert.equal(j.firstDay, '2026-08-31');
|
||||
assert.equal(j.previous.complete, true);
|
||||
a.close();
|
||||
});
|
||||
|
||||
test('Stats: avgSec/avgScroll gerundet, null ohne en; Tageswert avgSec', async () => {
|
||||
const { db, ev } = mk();
|
||||
const t = NOW - D; // 9.10.
|
||||
ev({ ts: t, type: 'en', path: '/', vp: 'd', visitor: 'v1', sec: 30, scroll: 50 });
|
||||
ev({ ts: t + 1, type: 'en', path: '/a', vp: 'd', visitor: 'v2', sec: 91, scroll: 100 });
|
||||
const a = await admin(db);
|
||||
const j = await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json();
|
||||
assert.equal(j.totals.engaged, 2);
|
||||
assert.equal(j.totals.avgSec, 61); // 60,5 gerundet
|
||||
assert.equal(j.totals.avgScroll, 75);
|
||||
assert.equal(j.series[6].avgSec, 61); // 9.10. ist der letzte abgeschlossene Tag
|
||||
assert.equal(j.series[5].avgSec, null);
|
||||
// ohne en: beide null, engaged 0
|
||||
const leer = await (await a.get(`/a/admin/stats/${KEY}?range=30`)).json();
|
||||
assert.equal(leer.previous.totals.engaged, 0);
|
||||
assert.equal(leer.previous.totals.avgSec, null);
|
||||
assert.equal(leer.previous.totals.avgScroll, null);
|
||||
a.close();
|
||||
});
|
||||
|
||||
test('Stats: Vorzeitraum über die Zeitumstellung lückenlos und ohne Überlappung', async () => {
|
||||
// range 30 um 23:30 Berlin: Zeitumstellung am 25.10. liegt im Zeitraum. Spätabends wäre now - 30 * 24 h
|
||||
// schon der 12.10. (00:30 Berlin) und ein naives Fenster würde sich überlappen; mittags fiele das nicht auf.
|
||||
const NOV = Date.UTC(2026, 10, 10, 22, 30);
|
||||
const { db, ev } = mk();
|
||||
ev({ ts: Date.UTC(2026, 9, 10, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // 10.10. mittags: Vorzeitraum
|
||||
ev({ ts: Date.UTC(2026, 9, 11, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // 11.10. mittags: aktueller Zeitraum
|
||||
ev({ ts: Date.UTC(2026, 10, 10, 12), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // 10.11. mittags: heute
|
||||
const a = await admin(db, () => NOV);
|
||||
const j = await (await a.get(`/a/admin/stats/${KEY}?range=30`)).json();
|
||||
assert.equal(j.series[0].day, '2026-10-11');
|
||||
assert.equal(j.series[29].day, '2026-11-09');
|
||||
assert.equal(j.previous.series[29].day, '2026-10-10');
|
||||
assert.equal(j.previous.series[0].day, '2026-09-11');
|
||||
assert.equal(j.from, '2026-10-11'); assert.equal(j.to, '2026-11-09');
|
||||
assert.equal(j.previous.from, '2026-09-11'); assert.equal(j.previous.to, '2026-10-10');
|
||||
const all = [...j.series, ...j.previous.series].map((s) => s.day);
|
||||
assert.equal(all.length, 60);
|
||||
assert.equal(new Set(all).size, 60);
|
||||
assert.equal(j.previous.totals.pv, 1); assert.equal(j.previous.series[29].pv, 1);
|
||||
assert.equal(j.totals.pv, 1); assert.equal(j.series[0].pv, 1);
|
||||
assert.equal(j.today.day, '2026-11-10'); assert.equal(j.today.pv, 1);
|
||||
a.close();
|
||||
});
|
||||
|
||||
test('Stats: Vorzeitraum aus daily und live, nie doppelt', async () => {
|
||||
const { db, ev } = mk();
|
||||
ev({ ts: Date.UTC(2026, 9, 1, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
|
||||
ev({ ts: Date.UTC(2026, 9, 1, 11), type: 'ct', path: '/', visitor: 'v1', kind: 'mail' });
|
||||
const a = await admin(db);
|
||||
const get = async () => (await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json()).previous;
|
||||
const live = await get(); // noch nicht verdichtet: live aus den Rohdaten
|
||||
assert.equal(live.totals.pv, 1);
|
||||
runNightly(db, { now: NOW }); // jetzt aus daily
|
||||
const verdichtet = await get();
|
||||
assert.equal(verdichtet.totals.pv, 1);
|
||||
assert.deepEqual(verdichtet, live);
|
||||
runNightly(db, { now: NOW + H });
|
||||
assert.deepEqual(await get(), live);
|
||||
a.close();
|
||||
});
|
||||
|
||||
test('Stats: Top 20 absteigend, consentRate 0 ohne Entscheidungen, Fehlerfälle', async () => {
|
||||
const { db, ev } = mk();
|
||||
for (let p = 0; p < 25; p++) for (let i = 0; i <= p; i++) ev({ ts: NOW - D - 1000, type: 'pv', path: `/p${p}`, vp: 'd', country: 'DE', device: 'd' });
|
||||
const a = await admin(db);
|
||||
const j = await (await a.get(`/a/admin/stats/${KEY}?range=30`)).json();
|
||||
assert.equal(j.pages.length, 20);
|
||||
assert.equal(j.pages[0].key, '/p24');
|
||||
assert.ok(j.pages.every((e, i, arr) => !i || arr[i - 1].count >= e.count));
|
||||
assert.equal(j.totals.consentRate, 0);
|
||||
assert.equal((await a.get(`/a/admin/stats/${KEY}?range=5`)).status, 400);
|
||||
assert.equal((await a.get(`/a/admin/stats/${KEY}?range=abc`)).status, 400);
|
||||
assert.equal((await a.get(`/a/admin/stats/${OTHER}?range=7`)).status, 404);
|
||||
assert.equal((await a.get(`/a/admin/stats/${KEY}?range=7`, null)).status, 401);
|
||||
assert.equal((await a.get(`/a/admin/stats/${KEY}?range=7`, 'Bearer nope')).status, 401);
|
||||
a.close();
|
||||
});
|
||||
|
||||
test('scheduleNightly: früh feuernder Timer läuft nicht vorzeitig und nie doppelt für dasselbe 02:30', () => {
|
||||
let t = Date.UTC(2026, 9, 10, 10, 0, 0);
|
||||
const pending = [];
|
||||
const timers = { setTimeout: (fn, ms) => { const h = { fn, at: t + ms }; pending.push(h); return h; }, clearTimeout: (h) => { const i = pending.indexOf(h); if (i >= 0) pending.splice(i, 1); } };
|
||||
const runs = [];
|
||||
const quiet = { log() {}, error() {} };
|
||||
const s = scheduleNightly({}, { startDelayMs: 1000, log: quiet, now: () => t, timers, job: (db, o) => { runs.push(o.now); return { aggregated: 0, sitesDeleted: 0 }; } });
|
||||
const fire = (clock) => { const h = pending.shift(); t = clock ?? h.at; h.fn(); };
|
||||
fire(); // Startlauf
|
||||
assert.equal(runs.length, 1);
|
||||
const target = nextRunAt(t);
|
||||
assert.equal(pending.length, 1);
|
||||
assert.equal(pending[0].at, target);
|
||||
fire(target - 500); // Timer feuert 500 ms zu früh
|
||||
assert.equal(runs.length, 1, 'kein vorzeitiger Lauf');
|
||||
assert.equal(pending.length, 1);
|
||||
assert.equal(pending[0].at, target, 'neu auf das Ziel gesetzt');
|
||||
fire(target); // pünktlich
|
||||
assert.equal(runs.length, 2);
|
||||
assert.equal(pending[0].at, nextRunAt(target + 1000));
|
||||
assert.ok(pending[0].at > target + 20 * 3600000, 'nächster Lauf am Folgetag');
|
||||
fire(pending[0].at - 1); // wieder zu früh
|
||||
assert.equal(runs.length, 2);
|
||||
s.stop();
|
||||
assert.equal(pending.length, 0);
|
||||
});
|
||||
|
||||
test('T2: pv ohne Einwilligung + cx: pv_total 1, pv_ref nur {google.com:1}; cx zählt nie pv_total', () => {
|
||||
const { db, ev } = mk();
|
||||
const t = NOW - D;
|
||||
ev({ ts: t, type: 'pv', path: '/', vp: null, ref: null, country: 'DE', device: 'd' });
|
||||
ev({ ts: t, type: 'cx', vp: 'd', ref: 'google.com' });
|
||||
ev({ ts: t, type: 'cx', vp: 'd', ref: null });
|
||||
ev({ ts: t, type: 'pv', path: '/', vp: 'd', ref: null, country: 'DE', device: 'd' });
|
||||
runNightly(db, { now: NOW });
|
||||
const d = '2026-10-09';
|
||||
assert.equal(get(db, d, 'pv_total'), 2);
|
||||
assert.equal(get(db, d, 'pv_ref', 'google.com'), 1);
|
||||
assert.equal(get(db, d, 'pv_ref', ''), 2); // pv mit vp und cx ohne ref
|
||||
const refs = db.prepare("SELECT key FROM daily WHERE site=? AND dim='pv_ref' ORDER BY key").all(KEY).map((r) => r.key);
|
||||
assert.deepEqual(refs, ['', 'google.com']);
|
||||
});
|
||||
|
||||
test('T2: Stats-Endpunkt refs live und aus daily, nie doppelt', async () => {
|
||||
const { db, ev } = mk();
|
||||
ev({ ts: NOW - D, type: 'pv', path: '/', vp: null, country: 'DE', device: 'd' });
|
||||
ev({ ts: NOW - D, type: 'cx', vp: 'd', ref: 'google.com' });
|
||||
const a = await admin(db);
|
||||
const live = await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json();
|
||||
assert.equal(live.totals.pv, 1);
|
||||
assert.deepEqual(live.refs, [{ key: 'google.com', count: 1 }]);
|
||||
runNightly(db, { now: NOW });
|
||||
const agg = await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json();
|
||||
assert.equal(agg.totals.pv, 1);
|
||||
assert.deepEqual(agg.refs, [{ key: 'google.com', count: 1 }]);
|
||||
a.close();
|
||||
});
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
21d406e6836a7d0c1afdb99dc259e4174fb3fe499afcbdac30c8a45a1aa8d605
|
||||
Binary file not shown.
@@ -0,0 +1,63 @@
|
||||
# News-Pinnwand auf der Kundenwebsite einbauen
|
||||
|
||||
Anleitung für das WEBklar-Team. Die Pinnwand zeigt die Beiträge, die der Kunde im Portal (Tab „Funktionen“) schreibt. Den `siteKey` findest Du im Portal-Admin beim Projekt (16 Zeichen, a–z und 2–7).
|
||||
|
||||
## 1. Die Stelle auf der Seite
|
||||
|
||||
```html
|
||||
<div data-webklar-news="SITEKEY" data-api="https://project.webklar.com">
|
||||
<!-- statischer Leerzustand, siehe unten -->
|
||||
</div>
|
||||
```
|
||||
|
||||
- `data-webklar-news`: der siteKey des Projekts (Pflicht).
|
||||
- `data-api`: Herkunft des Portals. Ohne Angabe nimmt das Skript die Herkunft aus der Adresse des Skripts. Das klappt nur, wenn es als normales `<script src>` geladen wird. Bei Modul-Import, Tag-Manager oder nachgeladenem Skript ist `data-api` Pflicht.
|
||||
- `data-signature` (optional): Unterschrift unter jedem Zettel, z. B. `data-signature="Opa Martin"`. Ohne Attribut gibt es keine Unterschrift.
|
||||
|
||||
## 2. Das Skript (einmal pro Seite, vor `</body>`)
|
||||
|
||||
```html
|
||||
<script src="https://project.webklar.com/embed/news.js" defer></script>
|
||||
```
|
||||
|
||||
Keine Abhängigkeiten, keine Cookies, kein Browser-Speicher. Kundentexte werden nur als Text eingesetzt, nie als HTML. Das Skript wird 5 Minuten gecacht und darf von jeder Domain geladen werden.
|
||||
|
||||
## 3. Statischer Leerzustand
|
||||
|
||||
Alles, was im `div` steht, gehört der Kundenwebsite. Das Skript ersetzt es nur, wenn die API mindestens einen Beitrag liefert. Bei 0 Beiträgen, Fehler, ausgeschaltetem JavaScript oder unbekanntem Schlüssel bleibt es stehen. Beispiel:
|
||||
|
||||
```html
|
||||
<div data-webklar-news="SITEKEY" data-api="https://project.webklar.com">
|
||||
<h3>Hier hängt bald was Neues.</h3>
|
||||
<p>Neuigkeiten findest Du bald an dieser Pinnwand.</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
Gestalte den Leerzustand im Stil der Kundenseite. Das Skript fasst ihn nicht an.
|
||||
|
||||
## 4. Aussehen anpassen (CSS-Variablen)
|
||||
|
||||
Setze sie auf dem `div` oder in der Seiten-CSS, z. B. `[data-webklar-news]{ --wkn-frame:#2f5d50; }`.
|
||||
|
||||
| Variable | Standard |
|
||||
| --- | --- |
|
||||
| `--wkn-board` (Korkfläche) | `#C08A52` |
|
||||
| `--wkn-frame` (Holzrahmen) | `#A8733F` |
|
||||
| `--wkn-font-display` (Überschriften) | `"Fredoka", system-ui, sans-serif` |
|
||||
| `--wkn-font-body` (Text) | `"Nunito", system-ui, sans-serif` |
|
||||
| `--wkn-paper-sun` | `#FFF6D6` |
|
||||
| `--wkn-paper-sky` | `#DDEEFB` |
|
||||
| `--wkn-paper-coral` | `#FFE1DA` |
|
||||
| `--wkn-paper-white` | `#fff` |
|
||||
|
||||
Die Schriften lädt das Skript nicht selbst. Sind sie auf der Kundenseite nicht eingebunden, greift die Systemschrift. Setze dann die Variablen auf die Schriften der Kundenseite.
|
||||
|
||||
Das Layout richtet sich nach der Breite des `div` (Container Query, braucht aktuelle Browser), nicht nach dem Bildschirm: ab 701 px Breite ein Korkbrett mit drei Spalten, darunter eine Wischreihe mit Punkten. Sechs Beiträge pro Seite, „Ältere Neuigkeiten ansehen“ lädt die nächsten nach. Alle Klassen beginnen mit `wkn-`, die Styles stehen einmalig in `<style id="wkn-style">`.
|
||||
|
||||
## 5. Datenschutzerklärung des Kunden
|
||||
|
||||
Das Skript lädt Beiträge und Bilder von `project.webklar.com`. Dabei sieht der Server die IP-Adresse des Besuchers. Die Datenschutzerklärung des Kunden braucht deshalb den Absatz aus `docs/funktionen/website-datenschutz-absatz.md`. Bau die Pinnwand erst ein, wenn der Absatz dort steht.
|
||||
|
||||
## 6. Testen
|
||||
|
||||
`/embed/news-demo.html` auf dem Portal ist eine nicht verlinkte Testseite. Automatische Prüfung: `node test/ui/embed.ui.mjs`.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Mustertext: Datenschutzerklärung der Kundenwebsite (Webshop)
|
||||
|
||||
**Muster – keine Rechtsberatung.** Der Absatz gehört in die Datenschutzerklärung jeder Kundenwebsite, auf der der Webshop läuft. Der Händler ist Verantwortlicher und verantwortet seine Datenschutzerklärung selbst; WEBklar liefert nur diesen Baustein (Leistungsbeschreibung Webshop Nr. 5 Abs. 10 und Nr. 6). Bau den Shop erst ein, wenn der Absatz in der Datenschutzerklärung steht.
|
||||
|
||||
Hinweise für das Team:
|
||||
|
||||
- Liegt die Datenschutzerklärung in der Website, die WEBklar betreut, fügen wir den Absatz beim Einbau ein (gehört zur Einrichtung). Liegt sie woanders, bekommt der Händler den Text im Chat und fügt ihn selbst ein.
|
||||
- Platzhalter in eckigen Klammern durch die Angaben des Händlers ersetzen. Nicht Zutreffendes streichen: Klarna nur, wenn der Händler Klarna in den Zahlungsarten eingeschaltet hat; den Satz zum Versanddienstleister nur, wenn er versendet; die Zeile zur Abholung nur bei Abholung.
|
||||
- Anrede und Ton an die übrige Erklärung der Website anpassen (meist Sie). Die Nummerierung der Überschrift der Website beachten.
|
||||
- Wenn der Händler im Impressum oder in der Erklärung einen Datenschutzbeauftragten nennt, dort dazuschreiben.
|
||||
- Die Löschfristen entsprechen dem AVV Webshop Nr. 9 und Abschnitt 6d der Datenschutzerklärung von WEBklar (project.webklar.com/datenschutz.html). Ändert sich dort etwas, hier mitändern. Die Frist für Server-Protokolle (14 Tage) steht in Abschnitt 3 dieser Erklärung.
|
||||
- Das Widget setzt keine Cookies. Der Warenkorb liegt in der `sessionStorage` des Tabs, das ist Absatz „Warenkorb im Browser“.
|
||||
- Im sichtbaren Text der Kundenwebsite die Wünsche des Händlers beachten (etwa keine Gedankenstriche).
|
||||
|
||||
---
|
||||
|
||||
## [Nummer]. Bestellungen in unserem Webshop
|
||||
|
||||
### Verantwortlicher und technischer Betrieb
|
||||
|
||||
Verantwortlich für die Verarbeitung Ihrer Daten bei einer Bestellung ist [Name des Händlers, Anschrift, E-Mail-Adresse], also der Verkäufer. Den Webshop betreibt technisch die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, in unserem Auftrag. Sie verarbeitet Ihre Daten nur nach unseren Weisungen und nicht für eigene Zwecke (Auftragsverarbeitung nach Art. 28 DSGVO). Der Server steht bei der Hetzner Online GmbH in Falkenstein (Deutschland); der Versand der E-Mails zu Ihrer Bestellung läuft über den E-Mail-Dienst der Hetzner Online GmbH in Nürnberg (Deutschland).
|
||||
|
||||
### Welche Daten wir bei einer Bestellung verarbeiten
|
||||
|
||||
Wenn Sie in unserem Webshop bestellen, verarbeiten wir die Artikel in Ihrem Warenkorb und Ihre Bestellung (Artikel, Preise, Versandart, Gesamtbetrag, Bestellnummer, Zeitpunkt), Ihren Namen, Ihre E-Mail-Adresse sowie Ihre Liefer- und Rechnungsanschrift, den Stand der Zahlung und den Stand Ihrer Bestellung (zum Beispiel bezahlt, versendet, abholbereit, erstattet). Name und Anschriften geben Sie auf der Zahlungsseite von Stripe ein (siehe unten); wir erhalten sie von dort, die Rechnungsanschrift nur, wenn Stripe sie für Ihre Zahlungsart abfragt. Eine Telefonnummer verlangen und speichern wir nicht. Die Verarbeitung ist nötig, um den Kaufvertrag mit Ihnen zu schließen und zu erfüllen (Art. 6 Abs. 1 lit. b DSGVO). Ohne diese Angaben können wir Ihre Bestellung nicht ausführen. Außerdem speichern wir in der noch offenen Bestellung einen Hashwert Ihrer IP-Adresse (nicht die IP-Adresse selbst; er wird mit einem geheimen Schlüssel des Servers berechnet, sodass sich daraus keine IP-Adresse ableiten lässt). Wir lassen damit je IP-Adresse höchstens drei offene, noch nicht bezahlte Bestellungen innerhalb von 30 Minuten zu und schützen den Webshop so vor automatisierten Massenbestellungen, die den Bestand blockieren. Das ist unser berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO); Sie können dieser Verarbeitung widersprechen (Art. 21 DSGVO). Wir verwenden den Hashwert nur dafür und löschen ihn, sobald Ihre Bestellung bezahlt oder storniert ist, spätestens mit der Löschung nicht bezahlter Bestellungen nach 30 Tagen.
|
||||
|
||||
### Zahlung über Stripe
|
||||
|
||||
Die Zahlung wickelt die Stripe Payments Europe, Limited, 1 Grand Canal Street Lower, Grand Canal Dock, Dublin, D02 H210, Irland („Stripe“) ab. Sie werden dazu auf die Zahlungsseite von Stripe weitergeleitet. Karten- und Kontodaten geben Sie nur dort ein; wir und der Betreiber des Webshops erhalten sie nicht. Stripe verarbeitet Ihre Daten teils im Rahmen der Zahlungsabwicklung für uns und teils als eigener Verantwortlicher, zum Beispiel zur Betrugsvorbeugung und zur Erfüllung gesetzlicher Pflichten; dafür gilt die Datenschutzerklärung von Stripe (stripe.com/de/privacy). Dabei können Daten an die Stripe, LLC in den USA übermittelt werden. Die Stripe, LLC ist nach dem EU-US Data Privacy Framework zertifiziert, für das ein Angemessenheitsbeschluss der EU-Kommission besteht. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO.
|
||||
|
||||
Welche Zahlungsarten angeboten werden, sehen Sie vor dem Kauf im Webshop. Zahlen Sie per SEPA-Lastschrift, gibt Stripe Ihre Kontodaten an die beteiligten Banken weiter und bucht den Betrag nach Ihrem Mandat ab. [Nur wenn Klarna eingeschaltet ist: Wählen Sie Klarna, übermittelt Stripe die zur Zahlung erforderlichen Daten an die Klarna Bank AB (publ), Sveavägen 46, 111 34 Stockholm, Schweden; es gelten die Datenschutzbestimmungen von Klarna, die Ihnen bei der Zahlung angezeigt werden.]
|
||||
|
||||
### Bestellbestätigung und weitere E-Mails
|
||||
|
||||
Zu Ihrer Bestellung senden wir Ihnen E-Mails an die angegebene E-Mail-Adresse: bei einer Zahlung, die noch aussteht (zum Beispiel SEPA-Lastschrift), zuerst eine Mail über den Eingang der Bestellung, Bestellbestätigung (mit den Vertragsbedingungen, der Widerrufsbelehrung und dem Muster-Widerrufsformular; es sind die Texte, die bei Ihrer Bestellung galten), Hinweise zum Versand [oder zur Abholbereitschaft], Bestätigungen zu einer Erstattung oder zu einem Widerruf und, wenn eine Zahlung nicht gelungen ist, einen Hinweis darauf. Das sind Mitteilungen zur Abwicklung Ihres Vertrags (Art. 6 Abs. 1 lit. b DSGVO), keine Werbung. Bis zum Versand liegt jede E-Mail in einer Warteschlange; nach dem Versand wird Empfängeradresse und Inhalt dort gelöscht, der Eintrag selbst nach sieben Tagen.
|
||||
|
||||
### Warenkorb im Browser
|
||||
|
||||
Ihren Warenkorb (Artikel und Mengen, keine persönlichen Daten) speichert der Webshop im Sitzungsspeicher (sessionStorage) Ihres Browsers, solange der Tab geöffnet ist. Das ist technisch erforderlich, damit der Warenkorb beim Durchklicken erhalten bleibt (§ 25 Abs. 2 Nr. 2 TDDDG); es werden keine Cookies gesetzt, und der Eintrag verschwindet, wenn Sie den Tab schließen. Beim Aufruf der Seite lädt Ihr Browser das Programm des Webshops und die Artikeldaten von der Adresse project.webklar.com. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem übertragen und für 14 Tage in Server-Protokollen gespeichert, um den sicheren und störungsfreien Betrieb zu gewährleisten (Art. 6 Abs. 1 lit. f DSGVO). Zum Schutz vor Missbrauch begrenzen wir die Zahl der Bestellungen und Widerrufe je IP-Adresse; die Zeitpunkte der Aufrufe werden nur im Arbeitsspeicher des Servers gehalten. Mit Ihrer Bestellung oder Ihrem Widerruf wird Ihre IP-Adresse nicht gespeichert.
|
||||
|
||||
### Widerruf über die Widerrufsfunktion
|
||||
|
||||
Über den Link „Vertrag widerrufen“ können Sie Ihren Vertrag ohne Anmeldung online widerrufen. Dabei verarbeiten wir Ihren Namen, Ihre E-Mail-Adresse, die Bestellnummer, Ihre Erklärung und den Zeitpunkt des Eingangs. Sie erhalten sofort eine Eingangsbestätigung mit Inhalt, Datum und Uhrzeit Ihrer Erklärung. Rechtsgrundlage ist Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit § 356a BGB sowie Art. 6 Abs. 1 lit. b DSGVO.
|
||||
|
||||
### Weitergabe
|
||||
|
||||
[Nur bei Versand: Zur Lieferung geben wir Ihren Namen und Ihre Lieferanschrift an das Versandunternehmen weiter, das wir beauftragen ([Name des Versanddienstleisters]).] Im Übrigen geben wir Ihre Daten nur weiter, soweit das für die Vertragsabwicklung nötig ist (Stripe und die genannten Zahlungsanbieter, Auftragsverarbeiter) oder wir gesetzlich dazu verpflichtet sind (zum Beispiel an das Finanzamt). Eine Weitergabe zu Werbezwecken findet nicht statt.
|
||||
|
||||
### Speicherdauer
|
||||
|
||||
Bestellungen, die nicht bezahlt oder die storniert wurden, löscht das Webshop-System nach 30 Tagen. Für bezahlte Bestellungen sind wir als Händler verpflichtet, Buchungsbelege und Geschäftsunterlagen nach Handels- und Steuerrecht aufzubewahren (in der Regel sechs bis zehn Jahre, § 257 HGB, § 147 AO; Art. 6 Abs. 1 lit. c DSGVO). Diese Unterlagen führen wir selbst und mit den Belegen von Stripe, nicht im Webshop-System. Im Webshop-System selbst werden Name, E-Mail-Adresse und Anschriften drei Jahre nach dem Ende des Jahres der Bestellung anonymisiert; die Bestellnummer, die Artikel und die Beträge bleiben ohne Personenbezug erhalten. Daten aus einem Widerruf werden ebenso behandelt wie die zugehörige Bestellung.
|
||||
|
||||
### Ihre Rechte
|
||||
|
||||
Sie haben das Recht auf Auskunft über Ihre Daten (Art. 15 DSGVO), auf Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung der Verarbeitung (Art. 18), Datenübertragbarkeit (Art. 20) und auf Widerspruch gegen Verarbeitungen, die auf Art. 6 Abs. 1 lit. f DSGVO beruhen (Art. 21). Wenden Sie sich dazu an uns unter [E-Mail-Adresse des Händlers]; wir leiten Ihre Anfrage, soweit nötig, an den technischen Betreiber des Webshops weiter. Außerdem können Sie sich bei einer Datenschutz-Aufsichtsbehörde beschweren, etwa bei der für uns zuständigen Behörde [zuständige Aufsichtsbehörde des Händlers].
|
||||
@@ -0,0 +1,98 @@
|
||||
# Webshop auf der Kundenwebsite einbauen
|
||||
|
||||
Anleitung für das WEBklar-Team. Das Widget lässt Besucher auf der Website des Kunden („Händler“) Waren bestellen und bezahlen. Artikel, Versand, Zahlungsarten und Rechtstexte pflegt der Händler im Kundenportal (Tab „Funktionen“, Kachel „Webshop“). Den `siteKey` (16 Zeichen, a–z und 2–7) findest Du im Einrichtungs-Ticket und im Ticketsystem (WOMS): Kunde → Reiter „Funktionen“, Zeile Webshop. Im Ticket stehen Snippet und Schritte schon fertig; dieses Dokument ist die Referenz dahinter. Ohne aktive Funktion, ohne verbundenes Stripe-Konto, ohne bestätigte Rechtstexte oder ohne lieferbaren Artikel ist nichts bestellbar: Das Widget zeigt dann „Der Shop ist gerade geschlossen“ (oder den Ausweichtext, siehe Abschnitt 4).
|
||||
|
||||
Der Webshop ist an den Vertrag über den Betrieb der Website gekoppelt (Leistungsbeschreibung Webshop Nr. 10), aber **nicht in der Software**: Das Portal liest keine Abo-Daten für den Shop, schaltet ihn nicht von selbst ab, und bestellt werden kann er auch ohne Abo. Das Abschalten zum Vertragsende machen wir von Hand (Abschnitt 8).
|
||||
|
||||
## 1. Das Snippet
|
||||
|
||||
```html
|
||||
<div data-webklar-shop="SITEKEY" data-api="https://project.webklar.com">
|
||||
<p>Unser Shop ist gerade nicht erreichbar. Bitte schreiben Sie uns: info@beispiel.de</p> <!-- Ausweichtext, Abschnitt 4 -->
|
||||
</div>
|
||||
<script src="https://project.webklar.com/embed/shop.js" defer></script>
|
||||
```
|
||||
|
||||
Das Skript steht einmal pro Seite, vor `</body>`. Es hat keine Abhängigkeiten, setzt keine Cookies, lädt nichts von Dritten und fragt nur `project.webklar.com` an. Einziger Speicher im Browser ist der Warenkorb (nur Artikelkennungen und Mengen) in der `sessionStorage` des Tabs; er ist technisch erforderlich (§ 25 Abs. 2 Nr. 2 TDDDG) und verschwindet mit dem Tab. Texte des Händlers setzt das Skript nur als Text ein, nie als HTML. Das Widget rechnet nie selbst: Preise, Summen, Versand und Steuerhinweise kommen fertig vom Server.
|
||||
|
||||
| Attribut | Pflicht | Bedeutung |
|
||||
| --- | --- | --- |
|
||||
| `data-webklar-shop` | ja | der `siteKey` des Projekts |
|
||||
| `data-api` | nein | Herkunft des Portals, Standard `https://project.webklar.com`. Bei Modul-Import, Tag-Manager oder nachgeladenem Skript ist `data-api` Pflicht. |
|
||||
| `data-ansicht` | nein | `widerruf` öffnet gleich die Widerrufsfunktion („Vertrag widerrufen“). Die Adresse der Seite kennt außerdem `?ansicht=widerruf` und `?ansicht=agb`. |
|
||||
|
||||
## 2. Wohin damit
|
||||
|
||||
1. **Eigene Seite „Shop“** anlegen (zum Beispiel `/shop` oder `/shop.html`) und das Snippet an die Stelle des Inhalts setzen. Das `div` braucht Breite (normaler Blockcontainer; in einem Flex- oder Grid-Element ohne Breite schrumpft es auf null). Der Warenkorb liegt nur im Tab, also darf die Seite beim Bestellen nicht in einem iframe liegen und der Händler braucht keine zweite Seite für die Kasse: Kasse, Danke-Seite und Widerruf laufen im selben Widget.
|
||||
2. **Menüpunkt „Shop“** im Hauptmenü auf diese Seite, auf allen Seiten.
|
||||
3. Vom Shop zurück: Nach der Zahlung bei Stripe kehrt der Käufer auf die Seite zurück, auf der er bestellt hat (nur Adressen der Website des Projekts sind erlaubt, sonst die Standalone-Seite). Die Seite darf also nicht hinter einer Weiterleitung oder einem Passwortschutz liegen.
|
||||
4. **Mehrere Widgets** auf einer Seite sind nicht vorgesehen. Der Shop ist ein Widget je Website.
|
||||
|
||||
Aussehen: Das Widget nimmt die Akzentfarbe aus den Einstellungen des Händlers. Wie beim Terminplaner stehen die Stile in `@layer wks`, jede Regel der Kundenseite gewinnt also. Anpassen per CSS-Variablen auf dem `div`, in der Seiten-CSS oder auf `:root`, zum Beispiel `[data-webklar-shop]{ --wks-akzent:#0f766e; --wks-akzent-text:#fff; }`:
|
||||
|
||||
| Variable | Standard |
|
||||
| --- | --- |
|
||||
| `--wks-akzent` und `--wks-akzent-text` (Knöpfe; setze beide zusammen) | Akzentfarbe des Händlers, sonst `#1d4ed8` und `#fff` |
|
||||
| `--wks-hintergrund` | `#fff` |
|
||||
| `--wks-text` | `#1f2937` |
|
||||
| `--wks-rand` | `#858f9e` |
|
||||
| `--wks-radius` | `10px` |
|
||||
| `--wks-schrift`, `--wks-schrift-titel` | Schrift der Seite |
|
||||
|
||||
Kontrast (WCAG 2.1 AA): Text auf dem Hintergrund und Schrift auf dem Akzent mindestens 4,5 : 1, Rand gegen den Hintergrund mindestens 3 : 1. Prüfe beim Abnehmen, ob globale Regeln des Themes (`button{…}`, `input{…}`) das Widget stören, und überschreibe sie gezielt (`[data-webklar-shop] .wks-btn{ … }`).
|
||||
|
||||
## 3. Link „Vertrag widerrufen“ auf jeder Seite (Pflicht)
|
||||
|
||||
Seit dem 19.06.2026 muss jeder Online-Shop eine elektronische Widerrufsfunktion bieten (§ 356a BGB). Sie muss **auf jeder Seite** der Website dauerhaft erreichbar sein, nicht nur im Shop:
|
||||
|
||||
- In der **Fußzeile jeder Seite** (auch Impressum, Datenschutz und 404-Seite) steht der Link **„Vertrag widerrufen“** auf `https://project.webklar.com/shop/<siteKey>/widerruf` (mit dem echten `siteKey`).
|
||||
- Der Linktext lautet genau „Vertrag widerrufen“. Er darf nicht ausgeblendet, versteckt oder in einem Aufklappmenü untergebracht werden.
|
||||
- Der Link braucht keine Anmeldung. Er führt zur Schaltfläche „Vertrag widerrufen“, danach zum Formular und zur Bestätigungsschaltfläche „Widerruf bestätigen“.
|
||||
- Im Shop-Widget selbst steht der Knopf „Vertrag widerrufen“ zusätzlich in der Fußzeile des Widgets.
|
||||
|
||||
Der Händler darf den Link nicht entfernen oder ändern (Leistungsbeschreibung Nr. 5 Abs. 6). Weise ihn bei der Übergabe darauf hin.
|
||||
|
||||
## 4. Ausweichtext
|
||||
|
||||
Alles, was im `div` steht, gehört der Kundenwebsite. Das Skript ersetzt es nur, wenn die Konfiguration geladen werden konnte. Bei ausgeschaltetem JavaScript, unbekanntem Schlüssel, abgeschalteter Funktion oder Netzfehler bleibt der Text stehen. Schreib dort einen Hinweis und die Kontaktdaten des Händlers hinein. Ist der Shop zwar eingebunden, aber (noch) nicht geöffnet, zeigt das Widget selbst „Der Shop ist gerade geschlossen“.
|
||||
|
||||
## 5. Vor der Abnahme prüfen
|
||||
|
||||
**Impressum des Händlers.** Der Händler ist der Verkäufer. Sein Impressum muss ihn nennen (Name beziehungsweise Firma, vollständige Anschrift, E-Mail, Telefon, gegebenenfalls Register und USt-IdNr.). Es reicht nicht, wenn nur WEBklar als Betreiber der Website erscheint. Die Adressen von Impressum und Datenschutzerklärung (https) hat der Händler im Portal unter Rechtliches eingetragen; das Widget verlinkt sie in der Fußzeile und in der Kasse. Prüfe, dass beide Adressen auf die echten Seiten der Website führen. Fällt Dir ein offensichtlicher Mangel auf, sag es dem Händler im Chat (Leistungsbeschreibung Nr. 5 Abs. 10: Eine Prüfpflicht entsteht daraus nicht).
|
||||
|
||||
**Datenschutz-Absatz.** Liegt die Datenschutzerklärung in der Website, die wir betreuen, fügen wir den Absatz aus `docs/funktionen/shop-datenschutz-absatz.md` dort ein (gehört zur Einrichtung). Liegt sie woanders, bekommt der Händler den Text per Chat und fügt ihn selbst ein. Anrede und Nummerierung an die übrige Erklärung anpassen; die Angaben zur Speicherdauer entsprechen dem AVV Webshop Nr. 9 und Abschnitt 6d unserer Datenschutzerklärung. Ändert sich dort etwas, ändere den Absatz mit.
|
||||
|
||||
**Rechtstexte.** AGB, Widerrufsbelehrung, Muster-Widerrufsformular sowie Versand und Zahlung bestätigt der Händler im Portal (Rechtliches). Die Muster liegen in `docs/funktionen/shop-muster-rechtstexte.md`. Ohne Bestätigung öffnet der Shop nicht.
|
||||
|
||||
**Artikel.** Wirf einen Blick auf die Pflichtangaben: Preis, Grundpreis (immer je 1 kg, 1 l, 1 m oder 1 m²), Lieferzeit, bei Lebensmitteln die Pflichtangaben und Allergene, die Angaben zur Produktsicherheit und Widerrufsausschlüsse. Das ist Sache des Händlers; ein Hinweis im Chat hilft.
|
||||
|
||||
## 6. Vorschau und Testkauf
|
||||
|
||||
1. **Vorschau:** `https://project.webklar.com/shop/<siteKey>` zeigt das Widget auf einer neutralen Seite. Prüfe dort Artikel, Warenkorb, Versand und die Fußzeile (AGB, Widerrufsbelehrung, Versand & Zahlung, Vertrag widerrufen, Impressum, Datenschutz). Prüfe danach dasselbe auf der echten Seite der Kundenwebsite, auch auf dem Handy.
|
||||
2. **Testkauf mit dem Händler.** Lege mit dem Händler einen Artikel für **1 €** an (nur für den Test, danach wieder löschen oder deaktivieren). Der Händler öffnet den Shop (Übersicht → „Shop geöffnet“, alle Haken grün), Du kaufst den Artikel auf der echten Seite mit Deiner E-Mail-Adresse und zahlst echt (das Stripe-Konto des Händlers ist ein Live-Konto). Prüfe: Bestätigungsseite „Danke“, Bestellbestätigungsmail beim Käufer (mit AGB, Widerrufsbelehrung und Muster-Widerrufsformular im Text), Mail „Neue Bestellung“ beim Händler, Bestellung im Portal als bezahlt.
|
||||
3. **Widerruf testen:** Rufe den Link „Vertrag widerrufen“ aus der Fußzeile auf, erkläre den Widerruf zur Testbestellung und bestätige ihn. Du musst sofort eine Eingangsbestätigung mit Inhalt, Datum und Uhrzeit bekommen; der Händler bekommt die Mitteilung, der Widerruf steht an der Bestellung im Portal.
|
||||
4. **Erstatten:** Der Händler erstattet den Testkauf im Portal (Bestellung → Erstatten). Prüfe die Erstattungsmail und dass Stripe die Rückzahlung zeigt. Den Testartikel danach löschen.
|
||||
5. Ist alles in Ordnung, **schließe das Einrichtungs-Ticket**. Das schaltet die Funktion frei (Kunde erhält die Mail „Webshop bereit“). Trag die Zeit im Arbeitsblatt ein; sie wird nicht auf Abo-Stunden angerechnet (Leistungsbeschreibung Nr. 9 Abs. 4).
|
||||
|
||||
## 7. Was das Widget zeigt (Kurzüberblick)
|
||||
|
||||
1. Ganz oben: Lieferländer und akzeptierte Zahlungsarten (Beginn des Bestellvorgangs, § 312j Abs. 1 BGB).
|
||||
2. Artikel (Kategorien, Raster), Detail mit Bildern, Beschreibung, Preis, Grundpreis, Varianten, Lieferhinweis, Widerrufsausschluss, Produktsicherheit und Lebensmittelangaben.
|
||||
3. Warenkorb, Lieferart (Versand oder Abholung).
|
||||
4. Kasse: Positionen mit Einzel- und Gesamtpreis, Versandkosten, Gesamtsumme, Lieferzeit, Zahlungsarten, Hinweis auf das Widerrufsrecht beziehungsweise seinen Ausschluss, Links zu AGB und Widerrufsbelehrung, **Hinweis auf die gesetzliche Gewährleistung** (Anhang I der Verordnung (EU) 2025/1960, hervorgehoben über dem Knopf) und der Knopf **„Zahlungspflichtig bestellen“**. Erst mit diesem Klick im Widget kommt der Vertrag zustande; Stripe Checkout ist danach nur noch die Zahlung (Name, Telefon und Lieferadresse sammelt Stripe).
|
||||
5. Danke-Seite mit dem Stand der Bestellung; die Bestellbestätigungsmail geht an den Käufer.
|
||||
6. Widerruf: Formular (Name, E-Mail, Bestellnummer, optional Erklärung) und „Widerruf bestätigen“.
|
||||
|
||||
## 8. Wenn der Vertrag über die Website endet (Abschalten von Hand)
|
||||
|
||||
Der Webshop läuft, solange WEBklar die Website des Händlers betreibt (Hosting-Abo, Wartungs-Abo oder ein anderer Vertrag über den Betrieb der Website; Leistungsbeschreibung Nr. 10). Die Software prüft das **nicht**. Du musst es selbst im Blick haben, wenn ein solcher Vertrag endet, gekündigt oder widerrufen wird:
|
||||
|
||||
1. **Vorher** schreibst Du dem Händler eine E-Mail (oder Chat): Der Vertrag endet am TT.MM.JJJJ, damit endet auch der Webshop, WEBklar schaltet ihn zum Vertragsende ab. Er soll offene Bestellungen und Widerrufe abarbeiten, seine Bestellungen als CSV exportieren (Aufbewahrungspflichten sind seine Sache) und seine Käufer informieren, dass der Shop nicht mehr angeboten wird.
|
||||
2. **Zum Vertragsende** im Ticketsystem (WOMS): Kunde → Reiter „Funktionen“ → Webshop → **„Abschalten“**. Danach nimmt der Shop keine Bestellungen mehr an, das Widget zeigt den Ausweichtext, unbezahlte Bestellungen laufen aus.
|
||||
3. Der Händler kann Bestellungen und Widerrufe danach **90 Tage** lang im Portal lesen und exportieren. Danach löscht der tägliche Job automatisch alle Shopdaten (Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen); das Einrichtungs-Ticket bleibt als Arbeitsnachweis ohne Käuferdaten. Die Widerrufsfunktion bleibt bis zu dieser Löschung erreichbar, solange das Widget eingebunden ist.
|
||||
4. Nimm nach dem Abschalten den Link „Vertrag widerrufen“ aus der Fußzeile und das Snippet aus der Website, sobald die 90 Tage um sind (oder der Händler den Shop nicht mehr braucht), und entferne den Shop-Menüpunkt.
|
||||
5. Beginnt **innerhalb der 90 Tage** wieder ein Vertrag über den Betrieb der Website, schaltest Du den Webshop auf Wunsch des Händlers in Textform ohne weitere Kosten wieder ein (WOMS → Funktionen → Webshop → Aktivieren; Daten und Einstellungen sind noch da). Nach der Löschung ist eine neue Anfrage und Einrichtung nötig (neuer Preis).
|
||||
6. **Erstattung (Leistungsbeschreibung Nr. 10 Abs. 6):** Endet der Vertrag über den Betrieb der Website, **bevor** wir den Webshop freigeschaltet haben (Ticket noch offen, Funktion im Zustand „in Einrichtung“), erstatten wir die 299 € vollständig, in Stripe von Hand. In allen anderen Fällen (Webshop war schon freigeschaltet, Abschalten auf Wunsch nach Abs. 7, Abschalten wegen verbotener Waren) gibt es keine Erstattung; nur der Widerruf der Bestellung richtet sich nach Nr. 9 Abs. 6.
|
||||
7. Widerruft ein Verbraucher oder Existenzgründer die Bestellung der Einrichtung (Seite „Vertrag widerrufen“, Auswahl „Webshop (Einrichtung)“), kommt die Mitteilung T09 mit Fristende und Preis; es wird nichts automatisch geändert. Du prüfst, erstattest in Stripe innerhalb von 14 Tagen (gegebenenfalls abzüglich Wertersatz für den begonnenen Einbau) und schaltest den Webshop ab. Mit dem Widerruf endet der Webshop.
|
||||
|
||||
Das Stripe-Konto des Händlers bleibt in jedem Fall beim Händler; wir rühren es nicht an.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Webshop – Livegang
|
||||
|
||||
Reihenfolge für den Inhaber. Jeder Schritt braucht seine Freigabe; nichts davon hat Claude gegen Produktion oder das
|
||||
Live-Stripe-Konto ausgeführt. Code: Portal `feature/webshop` (Worktree `dev-webshop`), WOMS `feature/webshop`
|
||||
(Worktree `ticket.webklar.com/dev-webshop`). Beide Zweige bauen auf `feature/terminplaner-tp2` auf.
|
||||
|
||||
## 0. Voraussetzung
|
||||
|
||||
- **TP2 (Terminplaner-Verkauf) ist live.** Der Webshop nutzt das Funktionen-Register aus TP2. Erst TP2 nach `main`
|
||||
(Portal) bzw. `test` (WOMS), dann diesen Zweig darauf mergen (`git merge origin/main` im Worktree, Tests laufen lassen).
|
||||
- Der Zweig `feature/oeffnungszeiten` baut ebenfalls auf TP2 auf. Wer zuerst live geht, ist egal; der zweite merged den
|
||||
ersten nach (Konflikte erwartbar in `server/config.js` features, `public/app.js`, `public/funktionen.js`,
|
||||
`test/mailTemplates.test.js` (Vorlagen-IDs), `server/index.js`).
|
||||
|
||||
## 1. Stripe-Dashboard (WEBklar-Konto, live)
|
||||
|
||||
1. **Connect aktivieren**: Einstellungen → Connect → Plattformprofil ausfüllen: „Plattform/Software, deren Nutzer an
|
||||
eigene Kunden verkaufen“, Geldfluss **Direct Charges**, **keine** Plattformgebühr, Konten mit eigenem vollen
|
||||
Stripe-Dashboard (Standard-Konten). Danach unter Connect → Branding Name „WEBklar“, Icon und Farbe für das Onboarding.
|
||||
2. **Connect-Webhook** anlegen: Entwickler → Webhooks → Endpunkt hinzufügen, **„Ereignisse in verbundenen Konten“**,
|
||||
URL `https://project.webklar.com/api/shop/stripe-webhook`, Ereignisse:
|
||||
`account.updated`, `checkout.session.completed`, `checkout.session.async_payment_succeeded`,
|
||||
`checkout.session.async_payment_failed`, `checkout.session.expired`, `charge.refunded`.
|
||||
Das Signatur-Geheimnis (`whsec_…`) kommt in Schritt 2 in die `.env`.
|
||||
3. Nichts weiter: Händler-Konten legt das Portal selbst an, wenn ein Händler „Stripe-Konto einrichten“ klickt.
|
||||
|
||||
## 2. Server-Konfiguration
|
||||
|
||||
In `/home/webklar/apps/.env`:
|
||||
```
|
||||
SHOP_ENABLED=pilot
|
||||
SHOP_PILOT_EMAILS=kenso.gri@gmail.com
|
||||
STRIPE_CONNECT_WEBHOOK_SECRET=whsec_…
|
||||
```
|
||||
In `/home/webklar/apps/docker-compose.yml` beim Dienst `kundenbereich-server` die drei Variablen durchreichen
|
||||
(`SHOP_ENABLED=${SHOP_ENABLED:-off}`, `SHOP_PILOT_EMAILS=${SHOP_PILOT_EMAILS:-}`,
|
||||
`STRIPE_CONNECT_WEBHOOK_SECRET=${STRIPE_CONNECT_WEBHOOK_SECRET:-}`) und danach
|
||||
`docker compose -f /home/webklar/apps/docker-compose.yml up -d --no-deps kundenbereich-server`
|
||||
(ein `docker restart` liest die `.env` nicht neu). Artikelbilder liegen unter `build/upload-data/shop/` (gitignored,
|
||||
überleben Deploys wie die News-Fotos).
|
||||
|
||||
## 3. Datenbank und Stripe-Preis (vor dem Deploy)
|
||||
|
||||
```bash
|
||||
cd /home/webklar/apps/kundenbereich.webklar.com/build
|
||||
docker exec -w /app webklar-kundenbereich-server node scripts/shop-setup.mjs --check
|
||||
docker exec -w /app webklar-kundenbereich-server node scripts/shop-setup.mjs
|
||||
docker exec -w /app webklar-kundenbereich-server node scripts/stripe-products.mjs --pruefen
|
||||
docker exec -w /app webklar-kundenbereich-server node scripts/stripe-products.mjs --anlegen # legt shopSetup 299 € an
|
||||
```
|
||||
`shop-setup.mjs` legt fünf Collections an (`shopConfig`, `shopProducts`, `shopOrders`, `shopWiderrufe`,
|
||||
`shopRechtstexteArchiv`). Die Käufer-Mails laufen über `termineOutbox`; die Collection kommt aus dem TP2-Livegang
|
||||
(`scripts/termine-setup.mjs`). Die Skripte müssen aus dem neuen Code laufen (nach Schritt 4 oder vorher aus dem Worktree
|
||||
mit derselben Umgebung).
|
||||
|
||||
## 4. Deploy
|
||||
|
||||
- Portal: `feature/webshop` → `main` pushen (Webhook deployt).
|
||||
- WOMS: `feature/webshop` → `test` pushen; danach im WOMS „Rechtliches“: `scripts/setup-legal.mjs` laufen lassen (neue
|
||||
Karten „Leistungsbeschreibung Webshop“, „AVV Webshop“) und die Fassungen 2026-10-05 hochladen
|
||||
(`public/rechtliches/leistungsbeschreibung-webshop-2026-10-05.pdf`, `avv-webshop-2026-10-05.pdf`).
|
||||
- Prüfen: `https://project.webklar.com/rechtliches/leistungsbeschreibung-webshop.html` und
|
||||
`…/avv-webshop.html` antworten 200; `https://project.webklar.com/embed/shop.js` antwortet 200.
|
||||
|
||||
## 5. Pilot mit dem Testkonto
|
||||
|
||||
1. Im Portal als Testkunde: Funktionen → Webshop → Anfrage stellen (Projekt `test-website`).
|
||||
2. WOMS → Kunde → Funktionen → Webshop → „Anfrage annehmen“ (Mail K24 kommt an).
|
||||
3. Einrichtung: entweder bestellen (299 €, danach in Stripe erstatten) oder im WOMS „Aktivieren (ohne Zahlung)“
|
||||
(Pilot-Adressen brauchen keine Textform).
|
||||
4. Einbau nach `docs/funktionen/shop-einbau.md` auf der Test-Website, Ticket schließen → Freischaltung, Mail K16.
|
||||
5. Portal → Webshop → Übersicht → „Stripe-Konto einrichten“ (echtes Onboarding mit den Daten von WEBklar oder einem
|
||||
Testbetrieb; Stripe prüft Identität und Bankkonto).
|
||||
6. Einstellungen ausfüllen (Steuerart wählen!), einen Artikel für 1,00 € anlegen, Rechtstexte: Muster einsetzen,
|
||||
Platzhalter ersetzen, Impressum-/Datenschutz-URL, bestätigen, „Shop öffnen“.
|
||||
7. Testkauf auf der Website mit echter Karte → Mails G09 (Käufer) und K25 (Händler) prüfen → im Portal „Versendet“
|
||||
(G10) → „Erstatten“ (G13) → im Stripe-Dashboard des Händlers sichtbar.
|
||||
8. Widerruf über den Fußzeilen-Link testen (G12 mit Datum und Uhrzeit, K26 an den Händler).
|
||||
|
||||
## 6. Freigabe für alle
|
||||
|
||||
- Kanzlei-Paket `docs/rechtliches/webshop-kanzlei-paket.md` an die Kanzlei, Befund einarbeiten (neue Fassung, falls
|
||||
sich Texte ändern; ausgelieferte Fassungen werden nie überschrieben).
|
||||
- Checkliste in Abschnitt 7 des Kanzlei-Pakets abhaken.
|
||||
- `SHOP_ENABLED=on`, Compose neu anwenden (wie Schritt 2).
|
||||
|
||||
## Zurück
|
||||
|
||||
`SHOP_ENABLED=off` blendet die Kachel aus und stoppt neue Bestellungen. Bestehende Shops (Zustand aktiv oder
|
||||
abgeschaltet) behalten ihre Widerrufsfunktion, ihre Fristen und die Löschjobs laufen weiter. Einzelne Shops schaltet das
|
||||
Team im WOMS ab (Funktionen → Webshop → „Abschalten“), die Daten werden 90 Tage danach gelöscht.
|
||||
@@ -0,0 +1,166 @@
|
||||
# Shop: Muster-Rechtstexte (Widerrufsbelehrung, Widerrufsformular, AGB)
|
||||
|
||||
**Muster – keine Rechtsberatung.** Diese Texte sind Muster, die das Kundenportal im Webshop vorbefüllt (`server/services/shop/rechtstexte.js`, dort mit den Angaben aus den Shop-Einstellungen). Der Händler prüft sie, passt sie an und bestätigt sie selbst; WEBklar berät nicht rechtlich. Diese Datei enthält dieselben Texte mit Platzhaltern in eckigen Klammern zur Prüfung durch eine Kanzlei (Plan 2026-10-05, Task C5; Offene Punkte 5 und 9 der Recherche).
|
||||
|
||||
Stand: 2026-10-05.
|
||||
|
||||
## Quellen und Fundstellen
|
||||
|
||||
- Widerrufsbelehrung: Anlage 1 zu Art. 246a § 1 Abs. 2 S. 2 EGBGB, Fassung 2026 mit dem Baustein zur Widerrufsfunktion (https://www.gesetze-im-internet.de/bgbeg/BJNR006049896.html). Ein richtig ausgefülltes Muster erfüllt die Informationspflicht (Art. 246a § 1 Abs. 2 S. 3 EGBGB).
|
||||
- Elektronische Widerrufsfunktion: § 356a BGB (seit 19.06.2026), Hinweispflicht in der Belehrung nach Art. 246a § 1 Abs. 2 S. 1 Nr. 1 EGBGB.
|
||||
- Widerrufsformular: Anlage 2 zu Art. 246a § 1 Abs. 2 S. 1 Nr. 1 EGBGB.
|
||||
- Ausschluss und vorzeitiges Erlöschen: § 312g Abs. 2 Nr. 1, 2, 3 BGB; Information nach Art. 246a § 1 Abs. 3 EGBGB.
|
||||
- Gewährleistungshinweis: Art. 246a § 1 Abs. 1 S. 1 Nr. 11 EGBGB i. V. m. Durchführungsverordnung (EU) 2025/1960 Anhang I (seit 27.09.2026).
|
||||
- Streitbeilegung: § 36 VSBG. Die EU-Plattform zur Online-Streitbeilegung wurde am 20.07.2025 eingestellt; es gibt keinen Link darauf.
|
||||
- Button und Vertragsschluss: § 312j Abs. 3 und 4 BGB, § 312i BGB. Recherche: `/root/reports/webshop-2026-10-05/recherche-recht-stripe.md` (B.1 bis B.10).
|
||||
|
||||
## Hinweise zur Prüfung (nicht Teil der Texte)
|
||||
|
||||
- Fristbeginn: Die Belehrung enthält beide gesetzlichen Alternativen (eine Ware bzw. die Waren; mehrere Waren einer Bestellung, getrennt geliefert: die letzte Ware), weil der Warenkorb beides erzeugen kann. Teilsendungen und regelmäßige Lieferung sind nicht vorgesehen.
|
||||
- Rücksendekosten: Standard ist „Käufer trägt“. Variante „Händler trägt“ steht unten.
|
||||
- Abholung: Bietet der Shop Abholung an, kommt der Satz „Haben Sie die Waren bei uns abgeholt …“ hinzu.
|
||||
- Ausschlussgründe: Der letzte Absatz der Belehrung nennt § 312g Abs. 2 Nr. 1, 2 und 3 BGB in allgemeiner Form. Ob und für welche Waren er zutrifft, entscheidet der Händler; Einordnung von Lieferung und Abholung von Speisen (§ 312g Abs. 2 Nr. 9, § 312 Abs. 2 Nr. 8 BGB) ist offen (Recherche, Offene Punkte 6).
|
||||
- Gewährleistungshinweis: amtliche Grafik und wörtlicher Text aus Anhang I eingesetzt; offen bleibt nur der Abgleich der Grafikdatei mit der Originaldatei der Kommission.
|
||||
- Widerrufsfunktion: Erreichbarkeit ohne Login bei Gastbestellungen, Identifikation der Bestellung und Zeitstempel mit der Kanzlei abstimmen (Recherche, Offene Punkte 7).
|
||||
|
||||
## Widerrufsbelehrung
|
||||
|
||||
Widerrufsbelehrung
|
||||
|
||||
Widerrufsrecht
|
||||
|
||||
Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
|
||||
|
||||
Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die Waren in Besitz genommen haben bzw. hat.
|
||||
|
||||
Haben Sie mehrere Waren im Rahmen einer einheitlichen Bestellung bestellt und werden diese getrennt geliefert, beträgt die Widerrufsfrist vierzehn Tage ab dem Tag, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die letzte Ware in Besitz genommen haben bzw. hat.
|
||||
|
||||
Um Ihr Widerrufsrecht auszuüben, müssen Sie uns ([Name des Händlers], [Straße Nr.], [PLZ] [Ort], E-Mail: [E-Mail-Adresse]) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
|
||||
|
||||
Sie können Ihr Widerrufsrecht auch online unter [Internetadresse der Widerrufsfunktion im Shop] ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.
|
||||
|
||||
Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
|
||||
|
||||
Folgen des Widerrufs
|
||||
|
||||
Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
|
||||
|
||||
Wir können die Rückzahlung verweigern, bis wir die Waren wieder zurückerhalten haben oder bis Sie den Nachweis erbracht haben, dass Sie die Waren zurückgesandt haben, je nachdem, welches der frühere Zeitpunkt ist.
|
||||
|
||||
Sie haben die Waren unverzüglich und in jedem Fall spätestens binnen vierzehn Tagen ab dem Tag, an dem Sie uns über den Widerruf dieses Vertrags unterrichten, an uns ([Name des Händlers], [Straße Nr.], [PLZ] [Ort]) zurückzusenden oder zu übergeben. Die Frist ist gewahrt, wenn Sie die Waren vor Ablauf der Frist von vierzehn Tagen absenden.
|
||||
|
||||
Sie tragen die unmittelbaren Kosten der Rücksendung der Waren.
|
||||
|
||||
Sie müssen für einen etwaigen Wertverlust der Waren nur aufkommen, wenn dieser Wertverlust auf einen zur Prüfung der Beschaffenheit, Eigenschaften und Funktionsweise der Waren nicht notwendigen Umgang mit ihnen zurückzuführen ist.
|
||||
|
||||
Ausschluss bzw. vorzeitiges Erlöschen des Widerrufsrechts
|
||||
|
||||
Das Widerrufsrecht besteht nicht bei Verträgen zur Lieferung von Waren, die nicht vorgefertigt sind und für deren Herstellung eine individuelle Auswahl oder Bestimmung durch den Verbraucher maßgeblich ist oder die eindeutig auf die persönlichen Bedürfnisse des Verbrauchers zugeschnitten sind (§ 312g Abs. 2 Nr. 1 BGB), z. B. Waren mit Wunschaufdruck oder Maßanfertigungen.
|
||||
Das Widerrufsrecht besteht nicht bei Verträgen zur Lieferung von Waren, die schnell verderben können oder deren Verfallsdatum schnell überschritten würde (§ 312g Abs. 2 Nr. 2 BGB), z. B. frische Lebensmittel.
|
||||
Das Widerrufsrecht erlischt vorzeitig bei Verträgen zur Lieferung versiegelter Waren, die aus Gründen des Gesundheitsschutzes oder der Hygiene nicht zur Rückgabe geeignet sind, wenn ihre Versiegelung nach der Lieferung entfernt wurde (§ 312g Abs. 2 Nr. 3 BGB).
|
||||
|
||||
Ende der Widerrufsbelehrung
|
||||
|
||||
### Variante: Händler trägt die Rücksendekosten
|
||||
|
||||
Statt „Sie tragen die unmittelbaren Kosten der Rücksendung der Waren.“ steht:
|
||||
|
||||
Wir tragen die Kosten der Rücksendung der Waren.
|
||||
|
||||
### Variante: Abholung angeboten
|
||||
|
||||
Nach dem Absatz zur Rücksendung folgt zusätzlich:
|
||||
|
||||
Haben Sie die Waren bei uns abgeholt, übergeben Sie diese zur Rückgabe an unserer Anschrift: [Abholadresse].
|
||||
|
||||
## Widerrufsformular
|
||||
|
||||
Muster-Widerrufsformular
|
||||
|
||||
(Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
|
||||
|
||||
An [Name des Händlers], [Straße Nr.], [PLZ] [Ort], E-Mail: [E-Mail-Adresse]:
|
||||
|
||||
Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)
|
||||
|
||||
– Bestellt am (*)/erhalten am (*)
|
||||
– Name des/der Verbraucher(s)
|
||||
– Anschrift des/der Verbraucher(s)
|
||||
– Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
|
||||
– Datum
|
||||
|
||||
(*) Unzutreffendes streichen.
|
||||
|
||||
## Allgemeine Geschäftsbedingungen
|
||||
|
||||
Die Kopfzeile „Muster – bitte prüfen und anpassen“ steht im vorbefüllten Text und wird vom Händler nach der Prüfung entfernt. Die Zahlungsarten, Länder und Abholung werden aus den Shop-Einstellungen eingesetzt; die Platzhalter unten zeigen den Standardfall.
|
||||
|
||||
Muster – bitte prüfen und anpassen
|
||||
|
||||
Allgemeine Geschäftsbedingungen
|
||||
|
||||
§ 1 Geltung und Anbieter
|
||||
|
||||
Diese Allgemeinen Geschäftsbedingungen gelten für alle Bestellungen von Verbrauchern in unserem Online-Shop. Anbieter und Vertragspartner ist [Name des Händlers], [Straße Nr.], [PLZ] [Ort], E-Mail: [E-Mail-Adresse].
|
||||
|
||||
§ 2 Vertragsschluss
|
||||
|
||||
Die Darstellung der Waren im Shop ist kein verbindliches Angebot. Mit dem Klick auf die Schaltfläche „Zahlungspflichtig bestellen“ geben Sie eine verbindliche Bestellung ab. Den Eingang Ihrer Bestellung bestätigen wir Ihnen sofort auf einer Bestätigungsseite im Shop. Bei den Zahlungsarten Kreditkarte, Apple Pay, Google Pay, Klarna erhalten Sie die Bestellbestätigung per E-Mail unmittelbar nach erfolgreicher Zahlung; mit dieser E-Mail nehmen wir Ihre Bestellung an und der Vertrag kommt zustande. Bei SEPA-Lastschrift senden wir Ihnen zunächst eine E-Mail über den Eingang Ihrer Bestellung; der Vertrag kommt in diesem Fall erst zustande, wenn wir Ihnen nach Eingang der Zahlung die Bestellbestätigung per E-Mail senden. Bestellungen, die nicht bezahlt werden, werden nach Ablauf der Zahlungsfrist verworfen.
|
||||
|
||||
§ 3 Preise und Versandkosten
|
||||
|
||||
Es gelten die zum Zeitpunkt der Bestellung im Shop angegebenen Preise in Euro. Alle Preise enthalten die gesetzliche Umsatzsteuer. Versandkosten und etwaige weitere Kosten werden vor Abgabe der Bestellung gesondert ausgewiesen und richten sich nach dem Zielland. Einzelheiten finden Sie unter „Versand & Zahlung“.
|
||||
|
||||
§ 4 Zahlung
|
||||
|
||||
Die Zahlung erfolgt über den Zahlungsdienstleister Stripe. Es stehen folgende Zahlungsarten zur Verfügung: Kreditkarte, Apple Pay, Google Pay, Klarna, SEPA-Lastschrift. Bei SEPA-Lastschrift versenden wir die Ware erst nach Eingang der Zahlung.
|
||||
|
||||
§ 5 Lieferung und Abholung
|
||||
|
||||
Wir liefern in folgende Länder: Deutschland. Die Lieferzeit beträgt [Lieferzeit], gerechnet ab Zahlungseingang. Ist eine Ware nicht verfügbar, informieren wir Sie unverzüglich.
|
||||
|
||||
§ 6 Eigentumsvorbehalt
|
||||
|
||||
Die Waren bleiben bis zur vollständigen Bezahlung unser Eigentum.
|
||||
|
||||
§ 7 Widerrufsrecht
|
||||
|
||||
Verbrauchern steht ein gesetzliches Widerrufsrecht zu. Einzelheiten ergeben sich aus der Widerrufsbelehrung, die Sie unter „Widerrufsbelehrung“ im Shop finden.
|
||||
|
||||
§ 8 Gewährleistung
|
||||
|
||||
Es gelten die gesetzlichen Mängelhaftungsrechte. Der Hinweis auf das gesetzliche Gewährleistungsrecht steht vor Abgabe der Bestellung im Shop.
|
||||
|
||||
§ 9 Technische Schritte, Vertragstext und Sprache
|
||||
|
||||
Zum Vertragsschluss führen folgende Schritte: Sie legen die gewünschten Waren in den Warenkorb, prüfen die Bestellübersicht mit allen Angaben und klicken auf „Zahlungspflichtig bestellen“. Danach zahlen Sie bei unserem Zahlungsdienstleister Stripe. Der Vertrag kommt nach § 2 mit unserer Bestellbestätigung per E-Mail zustande. Vor dem Klick auf „Zahlungspflichtig bestellen“ können Sie Eingabefehler erkennen und berichtigen: Im Warenkorb ändern Sie Mengen oder entfernen Waren, aus der Bestellübersicht gelangen Sie über „Zurück zum Warenkorb“ dorthin zurück, und Ihre Angaben zu Adresse und Zahlung prüfen und korrigieren Sie bei Stripe, bevor Sie die Zahlung abschließen. Wir speichern den Vertragstext nach Vertragsschluss nicht für Sie abrufbar. Ihre Bestelldaten, diese Allgemeinen Geschäftsbedingungen und die Widerrufsbelehrung erhalten Sie mit der Bestellbestätigung per E-Mail; bitte bewahren Sie diese E-Mail auf. Die Vertragssprache ist Deutsch. Wir unterwerfen uns keinen besonderen Verhaltenskodizes.
|
||||
|
||||
§ 10 Streitbeilegung
|
||||
|
||||
[Name des Händlers] ist nicht bereit und nicht verpflichtet, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen (§ 36 VSBG).
|
||||
|
||||
Zu § 2: Der Satz zur sofortigen Bestellbestätigung nennt nur die Zahlungsarten Karte/Apple Pay/Google Pay/Klarna; der SEPA-Satz erscheint nur, wenn SEPA-Lastschrift aktiv ist. Ohne SEPA entfällt er, bei ausschließlich SEPA entfällt der Satz davor. Der Ablauf im Shop: Bestätigungsseite sofort nach der Bestellung; bei Karte/Klarna folgt die Bestellbestätigung (G09, Vertragsschluss) unmittelbar nach der Zahlung; bei SEPA zuerst die Eingangsmail (G15), die Bestellbestätigung (G09) erst nach Zahlungseingang.
|
||||
|
||||
Zu § 9: enthält die Angaben nach Art. 246c EGBGB (technische Schritte, Eingabefehler erkennen und berichtigen, Speicherung des Vertragstextes, Sprache, Verhaltenskodizes).
|
||||
|
||||
Bei Kleinunternehmern steht in § 3 statt „Alle Preise enthalten die gesetzliche Umsatzsteuer.“ der Satz „Gemäß § 19 UStG wird keine Umsatzsteuer berechnet und ausgewiesen.“
|
||||
|
||||
## Hinweis auf das gesetzliche Gewährleistungsrecht (Kasse und Bestellbestätigung)
|
||||
|
||||
Amtliche Grafik nach Durchführungsverordnung (EU) 2025/1960 Anhang I (ABl. L vom 2.10.2025), nicht editierbar, online farbig: `public/img/gewaehrleistung-eu.webp` (aus dem Amtsblatt ausgeschnitten; vor Livegang mit der Originaldatei der Kommission abgleichen, Kanzlei-Frage). Wörtlicher Text der Grafik (Alternativtext, Bestellbestätigung):
|
||||
|
||||
GESETZLICHE GEWÄHRLEISTUNG
|
||||
|
||||
Mindestens zwei Jahre gesetzliche Gewährleistung der Vertragsmäßigkeit für Waren, die in der Europäischen Union verkauft werden.
|
||||
|
||||
Verbraucherinnen und Verbraucher können ihre Rechte im Rahmen des gesetzlichen Gewährleistungsrechts geltend machen, z. B. wenn die Waren nicht der Beschreibung entsprechen, nicht bestimmungsgemäß funktionieren.
|
||||
|
||||
Verkäufer haften für jede Vertragswidrigkeit, die zum Zeitpunkt der Lieferung der Waren bestand und innerhalb des Zeitraums der gesetzlichen Gewährleistung erkennbar wird. Verkäufer müssen in solchen Fällen Folgendes anbieten: kostenlose Nachbesserung oder kostenlose Ersatzlieferung, in bestimmten Fällen eine Preisminderung oder eine vollständige Erstattung des Kaufpreises.
|
||||
|
||||
In einigen Ländern gilt ein längerer Zeitraum für die gesetzliche Gewährleistung. Für gebrauchte Waren kann ein kürzerer Zeitraum gelten, jedoch nicht weniger als ein Jahr.
|
||||
|
||||
Für weitere Informationen zu Ihren Rechten in einem bestimmten Land scannen Sie den nachstehenden QR-Code oder fragen Sie den Verkäufer. europa.eu/youreurope/garantien
|
||||
|
||||
Was ist zu tun, wenn Sie vertragswidrige Waren erhalten? 1. Melden Sie dem Verkäufer das Problem so bald wie möglich. 2. Legen Sie einen Kaufnachweis vor, z. B. die Quittung, Rechnung oder einen Kontoauszug.
|
||||
|
||||
Verkäufer und Hersteller können auch gewerbliche Garantien gewähren, die unabhängig von der gesetzlichen Gewährleistung gelten. Diese GARAN-Kennzeichnung zeigt beispielsweise, dass der Hersteller eine gewerbliche Haltbarkeitsgarantie ohne zusätzliche Kosten gewährt, die die gesamte Ware abdeckt.
|
||||
@@ -0,0 +1,165 @@
|
||||
# Terminplaner auf der Kundenwebsite einbauen
|
||||
|
||||
Anleitung für das WEBklar-Team. Das Widget lässt Gäste auf der Website des Kunden Termine buchen. Leistungen, Personen und Öffnungszeiten pflegt der Kunde im Portal (Tab „Funktionen“, Kachel „Terminplaner“). Den `siteKey` (16 Zeichen, a–z und 2–7) und den fertigen Einbau-Code findest Du im Portal: in der Kundenansicht des Kunden unter Terminplaner → Einstellungen, Abschnitt „Einbau auf deiner Website“ (mit Knopf „Code kopieren“). Den `siteKey` zeigt auch das Ticketsystem (WOMS): Kunde → Reiter „Funktionen“, Zeile Terminplaner, mit Kopierknopf. Ohne aktive Funktion, ohne gespeicherte Einstellung und ohne Telefonnummer des Betriebs (bei Verbrauchern auch ohne Anschrift) ist nichts buchbar: Das Widget zeigt dann den Ausweichtext der Seite. Die Anleitung für einen bestimmten Kunden erzeugt das Ticketsystem (WOMS) selbst: Kunde → Reiter „Funktionen“ → Anleitung des Projekts, mit den Einstellungen des Betriebs, dem fertigen Einbau-Code, dem CSS-Vorschlag, den Datenschutz-Absätzen, der Abnahme-Checkliste und den Warnungen. Im Einrichtungs-Ticket steht davon die Kurzfassung (Stand der Einstellungen, Einbau-Code, Schritte) mit dem Link auf diese Anleitung. Dieses Dokument ist die allgemeine Referenz dahinter: Es erklärt Variablen, Stile, Datenschutz und API, die für jeden Betrieb gelten.
|
||||
|
||||
## 1. Das Snippet
|
||||
|
||||
```html
|
||||
<div data-webklar-termine="SITEKEY" data-api="https://project.webklar.com" data-modus="inline">
|
||||
<p>Termine telefonisch unter 06301 1234.</p> <!-- Ausweichtext, siehe Abschnitt 4 -->
|
||||
</div>
|
||||
<script src="https://project.webklar.com/embed/termine.js" defer></script>
|
||||
```
|
||||
|
||||
Das Skript steht einmal pro Seite, vor `</body>`. Es hat keine Abhängigkeiten, setzt keine Cookies, speichert nichts im Browser und lädt nichts von Dritten. Texte der Kunden setzt es nur als Text ein, nie als HTML. Es wird 5 Minuten gecacht und darf von jeder Domain geladen werden.
|
||||
|
||||
## 2. Variablen (Attribute am `div`)
|
||||
|
||||
| Attribut | Pflicht | Bedeutung |
|
||||
| --- | --- | --- |
|
||||
| `data-webklar-termine` | ja | der `siteKey` des Projekts |
|
||||
| `data-api` | nein | Herkunft des Portals, Standard `https://project.webklar.com`. Ohne Angabe nimmt das Skript die Herkunft seiner eigenen Adresse. Das klappt nur bei einem normalen `<script src>`. Bei Modul-Import, Tag-Manager oder nachgeladenem Skript ist `data-api` Pflicht. |
|
||||
| `data-modus` | nein | `inline` (Standard): die Buchungsstrecke steht direkt in der Seite und lädt beim Aufruf der Seite. `knopf`: ein Knopf öffnet die Strecke in einem Dialog (`<dialog>`). Beim Laden der Seite ruft das Widget einmal die Konfiguration ab (für den Knopf „Vertrag widerrufen“, siehe Abschnitt 5), alles Weitere erst beim Klick. |
|
||||
| `data-knopf` | nein | Text des Knopfes im Modus `knopf`, Standard „Termin buchen“. |
|
||||
| `data-leistung` | nein | ID einer Leistung (`s_` plus 8 Zeichen, steht im Portal bei der Leistung). Die Leistung ist vorgewählt, der Schritt „Leistung wählen“ entfällt. Praktisch für einen Knopf neben einer Leistung auf der Seite. Eine unbekannte ID wird ignoriert. |
|
||||
|
||||
Alle Klassen beginnen mit `wkt-`. Die Stile stehen einmalig in `<style id="wkt-style">` und zwar in `@layer wkt`. Das heißt: **jede** Regel der Kundenseite (ohne Layer) gewinnt gegen die Standard-Stile. Das ist gewollt, hat aber eine Folge: Hat das Theme der Kundenseite globale Regeln wie `button{…}` oder `input{…}`, greifen sie auch im Widget. Prüfe das beim Abnehmen und überschreibe bei Bedarf gezielt, z. B. `[data-webklar-termine] .wkt-btn{ … }`.
|
||||
|
||||
Das Layout richtet sich nach der Breite des `div` (Container Query, braucht aktuelle Browser), nicht nach dem Bildschirm. Das `div` muss eine Breite haben (normaler Blockcontainer; in einem Flex- oder Grid-Element ohne Breite schrumpft es auf null).
|
||||
|
||||
### Zwei Widgets auf einer Seite
|
||||
|
||||
Das geht. Jedes Widget bekommt eigene IDs, die Stile werden nur einmal eingefügt. Beispiel: zwei Knöpfe neben zwei Leistungen.
|
||||
|
||||
```html
|
||||
<div data-webklar-termine="SITEKEY" data-modus="knopf" data-knopf="Damenschnitt buchen" data-leistung="s_abcd1234"></div>
|
||||
<div data-webklar-termine="SITEKEY" data-modus="knopf" data-knopf="Färben buchen" data-leistung="s_efgh5678"></div>
|
||||
```
|
||||
|
||||
## 3. Aussehen anpassen (CSS-Variablen)
|
||||
|
||||
Setze die Variablen auf dem `div`, in der Seiten-CSS oder auf `:root`, z. B. `[data-webklar-termine]{ --wkt-akzent:#0f766e; }`. Alle sind optional.
|
||||
|
||||
| Variable | Standard |
|
||||
| --- | --- |
|
||||
| `--wkt-akzent` (Knöpfe, aktive Tage, Rahmen der freien Tage) | die Betriebsfarbe aus den Einstellungen, sonst `#1d4ed8` |
|
||||
| `--wkt-akzent-text` (Schrift auf dem Akzent) | passend zur Betriebsfarbe (hell oder dunkel), sonst `#fff` |
|
||||
| `--wkt-hintergrund` | `#fff` |
|
||||
| `--wkt-text` | `#1f2937` |
|
||||
| `--wkt-rand` (Eingabefelder, Karten) | `#858f9e` |
|
||||
| `--wkt-radius` | `10px` |
|
||||
| `--wkt-schrift` | die Schrift der Seite (`inherit`) |
|
||||
| `--wkt-schrift-titel` (Überschriften) | wie `--wkt-schrift` |
|
||||
|
||||
Die Betriebsfarbe gilt nur, solange `--wkt-akzent` nicht gesetzt ist; die Schrift darauf (Weiß, dunkles Grau oder Schwarz) wählt das Skript so, dass sie mindestens 4,5 : 1 erreicht. Setzt Du `--wkt-akzent`, setze auch `--wkt-akzent-text`. Im Modus `knopf` kennt das Widget die Betriebsfarbe erst nach dem Klick: Der Knopf selbst nimmt `--wkt-akzent` der Seite oder `#1d4ed8`. Setze `--wkt-akzent` dort also immer.
|
||||
|
||||
Kontrast (WCAG 2.1 AA): Text auf dem Hintergrund mindestens 4,5 : 1, Schrift auf dem Akzent mindestens 4,5 : 1, `--wkt-rand` gegen den Hintergrund mindestens 3 : 1 (sonst sind Eingabefelder schwer zu erkennen). Der Fokusrahmen nimmt die Textfarbe.
|
||||
|
||||
### Stil 1: hell und freundlich (Salon, Kosmetik)
|
||||
|
||||
```css
|
||||
[data-webklar-termine]{
|
||||
--wkt-akzent:#9d4b6f;
|
||||
--wkt-akzent-text:#fff;
|
||||
--wkt-hintergrund:#fff;
|
||||
--wkt-text:#3b2a33;
|
||||
--wkt-rand:#8c7580;
|
||||
--wkt-radius:14px;
|
||||
--wkt-schrift:"Nunito", system-ui, sans-serif;
|
||||
--wkt-schrift-titel:"Fredoka", system-ui, sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
### Stil 2: dunkel (Tattoo, Studio, Bar)
|
||||
|
||||
```css
|
||||
[data-webklar-termine]{
|
||||
--wkt-akzent:#f5c542;
|
||||
--wkt-akzent-text:#111;
|
||||
--wkt-hintergrund:#18181b;
|
||||
--wkt-text:#f4f4f5;
|
||||
--wkt-rand:#8b8b94;
|
||||
--wkt-radius:4px;
|
||||
--wkt-schrift:"Inter", system-ui, sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
### Stil 3: schlicht und eckig (Kanzlei, Handwerk, Beratung)
|
||||
|
||||
```css
|
||||
[data-webklar-termine]{
|
||||
--wkt-akzent:#1f3a5f;
|
||||
--wkt-akzent-text:#fff;
|
||||
--wkt-hintergrund:#fff;
|
||||
--wkt-text:#1a1a1a;
|
||||
--wkt-rand:#767676;
|
||||
--wkt-radius:0;
|
||||
--wkt-schrift:Georgia, "Times New Roman", serif;
|
||||
--wkt-schrift-titel:system-ui, sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
Die Schriften lädt das Skript nicht selbst. Sind sie auf der Kundenseite nicht eingebunden, greift die Systemschrift.
|
||||
|
||||
## 4. Ausweichtext
|
||||
|
||||
Alles, was im `div` steht, gehört der Kundenwebsite. Das Skript ersetzt es nur, wenn die Konfiguration geladen werden konnte. Bei ausgeschaltetem JavaScript, unbekanntem Schlüssel, abgeschalteter Funktion, fehlenden Pflichtangaben des Betriebs oder Netzfehler beim Laden bleibt der Text stehen. Schreib dort die Telefonnummer des Betriebs hinein und gestalte den Text im Stil der Seite.
|
||||
|
||||
Im Modus `knopf` ersetzt der Knopf den Text auf der Seite. Der Text erscheint dafür im Dialog, wenn das Laden beim Klick scheitert.
|
||||
|
||||
Bricht das Netz mitten in der Strecke ab, zeigt das Widget „Gerade nicht erreichbar. Bitte rufen Sie an: {Telefon des Betriebs}“ (bei Anrede „du“: „Bitte ruf an“).
|
||||
|
||||
## 5. Was das Widget zeigt
|
||||
|
||||
1. Leistung wählen (Name, Dauer, Preis, Kurzbeschreibung) mit der Zahlungsart („Bezahlung vor Ort: …“). Entfällt bei `data-leistung`.
|
||||
2. Person wählen („Egal“ zuerst). Nur wenn mehrere Personen infrage kommen und der Betrieb die Wahl erlaubt; bei Plätzen entfällt der Schritt.
|
||||
3. Tag (Monatsraster, nur Tage mit freien Zeiten sind wählbar) und Uhrzeit. Alle Daten und Uhrzeiten kommen vom Server in Berliner Zeit. Das Widget rechnet nie mit dem Datum oder der Zeitzone des Browsers.
|
||||
4. Angaben: Name, E-Mail, Telefon, Nachricht und Zusatzfelder nach den Einstellungen des Betriebs.
|
||||
5. Zusammenfassung mit Beschreibung der Leistung, Stornobedingungen (nur bei einer Leistung mit Preis; daneben der Link „AGB des Betriebs“, wenn im Portal eine https-Adresse hinterlegt ist), Widerrufsbelehrung (aufklappbar, bei Verbraucher-Betrieben), Datenschutz-Hinweis, den Haken (Storno nur bei einer Leistung mit Preis, vorzeitiger Beginn, Erinnerung) und dem Knopf („Zahlungspflichtig buchen“, „Termin buchen“ oder „Termin anfragen“, kommt vom Server).
|
||||
6. „Fast geschafft“: Der Gast bestätigt den Termin über den Link in der E-Mail. Die Reservierung gilt 30 Minuten.
|
||||
|
||||
Im Fuß stehen „Vertrag widerrufen“ als zweiter Knopf (kein bloßer Textlink; für Verbraucher bei jeder Leistung, ob sofort buchbar oder als Anfrage, weil auch ein angenommener Anfrage-Vertrag widerrufen werden kann), „Datenschutz“ (Link auf die Datenschutzerklärung des Betriebs) und klein „Terminbuchung über WEBklar“. Im Modus `knopf` steht der Fuß nur im geöffneten Dialog. Damit die Widerrufsfunktion (§ 356a BGB) trotzdem dauerhaft auffindbar ist, lädt das Widget dort schon beim Laden der Seite die Konfiguration (ein einziger Abruf, sonst nichts) und setzt, wenn der Betrieb ein Widerrufsrecht hat, den Knopf „Vertrag widerrufen“ als zweiten Knopf neben den Buchungsknopf. Er öffnet den Dialog gleich beim Widerrufsformular. Anrede (Sie oder Du) und Betriebsfarbe kommen aus den Einstellungen. Die rechtlich vorgeschriebenen Texte (Haken, Belehrung) stehen immer in der Sie-Form.
|
||||
|
||||
Bedienung nur mit der Tastatur: Tab und Umschalt+Tab zwischen den Elementen, im Monatsraster Pfeiltasten (Tag), Pos1 und Ende (Wochenanfang und -ende), Bild ↑ und Bild ↓ (Monat), Eingabe oder Leertaste wählt den Tag. Im Dialog schließt Esc.
|
||||
|
||||
## 6. Datenschutzerklärung des Kunden
|
||||
|
||||
Das Skript lädt beim Aufruf der Seite die Konfiguration (Texte, Leistungen) von `project.webklar.com`; im Modus `inline` folgen die Termine beim Durchklicken, im Modus `knopf` erst beim Klick auf den Knopf. Dabei sieht der Server die IP-Adresse des Besuchers. Die Datenschutzerklärung der Website braucht deshalb den Absatz „Online-Terminbuchung“ aus `docs/rechtliches/termine-datenschutz-absatz.md` (zwei Fassungen: Sie und Du, nimm die zur Erklärung der Website passende). Die `[[wenn …]]`-Sätze richten sich nach den Einstellungen des Betriebs (Telefon-Feld, Nachricht, Erinnerung, Verbraucher). Bau das Widget erst ein, wenn der Absatz in der Datenschutzerklärung steht. Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar den Absatz beim Einbau ein (Teil der Einrichtung, nicht auf das Stundenkontingent angerechnet); sonst erhält der Kunde den Absatz als Textbaustein per Chat und fügt ihn selbst ein. Die URL der Datenschutzerklärung (und des Impressums) muss in den Betriebsangaben im Portal stehen, sonst fehlt der Link im Fuß.
|
||||
|
||||
## 7. Abnahme-Checkliste
|
||||
|
||||
- [ ] Funktion im Ticketsystem aktiviert (Zustand „aktiv“), `TERMINE_ENABLED` auf `pilot` oder `on`, Kunde im Pilot. `siteKey` und Einbau-Code aus dem Portal (Terminplaner → Einstellungen → „Einbau auf deiner Website“) übernommen.
|
||||
- [ ] Einstellungen im Portal gespeichert, Telefon und Anschrift des Betriebs eingetragen (das Portal weist darauf hin, solange etwas fehlt), URLs von Impressum und Datenschutz mit `https://`.
|
||||
- [ ] Datenschutz-Absatz steht in der Datenschutzerklärung der Website (Abschnitt 6).
|
||||
- [ ] Ausweichtext prüfen: JavaScript aus, Funktion im Ticketsystem ausgeschaltet oder falscher `siteKey`: Der Text steht, nichts ist kaputt.
|
||||
- [ ] Komplett durchbuchen bis „Fast geschafft“; die Bestätigungs-E-Mail kommt an, der Link bestätigt, die zweite E-Mail (mit Kalenderdatei) kommt an, im Portal steht der Termin. Danach den Testtermin absagen (Verwaltungslink aus der E-Mail oder im Portal).
|
||||
- [ ] Bei Verbraucher-Betrieb (auch mit nur Anfrage-Leistungen): Widerrufsbelehrung aufklappbar, bei Preis erscheint der Haken für vorzeitigen Beginn bei Terminen in den nächsten 14 Tagen, „Vertrag widerrufen“ steht als Knopf im Fuß und das Formular funktioniert.
|
||||
- [ ] Modus `knopf` bei Verbraucher-Betrieb: Neben dem Buchungsknopf steht dauerhaft der Knopf „Vertrag widerrufen“ (nicht erst im Dialog) und öffnet den Dialog gleich beim Widerrufsformular; ohne Widerrufsrecht (Unternehmen, Freizeitleistung) fehlt er.
|
||||
- [ ] Nur mit der Tastatur bedienen: Fokus immer sichtbar, Monatsraster mit Pfeilen, Pos1/Ende und Bild ↑/↓, Esc schließt den Dialog und der Fokus geht zum Knopf zurück.
|
||||
- [ ] Handy (390 px Breite): kein seitliches Scrollen, Knöpfe gut treffbar, Dialog passt in den Bildschirm.
|
||||
- [ ] Zoom 200 % und große Schrift: nichts wird abgeschnitten.
|
||||
- [ ] Kontrast der gesetzten Variablen geprüft (Abschnitt 3), globale Regeln des Themes (`button`, `input`) stören nicht.
|
||||
- [ ] Zwei Widgets auf einer Seite (falls geplant) stören sich nicht.
|
||||
- [ ] Anderer Browser, andere Zeitzone (z. B. im Browser auf New York stellen): dieselben Tage und Uhrzeiten wie in Berlin.
|
||||
- [ ] Entwicklerwerkzeuge: keine Cookies, nichts in Local/Session Storage, keine Anfrage an fremde Hosts außer dem Portal.
|
||||
- [ ] Automatische Prüfung gegen die Testseite: `nice node test/ui/termine-widget.ui.mjs` (startet einen eigenen Testserver, Screenshots unter `/root/reports/terminplaner-ui/`).
|
||||
|
||||
## 8. API-Kurzreferenz
|
||||
|
||||
Alle Pfade unter `https://project.webklar.com/api/public/termine/v1`. Antworten sind JSON. Fehler haben die Form `{ "fehler": { "feld": "Text" }, "code": "…" }` (`_` = Fehler ohne Feld). Ist der `siteKey` unbekannt, die Funktion nicht aktiv oder der Betrieb noch nicht buchbar, antworten alle Pfade unter `/:siteKey` mit `200 { "aktiv": false }`. Appwrite nicht erreichbar: `503 { "fehler": { "_": "Gerade nicht erreichbar" } }`. Die Antworten enthalten nie interne IDs.
|
||||
|
||||
| Methode und Pfad | Zweck |
|
||||
| --- | --- |
|
||||
| `GET /:siteKey/config` | Betrieb (Name, Telefon, Adresse, URLs von Impressum, Datenschutz und AGB), Anrede, Farbe, Leistungen mit Knopftext, Personen, Regeln, Zahlungsart, Stornotext, `widerruf`, Texte der Haken und der Widerrufsbelehrung, Datenschutz-Hinweis, `formToken`. Eine Minute gecacht; das Widget lädt sie nach 100 Minuten neu. |
|
||||
| `GET /:siteKey/tage?leistung=&ressource=&monat=YYYY-MM` | Tage mit freien Zeiten plus Kalenderdaten des Monats (`monat`) |
|
||||
| `GET /:siteKey/zeiten?leistung=&ressource=&tag=YYYY-MM-DD` | freie Startzeiten, `datumLang`, `vorzeitigNoetig` (Haken für vorzeitigen Beginn an diesem Tag) |
|
||||
| `POST /:siteKey/buchungen` | Formular absenden. Antwort `201 { ok, email: "m***@gmail.com", minuten }`. Honeypot `kontakt_url` und Formular-Token (Fehlercode `neu_laden`) schützen vor Automaten. |
|
||||
| `POST /:siteKey/widerruf` | Widerruf ohne Link: `{ name, email, nummer }`, Antwort immer gleich `{ ok, eingang: { datum, uhrzeit } }` |
|
||||
| `POST /termin/vorschau`, `bestaetigen`, `absagen`, `verschieben`, `widerruf`, `tage`, `zeiten` | Seiten der Gäste (Token aus dem Link der E-Mail, `termin.html`) |
|
||||
| `GET /kalender/:siteKey/:feedToken.ics` | Kalender-Abo des Betriebs je Person (geheimer Link aus dem Portal) |
|
||||
|
||||
POST-Anfragen sind nur von Seiten des Betriebs erlaubt (Host des Projekts bzw. `liveDomain` samt `www`-Gegenstück) und vom Portal selbst; andere Herkunft: `403`. Grenzen: 120 Abrufe pro Minute und 10 Buchungen pro 10 Minuten je IP, 60 Buchungen pro Stunde je Funktion. Mehr in `server/routes/publicTermine.js`.
|
||||
|
||||
## 9. Testen
|
||||
|
||||
`/embed/termine-demo.html` auf dem Portal ist eine nicht verlinkte Testseite (Strecke inline und als Knopf). Sie braucht einen Testbetrieb mit dem `siteKey` `abcdefghijklmnop`. Automatische Prüfungen: `node --test test/termineEmbedStatic.test.js` (Quelltext) und `nice node test/ui/termine-widget.ui.mjs` (Browser, eigener Testserver mit Nachbau der Datenbank; vorher `free -m`).
|
||||
@@ -0,0 +1,46 @@
|
||||
# Buchungskalender: API-Vertrag
|
||||
|
||||
Verbindlich für Strang 2 (Widget und Buchungsseite, bauen gegen die öffentliche Schnittstelle) und Strang 3 (Portal, baut gegen die Kunden-API). Task 6 bis 8 erfüllen ihn. Fehlerform überall `{ fehler: code, felder?: {feld: text} }`.
|
||||
|
||||
## Livegang-Reihenfolge
|
||||
|
||||
Vor dem Anheben von `UNTERKUNFT_ENABLED` über `off` zuerst `scripts/unterkunft-setup.mjs` (legt die Collections samt Index `idx_account` an) und das termine-Setup (Attribut `anhang` der Mail-Warteschlange) auf Produktion laufen lassen.
|
||||
|
||||
## Öffentlich (`/api/public/unterkunft/v1`)
|
||||
|
||||
- `GET /:siteKey` → `{ aktiv:true, anbieter:{ name, ort, datenschutzUrl }, akzentfarbe, formToken, unterkuenfte:[{ id, name, maxPersonen, mindestNaechte, checkIn, checkOut }] }` oder `{ aktiv:false }`.
|
||||
- `GET /:siteKey/belegung?unterkunft=<id>&von=YYYY-MM-DD&bis=YYYY-MM-DD` → `{ belegt:[{von,bis}], heute:'YYYY-MM-DD', bisMax:'YYYY-MM-DD' }` (Quellen nicht offengelegt). Liegt `bis` hinter `bisMax` (heute + `horizontMonate`), wird es auf `bisMax` gekappt (kein 400); ein Zeitraum mit `bis < von` oder ungültigem Datum ergibt 400 `ungueltig`.
|
||||
- `POST /:siteKey/anfrage` Body `{ unterkunft, von, bis, personen, name, firma, email, telefon, nachricht, formToken, kontakt_url }` → `201 { nummer }`.
|
||||
- `GET /:siteKey/buchung/:id?t=` → `{ status, nummer, akzentfarbe, unterkunft:{name,checkIn,checkOut}, von, bis, naechte, personen, posten, summen, steuerart, fristAt, stornobedingungen, anbieter:{firma,inhaber,strasse,plz,ort,email,telefon,datenschutzUrl}, gast:{name,firma}, rechnungsadresse?, zahlarten:['vorort'|'online'], zahlart?, bezahlt:boolean, rechnungNummer? }`.
|
||||
- `POST /:siteKey/buchung/:id?t=` nach Fristablauf: 410 `{ fehler:'frist_abgelaufen' }`. Body `{ rechnungsadresse:{name,firma,strasse,plz,ort,land}, zahlart }` → `{ status:'gebucht', rechnungNummer, zahlungUrl? }` (bei `online` direkt Checkout-URL).
|
||||
- `POST /:siteKey/buchung/:id/zahlung?t=` → `{ url }`.
|
||||
- `GET /:siteKey/buchung/:id/rechnung.pdf?t=&art=rechnung|storno` → PDF.
|
||||
|
||||
## Kunde (`/api/unterkunft`, Session, `?projectId=` steht überall in der Query, auch bei POST/PUT)
|
||||
|
||||
Antwortformen, die die Portal-Oberfläche erwartet (Geld in Cent, Daten `YYYY-MM-DD`; Fehler überall `{ fehler, felder? }`, nie `tokenHash`/`ipHash`):
|
||||
|
||||
- `GET /status` → `{ state, steuerart?, stripe:{ status:'keins'|'onboarding'|'aktiv', vorhandenesShopKonto }, onlineZahlung, unterkuenfte:Zahl, rechnungenVorhanden }`; bei `state:'off'` zusätzlich `anfrage:{ status:'offen'|'abgelehnt'|'angenommen', at, begruendung?, anfrage:{ art, einheiten, portale, nachricht } } | null`.
|
||||
- `GET /einstellungen` → `{ einstellungen:{ firma, inhaber, strasse, plz, ort, email, telefon, steuernummer, ustId, iban, bic, bank, rechnungsPraefix, akzentfarbe, onlineZahlung:boolean, rechnungAuftrag:boolean }, stripe:{ status, vorhandenesShopKonto }, rechnungenVorhanden }`.
|
||||
- `PUT /einstellungen` Body = genau diese Felder ohne Wrapper. `rechnungAuftrag` (boolean) leitet der Server aus `rechnungAuftragAt` ab: `true` setzt es, falls leer, `false` widerruft (null). `onlineZahlung` nur `true`, wenn Stripe aktiv ist; der Präfix ist bei vorhandenen Rechnungen nicht änderbar. Feldfehler als `felder.<name>`.
|
||||
- `GET /unterkuenfte` → `{ unterkuenfte:[{ id, name, beschreibung, maxPersonen, mindestNaechte, checkIn, checkOut, sort, aktiv, icalImporte:[{ id, name, url, letzterAbrufAt, letzterErfolgAt, fehler }], icalExportUrl }] }` (`icalExportUrl` = volle URL mit Token).
|
||||
- `POST /unterkuenfte`, `PUT /unterkuenfte/:id` Body `{ name, beschreibung, maxPersonen, mindestNaechte, checkIn, checkOut, sort, aktiv }` → `{ unterkunft }`; Deaktivieren = `PUT` mit `aktiv:false`. `DELETE /unterkuenfte/:id`.
|
||||
- `GET|PUT /unterkuenfte/:id/preise` → `{ preise:{ staffeln:[{abNaechten,preisProNachtCent,personenCent?}], reinigungCent, personenInklusive, aufpreisPersonNachtCent, direktrabattProzent, steuerart:'kleinunternehmer'|'regel', ustUebernachtung, ustReinigung, fristTage, stornobedingungen } | null }`; PUT sendet genau diese Felder.
|
||||
- Preismodell „pauschal“ (ohne `personenCent`): `preisProNachtCent` gilt für `personenInklusive` Gäste, jede weitere Person kostet `aufpreisPersonNachtCent` je Nacht (Posten „Zusatzperson“).
|
||||
- Preismodell „je Personenzahl“: jede Staffel trägt `personenCent:[c1,c2,…,ck]` = Preis pro Nacht für die ganze Unterkunft bei 1, 2, …, k Gästen (k = 2 bis 10, Werte ganze Cent 100 bis 100000). Entweder haben alle Staffeln `personenCent` mit gleicher Länge k oder keine; sonst 400 `felder.staffeln`. Der Server setzt `preisProNachtCent = personenCent[0]` (eine mitgeschickte Zahl wird überschrieben). Eine Staffel gilt weiterhin ab ihrer Nächtezahl für alle Nächte. Der Posten heißt „Übernachtung, 7 Nächte, 3 Personen“; `personenInklusive` wird ignoriert. Bei mehr als k Gästen gilt `ck` plus der Posten „Zusatzperson“ (`aufpreisPersonNachtCent`, nur wenn größer 0) für die Gäste über k. Der Direktrabatt gilt auf Übernachtung und Zusatzpersonen.
|
||||
- Kein neues Schema: `personenCent` liegt im JSON-Text `ukPreise.staffeln` (2000 Zeichen; der Höchstfall 6 Staffeln × 10 Werte braucht rund 800).
|
||||
- `GET /unterkuenfte/:id/kalender?von&bis` → `{ bereiche:[{ von, bis (Abreisetag, exklusiv), quelle:'gebucht'|'reserviert'|'ical'|'manuell' (auch 'ical:<importId>', 'anfrage:<id>' zählt als gebucht), anfrageNummer?, sperreId?, notiz? }] }`.
|
||||
- `POST /unterkuenfte/:id/sperren` `{ von, bis, notiz }` → 201, 409 `zeitraum_belegt`; `DELETE /sperren/:id`.
|
||||
- `POST /unterkuenfte/:id/ical-importe` `{ name, url }` (nur https; Feldfehler `felder.url`) → 201; `DELETE /unterkuenfte/:id/ical-importe/:importId`; `POST /unterkuenfte/:id/ical-export/erneuern`.
|
||||
- `GET /anfragen?filter=offen|gebucht|alle` → `{ anfragen:[{ id, nummer, von, bis, naechte, personen, gast:{name}, status, summen:{bruttoCent}|null, unterkunftName }] }`.
|
||||
- `GET /anfragen/:id` → `{ anfrage:{ id, nummer, status, von, bis, naechte, personen, unterkunftId, unterkunftName, gast:{name,firma,email,telefon,nachricht}, rechnungsadresse|null, posten|null, summen|null, steuerart, zahlart|null, bezahltVia|null, bezahltAt|null, erstattetCent, rechnungId|null, rechnungNummer|null, stornoRechnungId|null, fristAt|null, nachrichtVermieter }, vorschlag:{ posten, summen } }` (`vorschlag` = `berechnePreis`).
|
||||
- `POST /anfragen/:id/freigeben` `{ posten, fristTage, nachricht }` → `{ anfrage, buchungsLink }`; `POST /anfragen/:id/ablehnen` `{ text }`; `POST /anfragen/:id/bezahlt`; `POST /anfragen/:id/stornieren` `{ grund, erstattungCent }` (502 `erstattung_fehlgeschlagen`, wenn die Stornierung galt, die Erstattung aber scheiterte).
|
||||
- `POST /preisvorschau` `{ unterkunftId, von, bis, personen }` → `{ naechte, posten, summen:{ bruttoCent } }` (rechnet mit den gespeicherten Preisen).
|
||||
- `GET /rechnungen` → `{ rechnungen:[{ id, nummer, datum, art:'rechnung'|'storno', gastName, bruttoCent (Storno negativ), anfrageNummer? }] }`; `GET /rechnungen/:id.pdf` (nur Einzel-PDF, kein ZIP, Spec N7).
|
||||
- `POST /stripe/verbinden` → `{ url }`; `POST /stripe/vorhandenes-konto` und `POST /stripe/aktualisieren` → `{ stripe:{ status } }`.
|
||||
- `POST /anfrage-funktion` `{ art:'ferienwohnung'|'monteurzimmer'|'pension'|'sonstiges' ('monteur' wird als Alias angenommen), einheiten:1-999, portale:Liste aus 'booking'|'airbnb', nachricht }` → `{ anfrage }`; Feldfehler `felder.art|einheiten|portale|nachricht`; 409, wenn schon eine Anfrage läuft. `POST /anfrage-funktion/zurueckziehen`.
|
||||
|
||||
## Team (`/api/team/unterkunft`)
|
||||
|
||||
- `GET /:customerId` → `{ projects:[{ projectId, name, state, anfrage:{ status:''|'offen'|'angenommen'|'abgelehnt'|'zurueckgezogen', at, art:'ferienwohnung'|'monteurzimmer'|'pension'|'sonstiges', einheiten, portale:boolean, nachricht, entschiedenAt, entschiedenVon, begruendung }, stripe, unterkuenfte:Zahl, offeneAnfragen:Zahl }] }` (Hülle wie `GET /api/team/shop/:customerId`).
|
||||
- `POST /:customerId/anfrage` `{ projectId, entscheidung:'annehmen'|'ablehnen', begruendung }` → `{ ok, anfrage, mailFehler? }`.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Mustertext: Datenschutzerklärung der Kundenwebsite (Buchungskalender)
|
||||
|
||||
**Muster – keine Rechtsberatung.** Entwurf, Prüfung durch Kanzlei offen (`docs/rechtliches/unterkunft-kanzlei-fragen.md`, Frage U12). Der Absatz gehört in die Datenschutzerklärung jeder Kundenwebsite, auf der der Buchungskalender läuft. Der Vermieter ist Verantwortlicher und verantwortet seine Datenschutzerklärung selbst; WEBklar liefert nur diesen Baustein (Leistungsbeschreibung Buchungskalender Nr. 5 Abs. 10 und Nr. 6). Bau das Widget erst ein, wenn der Absatz in der Datenschutzerklärung steht.
|
||||
|
||||
Hinweise für das Team:
|
||||
|
||||
- Liegt die Datenschutzerklärung in der Website, die WEBklar betreut, fügen wir den Absatz beim Einbau ein (gehört zur Einrichtung). Liegt sie woanders, bekommt der Vermieter den Text im Chat und fügt ihn selbst ein.
|
||||
- Platzhalter in eckigen Klammern durch die Angaben des Vermieters ersetzen. Nicht Zutreffendes streichen: Den Absatz „Zahlung über Stripe“ nur, wenn der Vermieter die Online-Zahlung eingeschaltet hat; den Satz zu anderen Plattformen nur, wenn er Kalender einbindet.
|
||||
- Anrede und Ton an die übrige Erklärung der Website anpassen (meist Sie). Die Nummerierung der Überschrift der Website beachten.
|
||||
- Wenn der Vermieter im Impressum oder in der Erklärung einen Datenschutzbeauftragten nennt, dort dazuschreiben.
|
||||
- Die Löschfristen entsprechen dem AVV Buchungskalender Nr. 9 und Abschnitt 6f der Datenschutzerklärung von WEBklar (project.webklar.com/datenschutz.html, nach dem Livegang). Ändert sich dort etwas, hier mitändern. Die Frist für Server-Protokolle (14 Tage) steht in Abschnitt 3 dieser Erklärung.
|
||||
- Das Widget setzt keine Cookies.
|
||||
- Im sichtbaren Text der Kundenwebsite die Wünsche des Vermieters beachten (etwa keine Gedankenstriche).
|
||||
|
||||
---
|
||||
|
||||
## [Nummer]. Anfragen und Buchungen über unseren Buchungskalender
|
||||
|
||||
### Verantwortlicher und technischer Betrieb
|
||||
|
||||
Verantwortlich für die Verarbeitung Ihrer Daten bei einer Anfrage oder Buchung ist [Name des Vermieters, Anschrift, E-Mail-Adresse], also der Vermieter. Den Buchungskalender betreibt technisch die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, in unserem Auftrag. Sie verarbeitet Ihre Daten nur nach unseren Weisungen und nicht für eigene Zwecke (Auftragsverarbeitung nach Art. 28 DSGVO). Der Server steht bei der Hetzner Online GmbH in Falkenstein (Deutschland); der Versand der E-Mails zu Ihrer Anfrage und Buchung läuft über den E-Mail-Dienst der Hetzner Online GmbH in Nürnberg (Deutschland).
|
||||
|
||||
### Welche Daten wir bei einer Anfrage und Buchung verarbeiten
|
||||
|
||||
Wenn Sie über den Buchungskalender anfragen, verarbeiten wir die gewünschte Unterkunft, den Zeitraum und die Zahl der Personen, Ihren Namen, Ihre E-Mail-Adresse und, wenn Sie es angeben, Ihre Firma, Ihre Telefonnummer und Ihre Nachricht. Bitte schreiben Sie keine Angaben zu Ihrer Gesundheit oder anderen besonders schutzbedürftigen Umständen in die Nachricht. Die Verarbeitung ist nötig, um Ihre Anfrage zu beantworten und gegebenenfalls einen Vertrag mit Ihnen zu schließen (Art. 6 Abs. 1 lit. b DSGVO). Ohne diese Angaben können wir Ihre Anfrage nicht bearbeiten. Eine Anfrage ist unverbindlich. Geben wir sie frei, erhalten Sie eine E-Mail mit dem Preis, den Stornobedingungen und einem Link zur Buchungsseite; erst dort buchen Sie verbindlich.
|
||||
|
||||
Wenn Sie buchen, verarbeiten wir zusätzlich Ihre Rechnungsadresse (Name oder Firma, Straße, Postleitzahl, Ort, Land), die gewählte Zahlart, die Buchung mit ihrem Stand (zum Beispiel gebucht, bezahlt, storniert) und die Rechnung. Die Rechnung wird in unserem Namen und in unserem Auftrag von der WEBklar GbR erstellt (§ 14 Abs. 2 Satz 4 UStG) und Ihnen als PDF mit der Buchungsbestätigung gesendet.
|
||||
|
||||
Außerdem speichern wir in der Anfrage einen Hashwert Ihrer IP-Adresse (nicht die IP-Adresse selbst; er wird mit einem geheimen Schlüssel des Servers berechnet, sodass sich daraus keine IP-Adresse ableiten lässt). Wir begrenzen damit die Zahl der Anfragen je IP-Adresse und schützen den Buchungskalender so vor automatisierten Massenanfragen. Das ist unser berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO); Sie können dieser Verarbeitung widersprechen (Art. 21 DSGVO). Wir verwenden den Hashwert nur dafür und entfernen ihn, sobald wir Ihre Anfrage freigeben oder ablehnen, spätestens nach 30 Tagen.
|
||||
|
||||
### Zahlung über Stripe
|
||||
|
||||
[Nur aufnehmen, wenn die Online-Zahlung eingeschaltet ist.] Wenn Sie online zahlen, werden Sie auf die Zahlungsseite der Stripe Payments Europe, Limited, 1 Grand Canal Street Lower, Grand Canal Dock, Dublin, D02 H210, Irland („Stripe“) weitergeleitet. Die Zahlung geht direkt auf unser Stripe-Konto. Online können Sie derzeit nur per Karte zahlen (auch mit Apple Pay und Google Pay), nicht per SEPA-Lastschrift. Kartendaten geben Sie nur dort ein; wir und der technische Betreiber des Buchungskalenders erhalten sie nicht. Beim Start der Zahlung übergibt der technische Betreiber Stripe die Bezeichnung des Aufenthalts mit der Rechnungsnummer, den Betrag, die Kennungen der Buchung, Ihre E-Mail-Adresse (zur Vorbelegung der Zahlungsseite) und als Rücksprungadresse den geheimen Link zur Buchungsseite; dieser Link ist damit für Stripe und in unserem Stripe-Dashboard sichtbar, gilt aber nur bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur Ihre eine Buchungsseite. Geht eine Zahlung erst nach einer Stornierung oder nach dem Ablauf der Reservierung ein oder zahlen Sie eine bereits bezahlte Buchung ein zweites Mal, wird der Betrag automatisch in voller Höhe an Sie zurückerstattet. Stripe verarbeitet Ihre Daten teils im Rahmen der Zahlungsabwicklung für uns und teils als eigener Verantwortlicher, zum Beispiel zur Betrugsvorbeugung und zur Erfüllung gesetzlicher Pflichten; dafür gilt die Datenschutzerklärung von Stripe (stripe.com/de/privacy). Dabei können Daten an die Stripe, LLC in den USA übermittelt werden. Die Stripe, LLC ist nach dem EU-US Data Privacy Framework zertifiziert, für das ein Angemessenheitsbeschluss der EU-Kommission besteht. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO. Zahlen Sie vor Ort, entfällt dieser Absatz für Sie.
|
||||
|
||||
### E-Mails zu Ihrer Anfrage und Buchung
|
||||
|
||||
Zu Ihrer Anfrage und Buchung senden wir Ihnen E-Mails an die angegebene E-Mail-Adresse: eine Bestätigung, dass Ihre Anfrage eingegangen ist, die Freigabe mit dem Link zur Buchungsseite oder die Absage, die Buchungsbestätigung mit den Vertragsangaben, den Stornobedingungen und der Rechnung, die Bestätigung eines Zahlungseingangs, einen Hinweis, wenn eine Reservierung abgelaufen ist, und bei einer Stornierung die Stornorechnung. Das sind Mitteilungen zur Abwicklung Ihres Vertrags (Art. 6 Abs. 1 lit. b DSGVO), keine Werbung. Bis zum Versand liegt jede E-Mail in einer Warteschlange; nach dem Versand wird Empfängeradresse und Inhalt dort gelöscht, der Eintrag selbst nach sieben Tagen. Der Link zur Buchungsseite enthält ein geheimes Kennzeichen, das wir nur als Prüfsumme speichern und das bei einer Online-Zahlung an Stripe übergeben wird (siehe oben); behandeln Sie ihn vertraulich.
|
||||
|
||||
### Besuch der Seite
|
||||
|
||||
Beim Aufruf der Seite lädt Ihr Browser das Programm des Buchungskalenders und die Belegung von der Adresse project.webklar.com. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem übertragen und für 14 Tage in Server-Protokollen gespeichert, um den sicheren und störungsfreien Betrieb zu gewährleisten (Art. 6 Abs. 1 lit. f DSGVO). Der Buchungskalender setzt keine Cookies. Zum Schutz vor Missbrauch begrenzen wir die Zahl der Anfragen je IP-Adresse; die Zeitpunkte der Aufrufe werden nur im Arbeitsspeicher des Servers gehalten. Mit Ihrer Anfrage wird Ihre IP-Adresse nicht gespeichert, sondern nur der oben beschriebene Hashwert.
|
||||
|
||||
### Abgleich mit anderen Plattformen
|
||||
|
||||
[Nur aufnehmen, wenn der Vermieter Kalender einbindet.] Wir gleichen die Belegung unserer Unterkünfte mit [Booking.com, Airbnb, …] über Kalenderdateien (iCal) ab, damit eine Unterkunft nicht doppelt vergeben wird. Dabei werden nur belegte Zeiträume ausgetauscht, keine Namen oder anderen Angaben zu Gästen.
|
||||
|
||||
### Weitergabe
|
||||
|
||||
Wir geben Ihre Daten nur weiter, soweit das für die Vertragsabwicklung nötig ist (technischer Betreiber des Buchungskalenders als Auftragsverarbeiter, Stripe bei Online-Zahlung) oder wir gesetzlich dazu verpflichtet sind (zum Beispiel an das Finanzamt oder, soweit vorgeschrieben, an die Meldebehörde). Eine Weitergabe zu Werbezwecken findet nicht statt.
|
||||
|
||||
### Speicherdauer
|
||||
|
||||
Anfragen, aus denen keine Buchung wird, anonymisiert das System nach sechs Monaten. Für gebuchte Aufenthalte sind wir als Vermieter verpflichtet, Rechnungen und Buchungsbelege nach Handels- und Steuerrecht aufzubewahren (in der Regel acht bis zehn Jahre, § 257 HGB, § 147 AO; Art. 6 Abs. 1 lit. c DSGVO); dafür bewahrt das System die Rechnungen und die Rechnungsadresse acht Jahre auf, solange der Buchungskalender läuft. Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht eines gebuchten Aufenthalts werden drei Jahre nach dem Ende des Jahres Ihrer Abreise anonymisiert.
|
||||
|
||||
### Ihre Rechte
|
||||
|
||||
Sie haben das Recht auf Auskunft über Ihre Daten (Art. 15 DSGVO), auf Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung der Verarbeitung (Art. 18), Datenübertragbarkeit (Art. 20) und auf Widerspruch gegen Verarbeitungen, die auf Art. 6 Abs. 1 lit. f DSGVO beruhen (Art. 21). Wenden Sie sich dazu an uns unter [E-Mail-Adresse des Vermieters]; wir leiten Ihre Anfrage, soweit nötig, an den technischen Betreiber des Buchungskalenders weiter. Außerdem können Sie sich bei einer Datenschutz-Aufsichtsbehörde beschweren, etwa bei der für uns zuständigen Behörde [zuständige Aufsichtsbehörde des Vermieters].
|
||||
@@ -0,0 +1,91 @@
|
||||
# Buchungskalender auf der Kundenwebsite einbauen
|
||||
|
||||
Anleitung für das WEBklar-Team. Das Widget lässt Besucher auf der Website des Kunden („Vermieter“) einen Zeitraum im Kalender wählen und eine unverbindliche Anfrage senden. Unterkünfte, Preise, Anfragen, Rechnungen und die Verbindung zu Stripe pflegt der Vermieter im Kundenportal (Tab „Funktionen“, Kachel „Buchungskalender“, Reiter „Buchungen“). Den `siteKey` (16 Zeichen, a–z und 2–7) findest Du im Einrichtungs-Ticket und im Ticketsystem (WOMS): Kunde → Reiter „Funktionen“, Zeile Buchungskalender. Im Ticket stehen Snippet und Schritte schon fertig; dieses Dokument ist die Referenz dahinter. Ohne aktive Funktion oder ohne freigeschaltete Unterkunft ist nichts buchbar: Das Widget zeigt dann den Ausweichtext (Abschnitt 4). API-Vertrag: `docs/funktionen/unterkunft-api.md`.
|
||||
|
||||
Der Buchungskalender ist an den Vertrag über den Betrieb der Website gekoppelt (Leistungsbeschreibung Buchungskalender Nr. 10), aber **nicht in der Software**: Das Portal schaltet ihn nicht von selbst ab. Das Abschalten zum Vertragsende machen wir von Hand (Abschnitt 8).
|
||||
|
||||
## 1. Das Snippet
|
||||
|
||||
```html
|
||||
<div data-webklar-unterkunft="SITEKEY" data-api="https://project.webklar.com">
|
||||
<p>Der Buchungskalender ist gerade nicht erreichbar. Bitte schreiben Sie uns: info@beispiel.de</p> <!-- Ausweichtext, Abschnitt 4 -->
|
||||
</div>
|
||||
<script src="https://project.webklar.com/embed/unterkunft.js" defer></script>
|
||||
```
|
||||
|
||||
Das Skript steht einmal pro Seite, vor `</body>`. Es hat keine Abhängigkeiten, setzt keine Cookies, lädt nichts von Dritten und fragt nur `project.webklar.com` an. Texte des Vermieters setzt das Skript nur als Text ein, nie als HTML. Das Widget rechnet nie selbst: Preise, Summen und belegte Nächte kommen fertig vom Server (die Anfrage ist unverbindlich; der Preis wird erst bei der Freigabe festgelegt).
|
||||
|
||||
| Attribut | Pflicht | Bedeutung |
|
||||
| --- | --- | --- |
|
||||
| `data-webklar-unterkunft` | ja | der `siteKey` des Projekts |
|
||||
| `data-api` | nein | Herkunft des Portals, Standard `https://project.webklar.com`. Bei Modul-Import, Tag-Manager oder nachgeladenem Skript ist `data-api` Pflicht. |
|
||||
|
||||
## 2. Wohin damit
|
||||
|
||||
1. Auf der Seite einen **Bereich „Anfrage“** (Anker `#anfrage` oder eine eigene Seite) anlegen und das Snippet an die Stelle des Formulars setzen. Ein bisheriges Anfrageformular wird ersetzt; Telefon und E-Mail des Vermieters stehen unter dem Kalender. Das `div` braucht Breite (normaler Blockcontainer; in einem Flex- oder Grid-Element ohne Breite schrumpft es auf null).
|
||||
2. Layout: Das Widget zeigt zwei Spalten (Kalender links, Formular rechts) und unter 760 px Breite beide untereinander; der Kalender zeigt einen Monat, ab 1100 px Breite zwei.
|
||||
3. Menüpunkt oder Knopf „Anfragen“ auf den Bereich verlinken.
|
||||
4. **Mehrere Widgets** auf einer Seite sind nicht vorgesehen. Der Buchungskalender ist ein Widget je Website; mehrere Unterkünfte wählt der Gast im Widget (bei nur einer Unterkunft entfällt die Auswahl).
|
||||
|
||||
Aussehen: Das Widget nimmt die Akzentfarbe aus den Einstellungen des Vermieters. Die Stile stehen in `@layer wku`, jede Regel der Kundenseite gewinnt also. Anpassen per CSS-Variablen auf dem `div`, in der Seiten-CSS oder auf `:root`, zum Beispiel `[data-webklar-unterkunft]{ --wku-akzent:#8a5a2b; --wku-akzent-text:#fff; }`:
|
||||
|
||||
| Variable | Standard |
|
||||
| --- | --- |
|
||||
| `--wku-akzent` und `--wku-akzent-text` (Auswahl, Knöpfe; setze beide zusammen) | Akzentfarbe des Vermieters, sonst Standardblau und `#fff` |
|
||||
| `--wku-hintergrund` | `#fff` |
|
||||
| `--wku-text` | `#1f2937` |
|
||||
| `--wku-rand` | `#858f9e` |
|
||||
| `--wku-radius` | `10px` |
|
||||
| `--wku-schrift`, `--wku-schrift-titel` | Schrift der Seite |
|
||||
|
||||
Kontrast (WCAG 2.1 AA): Text auf dem Hintergrund und Schrift auf dem Akzent mindestens 4,5 : 1, Rand gegen den Hintergrund mindestens 3 : 1. Prüfe beim Abnehmen, ob globale Regeln des Themes (`button{…}`, `input{…}`) das Widget stören, und überschreibe sie gezielt (`[data-webklar-unterkunft] .wku-btn{ … }`).
|
||||
|
||||
## 3. Datenschutz-Absatz (Pflicht vor dem Einbau)
|
||||
|
||||
Bau das Widget erst ein, wenn der Absatz aus `docs/funktionen/unterkunft-datenschutz-absatz.md` in der Datenschutzerklärung der Website steht. Liegt sie in der Website, die WEBklar betreut, fügst Du ihn ein (gehört zur Einrichtung), Platzhalter und nicht Zutreffendes angepasst. Liegt sie woanders, schickst Du ihn dem Vermieter im Chat (Leistungsbeschreibung Nr. 5 Abs. 10). Dasselbe gilt für Impressum und Verlinkung der Datenschutzerklärung: Die Adresse der Datenschutzerklärung trägt der Vermieter in den Einstellungen ein; das Widget und die Buchungsseite verlinken sie.
|
||||
|
||||
## 4. Ausweichtext
|
||||
|
||||
Der Inhalt des `div` bleibt stehen, solange das Widget nicht lädt oder die Funktion nicht aktiv ist (zum Beispiel vor der Freischaltung, bei `UNTERKUNFT_ENABLED=off`, bei Wartung). Schreib dort Kontaktdaten des Vermieters hinein, damit niemand ohne Möglichkeit zur Anfrage bleibt. Ein aktives Widget ersetzt den Inhalt.
|
||||
|
||||
## 5. Vor der Abnahme prüfen
|
||||
|
||||
- Im Portal: mindestens eine aktive Unterkunft mit Preisen und Stornobedingungen, Firmendaten vollständig, Steuernummer oder USt-ID, Steuerart gewählt. Sonst ist die Funktion nicht freischaltbar.
|
||||
- Der Datenschutz-Absatz steht in der Datenschutzerklärung.
|
||||
- Auf dem Handy und am Desktop: Anreise und Abreise wählbar, belegte Nächte nicht wählbar, Mindestnächte und Höchstzahl der Personen werden geprüft, Anfrage senden zeigt die Nummer.
|
||||
- Tastatur: Pfeiltasten im Kalender, Enter wählt; jeder Tag hat ein `aria-label` mit Datum und Status.
|
||||
- Kein Konflikt mit globalen Stilen des Themes (Abschnitt 2).
|
||||
- Testanfrage mit eigener Adresse: E-Mail G16 (Eingang) beim Gast, K36 beim Vermieter.
|
||||
|
||||
## 6. Vorschau und Testanfrage
|
||||
|
||||
1. Vorschau-Subdomain des Projekts (`<subdomain>.project.webklar.com`) ansehen.
|
||||
2. Anfrage senden, im Portal unter „Buchungen → Anfragen“ öffnen, Preis prüfen, **Freigeben** (E-Mail G17 mit Buchungslink).
|
||||
3. Buchungslink öffnen: Rechnungsadresse eintragen, Zahlart „vor Ort“ wählen, **„Zahlungspflichtig buchen“**. E-Mail G19 mit Rechnungs-PDF prüfen (Rechnung trägt Name und Daten des Vermieters, keinen WEBklar-Hinweis).
|
||||
4. Online-Zahlung nur testen, wenn das Stripe-Konto verbunden ist (kleiner Betrag, danach im Portal stornieren und erstatten).
|
||||
5. Im Portal „bezahlt (vor Ort)“ setzen, dann stornieren (Stornorechnung, E-Mail G22).
|
||||
6. Testanfragen danach im Portal stornieren oder ablehnen; Sperren des Tests im Kalender entfernen.
|
||||
|
||||
## 7. Was das Widget zeigt (Kurzüberblick)
|
||||
|
||||
- Kalender mit belegten Nächten (durchgestrichen); vergangene Tage und Zeiträume über eine belegte Nacht hinweg sind nicht wählbar. Anreise am Abreisetag eines anderen Gastes ist möglich.
|
||||
- Zusammenfassung des Zeitraums („12.–30. Nov. · 18 Nächte“), Personenzahl, Name, E-Mail, Telefon und Nachricht (optional), Firma (optional), Datenschutzhinweis.
|
||||
- Schaltfläche „Anfrage senden“ (unverbindlich). Danach die Anfragenummer und der Hinweis, dass der Vermieter sich meldet.
|
||||
- Die Buchung selbst läuft nicht im Widget, sondern auf der Buchungsseite bei WEBklar (Link in der E-Mail G17).
|
||||
|
||||
## 8. Wenn der Vertrag über die Website endet (Abschalten von Hand)
|
||||
|
||||
Der Buchungskalender läuft, solange WEBklar die Website des Vermieters betreibt (Leistungsbeschreibung Nr. 10). Die Software prüft das **nicht**. Du musst es selbst im Blick haben, wenn ein solcher Vertrag endet, gekündigt oder widerrufen wird:
|
||||
|
||||
1. **Vorher** schreibst Du dem Vermieter eine E-Mail (oder Chat): Der Vertrag endet am TT.MM.JJJJ, damit endet auch der Buchungskalender. Er soll **seine Rechnungen als PDF herunterladen** (WEBklar löscht sie 90 Tage nach dem Abschalten), offene Anfragen bearbeiten und seine Gäste informieren.
|
||||
2. **Zum Vertragsende** im Ticketsystem (WOMS): Kunde → Reiter „Funktionen“ → Buchungskalender → **„Abschalten“**. Danach nimmt das Widget keine Anfragen mehr an und zeigt den Ausweichtext; Reservierungen laufen aus.
|
||||
3. Der Vermieter kann Anfragen, Buchungen und Rechnungen danach **90 Tage** lang im Portal lesen und die Rechnungen herunterladen. Danach löscht der tägliche Job automatisch alle Daten des Buchungskalenders; das Einrichtungs-Ticket bleibt als Arbeitsnachweis ohne Gästedaten.
|
||||
4. Nimm nach den 90 Tagen das Snippet und den Menüpunkt aus der Website.
|
||||
5. Beginnt **innerhalb der 90 Tage** wieder ein Vertrag über den Betrieb der Website, schaltest Du den Buchungskalender auf Wunsch des Vermieters in Textform ohne weitere Kosten wieder ein (WOMS → Funktionen → Buchungskalender → Aktivieren).
|
||||
6. **Erstattung (Leistungsbeschreibung Nr. 10 Abs. 6):** Endet der Vertrag, bevor wir den Buchungskalender freigeschaltet haben, erstatten wir den gezahlten Einrichtungspreis (199 €) vollständig, in Stripe von Hand. Sonst gibt es keine Erstattung.
|
||||
|
||||
Das Stripe-Konto des Vermieters bleibt in jedem Fall beim Vermieter; wir rühren es nicht an.
|
||||
|
||||
## 9. Kalenderabgleich (iCal) einrichten
|
||||
|
||||
Im Portal unter Buchungen → Unterkünfte & Preise → Kalenderabgleich: Der Vermieter trägt die iCal-Adresse von Booking.com oder Airbnb als Import ein (Name und `https`-Adresse) und kopiert die **Export-Adresse** des Buchungskalenders zu Booking.com („Kalender synchronisieren“) oder Airbnb. Der Abgleich ist zeitversetzt (Minuten bis Stunden, Leistungsbeschreibung Nr. 4); sag das dem Vermieter, damit er vor jeder Freigabe seine anderen Plattformen prüft.
|
||||
@@ -0,0 +1,125 @@
|
||||
# Buchungskalender – Livegang
|
||||
|
||||
Reihenfolge für den Inhaber. Jeder Schritt braucht seine Freigabe; nichts davon hat Claude gegen Produktion, das Live-Stripe-Konto oder die Website Donau Home ausgeführt. Code: Portal `feature/unterkunft`, WOMS `feature/unterkunft`. Erste Kundin ist Mimi (Donau Home, MIMI FEWO, Website `WEBklar/donau-home`, lokaler Klon `/root/reports/donau-home-website`).
|
||||
|
||||
**Wichtig beim Pushen:** Den Feature-Zweig nie unter seinem eigenen Namen pushen. Der Portal-Webhook deployt jeden Zweig. Immer mit Zielzweig:
|
||||
|
||||
- Portal: `git push origin feature/unterkunft:main`
|
||||
- WOMS: `git push origin feature/unterkunft:test`
|
||||
|
||||
## 0. Voraussetzung
|
||||
|
||||
- TP2, Webshop und Öffnungszeiten-Zweig liegen schon auf `origin/main` (Portal) bzw. `test` (WOMS) oder werden vorher gemerged: Das Funktionen-Register, die Mail-Warteschlange (`termineOutbox`) und die Connect-Anbindung stammen von dort. `git merge origin/main` im Worktree, Tests laufen lassen. Erwartbare Konflikte: `server/config.js` (features), `public/funktionen.js`, `test/mailTemplates.test.js` (Vorlagen-IDs: K36 bis K35, G16 bis G22, T23 sind für den Buchungskalender vergeben, vor dem Merge gegen `main` prüfen).
|
||||
- Die Rechtstexte der Fassung 2026-10-07 liegen als HTML und PDF im Zweig (`public/rechtliches/*-2026-10-07.*`, siehe Schritt 6).
|
||||
|
||||
## 1. Inhaber prüft die Texte
|
||||
|
||||
Vom Inhaber freizugeben, bevor irgendetwas live geht:
|
||||
|
||||
- `docs/rechtliches/leistungsbeschreibung-unterkunft.md` und `avv-unterkunft.md` (beide „Entwurf – Prüfung durch Kanzlei offen“),
|
||||
- `docs/rechtliches/unterkunft-datenschutz-6f-entwurf.md`,
|
||||
- `docs/funktionen/unterkunft-datenschutz-absatz.md` und `unterkunft-storno-muster.md`,
|
||||
- `docs/rechtliches/unterkunft-kanzlei-fragen.md` (Fragen U1 bis U12 für die Kanzlei).
|
||||
|
||||
**Achtung Widerrufsbelehrung:** Die Datei `widerrufsbelehrung-funktionen-2026-10-07.*` ist eine Kopie des gemeinsamen Textes der Funktionen (Quelle `docs/rechtliches/widerrufsbelehrung-funktionen.md`). Ändert sich dieser Text (neue Fassung beim Terminplaner oder Webshop), braucht der Buchungskalender eine neue Fassung mit neuem Datum; eine ausgelieferte Datei wird nie an Ort und Stelle neu gebaut.
|
||||
|
||||
Ändert sich ein Text, gibt es eine neue Fassung (neues Datum in `config.unterkunft.fassung` und im Zustimmungs-Pin der Bestelltexte); ausgelieferte Fassungen werden nie überschrieben.
|
||||
|
||||
## 2. Datenbank (nach dem Portal-Deploy, vor dem Pilot)
|
||||
|
||||
**Zeitpunkt:** Die Skripte laufen mit dem NEUEN Code, also erst nach dem Portal-Deploy (Schritt 5) und vor dem Einschalten des Piloten (`UNTERKUNFT_ENABLED=pilot`, Schritt 4 Teil 2), oder vorher aus dem Worktree `dev-unterkunft` (`node scripts/…` mit denselben Umgebungswerten gegen die Produktionsdatenbank). Der Schalter bleibt bis zum grünen `--check` auf `off`. Reihenfolge der Skripte: erst `termine-setup.mjs --check` und anwenden (Attribut `anhang`), dann `unterkunft-setup.mjs`:
|
||||
|
||||
```bash
|
||||
cd /home/webklar/apps/kundenbereich.webklar.com/build
|
||||
docker exec -w /app webklar-kundenbereich-server node scripts/termine-setup.mjs --check
|
||||
docker exec -w /app webklar-kundenbereich-server node scripts/termine-setup.mjs # zieht das Attribut anhang der Mail-Warteschlange nach (Rechnung als Anhang)
|
||||
docker exec -w /app webklar-kundenbereich-server node scripts/unterkunft-setup.mjs --check
|
||||
docker exec -w /app webklar-kundenbereich-server node scripts/unterkunft-setup.mjs # legt ukUnterkuenfte, ukPreise, ukSperren, ukAnfragen, ukRechnungen, ukEinstellungen samt Indizes an
|
||||
```
|
||||
|
||||
Wichtige Indizes (ohne sie sind Suche, Eindeutigkeit und Abfragen unsicher oder langsam; das Skript legt sie an, `--check` meldet fehlende): `idx_account` (ukEinstellungen, stripeAccountId), `idx_export_token` (ukUnterkuenfte, icalExportToken), `uniq_feature_nummer` (ukRechnungen, featureId+nummer, **unique**: verhindert doppelte Rechnungsnummern), `idx_bis` (ukSperren und ukAnfragen, Belegungsabfrage). Fehlt `termineOutbox.anhang`, meldet `unterkunft-setup.mjs --check` es ebenfalls (Exit 1) und der normale Lauf legt es an.
|
||||
|
||||
Die Skripte müssen aus dem neuen Code laufen (siehe Zeitpunkt oben). `termine-setup.mjs` ändert nichts an vorhandenen Daten, es fügt nur fehlende Attribute und Indizes hinzu.
|
||||
|
||||
## 3. Stripe-Preis (ebenfalls nach dem Portal-Deploy oder aus dem Worktree, vor dem Pilot)
|
||||
|
||||
```bash
|
||||
docker exec -w /app webklar-kundenbereich-server node scripts/stripe-products.mjs --pruefen
|
||||
docker exec -w /app webklar-kundenbereich-server node scripts/stripe-products.mjs --anlegen # legt unterkunftSetup (199 €, Lookup-Key webklar_unterkunft_einrichtung_19900) an
|
||||
docker exec -w /app webklar-kundenbereich-server node scripts/stripe-products.mjs --pruefen # Preis-Lookup nach --anlegen prüfen
|
||||
```
|
||||
|
||||
Der vorhandene Connect-Webhook (`https://project.webklar.com/api/shop/stripe-webhook`) bleibt unverändert; er leitet nach dem Metadatum `wkUk` an den Buchungskalender weiter. Kontrolle im Stripe-Dashboard: Die Ereignisse `account.updated`, `checkout.session.completed`, `checkout.session.async_payment_succeeded`, `checkout.session.async_payment_failed`, `checkout.session.expired` und `charge.refunded` sind schon eingetragen (Webshop-Livegang).
|
||||
|
||||
## 4. Server-Konfiguration (Pilot)
|
||||
|
||||
Sicherungen zuerst: `cp /home/webklar/apps/docker-compose.yml /home/webklar/apps/docker-compose.yml.bak-2026-10-07-unterkunft` und `(umask 077; cp /home/webklar/apps/.env /home/webklar/apps/.env.bak-2026-10-07-unterkunft)` (`.env` enthält Zugangsdaten).
|
||||
|
||||
**Teil 1 (vor dem Push):** In `/home/webklar/apps/.env` zunächst nur
|
||||
```
|
||||
UNTERKUNFT_ENABLED=off
|
||||
UNTERKUNFT_PILOT_EMAILS=kenso.gri@gmail.com,<Mimis Portal-Adresse>
|
||||
```
|
||||
In `/home/webklar/apps/docker-compose.yml` beim Dienst `kundenbereich-server` neben `ZEITEN_ENABLED` und `SHOP_ENABLED` zwei Zeilen:
|
||||
```
|
||||
- UNTERKUNFT_ENABLED=${UNTERKUNFT_ENABLED:-off}
|
||||
- UNTERKUNFT_PILOT_EMAILS=${UNTERKUNFT_PILOT_EMAILS:-}
|
||||
```
|
||||
**Teil 2 (nach Push, grünem Portal-Deploy und Schritt 2/3):** `UNTERKUNFT_ENABLED=pilot` setzen und Compose neu anwenden (derselbe Befehl).
|
||||
|
||||
Prüfen mit `docker compose -f /home/webklar/apps/docker-compose.yml config -q` (keine Ausgabe = gültig), dann übernehmen mit `docker compose -f /home/webklar/apps/docker-compose.yml up -d --no-deps kundenbereich-server`. Ein bloßes `docker restart` liest neue Umgebungswerte nicht. Das alles vor dem Push (Schritt 5): Der Container läuft dann mit dem alten Code und den neuen Werten (die kennt er nicht, harmlos), und der Neustart durch den Deploy nach dem Push ist der einzige mit dem neuen Code. Rückweg: `UNTERKUNFT_ENABLED=off`, Compose neu anwenden; das Schema ist rein additiv, es gibt nichts zurückzubauen.
|
||||
|
||||
## 5. Push (durch den Inhaber)
|
||||
|
||||
- Portal: `git push origin feature/unterkunft:main` (der Webhook deployt),
|
||||
- WOMS: `git push origin feature/unterkunft:test` **erst nach grünem Portal-Deploy** (das WOMS ruft die neue Portal-Schnittstelle, z. B. das Feld `unterkunft` der Funktionen-Antwort, auf).
|
||||
|
||||
Nie `git push origin feature/unterkunft` (eigener Name). Danach (Portal) prüfen: `https://project.webklar.com/embed/unterkunft.js` antwortet 200; `https://project.webklar.com/rechtliches/leistungsbeschreibung-unterkunft.html` und `…/avv-unterkunft.html` antworten 200.
|
||||
|
||||
## 6. Rechtstexte in WOMS „Rechtliches“ hochladen (macht Claude)
|
||||
|
||||
Das macht Claude selbst (Wunsch des Inhabers). Dafür legt der Inhaber vorher den **kurzlebigen** Appwrite-Schlüssel mit dem Recht für Rechtliches unter `/root/.appwrite-rechtliches.key` ab und löscht ihn danach. Claude legt (falls nötig, `scripts/setup-legal.mjs` im WOMS) die Karten „Leistungsbeschreibung Buchungskalender“ und „AVV Buchungskalender“ an und lädt die Fassung 2026-10-07 hoch:
|
||||
|
||||
- `public/rechtliches/leistungsbeschreibung-unterkunft-2026-10-07.pdf`
|
||||
- `public/rechtliches/avv-unterkunft-2026-10-07.pdf`
|
||||
|
||||
(Die Widerrufsbelehrung `widerrufsbelehrung-funktionen-2026-10-07.pdf` ist ein Anhang der Bestellmail und der Bestellseite; sie ist inhaltlich die der Funktionen.)
|
||||
|
||||
## 7. Mimi freischalten und Donau Home einbauen
|
||||
|
||||
**Für Mimi ist der Buchungskalender kostenlos** (Entscheidung des Inhabers, 2026-10-07): keine Funktions-Anfrage, keine Bestellung und keine Zahlung der 199 €, kein Stripe-Checkout für die Einrichtung; freigeschaltet wird nur über „Aktivieren (ohne Zahlung)“. In der Textform-Nachricht an Mimi ausdrücklich schreiben, dass die Einrichtung für sie kostenlos ist (abweichend vom Preis in der Leistungsbeschreibung, Abschnitt „9. Preis, Abrechnung und Widerruf“ Abs. 1), damit keine Zahlungspflicht entsteht.
|
||||
|
||||
0. **Vorher:** die Zustimmung von Mimi in Textform einholen (Chat im Kundenportal oder E-Mail: Leistungsbeschreibung und AVV Buchungskalender in der Fassung 2026-10-07). Datum und Weg trägt das Team beim Aktivieren im WOMS ein; ohne beides schaltet das Portal nicht ein. Das Portal speichert den Nachweis und schickt Mimi die Unterlagen als PDF (K16).
|
||||
1. WOMS → Kunde (Mimi) → Funktionen → Buchungskalender → **„Aktivieren (ohne Zahlung)“** (`action: 'activate'`; der Weg für Kunden, die die Funktion als Teil ihres Projekts bekommen; verlangt Datum und Weg der Zustimmung in Textform, auch im Pilot).
|
||||
2. Mimi richtet im Portal ein: Unterkünfte, Preise, Stornobedingungen (Muster `docs/funktionen/unterkunft-storno-muster.md`), Firmendaten, Steuerart, optional Stripe; mit dem Team zusammen.
|
||||
3. **siteKey in Donau Home:** den `siteKey` aus dem Ticket oder dem WOMS in `/root/reports/donau-home-website/index.html` im Abschnitt `#anfrage` einsetzen (`data-webklar-unterkunft="SITEKEY"`), das Skript `https://project.webklar.com/embed/unterkunft.js` einbinden, CSS-Variablen (`--wku-*`) auf Palette A (Eiche & Leinen) und Schrift Figtree stellen, den Datenschutz-Absatz in die Datenschutzerklärung der Website einfügen, den Einleitungstext anpassen. Einbau nach `docs/funktionen/unterkunft-einbau.md`.
|
||||
4. Donau Home pushen (Inhaber, nicht Claude); die Vorschau zeigt das Widget, solange `UNTERKUNFT_ENABLED=pilot` mit Mimis siteKey läuft.
|
||||
|
||||
## 8. Pilot mit Mimi
|
||||
|
||||
1. Testanfrage im Widget (eigene Adresse): Mails G16 und K36 prüfen.
|
||||
2. Im Portal freigeben (G17), Buchungslink öffnen, „vor Ort“ buchen (G19 mit Rechnung als PDF, K37).
|
||||
3. Rechnung prüfen: Mimis Name und Anschrift, Steuernummer oder USt-ID, Nummer lückenlos, Steuerart, kein WEBklar-Hinweis.
|
||||
4. Mit Stripe-Konto: Online-Zahlung mit kleinem Betrag testen (G20), dann stornieren mit Erstattung (G22, Stornorechnung).
|
||||
5. Stornieren (Stornorechnung), Testbuchungen wieder entfernen; iCal-Import und -Export mit einem Test-Kalender prüfen.
|
||||
|
||||
## 9. Datenschutz § 6f übernehmen (VOR dem Pilotstart, also vor Schritt 8)
|
||||
|
||||
Den Abschnitt 6f aus `docs/rechtliches/unterkunft-datenschutz-6f-entwurf.md` in `public/datenschutz.html` übernehmen (hinter 6d, vor Abschnitt 7; Abschnitt 12 anpassen), danach `FORCE=1 scripts/build-legal-pdfs.sh datenschutz`. Erst mit der Freigabe des Inhabers, aber vor dem ersten Gast-Test mit Mimi: Die Anfrageformulare verweisen auf die Datenschutzerklärung.
|
||||
|
||||
## 10. Freigabe für alle
|
||||
|
||||
- Kanzlei-Fragen (`unterkunft-kanzlei-fragen.md`) beantworten lassen, Befund einarbeiten (neue Fassung, falls sich Texte ändern; neue Texte hochladen wie in Schritt 6).
|
||||
- Dann `UNTERKUNFT_ENABLED=on` in `.env`, Compose neu anwenden (wie Schritt 4).
|
||||
|
||||
## PDFs der Rechtstexte erzeugen
|
||||
|
||||
Die PDFs entstehen aus den HTML-Seiten auf dem Host mit Chrome (Memory prüfen, `free -m` > 1500 MB verfügbar):
|
||||
|
||||
```bash
|
||||
node --max-old-space-size=512 scripts/build-legal-html.mjs unterkunft # schreibt die HTML-Seiten
|
||||
node --max-old-space-size=512 scripts/build-legal-html.mjs unterkunft --check # Exit 0 = ok
|
||||
nice scripts/build-legal-pdfs.sh unterkunft # druckt die PDFs samt .sha256
|
||||
```
|
||||
|
||||
Änderst Du einen Text, HTML neu erzeugen und die PDFs neu drucken (Prüfsumme entscheidet, was neu gedruckt wird; `FORCE=1` druckt alle neu).
|
||||
@@ -0,0 +1,36 @@
|
||||
# Muster: Stornobedingungen für den Buchungskalender
|
||||
|
||||
**Vorlage, keine Rechtsberatung.** Entwurf, Prüfung durch Kanzlei offen (`docs/rechtliches/unterkunft-kanzlei-fragen.md`, Frage U11). Der Vermieter ist für seine Stornobedingungen allein verantwortlich (Leistungsbeschreibung Buchungskalender Nr. 5 Abs. 3 und Nr. 6). Das Portal bietet diesen Text in den Einstellungen („Mustertext Stornobedingungen zum Übernehmen“) an; der Vermieter prüft ihn, passt ihn an seinen Betrieb an und kann ihn von einer Rechtsanwältin oder einem Rechtsanwalt prüfen lassen. Ohne Stornobedingungen kann der Buchungskalender nicht freigeschaltet werden.
|
||||
|
||||
Hinweise für das Team:
|
||||
|
||||
- Die Bedingungen werden bei der Freigabe einer Anfrage als Schnappschuss in der Anfrage gespeichert; die Buchungsseite und die Buchungsbestätigung zeigen genau diese Fassung. Eine spätere Änderung gilt nur für neue Freigaben.
|
||||
- Der Buchungskalender erstattet eine Online-Zahlung nur auf Anweisung des Vermieters (Stornieren mit Erstattungsbetrag 0 bis Gesamtbetrag); die Staffel unten ist Text, die Software wendet sie nicht von selbst an. Der Vermieter trägt den Betrag beim Stornieren selbst ein.
|
||||
- Storniert der Vermieter, schließt der Buchungskalender eine noch offene Zahlungsseite bei Stripe. Geht trotzdem eine Zahlung nach der Stornierung oder dem Ablauf der Reservierung ein, und bei einer zweiten Zahlung auf eine bezahlte Buchung, erstattet WEBklar sie automatisch in voller Höhe über das Stripe-Konto des Vermieters (dauerhafte Weisung, der Stand der Buchung bleibt). Das ist keine Erstattung nach Nr. 3 des Musters, sondern die Rückgabe einer irrtümlichen Zahlung.
|
||||
- Zahlt der Gast vor Ort, entfällt eine Erstattung; die Stornierung stellt die Stornorechnung aus.
|
||||
- Gäste können nicht selbst stornieren: Sie schreiben dem Vermieter. Der Text sagt das.
|
||||
- Prozentsätze und Fristen sind Platzhalter und Beispiele; der Vermieter wählt eigene Werte. Pauschalen müssen den üblichen Schaden angemessen berücksichtigen (§ 309 Nr. 5 BGB); bei Verbrauchern sind überhöhte Pauschalen unwirksam.
|
||||
|
||||
---
|
||||
|
||||
## Stornobedingungen (Muster)
|
||||
|
||||
**1. Stornierung durch den Gast.** Eine Stornierung der Buchung ist nur in Textform möglich (E-Mail an [E-Mail-Adresse des Vermieters]) und wird erst mit unserer Bestätigung wirksam; wir stornieren die Buchung in unserem System und senden Ihnen eine Stornorechnung. Maßgeblich für die Frist ist der Eingang Ihrer Nachricht bei uns.
|
||||
|
||||
**2. Kosten der Stornierung.** Bei einer Stornierung berechnen wir folgenden Anteil des Gesamtpreises (ohne Endreinigung, soweit der Aufenthalt nicht angetreten wird):
|
||||
- bis [30] Tage vor der Anreise: kostenfrei,
|
||||
- [29] bis [14] Tage vor der Anreise: [30] Prozent,
|
||||
- [13] bis [7] Tage vor der Anreise: [60] Prozent,
|
||||
- ab [6] Tage vor der Anreise und bei Nichtanreise: [90] Prozent.
|
||||
|
||||
Ihnen bleibt der Nachweis gestattet, dass uns kein oder ein geringerer Schaden entstanden ist. Uns bleibt unser Anspruch unberührt, wenn wir die Unterkunft anderweitig vermieten können: In diesem Fall berechnen wir nur den tatsächlichen Ausfall.
|
||||
|
||||
**3. Erstattung.** Haben Sie bereits online bezahlt, erstatten wir den Teil des Preises, der nach Nr. 2 nicht anfällt, auf das Zahlungsmittel, mit dem Sie bezahlt haben. Die Erstattung geht über unser Stripe-Konto; die Gutschrift auf Ihrem Konto kann einige Tage dauern. Haben Sie noch nicht bezahlt, zahlen Sie den nach Nr. 2 anfallenden Betrag vor Ort oder nach Rechnung.
|
||||
|
||||
**4. Verspätete Anreise und vorzeitige Abreise.** Reisen Sie später an oder früher ab, besteht kein Anspruch auf Erstattung des Preises für die nicht genutzten Nächte, es sei denn, wir können die Unterkunft anderweitig vermieten.
|
||||
|
||||
**5. Stornierung durch uns.** Können wir die Unterkunft aus einem Grund, den wir zu vertreten haben, nicht bereitstellen, stornieren wir die Buchung, bieten nach Möglichkeit eine gleichwertige Unterkunft an und erstatten bereits gezahlte Beträge vollständig.
|
||||
|
||||
**6. Kein Widerrufsrecht.** Für die Buchung zu einem bestimmten Zeitraum besteht kein gesetzliches Widerrufsrecht (§ 312g Abs. 2 Nr. 9 BGB). Diese Stornobedingungen gelten stattdessen.
|
||||
|
||||
**7. Reiserücktrittsversicherung.** Wir empfehlen Ihnen eine Reiserücktrittsversicherung.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Mustertext: Datenschutzerklärung der Kundenwebsite (News-Pinnwand, Öffnungszeiten live)
|
||||
|
||||
Entwurf, Freigabe Nutzer ausstehend. Der Absatz gehört in die Datenschutzerklärung jeder Kundenwebsite, auf der die News-Pinnwand läuft. Bau die Pinnwand erst ein, wenn er dort steht. Dasselbe gilt für „Öffnungszeiten live“: Der Absatz „Öffnungszeiten auf dieser Website“ (je Variante unten) gehört in die Erklärung jeder Website mit dieser Funktion; bau die Funktion erst ein, wenn er dort steht (Einbau-Anleitung: `docs/funktionen/zeiten-einbau.md`).
|
||||
|
||||
Hinweise für das Team:
|
||||
|
||||
- Variante A, wenn WEBklar die Website hostet (Abo). Variante B, wenn die Website woanders liegt und die Pinnwand von `project.webklar.com` lädt.
|
||||
- Im sichtbaren Text der Kundenwebsite keine Gedankenstriche (Wunsch von Emma's Schatzkiste). Den Text unverändert übernehmen oder ohne Gedankenstriche anpassen.
|
||||
- Anrede und Ton an die übrige Erklärung der Website anpassen (meist Sie). `[Überschrift]`-Nummerierung der Website beachten.
|
||||
- Die Löschfrist von 14 Tagen für Server-Protokolle entspricht Abschnitt 3 der Datenschutzerklärung von WEBklar (project.webklar.com/datenschutz.html). Ändert sich dort die Frist, hier mitändern.
|
||||
- Beide Varianten setzen voraus, dass das Skript keine Cookies setzt und nichts im Browser speichert. Das gilt für `news.js` (nur lesende Abrufe) und für `zeiten.js` (ein Abruf je Seite, keine Anfragen an Dritte).
|
||||
- „Öffnungszeiten auf dieser Website“: den Absatz und nur die Bausteine übernehmen, die die Seite wirklich zeigt. Der Baustein „Route“ gehört dazu, wenn die Kontakt-Leiste den Knopf „Route“ hat, der Baustein „WhatsApp“, wenn sie den Knopf „WhatsApp“ hat. Der WhatsApp-Baustein ist der „WhatsApp-Hinweis“, den der Kunde im Portal mit einem Haken bestätigt, bevor die WhatsApp-Schaltfläche erscheint. Beide Dienste öffnen sich erst nach dem Klick des Besuchers. Anschriften von Google und WhatsApp vor der Übernahme prüfen. Auch hier gilt der Entwurfsstatus: erst nach der Freigabe durch den Inhaber (Rechtsrunde „Öffnungszeiten live“) verwenden.
|
||||
|
||||
## Variante A: Website wird von WEBklar gehostet
|
||||
|
||||
### Neuigkeiten auf dieser Website
|
||||
|
||||
Auf dieser Website zeigen wir unter „Neuigkeiten“ kurze Beiträge mit Text und Fotos. Beim Aufruf der Seite lädt Ihr Browser diese Beiträge und Bilder von dem Server, den die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, in unserem Auftrag betreibt und der bei der Hetzner Online GmbH in Falkenstein (Deutschland) steht. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem übertragen und kurzzeitig in Server-Protokollen gespeichert. Die Pinnwand setzt keine Cookies und speichert nichts in Ihrem Browser. Wir brauchen diese Daten, um die Beiträge auszuliefern und den Betrieb sicher zu halten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt in der Darstellung aktueller Informationen und im sicheren, funktionsfähigen Betrieb der Website. Die Protokolle werden nach 14 Tagen gelöscht.
|
||||
|
||||
### Öffnungszeiten auf dieser Website
|
||||
|
||||
Auf dieser Website zeigen wir unsere Öffnungszeiten und ob wir gerade geöffnet haben. Dazu können Hinweise, zum Beispiel zu Betriebsurlaub, am Seitenanfang und auf dem Handy eine Leiste mit Knöpfen zum Anrufen und für die Route kommen. Beim Aufruf der Seite lädt Ihr Browser ein kleines Programm und unsere Öffnungszeiten von dem Server, den die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, in unserem Auftrag betreibt und der bei der Hetzner Online GmbH in Falkenstein (Deutschland) steht. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem übertragen und kurzzeitig in Server-Protokollen gespeichert. Das Programm setzt keine Cookies und speichert nichts in Ihrem Browser. Wir brauchen diese Daten, um die Öffnungszeiten auszuliefern und den Betrieb sicher zu halten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt in der Darstellung aktueller Öffnungszeiten und im sicheren, funktionsfähigen Betrieb der Website. Die Protokolle werden nach 14 Tagen gelöscht. Für Suchmaschinen stehen unsere Öffnungszeiten außerdem in maschinenlesbarer Form im Seitencode.
|
||||
|
||||
#### Baustein Route (nur aufnehmen, wenn die Leiste den Knopf „Route“ zeigt)
|
||||
|
||||
Der Knopf „Route“ ist ein Link zu Google Maps. Erst wenn Sie darauf klicken, öffnet sich Google Maps (Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland) mit unserer Adresse als Ziel. Vorher werden keine Daten an Google übertragen. Nach dem Klick gelten die Datenschutzhinweise von Google.
|
||||
|
||||
#### Baustein WhatsApp (nur aufnehmen, wenn die Leiste den Knopf „WhatsApp“ zeigt)
|
||||
|
||||
Der Knopf „WhatsApp“ ist ein Link zu WhatsApp. Erst wenn Sie darauf klicken, öffnet sich WhatsApp (WhatsApp Ireland Limited, Merrion Road, Dublin 4, D04 X2K5, Irland, ein Unternehmen von Meta) mit unserer Nummer und gegebenenfalls einem vorgeschlagenen Starttext Ihrer Nachricht. Vorher werden keine Daten an WhatsApp oder Meta übertragen. Die Nachricht senden Sie selbst ab; nach dem Klick gelten die Datenschutzhinweise von WhatsApp.
|
||||
|
||||
## Variante B: Website liegt bei einem anderen Anbieter
|
||||
|
||||
### Neuigkeiten auf dieser Website
|
||||
|
||||
Auf dieser Website zeigen wir unter „Neuigkeiten“ kurze Beiträge mit Text und Fotos. Diese Beiträge und Bilder lädt Ihr Browser beim Aufruf der Seite von project.webklar.com, einem Server der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, der bei der Hetzner Online GmbH in Falkenstein (Deutschland) steht. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem an project.webklar.com übertragen und kurzzeitig in Server-Protokollen gespeichert. Die Pinnwand setzt keine Cookies und speichert nichts in Ihrem Browser. Wir nutzen den Dienst, damit wir Ihnen aktuelle Informationen zeigen können, ohne die Website jedes Mal neu zu veröffentlichen. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt in der einfachen und aktuellen Darstellung unserer Neuigkeiten. Die Protokolle werden nach 14 Tagen gelöscht. WEBklar verarbeitet die Daten in unserem Auftrag. [Prüfen: AVV mit WEBklar erforderlich]
|
||||
|
||||
### Öffnungszeiten auf dieser Website
|
||||
|
||||
Auf dieser Website zeigen wir unsere Öffnungszeiten und ob wir gerade geöffnet haben. Dazu können Hinweise, zum Beispiel zu Betriebsurlaub, am Seitenanfang und auf dem Handy eine Leiste mit Knöpfen zum Anrufen und für die Route kommen. Diese Angaben lädt Ihr Browser beim Aufruf der Seite als kleines Programm und als Öffnungszeiten von project.webklar.com, einem Server der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, der bei der Hetzner Online GmbH in Falkenstein (Deutschland) steht. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem an project.webklar.com übertragen und kurzzeitig in Server-Protokollen gespeichert. Das Programm setzt keine Cookies und speichert nichts in Ihrem Browser. Wir nutzen den Dienst, damit unsere Öffnungszeiten auf der Website überall gleich und aktuell erscheinen, ohne dass wir die Website bei jeder Änderung neu veröffentlichen müssen. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt in der einfachen und aktuellen Darstellung unserer Öffnungszeiten. Die Protokolle werden nach 14 Tagen gelöscht. Für Suchmaschinen stehen unsere Öffnungszeiten außerdem in maschinenlesbarer Form im Seitencode. WEBklar verarbeitet die Daten in unserem Auftrag. [Prüfen: AVV mit WEBklar erforderlich]
|
||||
|
||||
#### Baustein Route (nur aufnehmen, wenn die Leiste den Knopf „Route“ zeigt)
|
||||
|
||||
Der Knopf „Route“ ist ein Link zu Google Maps. Erst wenn Sie darauf klicken, öffnet sich Google Maps (Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland) mit unserer Adresse als Ziel. Vorher werden keine Daten an Google übertragen. Nach dem Klick gelten die Datenschutzhinweise von Google.
|
||||
|
||||
#### Baustein WhatsApp (nur aufnehmen, wenn die Leiste den Knopf „WhatsApp“ zeigt)
|
||||
|
||||
Der Knopf „WhatsApp“ ist ein Link zu WhatsApp. Erst wenn Sie darauf klicken, öffnet sich WhatsApp (WhatsApp Ireland Limited, Merrion Road, Dublin 4, D04 X2K5, Irland, ein Unternehmen von Meta) mit unserer Nummer und gegebenenfalls einem vorgeschlagenen Starttext Ihrer Nachricht. Vorher werden keine Daten an WhatsApp oder Meta übertragen. Die Nachricht senden Sie selbst ab; nach dem Klick gelten die Datenschutzhinweise von WhatsApp.
|
||||
@@ -0,0 +1,123 @@
|
||||
# Öffnungszeiten live auf der Kundenwebsite einbauen
|
||||
|
||||
Anleitung für das WEBklar-Team. „Öffnungszeiten live“ zeigt die Zeiten, die der Kunde im Portal einträgt (Tab „Funktionen“, Kachel „Öffnungszeiten live“), auf seiner Website: als Tabelle, als Live-Status und als Kurzanzeige, dazu eine Hinweis-Leiste, auf dem Handy eine Kontakt-Leiste und Google-Angaben (JSON-LD) im Seitencode. Den `siteKey` (16 Zeichen, a–z und 2–7) findest Du im WOMS: Kunde → Reiter „Funktionen“, Zeile „Öffnungszeiten live“ (mit Kopierknopf). Im Einrichtungs-Ticket „Öffnungszeiten live einbauen – <Projekt>“ stehen Einbau-Zeile, Ergebnis des automatischen Einbaus (`einbau`), Beispiele der Platzhalter mit den Plan-Kurznamen und die Prüfliste. Dieses Dokument ist die Referenz dahinter. Die Datenschutz-Absätze für die Kundenseite stehen in `docs/funktionen/website-datenschutz-absatz.md`.
|
||||
|
||||
Die Spec ist `docs/superpowers/specs/2026-10-04-oeffnungszeiten-live-design.md` (Abschnitte 4.2, 4.3, 4.5, 4.6).
|
||||
|
||||
## 1. Was automatisch geschieht und was Du tust
|
||||
|
||||
| Teil | Wer | Wie |
|
||||
| --- | --- | --- |
|
||||
| Einbau-Zeile (Skript) | Runner, automatisch | beim nächsten Veröffentlichen in jede HTML-Datei vor `</head>`, nur wenn die Funktion in `setup` oder `active` steht |
|
||||
| Hinweis-Leiste, Kontakt-Leiste, JSON-LD | Skript | kommen mit den Daten, ohne Platzhalter |
|
||||
| Platzhalter (Tabelle, Status, Badge) mit Ersatz-HTML | Du | an die Stellen setzen, an denen feste Zeiten stehen, und die festen Zeiten ersetzen |
|
||||
| Datenschutz-Absatz auf der Website | Du | Abschnitt 8, Punkt 5 |
|
||||
| Einbau-Zeile bei `einbau: manuell` oder wenn die Seite woanders liegt | Du | Abschnitt 9 |
|
||||
|
||||
Der Einbau ist **ein Einbau je Website** und ist im Preis (30 € oder im Wartungs-Abo ohne Aufpreis) enthalten. Er wird nicht auf das Stundenkontingent angerechnet. Sonderwünsche (zusätzliche Stellen, Sondergestaltung) gehen über ein Angebot oder, mit Abo, über Stunden.
|
||||
|
||||
## 2. Die Einbau-Zeile
|
||||
|
||||
```html
|
||||
<script data-wk-zeiten src="https://project.webklar.com/embed/zeiten.js" data-site="SITEKEY" defer></script>
|
||||
```
|
||||
|
||||
- Ins `<head>` jeder HTML-Seite, einmal pro Seite. Der Runner setzt sie vor `</head>` ein (liegt kein `</head>` vor, nach `<body>`); eine schon vorhandene Zeile mit `data-wk-zeiten` ersetzt er, der Einbau ist also wiederholbar und doppelt nichts.
|
||||
- `data-site` ist der `siteKey`. Optional `data-api="https://project.webklar.com"` (nur am Script-Tag, nie an einem Platzhalter); ohne Angabe nimmt das Skript die Herkunft seiner eigenen Adresse (das klappt nur bei einem normalen `<script src>`; bei Tag-Manager oder nachgeladenem Skript ist `data-api` Pflicht). Den siteKey darf auch ein Platzhalter tragen (`data-webklar-zeiten="SITEKEY"`).
|
||||
- Die Adresse des Skripts kommt aus der Runner-Variable `ZEITEN_PUBLIC_BASE` (Vorgabe `https://project.webklar.com/embed/`, muss mit `https://` beginnen und mit `/` enden). Für die Produktion nichts ändern.
|
||||
- Das Skript hat keine Abhängigkeiten, setzt keine Cookies (keine Cookies, kein `localStorage`, kein `sessionStorage`), fragt nur `project.webklar.com` ab und lädt nichts von Dritten. Kundentexte setzt es nur als Text ein, nie als HTML. Es wird 5 Minuten gecacht und darf von jeder Domain geladen werden. Ein Abruf je Seite für alle Teile, danach höchstens alle 10 Minuten.
|
||||
- Hat die Seite eine Content-Security-Policy, muss `project.webklar.com` in `script-src` und `connect-src` stehen, und `style-src` muss Inline-Stile erlauben (`'unsafe-inline'`): das Skript legt ein `<style>`-Element an (Tabelle, Status, Leisten). Ohne diese Erlaubnis erscheinen sie ohne Gestaltung.
|
||||
|
||||
## 3. Platzhalter
|
||||
|
||||
Ein Platzhalter ist ein Element mit dem Attribut `data-webklar-zeiten`. Innen steht das Ersatz-HTML (Abschnitt 4).
|
||||
|
||||
```html
|
||||
<div data-webklar-zeiten data-modus="tabelle" data-plan="oeffnungszeiten">…Ersatz-HTML…</div>
|
||||
<div data-webklar-zeiten data-modus="status" data-plan="oeffnungszeiten">Mo–Fr 9–18 Uhr</div>
|
||||
<span data-webklar-zeiten data-modus="badge" data-plan="oeffnungszeiten">Mo–Fr 9–18 Uhr</span>
|
||||
```
|
||||
|
||||
| `data-modus` | Zeigt | Gedacht für |
|
||||
| --- | --- | --- |
|
||||
| `tabelle` | Wochentabelle (echtes `<table>` mit Caption und Zeilenköpfen, heutiger Tag mit `aria-current="date"`), darunter „Abweichungen in den nächsten 14 Tagen“ | Kontaktseite, Footer |
|
||||
| `status` | Punkt und Text, zum Beispiel „Jetzt geöffnet · schließt um 18 Uhr“, ohne `aria-live` | Kopfbereich, Hero |
|
||||
| `badge` | Kurzform „Geöffnet“, „Geschlossen“, „Nach Vereinbarung“ | Menü, Header |
|
||||
|
||||
- `data-plan` ist der **Kurzname** des Zeitplans (Kleinbuchstaben, Ziffern und Bindestrich, 1–24 Zeichen, nach dem Anlegen unveränderlich). Der Hauptplan heißt `oeffnungszeiten`. Weitere Pläne (zum Beispiel `kueche`, `lieferung`) hat der Kunde im Editor angelegt; die Kurznamen stehen im Editor und im Einrichtungs-Ticket („Pläne (Kurznamen für data-plan)“). Ohne `data-plan` gilt der Hauptplan.
|
||||
- **Einzelseiten-Apps (Vite/React):** Das Skript beobachtet `<body>` und füllt Platzhalter, die erst nach einem Seitenwechsel auf die Seite kommen, nach etwa 0,2 Sekunden (auch einen wieder eingehängten Knoten). Setze Platzhalter nicht in Teile, die die App beim Rendern entfernt oder neu aufbaut: ein neu gebauter Knoten startet wieder mit seinem Ersatz-HTML, und was das Skript hineingeschrieben hat, kann die App überschreiben.
|
||||
- Ein **unbekannter** `data-plan` oder `data-modus` lässt das Ersatz-HTML stehen und schreibt eine Warnung in die Konsole. Es gibt kein stilles Umschalten auf den Hauptplan. Bei Fehlern, ohne JavaScript, bei Daten älter als 24 Stunden und nach dem Abschalten bleibt ebenfalls das Ersatz-HTML.
|
||||
- Das Skript füllt die Platzhalter ab `setup`, damit Du vor dem Schließen des Tickets prüfen kannst.
|
||||
- Gestaltung: Light DOM, alle Klassen beginnen mit `wkz-`, die Stile stehen in `@layer wkz`. **Jede** Regel der Kundenseite (ohne Layer) gewinnt also gegen die Standard-Stile. Schrift und Farben erbt die Seite. Variablen (alle optional, am Platzhalter oder auf `:root`): `--wkz-offen`, `--wkz-zu`, `--wkz-unklar` (Farben der Punkte), `--wkz-offen-hell`, `--wkz-zu-hell`, `--wkz-unklar-hell`, `--wkz-offen-text`, `--wkz-zu-text`, `--wkz-unklar-text` (Badge), `--wkz-rand`, `--wkz-heute`. Kontrast mindestens 4,5 : 1 prüfen.
|
||||
- Hat das Theme globale Regeln wie `table{…}` oder `td{…}`, greifen sie auch in der Tabelle. Prüfe das bei der Abnahme und überschreibe gezielt, zum Beispiel `[data-webklar-zeiten] table{ … }`.
|
||||
|
||||
## 4. Ersatz-HTML
|
||||
|
||||
Jeder Platzhalter behält eine **statische Ersatz-Tabelle**: Sie steht ohne JavaScript, bei einem Fehler und nach der Abschaltung. Ohne sie wäre die Stelle leer.
|
||||
|
||||
1. WOMS → Funktionen → Ersatz-HTML kopieren (je Plan ein Knopf; bei mehreren Plänen mit Namen dahinter). Erst nach dem ersten Speichern des Kunden; vorher meldet das WOMS „Der Kunde hat noch keine Zeiten eingetragen“ (die Schnittstelle antwortet 409: `GET /api/team/funktionen/:customerId/zeiten/ersatz?projectId=…&plan=<kurz>`).
|
||||
2. In jeden Platzhalter einsetzen. Das Ersatz-HTML ist eine schlichte Wochentabelle als reines ASCII mit Entities (`ä`, `–`), also in jeder Dateikodierung sicher. Ein Fensterende um Mitternacht steht als `00:00` („18:00–00:00“), ein Fenster von 00:00 bis 24:00 als „rund um die Uhr“.
|
||||
3. Die Tabelle zeigt den Wochenplan **zum Zeitpunkt des Einbaus**. Ändert der Kunde seinen Wochenplan später, bleibt das Ersatz-HTML zunächst stehen (mit Skript unsichtbar).
|
||||
4. **Beim Abschalten** der Funktion, wenn der aktuelle Wochenplan des Hauptplans vom Stand beim Einbau abweicht, entsteht ein WOMS-Ticket **„Ersatz-Tabelle aktualisieren – <Projekt>“** mit dem aktuellen Ersatz-HTML. Ersetze die Tabelle in allen Platzhaltern (gehört zum enthaltenen Einbau, Leistungsbeschreibung Nr. 4 Abs. 3). Die Mail K30 sagt dem Kunden, dass wieder feste Zeiten auf der Seite stehen. Liegt die Seite woanders, schicke dem Kunden den Text.
|
||||
|
||||
## 5. Header-Selektor (`navSelektor`)
|
||||
|
||||
Die Hinweis-Leiste steht als erstes Kind von `<body>` im Fluss und schiebt den Inhalt nach unten. Feste und klebende Elemente mit `top: 0` rückt das Skript, solange die Leiste sichtbar ist, um `max(0, Leistenhöhe − Scrollposition)` nach unten (Inline-`top`, beim Entfernen der Leiste zurückgesetzt) und erhöht `scroll-padding-top`.
|
||||
|
||||
Überlappt die Leiste trotzdem den Kopf der Seite (zum Beispiel ein fester Header mit `top: 12px`, mit `transform` oder in einem Wrapper), setze im WOMS bei Kunde → Funktionen → „Öffnungszeiten live“ das Feld **Header-Selektor** (Team-Einstellung `navSelektor`):
|
||||
|
||||
- ein CSS-Selektor des Headers, höchstens 120 Zeichen, erlaubt sind nur `A–Z a–z 0–9`, Leerzeichen und `# . - _ > : [ ] = " '`, nicht erlaubt `<`, `{`, `}` und `;`. Beispiel: `header.site-header` oder `#nav`.
|
||||
- Das Skript wendet ihn mit `querySelectorAll` in einem `try/catch` an; ein falscher Selektor bricht nichts, er bewirkt nur nichts.
|
||||
- Leer lassen = Standard (nur `top: 0`). Speichern mit `teamVersion` (bei 409 „Inzwischen geändert – bitte neu laden“).
|
||||
- Prüfe nach dem Setzen: oben auf der Seite, beim Scrollen (die Leiste scrollt weg, der Header klebt wieder oben) und auf dem Handy.
|
||||
|
||||
## 6. Leisten ausschalten
|
||||
|
||||
Im selben Bereich (WOMS → Funktionen → „Öffnungszeiten live“) gibt es zwei Haken: **`kontaktleisteAus`** („Kontakt-Leiste aus“) und **`hinweisleisteAus`** („Hinweis-Leiste aus“). Sie gelten je Website.
|
||||
|
||||
- Die **Kontakt-Leiste** erscheint nur bei `(max-width: 768px)`, unten fest mit `z-index: 40`, Knöpfe „Anrufen“, „Route“ und, wenn der Kunde sie eingerichtet hat, „WhatsApp“. Hat die Seite selbst ein festes Element am unteren Rand mit einem `a[href^="tel:"]`, bleibt sie **automatisch aus**. Setze den Haken, wenn die Seite eine eigene Anruf-Leiste hat, die das Skript nicht erkennt, oder der Kunde die Leiste nicht möchte.
|
||||
- WhatsApp und die Route (Google Maps) öffnen erst beim Klick. Den Haken „Ich habe den WhatsApp-Hinweis in meine Datenschutzerklärung aufgenommen“ setzt der Kunde im Portal; in der Kundenansicht setzt ihn kein Mitarbeiter.
|
||||
- Die **Hinweis-Leiste** zeigt zu jedem Zeitpunkt den gültigen Hinweis mit dem höchsten Rang (Wichtig, Urlaub, Aktion, Info). Sie hat keinen Schließen-Knopf (das Skript speichert nichts im Browser).
|
||||
|
||||
## 7. JSON-LD
|
||||
|
||||
Das Skript pflegt die Öffnungszeiten im Seitencode: Gibt es einen `application/ld+json`-Block mit einem Knoten vom Typ `LocalBusiness` oder einem Untertyp (auch in `@graph`), ersetzt es dort **nur** `openingHoursSpecification` und `specialOpeningHoursSpecification` (alles andere im Block bleibt); unlesbare Blöcke bleiben unberührt. Gibt es keinen, legt es einen eigenen Block `[data-wkz-jsonld]` mit `name`, `address` und `url` an. Prüfe auf der Seite, ob JSON-LD mit festen Öffnungszeiten an einer anderen Stelle steht (zum Beispiel als Mikrodaten im Footer); das würde dem Skript widersprechen.
|
||||
|
||||
## 8. Prüfliste
|
||||
|
||||
Das Einrichtungs-Ticket nennt dieselben fünf Punkte:
|
||||
|
||||
1. **Feste Zeiten im Repo suchen und ersetzen**: `grep -ril "öffnungszeit" .` und Ähnliches (`geöffnet`, `Mo-Fr`, `Uhr`), auch Footer, Kontaktseite, `<meta>` und eigene Status-Skripte. Jede Stelle wird ein Platzhalter mit Ersatz-HTML oder fällt weg.
|
||||
2. **Header**: Überlappt die Hinweis-Leiste den Kopf der Seite? Sonst `navSelektor` setzen (Abschnitt 5).
|
||||
3. **Anruf-Leiste**: Hat die Seite schon eine eigene feste Anruf-Leiste? Dann `kontaktleisteAus` (Abschnitt 6), damit sie nicht doppelt erscheint.
|
||||
4. **Vorhandenes JSON-LD** (Öffnungszeiten der Seite) prüfen, damit nichts doppelt oder widersprüchlich steht (Abschnitt 7).
|
||||
5. **Datenschutz-Absatz auf der Website**: den Absatz „Öffnungszeiten auf dieser Website“ aus `docs/funktionen/website-datenschutz-absatz.md` einsetzen (Variante A bei WEBklar-Hosting, Variante B bei Fremdhosting). Die Bausteine „Route“ und „WhatsApp“ nur, wenn die Kontakt-Leiste die Knöpfe zeigt. Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, setzt Du den Absatz ein (gehört zum Einbau, nicht auf das Stundenkontingent anrechnen); liegt sie woanders (externer Generator), schicke dem Kunden den Textbaustein, er fügt ihn selbst ein. Die Texte sind Entwurf, bis die Rechtsrunde sie freigibt.
|
||||
|
||||
Zur Abnahme außerdem: Seite in der Vorschau öffnen und mit ausgeschaltetem JavaScript ansehen (Ersatz-Tabelle muss stehen), Konsole auf Warnungen zu `data-plan` prüfen, auf einem Handy (höchstens 768 px Breite) die Kontakt-Leiste, den Regler „Uhrzeit testen“ im Editor gegen die Anzeige halten, Kontrast und Tippflächen (mindestens 44 px) ansehen.
|
||||
|
||||
## 9. Sperrliste `ZEITEN_KEIN_NEUBAU` und Seiten ohne automatischen Einbau
|
||||
|
||||
**Neubau und Einsetzen.** Bei jedem Übergang in `setup` oder `active` (Einschalten im Abo, Zahlung, Aktivierung durch das Team, Rückkehr des Abos, Behalten) stößt das Portal einen Neubau der Seite an (höchstens einer gleichzeitig). Das Ergebnis steht in `einbau`: `angestossen` (Auftrag angenommen, die Zeile erscheint nach dem Veröffentlichen) oder `einbau: manuell`. Änderungen an Zeiten und Leisten brauchen keinen Neubau.
|
||||
|
||||
`einbau: manuell` heißt: kein Repo, Vorschau aus, Dev-Modus, Projekt auf der Sperrliste, Fehler oder Runner ausgelastet. Dann setzt Du die Einbau-Zeile von Hand ins Repo.
|
||||
|
||||
**Sperrliste.** `ZEITEN_KEIN_NEUBAU` (Umgebungsvariable des Portals, Liste von Subdomains, durch Komma getrennt): Für diese Projekte gibt es weder einen angestoßenen Neubau noch das automatische Einsetzen bei normalen Deploys; die Funktion zählt sie immer als manuell. Vorgabe ist `emmas-schatzkiste` (Emmas Seiten, deren Dateien teilweise nicht in Git liegen). **Achtung:** Setzt Du die Variable, **ersetzt** sie die Vorgabe. Willst Du eine Seite zusätzlich sperren, nenne `emmas-schatzkiste` mit in der Liste. Änderung in `compose` und `.env` des Portals, danach das Portal mit `docker compose -f /home/webklar/apps/docker-compose.yml up -d --no-deps kundenbereich-server` neu erstellen (kein `docker restart`: es liest geänderte Werte nicht neu ein). Für gesperrte Seiten setzt Du die Zeile und die Platzhalter von Hand ein (im Repo, soweit die Dateien dort liegen, sonst direkt auf dem Server) und prüfst nach jedem Deploy, ob sie noch da sind.
|
||||
|
||||
**Seite liegt woanders** (anderer Hoster, keine WEBklar-Veröffentlichung): Das Ticket enthält die Einbau-Zeile; Du setzt sie ein, soweit WEBklar Zugriff auf die Seite hat. Hat WEBklar keinen Zugriff, schicke dem Kunden im Chat Einbau-Zeile, Platzhalter-Beispiele und Ersatz-HTML und bitte ihn, sie selbst oder durch seinen Anbieter einzufügen. Schließe das Ticket erst, wenn die Anzeige auf der Live-Seite stimmt: Öffne die Seite, sieh in den Netzwerk-Aufrufen nach der Antwort von `/api/public/zeiten/v1/<siteKey>` (`"aktiv": true`) und nach der gefüllten Tabelle.
|
||||
|
||||
## 10. Freischalten und Ende
|
||||
|
||||
- **Freischalten:** Der Job setzt `active` (und sendet K16), wenn das Ticket geschlossen ist und der Kunde Zeiten eingetragen hat (Hauptplan gespeichert, mindestens ein Tag nicht „zu“). Sonst bleibt `setup`, das WOMS zeigt „wartet auf die Zeiten des Kunden“. Solange der Kunde nichts eingetragen hat, kannst Du die Tabelle nicht einbauen.
|
||||
- **Ende des Abos** (Gratisweg): K29 mit Frist zum Behalten (30 Tage), danach `disabled` und K30, Eingaben noch 30 Tage, dann gelöscht. Das Ticket „Ersatz-Tabelle aktualisieren“ gehört zum Abschalten (Abschnitt 4).
|
||||
- **Abschalten durch das Team** (Grund `team`): keine automatische Löschung.
|
||||
|
||||
## 11. Häufige Fehlerbilder
|
||||
|
||||
| Symptom | Meist liegt es an |
|
||||
| --- | --- |
|
||||
| Es erscheint nur das Ersatz-HTML | Funktion nicht in `setup`/`active`, `einbau: manuell` und Zeile fehlt, unbekannter `data-plan`, kein JavaScript, Daten älter als 24 Stunden, CSP blockiert `project.webklar.com` |
|
||||
| Hinweis-Leiste verdeckt den Header | `navSelektor` nicht gesetzt (Abschnitt 5) |
|
||||
| Zwei Anruf-Leisten auf dem Handy | `kontaktleisteAus` setzen (Abschnitt 6) |
|
||||
| Kontakt-Leiste fehlt auf dem Handy | Seite hat schon eine feste Anruf-Leiste unten, oder Breite über 768 px, oder `kontaktleisteAus` |
|
||||
| Öffnungszeiten doppelt im Suchergebnis oder Test | festes JSON-LD an anderer Stelle (Abschnitt 7) |
|
||||
| Tabelle sieht fremd aus | globale Regeln des Themes (Abschnitt 3) |
|
||||
@@ -4,21 +4,23 @@
|
||||
<meta charset="utf-8">
|
||||
<title>Empfehlen für WEBklar – Leitfaden</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 13mm 16mm 12mm; }
|
||||
@page { size: A4; margin: 10mm 13mm 9mm; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; color: #1d1d1f; font: 10pt/1.45 "DejaVu Sans", system-ui, sans-serif; }
|
||||
body { margin: 0; color: #1d1d1f; font: 9.5pt/1.38 "DejaVu Sans", system-ui, sans-serif; }
|
||||
.brand { font-weight: 800; letter-spacing: 0.12em; font-size: 10pt; color: #6d1fd1; }
|
||||
h1 { font-size: 20pt; line-height: 1.15; margin: 3mm 0 1.5mm; }
|
||||
.lead { font-size: 10.5pt; color: #444; margin: 0 0 4mm; }
|
||||
h2 { font-size: 12pt; margin: 4.5mm 0 2mm; }
|
||||
ol.steps { list-style: none; padding: 0; margin: 0; display: grid; gap: 3mm; }
|
||||
h1 { font-size: 19pt; line-height: 1.15; margin: 2.5mm 0 1.2mm; }
|
||||
.lead { font-size: 10.5pt; color: #444; margin: 0 0 2.5mm; }
|
||||
h2 { font-size: 12pt; margin: 3mm 0 1.2mm; }
|
||||
ol.steps { list-style: none; padding: 0; margin: 0; display: grid; gap: 1.5mm; }
|
||||
ol.steps li { display: grid; grid-template-columns: 9mm 1fr; gap: 3mm; align-items: start; }
|
||||
.num { width: 8mm; height: 8mm; border-radius: 50%; background: #6d1fd1; color: #fff; font-weight: 700; display: flex; align-items: center; justify-content: center; }
|
||||
ol.steps strong { display: block; }
|
||||
.box { border: 1px solid #e0dccf; border-radius: 3mm; padding: 3mm 4mm; margin: 3.5mm 0; background: #faf9f6; }
|
||||
.box { border: 1px solid #e0dccf; border-radius: 3mm; padding: 2mm 4mm; margin: 2.5mm 0; background: #faf9f6; }
|
||||
.rules { margin: 0; padding-left: 5mm; }
|
||||
.rules li { margin: 1.4mm 0; }
|
||||
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 4mm; }
|
||||
.rules li { margin: 0.8mm 0; }
|
||||
.abos { margin: 1mm 0; padding-left: 4.5mm; }
|
||||
.abos li { margin: 0.6mm 0; }
|
||||
.grid2 { display: grid; grid-template-columns: 1fr 1.2fr; gap: 4mm; }
|
||||
.muted { color: #666; font-size: 9pt; }
|
||||
.contact { font-size: 10pt; }
|
||||
.do::marker { content: "✓ "; color: #2f7d3b; }
|
||||
@@ -39,14 +41,24 @@
|
||||
Frag den Betrieb, ob du seine Kontaktdaten an uns weitergeben darfst. Oder gib ihm unsere Kontaktdaten (unten).
|
||||
Schick uns dann Name des Betriebs, Ansprechpartner, Telefon oder E-Mail, worum es kurz geht, und dass er mit der Weitergabe einverstanden ist.</div></li>
|
||||
<li><span class="num">3</span><div><strong>Den Rest machen wir</strong>
|
||||
Beratung, Angebot und Website übernehmen wir. Für das Wartungs-Abo bekommt der Kunde von uns einen Zugang zum Kundenportal
|
||||
und schließt es dort <b>selbst online</b> ab: ohne Unterschrift, ohne Termin, Zahlung sicher über Stripe.</div></li>
|
||||
Beratung, Angebot und Website übernehmen wir. Für das Hosting- oder Wartungs-Abo bekommt der Kunde von uns einen Zugang zum Kundenportal
|
||||
und schließt es dort im Bereich „Verträge“ <b>selbst online</b> ab: ohne Unterschrift, ohne Termin, Zahlung sicher über Stripe.</div></li>
|
||||
</ol>
|
||||
|
||||
<div class="box">
|
||||
<strong>Das Wartungs-Abo auf einen Blick</strong><br>
|
||||
50 € pro Monat, Mindestlaufzeit 12 Monate, danach monatlich kündbar. Enthalten sind Betrieb und Pflege der Website,
|
||||
1 Domain, 1 E-Mail-Postfach und 1 Stunde Arbeitszeit pro Monat (sammelt sich an, jede Stunde 12 Monate gültig).
|
||||
<strong>Unsere Abos auf einen Blick</strong> · beide mit Mindestlaufzeit 12 Monate, danach monatlich kündbar
|
||||
<ul class="abos">
|
||||
<li><b>Hosting-Abo, 20,00 € pro Monat</b> (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer): nur statische Websites (ohne CMS, Login,
|
||||
Datenbank oder Formulare); 1 Domain, 1 E-Mail-Postfach mit 5 GB; keine Pflege der Website und kein Stundenkontingent
|
||||
(Änderungen nur nach Freigabe, gesondert berechnet).</li>
|
||||
<li><b>Wartungs-Abo, 50,00 € pro Monat</b> (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer): alle Websites und Web-Anwendungen; Betrieb und Pflege,
|
||||
1 Domain, beliebig viele Postfächer mit zusammen 5 GB, 1 Stunde Arbeitszeit pro Monat (nicht genutzte Zeit bleibt 12 Monate verfügbar).</li>
|
||||
<li><b>In beiden Abos:</b> E-Mail-Zusatzspeicher je 5 GB für 1,00 € pro Monat (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer).
|
||||
Außerdem kann der Kunde die freiwillige Übernahmegutschrift beantragen, wenn er Domain, Hosting oder E-Mail beim bisherigen Anbieter
|
||||
im Voraus bezahlt hat: Nach unserer Prüfung verrechnen wir den doppelt bezahlten Anteil mit seinen Monatsgebühren (keine Auszahlung).</li>
|
||||
<li><b>Wechsel</b> vom Hosting- ins Wartungs-Abo im Kundenportal unter „Verträge“ (Hosting-Abo läuft, nicht gekündigt, keine offene Zahlung);
|
||||
es beginnt eine neue Mindestlaufzeit von 12 Monaten.</li>
|
||||
</ul>
|
||||
Alle Details stehen in den Abo-Bedingungen: project.webklar.com/abo-bedingungen.html
|
||||
</div>
|
||||
|
||||
@@ -61,7 +73,7 @@
|
||||
</ul>
|
||||
<ul class="rules">
|
||||
<li class="dont">Keine Werbe-E-Mails, WhatsApp- oder SMS-Nachrichten und keine Werbeanrufe an Leute, die dem nicht zugestimmt haben.</li>
|
||||
<li class="dont">Keine Zusagen zu Preisen, Rabatten, Terminen oder Leistungen, die nicht auf webklar.com oder in unserem Angebot stehen.</li>
|
||||
<li class="dont">Keine Zusagen zu Preisen, Rabatten, Terminen oder Leistungen, die nicht auf webklar.com, in den Abo-Bedingungen oder in unserem Angebot stehen.</li>
|
||||
<li class="dont">Nichts für WEBklar unterschreiben, keine Verträge abschließen, kein Geld annehmen.</li>
|
||||
<li class="dont">Kundendaten nicht sammeln, speichern oder anders verwenden.</li>
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# Ergänzung der Abo-Bedingungen: Website-Analyse (Vorschlag)
|
||||
|
||||
> **Entwurf – vor Verwendung anwaltlich prüfen lassen.** `public/abo-bedingungen.html` wird NICHT geändert; Nutzer lädt die neue Fassung in WOMS „Rechtliches“ hoch.
|
||||
|
||||
Bezug: aktuelle Abo-Bedingungen (Aufbau § 1 bis § 16, Leistungsbeschreibung B = Wartungs-Abo, Anlagen 1 bis 3). Die Paragraphenfolge bleibt unverändert, es entstehen keine neuen Verweise in anderen §§.
|
||||
|
||||
## 1. Wartungs-Abo, Nr. 1 „Umfang“: neuer Spiegelstrich (nach „Servicezeiten und Reaktion“)
|
||||
|
||||
```diff
|
||||
tägliche Datensicherung (§ 3 Abs. 5)
|
||||
1 Stunde Arbeitszeit je bezahltem Monat; nicht genutzte Zeit bleibt 12 Monate verfügbar (§ 4)
|
||||
Servicezeiten und Reaktion (§ 3 Abs. 6)
|
||||
+ Website-Analyse für öffentliche Websites auf Wunsch des Kunden (Leistungsbeschreibung „Website-Analyse“, § 3 Abs. 8)
|
||||
```
|
||||
|
||||
## 2. § 3 Leistungen: neuer Absatz (8) nach Absatz (7)
|
||||
|
||||
```diff
|
||||
+ (8) Website-Analyse
|
||||
+ Im Wartungs-Abo kann der Kunde im Kundenportal für seine öffentliche, bei uns gehostete Website eine
|
||||
+ Reichweitenmessung mit Cookie-Banner einschalten. Umfang, Voraussetzungen, Pflichten des Kunden und Ende
|
||||
+ der Leistung ergeben sich aus der Leistungsbeschreibung „Website-Analyse“. Die Website-Analyse ist im
|
||||
+ Hosting-Abo nicht enthalten. Wir schulden dafür keine bestimmte Verfügbarkeit; ein Ausfall der Analyse
|
||||
+ beeinträchtigt die Website nicht. Sie endet mit dem Ausschalten durch den Kunden oder mit dem Ende des
|
||||
+ Wartungs-Abos; die Daten löschen wir 60 Tage danach.
|
||||
```
|
||||
|
||||
## 3. § 6 Pflichten des Kunden: neuer Absatz nach dem Absatz zu Impressum und Datenschutzerklärung
|
||||
|
||||
```diff
|
||||
Für Impressum, Datenschutzerklärung und Inhalte seiner Website ist der Kunde verantwortlich. Die Angaben zum Hosting, ... stehen in den Anlagen 1 bis 3.
|
||||
+ Schaltet der Kunde die Website-Analyse ein, ist er für die Messung auf seiner Website Verantwortlicher. Er
|
||||
+ entfernt oder umgeht das Cookie-Banner nicht, bindet ohne Abstimmung mit uns keine weiteren Mess- oder
|
||||
+ Tracking-Werkzeuge ein und ergänzt seine Datenschutzerklärung um einen Hinweis auf die von uns erzeugte
|
||||
+ Zusatzseite „/datenschutz-analyse.html“.
|
||||
```
|
||||
|
||||
## 4. § 11 Vertragsende: neuer Satz am Ende
|
||||
|
||||
```diff
|
||||
+ Mit dem Vertragsende endet auch die Website-Analyse; wir speichern dann keine Messdaten mehr, entfernen
|
||||
+ Skript und Banner beim nächsten Veröffentlichen der Website und löschen die Messdaten 60 Tage nach Abschaltung.
|
||||
```
|
||||
|
||||
## 5. § 14 Datenschutz, Absatz (2): Ergänzung
|
||||
|
||||
```diff
|
||||
- Soweit wir im Rahmen eines Abos personenbezogene Daten im Auftrag des Kunden verarbeiten, etwa E-Mails in seinen Postfächern, Eingaben in Kontaktformulare oder Server-Protokolle seiner Website, gilt der Auftragsverarbeitungsvertrag in Anlage 1.
|
||||
+ Soweit wir im Rahmen eines Abos personenbezogene Daten im Auftrag des Kunden verarbeiten, etwa E-Mails in seinen Postfächern, Eingaben in Kontaktformulare oder Server-Protokolle seiner Website, gilt der Auftragsverarbeitungsvertrag in Anlage 1. Für die Website-Analyse gilt zusätzlich der Auftragsverarbeitungsvertrag Website-Analyse, dem der Kunde beim Einschalten im Kundenportal zustimmt.
|
||||
```
|
||||
|
||||
## 6. Anlage 3 Unterauftragsverarbeiter: neue Zeile
|
||||
|
||||
```diff
|
||||
Hetzner Online GmbH ... | E-Mail-Postfächer (Webhosting) | Deutschland (Rechenzentrum Nürnberg)
|
||||
+ Hetzner Online GmbH ... | Server für die Website-Analyse (Messdienst, nur bei eingeschalteter Funktion) | Deutschland (Rechenzentrum Falkenstein)
|
||||
```
|
||||
|
||||
## 7. Fassung und Datum
|
||||
|
||||
Neue Fassung der Abo-Bedingungen mit neuem Stand-Datum hochladen; § 15 (Änderungen dieser Bedingungen) regelt die Mitteilung. Die Website-Analyse ist eine zusätzliche Leistung ohne Aufpreis, erhöht Preise nicht und greift nicht in bestehende Leistungen ein.
|
||||
|
||||
<!-- Prüfpunkt Anwalt P10: Wirksame Einbeziehung der geänderten Fassung bei Bestandskunden (§ 15, § 308 Nr. 5 / § 307 BGB); Wettbewerbs-/Transparenzfragen zur kostenlosen Zusatzleistung. -->
|
||||
<!-- Prüfpunkt Anwalt P14: Verweis Wartungs-Abo (Leistungsbeschreibung B) und Hosting-Abo-Ausschluss konsistent mit § 9 Wechsel. -->
|
||||
@@ -0,0 +1,155 @@
|
||||
# Abo-Bedingungen Fassung 6 (Entwurf)
|
||||
|
||||
Fassung 2026-10-16-v6 · Stand 16.10.2026 · Entwurf – Prüfung durch Kanzlei offen · Entwurf zur Freigabe, keine Rechtsberatung · Basis: Fassung 5 (Stand 30.09.2026, `public/vertragsunterlagen/v5/abo-bedingungen.html`)
|
||||
|
||||
Dieser Entwurf listet alle Änderungen gegenüber Fassung 5 im Wortlaut. Die Paragrafenfolge § 1 bis § 16 und alle Anker bleiben, damit Querverweise in Mails und der Paragraf zum Vertragsende (§ 11) stimmen. Die Sätze unter jeder Überschrift sind die Pflichtsätze; sie stehen unverändert in der Seite `public/neu/rechtliches/abo-bedingungen.html`. Zu jeder Stelle nennt die Zeile „Begründung“ die Entscheidung des Inhabers (E…), die Festlegung nach dem Builder-Check (C…) oder den Spec-Abschnitt und verweist bei offenen Punkten auf die Entscheidungsliste (`docs/rechtliches/kundenansicht-entscheidungen.md`, R… und KQ…). Voraussetzung für den Livegang ist die Freigabe durch den Inhaber (E20).
|
||||
|
||||
## Das Wichtigste auf einen Blick
|
||||
|
||||
Das Hosting-Abo enthält die Website-Analyse, das Wartungs-Abo zusätzlich News-Pinnwand, Terminplaner, Webshop, Buchungskalender und Öffnungszeiten live. Das Einschalten kostet nichts extra; die Einrichtung geht vom Stundenkontingent ab (§ 4 Abs. 8).
|
||||
|
||||
**Einordnung.** Neue Zeile „Funktionen“ in der Übersichtsbox, hinter der Zeile „Wartungs-Abo“. Die Box behält ihre Preiszeilen und die Anrede „Ihr“.
|
||||
|
||||
**Begründung.** E6 (die Funktionen stecken im Abo: Hosting-Abo mit Analyse, Wartungs-Abo mit allen Funktionen), E7 und E17 (Einrichtung vom Stundenkonto, keine Extra-Rechnung), C10 (Analyse im Hosting-Abo), Spec 5.9. Die Zeile nennt für Funktionen keinen Preis. Offen: Geltung für laufende Abos (R2, KQ1), Anrede der Rechtsdokumente (R12).
|
||||
|
||||
## § 1 Abs. 2
|
||||
|
||||
Die Bestellseiten richten sich nach der Kundenart, die wir im Kundenkonto hinterlegt haben („Betrieb“ oder „privat“). Wir hinterlegen sie nach den Angaben des Kunden; ist keine hinterlegt, gilt der Kunde als privat. Ob der Kunde Verbraucher oder Unternehmer ist, richtet sich nach dem Zweck, zu dem er den Vertrag abschließt, nicht nach dieser Angabe.
|
||||
|
||||
**Einordnung.** Hängt an den bestehenden Absatz 2 an („Sie gelten für Verbraucher und Unternehmer“).
|
||||
|
||||
**Begründung.** E2 bis E4 und C13: Die Kundenart legt das Team beim Anlegen fest, leer gilt als privat, und die Bestellseiten richten sich danach. Rechtlich entscheidet der Zweck des Vertrags (§§ 13, 14 BGB), nicht die hinterlegte Angabe; der Satz sagt das ausdrücklich, damit die Angabe nicht als Selbstauskunft oder als Rechtsfolge gelesen wird. Offen: Belastbarkeit der Kundenart (R5, KQ4).
|
||||
|
||||
## § 2 Abs. 2 c
|
||||
|
||||
Direkt über der Schaltfläche steht der Hinweis, dass diese Bedingungen mit den Leistungsbeschreibungen, dem Auftragsverarbeitungsvertrag (Anlage 1) und der Widerrufsbelehrung gelten, jeweils mit Fassung und Link; die Unterlagen erhält der Kunde nach der Bestellung zusätzlich als PDF per E-Mail. Ist die Kundenart „privat“ hinterlegt, kann der Kunde dort durch ein Kästchen verlangen, dass wir sofort mit der Leistung beginnen; ohne das Kästchen beginnen wir erst nach Ablauf der 14-tägigen Widerrufsfrist. Ist die Kundenart „Betrieb“ hinterlegt, beginnen wir mit Vertragsschluss, ohne dass der Kunde etwas markieren muss.
|
||||
|
||||
**Einordnung.** Ersetzt den bisherigen Buchstaben c (Zustimmung zu den Bedingungen und Wahl des Beginns). Die Buchstaben a, b, d und e bleiben.
|
||||
|
||||
**Begründung.** Spec 5.6, E1, E3, E4, C4 und recht.md Kurzfassung 3 und 4: Eine Linkzeile über dem Knopf ersetzt die Häkchen für Bedingungen, Leistungsbeschreibung, Auftragsverarbeitung und Belehrung; übrig bleibt das eine Häkchen „Bitte beginnt sofort …“ nur für Privatkunden in der Widerrufsfrist. Bei Betrieben (kein Widerrufsrecht) beginnt die Leistung mit Vertragsschluss. Die Kundenart kommt vom Server (§ 1 Abs. 2). Offen: Wortlaut des Häkchens ohne Normzitat (R6, KQ5), Belehrung nur per Link und PDF (R11, KQ6), AVV per Klick ohne eigenes Häkchen (R10, KQ7).
|
||||
|
||||
## § 2 Abs. 6
|
||||
|
||||
Wir speichern außerdem den Wortlaut der angezeigten Hinweise und Kästchen, die hinterlegte Kundenart sowie Zeit, IP-Adresse und Browserkennung des Klicks.
|
||||
|
||||
**Einordnung.** Hängt an den bestehenden Absatz 6 (Vertragssprache und Speicherung des Vertragstextes) an.
|
||||
|
||||
**Begründung.** Nachweise (Global Constraints der Übersicht, N34): Wortlaut, Fassung, Zeit, IP-Adresse, Browserkennung und Konto werden weiter gespeichert, ab jetzt mit der Kundenart zum Klickzeitpunkt. Der Satz macht das für den Kunden sichtbar; die Datenschutzerklärung nennt die Speicherung heute schon (Prüfpunkte D1 bis D3). Offen: Datenschutz-Prüfpunkte (R14).
|
||||
|
||||
## § 3 Abs. 8
|
||||
|
||||
Zum Abo gehören Funktionen, die der Kunde im Kundenportal unter „Funktionen“ einschaltet. Das Hosting-Abo enthält die Website-Analyse. Das Wartungs-Abo enthält News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse. Umfang, Voraussetzungen und Pflichten jeder Funktion beschreiben ihre Leistungsbeschreibung und, soweit die Funktion personenbezogene Daten im Auftrag des Kunden verarbeitet, ihr Auftragsverarbeitungsvertrag, jeweils in der Fassung, die das Kundenportal beim Einschalten anzeigt. Eine Funktion kostet keine zusätzliche Gebühr; die Einrichtung richtet sich nach § 4 Abs. 8 und 9.
|
||||
|
||||
Vorbehaltlich § 11 Abs. 4 läuft eine Funktion nur, solange das Abo läuft, das sie enthält. Das gilt auch für eine Funktion, die wir auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut haben. Funktionen, die der Kunde vor Veröffentlichung dieser Fassung einzeln gekauft hat, laufen nach den Bedingungen weiter, unter denen er sie gekauft hat.
|
||||
|
||||
**Einordnung.** Neuer Absatz „(8) Funktionen“ nach Absatz (7) mit zwei Absätzen.
|
||||
|
||||
**Begründung.** E6, E9, E10, E21, C10, C11, C12 und Spec 5.5, 5.9: Funktionen sind Teil des Abos und kosten keine zusätzliche Gebühr; die Einrichtung läuft nach § 4 Abs. 8 und 9. Eine Funktion läuft nur mit dem Abo, das sie enthält, auch wenn sie schon bei der Website-Erstellung eingebaut wurde (E21). Einzeln gekaufte Funktionen behalten ihre Bedingungen (E10). Offen: Geltung für laufende Abos (R2, KQ1), Einrichtung beim Projektstart (R4, KQ8), Ende und Fristen (R8, KQ8), Analyse im Hosting-Abo (R9).
|
||||
|
||||
## § 4 Abs. 2
|
||||
|
||||
Einen Vorschuss nach Absatz 8 gleichen die folgenden Kontingente zuerst aus.
|
||||
|
||||
**Einordnung.** Hängt als letzter Satz an den bestehenden Absatz 2 (Verfügbarkeit und Verfall der Kontingente) an.
|
||||
|
||||
**Begründung.** E17: Ein offener Vorschuss wird vor allem anderen aus den folgenden Monatskontingenten ausgeglichen (FIFO, Spec 5.5). Der Satz verknüpft die bestehende Verbrauchsregel mit Absatz 8. Offen: Vorschuss-Regel insgesamt (R3, KQ2).
|
||||
|
||||
## § 4 Abs. 8
|
||||
|
||||
Die Einrichtung einer Funktion rechnen wir als Arbeitszeit auf das Stundenkontingent an. Angerechnet wird die tatsächlich aufgewendete Zeit nach Absatz 4. Reicht das verfügbare Kontingent nicht aus, führen wir die Einrichtung trotzdem ohne weitere Rückfrage aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen, sobald sie bereitstehen. Dafür berechnen wir nichts, weder als Mehraufwand nach Absatz 6 noch in einer gesonderten Rechnung. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest; der Kunde schuldet ihn nicht.
|
||||
|
||||
**Einordnung.** Neuer Absatz 8 nach Absatz 7.
|
||||
|
||||
**Begründung.** E7, E8 und E17 (E17 ersetzt die Absprache aus E8), Spec 5.5: Die Einrichtung startet ohne Rückfrage und geht mit der echten Arbeitszeit (Absatz 4) vom Stundenkonto ab. Reicht das Kontingent nicht, ist die fehlende Zeit ein Vorschuss, nie Mehraufwand nach Absatz 6; ein Rest bei Vertragsende verfällt zugunsten des Kunden. Normale Arbeit außerhalb von Einrichtungen bleibt beim Mehraufwand nach Absatz 6. Offen: Vorschuss ohne Obergrenze (R3, KQ2).
|
||||
|
||||
## § 4 Abs. 9
|
||||
|
||||
Im Hosting-Abo ist die Einrichtung der Website-Analyse für den Kunden kostenlos; ein Stundenkontingent gibt es dort nicht (Absatz 7). Lässt der Kunde eine Funktion auf seinen Wunsch schon bei der Erstellung seiner Website einbauen, rechnen wir die Einrichtung nicht über das Stundenkontingent ab, sondern über die Kosten des Projekts, wie wir es mit ihm vereinbart haben.
|
||||
|
||||
**Einordnung.** Neuer Absatz 9 nach Absatz 8.
|
||||
|
||||
**Begründung.** E9, E21, C6 und C7, Spec 5.5: Im Hosting-Abo gibt es kein Kontingent (Absatz 7), die Einrichtung der Analyse ist dort kostenlos (C6). Bei einer Funktion, die schon bei der Erstellung der Website eingebaut wird, zählen die Einrichtungs-Arbeitsblätter nicht aufs Stundenkonto, sondern laufen über die Projektkosten, wie mit dem Kunden vereinbart (C7). Offen: Einrichtung beim Projektstart (R4, KQ8), Analyse im Hosting-Abo (R9).
|
||||
|
||||
## § 6 Abs. 6
|
||||
|
||||
Schaltet der Kunde die Website-Analyse ein, ist er für die Messung auf seiner Website Verantwortlicher. Er entfernt oder umgeht das Cookie-Banner nicht, bindet ohne Abstimmung mit uns keine weiteren Mess- oder Tracking-Werkzeuge ein und ergänzt seine Datenschutzerklärung um einen Hinweis auf die von uns erzeugte Zusatzseite „/datenschutz-analyse.html“.
|
||||
|
||||
**Einordnung.** Neuer Absatz 6 nach Absatz 5.
|
||||
|
||||
**Begründung.** C10 und Spec 5.5 (Analyse): Wer die Website-Analyse einschaltet, ist für die Messung auf seiner Website Verantwortlicher. Der Absatz stellt die Pflichten aus der Abo-Ergänzung Analyse (Cookie-Banner, keine weiteren Mess- oder Tracking-Werkzeuge ohne Abstimmung, Hinweis auf die Zusatzseite) in die Abo-Bedingungen, weil die Analyse jetzt auch im Hosting-Abo liegt. Die Tatsachenangabe zur Zielgruppe bleibt als Zeile mit Häkchen in der Oberfläche (Spec 5.5, N22). Offen: Analyse im Hosting-Abo (R9, KQ8), Ausschlüsse als Vertragsklausel (R13, KQ9).
|
||||
|
||||
## § 9 Abs. 6
|
||||
|
||||
Will der Kunde im Hosting-Abo eine Funktion einschalten, die nur das Wartungs-Abo enthält, führt ihn das Kundenportal zum Wechsel. Nach Wirksamkeit des Wechsels schaltet er die Funktion in einem eigenen Vorgang ein. Der Wechsel bleibt eine eigene Bestellung nach Absatz 2.
|
||||
|
||||
**Einordnung.** Neuer Absatz 6 nach Absatz 5.
|
||||
|
||||
**Begründung.** E14 und C5: Wer im Hosting-Abo eine Funktion einschalten will, die nur das Wartungs-Abo enthält, wird zum Wechsel geführt und schaltet danach in einem eigenen Vorgang ein. Der Wechsel bleibt eine eigene Bestellung mit eigener Widerrufsfrist (§ 2 Abs. 5); das Einschalten ist keine Bestellung. Offen: Sofortbeginn des Wechsels nur mit Häkchen bei Privatkunden (R6, KQ5).
|
||||
|
||||
## § 11 Abs. 4
|
||||
|
||||
Mit dem Ende des Abos, das eine Funktion enthält, endet die Funktion; abweichend davon läuft Öffnungszeiten live noch 30 Tage unverändert weiter. Wir schalten sie ab und löschen ihre Daten nach den Fristen in ihrer Leistungsbeschreibung; die Messdaten der Website-Analyse löschen wir 60 Tage nach der Abschaltung. Beginnt innerhalb dieser Fristen wieder ein Abo, das die Funktion enthält, schalten wir sie mit den vorhandenen Daten wieder ein. Wechselt der Kunde vom Wartungs- ins Hosting-Abo, bleibt die Website-Analyse; die übrigen Funktionen enden. Das Gleiche gilt, wenn der Kunde den Wechsel ins Wartungs-Abo widerruft.
|
||||
|
||||
**Einordnung.** Neuer Absatz 4 nach Absatz 3.
|
||||
|
||||
**Begründung.** E21, C12 und Nachtrag N31 (mit P4 und P6 abgestimmt): Funktionen enden mit dem Abo, das sie enthält. Terminplaner, News, Webshop und Buchungskalender schalten spätestens am Folgetag ab (Tagesjob); Öffnungszeiten live läuft nach dem Ende noch 30 Tage unverändert weiter, ein „Behalten“ gegen Zahlung gibt es nicht mehr. Gelöscht wird nach den Fristen der Leistungsbeschreibungen: 30 Tage (Terminplaner, News, Öffnungszeiten live), 90 Tage (Webshop, Buchungskalender), 60 Tage für die Messdaten der Analyse. Ein neues passendes Abo in der Frist holt die Funktion zurück. Beim Wechsel ins Hosting-Abo und beim Widerruf des Wechsels bleibt die Analyse (E6, C10). Offen: R8 (KQ8).
|
||||
|
||||
## § 14 Abs. 2
|
||||
|
||||
Für Funktionen, die personenbezogene Daten im Auftrag des Kunden verarbeiten, gilt zusätzlich der Auftragsverarbeitungsvertrag der Funktion, dem der Kunde beim Einschalten mit dem Klick auf „Einschalten“ zustimmt.
|
||||
|
||||
**Einordnung.** Hängt an den bestehenden Absatz 2 (Auftragsverarbeitungsvertrag in Anlage 1) an.
|
||||
|
||||
**Begründung.** Spec 5.9 (AVV je Funktion): Option A der AVV-Struktur, die Auftragsverarbeitungsverträge der Funktionen bleiben eigene Verträge mit neuer Vorbemerkung, Anlage 1 bleibt der Rahmen; der Klick auf „Einschalten“ unter der Linkzeile ist die Zustimmung. Offen: Struktur (R10, KQ7), Klick statt eigenes Häkchen (KQ7).
|
||||
|
||||
## § 15
|
||||
|
||||
Fassung 6 gilt für Abos, die ab ihrer Veröffentlichung abgeschlossen werden. Für Abos, die nach einer früheren Fassung abgeschlossen wurden, gelten diese Fassungen weiter. Schaltet der Kunde mit einem solchen Abo eine Funktion ein, gelten für die Funktion ihre Leistungsbeschreibung und ihr Auftragsverarbeitungsvertrag in der angezeigten Fassung, einschließlich der Regeln zur Einrichtung und zum Stundenkontingent.
|
||||
|
||||
**Einordnung.** Neuer zweiter Absatz hinter dem bisherigen Text. Der bisherige Text bleibt wörtlich.
|
||||
|
||||
**Begründung.** C11 und E10: Fassung 6 gilt für neue Abschlüsse; laufende Abos der Fassungen 3 bis 5 behalten ihren Vertrag. Beim Einschalten gelten die Leistungsbeschreibung und der Auftragsverarbeitungsvertrag der Funktion über die Linkzeile und den Klick, auch mit den Regeln zur Einrichtung und zum Stundenkonto. Die Änderung ist für laufende Abos vorteilhaft (Spec 5.9), deshalb keine Zustimmungsabfrage nach dem bisherigen Text. Offen: Info-Mail an laufende Abos (R2, KQ1).
|
||||
|
||||
## Leistungsbeschreibung A Nr. 1
|
||||
|
||||
Website-Analyse im Kundenportal (§ 3 Abs. 8)
|
||||
|
||||
**Einordnung.** Neuer Listenpunkt am Ende von Nr. 1 „Umfang“.
|
||||
|
||||
**Begründung.** E6 und C10: Das Hosting-Abo enthält die Website-Analyse. Offen: Analyse im Hosting-Abo (R9, KQ8).
|
||||
|
||||
## Leistungsbeschreibung B Nr. 1
|
||||
|
||||
Funktionen im Kundenportal: News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse (§ 3 Abs. 8)
|
||||
|
||||
**Einordnung.** Neuer Listenpunkt am Ende von Nr. 1 „Umfang“.
|
||||
|
||||
**Begründung.** E6: Das Wartungs-Abo enthält alle Funktionen und als Obermenge die Analyse.
|
||||
|
||||
## Widerruf: Hinweise
|
||||
|
||||
Das Einschalten einer Funktion ist kein eigener Vertrag, sondern Teil des Abos. Es verlängert die Widerrufsfrist des Abos nicht. Widerruft ein Verbraucher das Abo, endet auch die Funktion (§ 11 Abs. 4).
|
||||
|
||||
**Einordnung.** Neue Nummer 5 am Ende der „Hinweise zum Widerruf“. Widerrufsbelehrung und Muster-Widerrufsformular bleiben wortgleich mit Fassung 5.
|
||||
|
||||
**Begründung.** Spec 5.5 und 5.9, recht.md § 2: Das Einschalten löst keine neue Zahlungspflicht aus und ist kein eigener Vertrag; deshalb gibt es keine eigene Widerrufsfrist. Bei Widerruf des Abos endet die Funktion (§ 11 Abs. 4). Wie sich die Einrichtungszeit innerhalb der Frist auf den Wertersatz auswirkt, regelt das Häkchen für Privatkunden (§ 2 Abs. 2 c); die Entscheidung dazu steht in R7. Offen: R7 (KQ3), Belehrung per Link (R11, KQ6).
|
||||
|
||||
## Anlage 3
|
||||
|
||||
Server für die Website-Analyse (Messdienst, nur bei eingeschalteter Funktion)
|
||||
|
||||
Deutschland (Rechenzentrum Falkenstein)
|
||||
|
||||
**Einordnung.** Neue Tabellenzeile hinter den beiden Hetzner-Zeilen (Unternehmen wie dort, Leistung und Ort wie im Pflichtsatz).
|
||||
|
||||
**Begründung.** Spec 5.9 (AVV je Funktion), Art. 28 Abs. 2 DSGVO: Der Server des Messdienstes der Website-Analyse (Hetzner, Rechenzentrum Falkenstein, wie in der Auftragsverarbeitung der Analyse) wird als Unterauftragsverarbeiter genannt, solange die Analyse eingeschaltet ist. Offen: AVV-Struktur (R10, KQ7).
|
||||
|
||||
## Was gleich bleibt
|
||||
|
||||
- Preise: Hosting-Abo 20,00 € pro Monat, Wartungs-Abo 50,00 € pro Monat, E-Mail-Zusatzspeicher 1,00 € pro Monat je Paket von 5 GB, Mehraufwand 100 € pro Stunde (alle Endpreise, `§ 19 UStG`). Funktionen bekommen keinen Preis.
|
||||
- Laufzeiten: Mindestlaufzeit zwölf Monate ab Leistungsbeginn, Kündigung zum Ende der Mindestlaufzeit mit einem Monat Frist, danach monatlich (§ 8), Sonderkündigung bei Geschäftsaufgabe.
|
||||
- Paragrafenfolge § 1 bis § 16 und alle Anker (`p1` bis `p16`, `wechsel`, `zusatzspeicher`, `hosting`, `wartung`, `widerruf`, `formular`, `widerruf-hinweise`, `avv`, `tom`, `sub`).
|
||||
- Widerrufsbelehrung und Muster-Widerrufsformular: wortgleich mit Fassung 5; die Hinweise zum Widerruf Nr. 1 bis 4 bleiben.
|
||||
- § 10 E-Mail-Zusatzspeicher mit Absatz 3 (Bereitstellung sofort, kein Wertersatz) und den Hinweisen zum Widerruf Nr. 4.
|
||||
- Anlage 1 (Auftragsverarbeitungsvertrag) und Anlage 2 (technische und organisatorische Maßnahmen); Anlage 3 behält ihre beiden Hetzner-Zeilen.
|
||||
- Die Rechtsdokumente bleiben in der bisherigen Anrede („der Kunde“, in der Übersichtsbox „Ihr“); nur Oberflächentexte stehen in „du“ (R12).
|
||||
@@ -0,0 +1,149 @@
|
||||
# Allgemeine Geschäftsbedingungen
|
||||
|
||||
**Fassung 2026-09-30** · Wortgleiche Kopie der Allgemeinen Geschäftsbedingungen auf webklar.com/agb
|
||||
|
||||
## Kaufvertrag – WEBklar (Modularer Projektvertrag)
|
||||
|
||||
**zwischen**
|
||||
|
||||
WEBklar (im Folgenden „Anbieter“)
|
||||
|
||||
**und**
|
||||
|
||||
Kunde laut Angebot (im Folgenden „Kunde“)
|
||||
|
||||
## 1. Vertragsgegenstand
|
||||
|
||||
(1) Gegenstand dieses Vertrages ist die Erbringung der im Angebot definierten Leistungen.
|
||||
|
||||
(2) Der Vertrag besteht aus diesem Grundvertrag sowie den ausgewählten Leistungsmodulen.
|
||||
|
||||
(3) Maßgeblich ist das jeweils angenommene Angebot von WEBklar.
|
||||
|
||||
## 2. Leistungsart
|
||||
|
||||
(1) Sämtliche Leistungen von WEBklar stellen Dienst- und Entwicklungsleistungen dar.
|
||||
|
||||
(2) Ein bestimmter wirtschaftlicher, technischer oder rechtlicher Erfolg wird nicht geschuldet.
|
||||
|
||||
(3) WEBklar erbringt keinen laufenden Betrieb, sofern dieser nicht explizit vereinbart wurde.
|
||||
|
||||
## 3. Leistungsmodul A – Webseite (einmalig)
|
||||
|
||||
(aktiv, wenn im Angebot enthalten)
|
||||
|
||||
(1) WEBklar erstellt eine individuelle Webseite gemäß Angebot.
|
||||
|
||||
(2) Die Umsetzung erfolgt nach den vom Kunden gelieferten Inhalten und Vorgaben.
|
||||
|
||||
(3) Zusätzliche Leistungen wie Domain, Hosting, Wartung oder SEO sind nicht Bestandteil, sofern sie nicht gesondert beauftragt wurden.
|
||||
|
||||
(4) Der Kunde ist nicht berechtigt, Änderungen am Quellcode selbst vorzunehmen.
|
||||
|
||||
(5) Änderungen erfolgen ausschließlich durch WEBklar gegen gesonderte Vergütung.
|
||||
|
||||
## 4. Leistungsmodul B – Automatisierung / Virtualisierung (einmalig)
|
||||
|
||||
(aktiv, wenn im Angebot enthalten)
|
||||
|
||||
(1) WEBklar entwickelt individuelle Automatisierungen, Apps oder Virtualisierungssysteme.
|
||||
|
||||
(2) Die Leistung stellt eine reine Entwicklungsleistung dar.
|
||||
|
||||
(3) Optional kann eine Beratungsleistung Bestandteil des Projektes sein.
|
||||
|
||||
(4) Ein laufender Betrieb, Monitoring oder Wartung ist nicht geschuldet, außer dies wurde explizit vereinbart.
|
||||
|
||||
(5) Der Kunde entscheidet über Inhalte, Daten und Prozesse und trägt dafür die rechtliche Verantwortung.
|
||||
|
||||
## 5. Leistungsmodul C – Hosting
|
||||
|
||||
(aktiv, wenn im Angebot enthalten)
|
||||
|
||||
(1) Hosting bietet WEBklar als Hosting-Abo an. Für das Hosting-Abo und das Wartungs-Abo gelten die Vertragsbedingungen für die WEBklar-Abos mit Leistungsbeschreibungen; sie gehen diesen AGB vor.
|
||||
|
||||
(2) Bestehende Hosting-Verträge mit jährlicher Laufzeit behalten die bei ihrem Abschluss vereinbarten Bedingungen. Abweichend davon können Verbraucher solche Verträge nach einer automatischen Verlängerung jederzeit mit einer Frist von einem Monat kündigen.
|
||||
|
||||
(3) WEBklar ist berechtigt, externe Anbieter (z. B. Rechenzentren) einzusetzen.
|
||||
|
||||
## 6. Quellcode und Eigentum
|
||||
|
||||
### 6.1 Webseite
|
||||
|
||||
(1) Der Kunde erhält ein einfaches, zeitlich unbegrenztes Nutzungsrecht an der fertigen Webseite.
|
||||
|
||||
(2) Der Quellcode der Webseite wird nur auf ausdrückliche Anfrage und nach Vereinbarung herausgegeben.
|
||||
|
||||
(3) Ohne Vereinbarung verbleibt der Quellcode bei WEBklar und wird archiviert.
|
||||
|
||||
### 6.2 Apps, Automatisierungen und Backend
|
||||
|
||||
(1) Der Quellcode von Apps, Automatisierungen und Backend-Systemen verbleibt vollständig bei WEBklar.
|
||||
|
||||
(2) Eine Herausgabe erfolgt ausschließlich nach gesonderter schriftlicher Vereinbarung.
|
||||
|
||||
(3) Der Kunde erhält lediglich Zugriff auf die Bedienoberfläche bzw. das Frontend.
|
||||
|
||||
## 7. Mitwirkungspflichten des Kunden
|
||||
|
||||
(1) Der Kunde stellt alle benötigten Inhalte, Daten und Freigaben rechtzeitig bereit.
|
||||
|
||||
(2) Verzögerungen durch fehlende Mitwirkung gehen nicht zu Lasten von WEBklar.
|
||||
|
||||
(3) WEBklar ist nicht verpflichtet, rechtliche Prüfungen der Inhalte vorzunehmen.
|
||||
|
||||
## 8. Abnahme
|
||||
|
||||
(1) Nach Fertigstellung wird dem Kunden die Leistung zur Abnahme bereitgestellt.
|
||||
|
||||
(2) Erfolgt innerhalb von 14 Tagen keine Rückmeldung, gilt die Leistung als abgenommen.
|
||||
|
||||
(3) Nach Abnahme sind nur noch kostenpflichtige Änderungen möglich.
|
||||
|
||||
## 9. Vergütung und Zahlung
|
||||
|
||||
(1) Die Vergütung ergibt sich aus dem Angebot.
|
||||
|
||||
(2) Wird nach Aufwand abgerechnet, erfasst WEBklar die Arbeitszeit je Arbeitsschritt in Schritten von 15 Minuten; angefangene 15 Minuten werden aufgerundet. Wirkt eine weitere Person unterstützend mit, rechnet WEBklar für sie die Hälfte der Arbeitszeit des Arbeitsschritts an, aufgerundet auf volle 15 Minuten; bei mehreren unterstützenden Personen gilt das für jede. Jeder Arbeitsschritt und jede Unterstützung ist im Kundenportal einsehbar.
|
||||
|
||||
(3) Projektleistungen sind nach Vereinbarung fällig.
|
||||
|
||||
(4) Für die Abos gelten die Vertragsbedingungen für die WEBklar-Abos.
|
||||
|
||||
## 10. Zahlungsverzug
|
||||
|
||||
(1) Bei Zahlungsverzug erfolgen bis zu zwei Mahnungen.
|
||||
|
||||
(2) Danach ist WEBklar berechtigt:
|
||||
|
||||
- Leistungen zu sperren
|
||||
- Verzugszinsen zu berechnen
|
||||
- den Vertrag außerordentlich zu kündigen
|
||||
|
||||
## 11. Haftung
|
||||
|
||||
(1) WEBklar haftet unbeschränkt bei Vorsatz und grober Fahrlässigkeit, bei Verletzung von Leben, Körper oder Gesundheit, nach dem Produkthaftungsgesetz und soweit WEBklar eine Garantie übernommen hat.
|
||||
|
||||
(2) Bei leicht fahrlässiger Verletzung einer wesentlichen Vertragspflicht haftet WEBklar nur für den vertragstypischen, vorhersehbaren Schaden. Wesentlich sind Pflichten, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Kunde regelmäßig vertrauen darf. Im Übrigen ist die Haftung für leichte Fahrlässigkeit ausgeschlossen.
|
||||
|
||||
(3) Der Kunde bewahrt Kopien der Inhalte und Daten auf, die er WEBklar übergibt oder selbst einpflegt. Für den Verlust solcher Inhalte und Daten haftet WEBklar außer in den Fällen von Absatz 1 nur in Höhe des Aufwands, der für ihre Wiederherstellung nötig gewesen wäre, wenn der Kunde diese Kopien aufbewahrt hätte.
|
||||
|
||||
(4) Diese Regeln gelten auch zugunsten der Mitarbeiter und Beauftragten von WEBklar.
|
||||
|
||||
## 12. Kündigung
|
||||
|
||||
(1) Laufzeitverträge können zum Ende der jeweiligen Laufzeit gekündigt werden. Laufzeit und Kündigung der Abos richten sich nach den Vertragsbedingungen für die WEBklar-Abos.
|
||||
|
||||
(2) Das Recht zur außerordentlichen Kündigung bleibt unberührt.
|
||||
|
||||
## 13. Referenzen
|
||||
|
||||
WEBklar darf das Projekt nur nach ausdrücklicher Zustimmung des Kunden als Referenz verwenden.
|
||||
|
||||
## 14. Schlussbestimmungen
|
||||
|
||||
(1) Es gilt deutsches Recht.
|
||||
|
||||
(2) Gerichtsstand ist der Sitz von WEBklar, soweit zulässig.
|
||||
|
||||
(3) Sollten einzelne Bestimmungen unwirksam sein, bleibt der Vertrag im Übrigen wirksam.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Archiv der Analyse-Texte
|
||||
|
||||
Jede ausgelieferte Fassung bleibt hier nachvollziehbar. `BANNER_VERSION` im Client (`analytics/client/core.js`) und `config.analyse.avvVersion` tragen dieselbe Fassung.
|
||||
|
||||
| Fassung | gültig ab | Banner-Text | Zusatzseite |
|
||||
|---|---|---|---|
|
||||
| 2026-10-02 | offen bis Livegang | [banner-2026-10-02.md](banner-2026-10-02.md) (`BANNER_VERSION` 2026-10-02) | [datenschutz-analyse-2026-10-02.html](datenschutz-analyse-2026-10-02.html) |
|
||||
|
||||
Änderung der Zusatzseite am 02.10.2026 (Fassung bleibt 2026-10-02, Banner-Text unverändert): Der Knopf zum Wiederöffnen des Banners ist jetzt ein Keks-Symbol (Name für Screenreader und Beschriftung beim Darüberfahren weiter „Datenschutz-Einstellungen“). Die Zusatzseite nennt deshalb bei Widerruf und Widerspruch zusätzlich „(das Keks-Symbol)“. Die vorige Formulierung war bis dahin nur auf der WEBklar-Testseite (test-website.project.webklar.com) ausgeliefert, keiner Kunden-Website: [datenschutz-analyse-2026-10-02-erstfassung.html](datenschutz-analyse-2026-10-02-erstfassung.html). In WOMS → Rechtliches als neue Version der Karte „datenschutz-website-analyse“ hinterlegt.
|
||||
|
||||
## Eigene Website webklar.com
|
||||
|
||||
Auf webklar.com läuft dieselbe Messung mit WEBklar als Verantwortlichem (keine AVV). Das Skript-Tag trägt `data-variant="eigen"`; der Banner hat dann eine eigene Sie-Fassung (`BANNER_VERSION_EIGEN`), die Zusatzseite liegt im Repository der Website (`public/datenschutz-analyse.html`), die Kopie hier ist byte-gleich zu der ausgelieferten Fassung. Der Abschnitt 4a der Datenschutzerklärung fasst sie zusammen.
|
||||
|
||||
| Fassung | gültig ab | Banner-Text | Zusatzseite |
|
||||
|---|---|---|---|
|
||||
| 2026-10-17 | offen bis Livegang | [banner-eigen-2026-10-17.md](banner-eigen-2026-10-17.md) (`BANNER_VERSION_EIGEN` 2026-10-17) | [datenschutz-analyse-webklar-com-2026-10-17.html](datenschutz-analyse-webklar-com-2026-10-17.html) |
|
||||
|
||||
AVV und Leistungsbeschreibung je Fassung: `public/rechtliches/avv-website-analyse-<Fassung>.html|pdf` und `leistungsbeschreibung-website-analyse-<Fassung>.html|pdf`. „Gültig ab“ wird beim Livegang eingetragen.
|
||||
|
||||
## Terminplaner
|
||||
|
||||
Die Texte des Terminplaners (Leistungsbeschreibung, AVV, Widerrufsbelehrung mit Muster-Widerrufsformular) tragen die Fassung `config.termine.avvVersion`. Quelle ist je Dokument eine Markdown-Datei unter `docs/rechtliches/`; `node scripts/build-legal-html.mjs termine` erzeugt daraus die HTML-Seiten, `scripts/build-legal-pdfs.sh termine` die PDFs (die Mail K15 hängt sie an). Eine ausgelieferte Fassung wird nie überschrieben: jede Änderung nach der Übergabe an die Kanzlei oder dem Livegang bekommt ein neues Datum und neue Dateien.
|
||||
|
||||
| Fassung | gültig ab | Leistungsbeschreibung | AVV | Widerrufsbelehrung |
|
||||
|---|---|---|---|---|
|
||||
| 2026-10-05 | offen bis Livegang | [HTML](../../../public/rechtliches/leistungsbeschreibung-terminplaner-2026-10-05.html) · [PDF](../../../public/rechtliches/leistungsbeschreibung-terminplaner-2026-10-05.pdf) | [HTML](../../../public/rechtliches/avv-terminplaner-2026-10-05.html) · [PDF](../../../public/rechtliches/avv-terminplaner-2026-10-05.pdf) | [HTML](../../../public/rechtliches/widerrufsbelehrung-funktionen-2026-10-05.html) · [PDF](../../../public/rechtliches/widerrufsbelehrung-funktionen-2026-10-05.pdf) |
|
||||
|
||||
Adressen ohne Fassung (`/rechtliches/leistungsbeschreibung-terminplaner.html`, `/rechtliches/avv-terminplaner.html`, `/rechtliches/widerrufsbelehrung-funktionen.html`) liefern die Seite der aktuellen Fassung; Mails und Zustimmungstexte verlinken nur die Adressen mit Fassung. „Gültig ab“ wird beim Livegang eingetragen.
|
||||
|
||||
## Terminplaner-Gästetexte
|
||||
|
||||
Die Texte, die ein Gast beim Buchen sieht (Hinweis am Formular, Texte der Haken, Gesundheitshinweis, Knopftexte, Widerrufsbelehrung für Gäste) und der Datenschutz-Absatz für die Website des Betriebs tragen die Fassung `RECHTSTEXTE_VERSION` in `server/services/termine/rechtstexte.js`. Der Nachweis einer Buchung speichert die Fassung und Hashes der Texte, nicht die Texte selbst (AVV Terminplaner Nr. 8 Abs. 5); den Wortlaut je Fassung hält dieses Archiv. `node scripts/termine-texte-archiv.mjs` erzeugt `termine-gaestetexte-<Fassung>.md` aus den Texten (Betriebsangaben als Platzhalter), `--check` vergleicht mit der eingecheckten Datei. Eine Datei einer früheren Fassung wird nie neu erzeugt oder geändert (`test/termineRechtstexteArchiv.test.js` pinnt ihren Hash); jede Textänderung bekommt ein neues Datum und eine neue Datei. Das Archiv bleibt bis drei Jahre nach dem Ende des Terminplaners erhalten.
|
||||
|
||||
| Fassung | gültig ab | Archiv |
|
||||
|---|---|---|
|
||||
| 2026-10-02 | 2026-10-03 (Pilot) | [termine-gaestetexte-2026-10-02.md](termine-gaestetexte-2026-10-02.md) |
|
||||
| 2026-10-05 | offen bis Livegang | [termine-gaestetexte-2026-10-05.md](termine-gaestetexte-2026-10-05.md) |
|
||||
|
||||
„Gültig ab“ einer neuen Fassung wird beim Livegang eingetragen; ihr Zeitraum endet mit dem „gültig ab“ der nächsten.
|
||||
|
||||
Änderungen der Fassung 2026-10-05 gegenüber 2026-10-02 (Textänderungen: nur diese beiden Stellen): Der Haken für die Erinnerung endet mit dem Satz „Diese Einwilligung kann ich jederzeit widerrufen.“ (Art. 7 Abs. 3 DSGVO); der Datenschutz-Absatz sagt bei der Abschaltung: „Wird die Online-Terminbuchung abgeschaltet, zum Beispiel weil unser Vertrag mit WEBklar endet, werden alle Termindaten 30 Tage nach dem Abschalten gelöscht.“ Außerdem ändert sich die Regel zum Haken „Stornobedingungen“, nicht sein Text: Pflicht ist er nur noch bei einer Leistung mit Preis, bei kostenlosen Leistungen fragt der Terminplaner nicht danach (die Fassung 2026-10-02 verlangte ihn bei jeder Leistung, sobald der Betrieb Stornobedingungen hat). Buchungen, die vor dem Livegang der neuen Fassung entstanden sind, behalten im Nachweis die Fassung 2026-10-02.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Banner-Text Fassung 2026-10-02
|
||||
|
||||
Quelle: `analytics/client/core.js` (Konstanten `BANNER_*`). Wortgleich zu `banner-portal-mailtexte.md` § 1.
|
||||
|
||||
- `BANNER_VERSION`: 2026-10-02
|
||||
|
||||
## Fließtext (BANNER_TEXT)
|
||||
|
||||
Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: [Datenschutz](/datenschutz-analyse.html)
|
||||
|
||||
## aria-label (BANNER_ARIA)
|
||||
|
||||
Einwilligung zur Reichweitenmessung
|
||||
|
||||
## Schaltflächen
|
||||
|
||||
„Ablehnen“ · „Annehmen“ (gleich gestaltet, Ablehnen zuerst)
|
||||
|
||||
## Einstellungs-Link (SETTINGS_LABEL)
|
||||
|
||||
Datenschutz-Einstellungen
|
||||
|
||||
Ab 02.10.2026: als angeknabbertes, sich langsam drehendes Keks-Symbol unten rechts (44 px); „Datenschutz-Einstellungen“ ist sein Name (`aria-label`) und erscheint beim Darüberfahren und bei Tastaturfokus. Bei „Bewegung reduzieren“ dreht er sich nicht. Bis dahin: Textknopf „Datenschutz-Einstellungen“.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Banner-Text Fassung 2026-10-17 (eigene Websites der WEBklar GbR)
|
||||
|
||||
Quelle: `analytics/client/core.js` (Konstanten `BANNER_TEXT_EIGEN`, `BANNER_VERSION_EIGEN`). Gilt nur, wenn das Skript-Tag `data-variant="eigen"` trägt, derzeit nur auf webklar.com. Kunden-Websites behalten die Fassung 2026-10-02 ([banner-2026-10-02.md](banner-2026-10-02.md)).
|
||||
|
||||
- `BANNER_VERSION_EIGEN`: 2026-10-17
|
||||
|
||||
## Fließtext (BANNER_TEXT_EIGEN)
|
||||
|
||||
Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: [Datenschutz](/datenschutz-analyse.html)
|
||||
|
||||
## aria-label, Schaltflächen, Einstellungs-Link
|
||||
|
||||
Wie in der Kunden-Fassung: „Einwilligung zur Reichweitenmessung“; „Ablehnen“ · „Annehmen“ (gleich gestaltet, Ablehnen zuerst); Keks-Symbol „Datenschutz-Einstellungen“ unten rechts.
|
||||
@@ -0,0 +1,84 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Datenschutz – Reichweitenmessung</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.6;color:#111;background:#fff;max-width:46rem;margin:0 auto;padding:1.5rem 1rem 16rem}
|
||||
h1{font-size:1.6rem}h2{font-size:1.15rem;margin-top:2rem}
|
||||
.stand{color:#555;font-size:.9rem}
|
||||
ul{padding-left:1.25rem}
|
||||
a{color:#0b57d0}
|
||||
.box{border:2px solid #111;border-radius:10px;padding:.25rem 1rem;margin:1.5rem 0}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p><a href="/">← Zur Startseite</a></p>
|
||||
|
||||
<h1>Datenschutz bei der Reichweitenmessung</h1>
|
||||
<p class="stand">Ergänzung zur Datenschutzerklärung dieser Website · Stand: 02.10.2026</p>
|
||||
<p>Diese Seite beschreibt nur die Reichweitenmessung. Alle übrigen Datenschutzhinweise findest Du in der Datenschutzerklärung dieser Website; Impressum und Datenschutzerklärung erreichst Du über die Startseite.</p>
|
||||
|
||||
<h2>Verantwortlicher</h2>
|
||||
<p>{{company}}<br>{{#address}}{{address}}<br>{{/address}}E-Mail: <a href="mailto:{{email}}">{{email}}</a></p>
|
||||
<p>Falls es einen Datenschutzbeauftragten gibt, findest Du seine Kontaktdaten in der Datenschutzerklärung dieser Website.</p>
|
||||
|
||||
<h2>Worum es geht</h2>
|
||||
<p>Wir möchten verstehen, wie diese Website genutzt wird, um sie zu verbessern: Welche Seiten werden gelesen, woher kommen die Besucher, wo wird geklickt? Dafür nutzen wir eine eigene Messung ohne Cookies und ohne Dienste von Google, Meta oder anderen Werbenetzwerken. Wir nutzen die Daten nicht für Werbung und bilden keine Profile einzelner Besucher. Die Messung läuft auf einem Server in Deutschland.</p>
|
||||
<p>Damit die Messung arbeiten kann, lädt Dein Browser bei jedem Seitenaufruf ein Skript von einem Server der WEBklar GbR. Dabei erhält dieser Server, wie bei jeder Datei, die eine Website lädt, technisch Deine IP-Adresse und Dein Browser-Kennzeichen (User-Agent). Das gilt auch, wenn Du ablehnst oder „Global Privacy Control“ aktiviert hast. Wir speichern und protokollieren beides nicht.</p>
|
||||
|
||||
<h2>Solange Du im Banner nichts ausgewählt hast</h2>
|
||||
<p>Dann zählen wir den Aufruf der Seite. Das Skript übermittelt dafür nur die Adresse der Seite und die Angabe, dass das Banner angezeigt wurde. Gespeichert werden:</p>
|
||||
<ul>
|
||||
<li>der Seitenpfad (ohne Suchbegriffe oder Parameter hinter dem „?“),</li>
|
||||
<li>das Land, das wir aus Deiner IP-Adresse ableiten,</li>
|
||||
<li>die Geräteklasse (Handy, Tablet oder Computer), die wir aus dem Browser-Kennzeichen ableiten,</li>
|
||||
<li>der Zeitpunkt, auf die volle Stunde gerundet, und</li>
|
||||
<li>ein Zähler dafür, dass das Banner angezeigt wurde.</li>
|
||||
</ul>
|
||||
<p>IP-Adresse und Browser-Kennzeichen verwenden wir nur kurz im Arbeitsspeicher: um Land und Geräteklasse zu bestimmen, um automatische Abrufe durch Programme (Bots) auszusortieren und um zu verhindern, dass die Messung durch massenhafte Anfragen verfälscht wird. Für diese Mengenbegrenzung bilden wir aus Deiner IP-Adresse einen Prüfwert (Hash), den wir nur im Arbeitsspeicher halten und nicht speichern. Eine Kennung, mit der wir Dich wiedererkennen könnten, speichern wir ohne Deine Einwilligung nicht.</p>
|
||||
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse ist, die Nutzung unserer Website in Zahlen zu kennen, das Banner zu betreiben und die Messung vor Missbrauch zu schützen.</p>
|
||||
|
||||
<h2>Wenn Du „Ablehnen“ wählst</h2>
|
||||
<p>Dann zählen wir einmal, dass abgelehnt wurde, und das Skript sendet danach nichts mehr, auch keine Seitenaufrufe. Sendet Dein Browser das Signal „Global Privacy Control“ oder „Do Not Track“, zeigen wir das Banner gar nicht an und zählen nichts.</p>
|
||||
|
||||
<h2>Wenn Du „Annehmen“ wählst</h2>
|
||||
<p>Dann messen wir zusätzlich:</p>
|
||||
<ul>
|
||||
<li>von welcher Website Du gekommen bist (nur der Domainname),</li>
|
||||
<li>die Größenklasse Deines Browserfensters,</li>
|
||||
<li>eine Besucher-Kennung, die täglich wechselt. Wir berechnen sie aus IP-Adresse und Browser-Kennzeichen mit einem Schlüssel, der jeden Tag neu erzeugt und nicht gespeichert wird. Am selben Tag könnten wir die Kennung damit nachrechnen. Nach dem Wechsel des Schlüssels lässt sie sich nicht mehr aus Deiner IP-Adresse berechnen und nicht mit Kennungen anderer Tage verknüpfen,</li>
|
||||
<li>wie lange Du auf einer Seite bleibst (ab dem Laden der Seite) und wie weit Du scrollst,</li>
|
||||
<li>wo auf der Seite geklickt wird: die Klickposition und den Aufbau des angeklickten Elements, ohne Texte und ohne Eingaben in Formulare (für eine „Heatmap“),</li>
|
||||
<li>ob Du auf eine Telefonnummer, eine E-Mail-Adresse, einen WhatsApp-Link oder einen Link zu einer anderen Website klickst (dann mit deren Domainname, nicht mit der Nummer oder Adresse selbst), und</li>
|
||||
<li>einen Zähler dafür, dass Du „Annehmen“ gewählt hast.</li>
|
||||
</ul>
|
||||
<p>Rechtsgrundlage ist Deine Einwilligung nach § 25 Abs. 1 TDDDG und Art. 6 Abs. 1 lit. a DSGVO.</p>
|
||||
|
||||
<h2>Was in Deinem Browser gespeichert wird</h2>
|
||||
<p>Wir speichern Deine Auswahl („Annehmen“ oder „Ablehnen“) mit Datum und der Fassung des Banner-Textes im lokalen Speicher Deines Browsers (Eintrag „wk-consent“), damit wir Dich nicht bei jedem Seitenaufruf erneut fragen. Das ist ohne Einwilligung zulässig (§ 25 Abs. 2 Nr. 2 TDDDG). Die Auswahl gilt 183 Tage, danach fragen wir erneut. Du kannst den Eintrag jederzeit selbst löschen, indem Du die Website-Daten in Deinem Browser löschst.</p>
|
||||
|
||||
<h2>Speicherdauer</h2>
|
||||
<p>Einzelne Seitenaufrufe, Ereignisse und Klickpositionen löschen wir nach 30 Tagen. Danach bleiben nur zusammengefasste Tageswerte, also Summen je Tag, etwa Aufrufe je Seite oder je Land, ohne Besucher-Kennung. Wir löschen sie nach 25 Monaten, spätestens rund 60 Tage nach dem Ende der Messung. Unser Dienstleister sichert den Server täglich; gelöschte Daten bleiben dort bis zu sieben Tage erhalten.</p>
|
||||
|
||||
<h2>Empfänger</h2>
|
||||
<p>Die Messung betreibt für uns die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, als Auftragsverarbeiter. WEBklar darf die Daten nur auf unsere Weisung und für unsere Zwecke verwenden. Der Server steht bei der Hetzner Online GmbH in Deutschland, die als weiterer Auftragsverarbeiter eingesetzt wird. Eine Übermittlung in Länder außerhalb der EU bzw. des EWR findet nicht statt.</p>
|
||||
|
||||
<h2>Freiwilligkeit</h2>
|
||||
<p>Du bist weder gesetzlich noch vertraglich verpflichtet, uns diese Daten bereitzustellen. Wenn Du ablehnst oder Deine Einwilligung widerrufst, kannst Du die Website trotzdem uneingeschränkt nutzen. Eine automatisierte Entscheidungsfindung findet nicht statt.</p>
|
||||
|
||||
<h2>Einwilligung widerrufen</h2>
|
||||
<p>Du kannst Deine Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen: Klicke auf „Datenschutz-Einstellungen“ (in der Regel unten rechts auf jeder Seite) und wähle „Ablehnen“. Die Rechtmäßigkeit der Verarbeitung bis zum Widerruf bleibt unberührt.</p>
|
||||
|
||||
<div class="box">
|
||||
<h2>Widerspruchsrecht</h2>
|
||||
<p>Du kannst der Zählung nach Art. 6 Abs. 1 lit. f DSGVO jederzeit aus Gründen widersprechen, die sich aus Deiner besonderen Situation ergeben (Art. 21 DSGVO). Am einfachsten wählst Du im Banner oder über „Datenschutz-Einstellungen“ „Ablehnen“ oder aktivierst in Deinem Browser das Signal „Global Privacy Control“. Dann sendet das Skript keine Daten mehr. Du kannst Deinen Widerspruch auch an die oben genannte E-Mail-Adresse richten.</p>
|
||||
</div>
|
||||
|
||||
<h2>Deine Rechte</h2>
|
||||
<p>Du hast das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit (Art. 15 bis 18 und 20 DSGVO). In den Messdaten speichern wir keine Namen, keine IP-Adressen und keine Kennung, die länger als einen Tag gilt, und wir runden Zeitpunkte auf die volle Stunde. Deshalb können wir gespeicherte Messdaten in der Regel nicht Dir zuordnen (Art. 11 DSGVO). Anders kann es sein, solange die Zugriffsprotokolle des Webservers noch vorhanden sind (14 Tage). Wenn Du uns Datum, ungefähre Uhrzeit, besuchte Seiten und, wenn Du möchtest, Deine IP-Adresse nennst, prüfen wir, ob eine Zuordnung möglich ist. Ist das nicht möglich, teilen wir Dir das mit.</p>
|
||||
<p>Mit Deinen Anliegen kannst Du Dich jederzeit an die oben genannte Adresse wenden. Außerdem hast Du das Recht, Dich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO).</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,84 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Datenschutz – Reichweitenmessung</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.6;color:#111;background:#fff;max-width:46rem;margin:0 auto;padding:1.5rem 1rem 16rem}
|
||||
h1{font-size:1.6rem}h2{font-size:1.15rem;margin-top:2rem}
|
||||
.stand{color:#555;font-size:.9rem}
|
||||
ul{padding-left:1.25rem}
|
||||
a{color:#0b57d0}
|
||||
.box{border:2px solid #111;border-radius:10px;padding:.25rem 1rem;margin:1.5rem 0}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p><a href="/">← Zur Startseite</a></p>
|
||||
|
||||
<h1>Datenschutz bei der Reichweitenmessung</h1>
|
||||
<p class="stand">Ergänzung zur Datenschutzerklärung dieser Website · Stand: 02.10.2026</p>
|
||||
<p>Diese Seite beschreibt nur die Reichweitenmessung. Alle übrigen Datenschutzhinweise findest Du in der Datenschutzerklärung dieser Website; Impressum und Datenschutzerklärung erreichst Du über die Startseite.</p>
|
||||
|
||||
<h2>Verantwortlicher</h2>
|
||||
<p>{{company}}<br>{{#address}}{{address}}<br>{{/address}}E-Mail: <a href="mailto:{{email}}">{{email}}</a></p>
|
||||
<p>Falls es einen Datenschutzbeauftragten gibt, findest Du seine Kontaktdaten in der Datenschutzerklärung dieser Website.</p>
|
||||
|
||||
<h2>Worum es geht</h2>
|
||||
<p>Wir möchten verstehen, wie diese Website genutzt wird, um sie zu verbessern: Welche Seiten werden gelesen, woher kommen die Besucher, wo wird geklickt? Dafür nutzen wir eine eigene Messung ohne Cookies und ohne Dienste von Google, Meta oder anderen Werbenetzwerken. Wir nutzen die Daten nicht für Werbung und bilden keine Profile einzelner Besucher. Die Messung läuft auf einem Server in Deutschland.</p>
|
||||
<p>Damit die Messung arbeiten kann, lädt Dein Browser bei jedem Seitenaufruf ein Skript von einem Server der WEBklar GbR. Dabei erhält dieser Server, wie bei jeder Datei, die eine Website lädt, technisch Deine IP-Adresse und Dein Browser-Kennzeichen (User-Agent). Das gilt auch, wenn Du ablehnst oder „Global Privacy Control“ aktiviert hast. Wir speichern und protokollieren beides nicht.</p>
|
||||
|
||||
<h2>Solange Du im Banner nichts ausgewählt hast</h2>
|
||||
<p>Dann zählen wir den Aufruf der Seite. Das Skript übermittelt dafür nur die Adresse der Seite und die Angabe, dass das Banner angezeigt wurde. Gespeichert werden:</p>
|
||||
<ul>
|
||||
<li>der Seitenpfad (ohne Suchbegriffe oder Parameter hinter dem „?“),</li>
|
||||
<li>das Land, das wir aus Deiner IP-Adresse ableiten,</li>
|
||||
<li>die Geräteklasse (Handy, Tablet oder Computer), die wir aus dem Browser-Kennzeichen ableiten,</li>
|
||||
<li>der Zeitpunkt, auf die volle Stunde gerundet, und</li>
|
||||
<li>ein Zähler dafür, dass das Banner angezeigt wurde.</li>
|
||||
</ul>
|
||||
<p>IP-Adresse und Browser-Kennzeichen verwenden wir nur kurz im Arbeitsspeicher: um Land und Geräteklasse zu bestimmen, um automatische Abrufe durch Programme (Bots) auszusortieren und um zu verhindern, dass die Messung durch massenhafte Anfragen verfälscht wird. Für diese Mengenbegrenzung bilden wir aus Deiner IP-Adresse einen Prüfwert (Hash), den wir nur im Arbeitsspeicher halten und nicht speichern. Eine Kennung, mit der wir Dich wiedererkennen könnten, speichern wir ohne Deine Einwilligung nicht.</p>
|
||||
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse ist, die Nutzung unserer Website in Zahlen zu kennen, das Banner zu betreiben und die Messung vor Missbrauch zu schützen.</p>
|
||||
|
||||
<h2>Wenn Du „Ablehnen“ wählst</h2>
|
||||
<p>Dann zählen wir einmal, dass abgelehnt wurde, und das Skript sendet danach nichts mehr, auch keine Seitenaufrufe. Sendet Dein Browser das Signal „Global Privacy Control“ oder „Do Not Track“, zeigen wir das Banner gar nicht an und zählen nichts.</p>
|
||||
|
||||
<h2>Wenn Du „Annehmen“ wählst</h2>
|
||||
<p>Dann messen wir zusätzlich:</p>
|
||||
<ul>
|
||||
<li>von welcher Website Du gekommen bist (nur der Domainname),</li>
|
||||
<li>die Größenklasse Deines Browserfensters,</li>
|
||||
<li>eine Besucher-Kennung, die täglich wechselt. Wir berechnen sie aus IP-Adresse und Browser-Kennzeichen mit einem Schlüssel, der jeden Tag neu erzeugt und nicht gespeichert wird. Am selben Tag könnten wir die Kennung damit nachrechnen. Nach dem Wechsel des Schlüssels lässt sie sich nicht mehr aus Deiner IP-Adresse berechnen und nicht mit Kennungen anderer Tage verknüpfen,</li>
|
||||
<li>wie lange Du auf einer Seite bleibst (ab dem Laden der Seite) und wie weit Du scrollst,</li>
|
||||
<li>wo auf der Seite geklickt wird: die Klickposition und den Aufbau des angeklickten Elements, ohne Texte und ohne Eingaben in Formulare (für eine „Heatmap“),</li>
|
||||
<li>ob Du auf eine Telefonnummer, eine E-Mail-Adresse, einen WhatsApp-Link oder einen Link zu einer anderen Website klickst (dann mit deren Domainname, nicht mit der Nummer oder Adresse selbst), und</li>
|
||||
<li>einen Zähler dafür, dass Du „Annehmen“ gewählt hast.</li>
|
||||
</ul>
|
||||
<p>Rechtsgrundlage ist Deine Einwilligung nach § 25 Abs. 1 TDDDG und Art. 6 Abs. 1 lit. a DSGVO.</p>
|
||||
|
||||
<h2>Was in Deinem Browser gespeichert wird</h2>
|
||||
<p>Wir speichern Deine Auswahl („Annehmen“ oder „Ablehnen“) mit Datum und der Fassung des Banner-Textes im lokalen Speicher Deines Browsers (Eintrag „wk-consent“), damit wir Dich nicht bei jedem Seitenaufruf erneut fragen. Das ist ohne Einwilligung zulässig (§ 25 Abs. 2 Nr. 2 TDDDG). Die Auswahl gilt 183 Tage, danach fragen wir erneut. Du kannst den Eintrag jederzeit selbst löschen, indem Du die Website-Daten in Deinem Browser löschst.</p>
|
||||
|
||||
<h2>Speicherdauer</h2>
|
||||
<p>Einzelne Seitenaufrufe, Ereignisse und Klickpositionen löschen wir nach 30 Tagen. Danach bleiben nur zusammengefasste Tageswerte, also Summen je Tag, etwa Aufrufe je Seite oder je Land, ohne Besucher-Kennung. Wir löschen sie nach 25 Monaten, spätestens rund 60 Tage nach dem Ende der Messung. Unser Dienstleister sichert den Server täglich; gelöschte Daten bleiben dort bis zu sieben Tage erhalten.</p>
|
||||
|
||||
<h2>Empfänger</h2>
|
||||
<p>Die Messung betreibt für uns die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, als Auftragsverarbeiter. WEBklar darf die Daten nur auf unsere Weisung und für unsere Zwecke verwenden. Der Server steht bei der Hetzner Online GmbH in Deutschland, die als weiterer Auftragsverarbeiter eingesetzt wird. Eine Übermittlung in Länder außerhalb der EU bzw. des EWR findet nicht statt.</p>
|
||||
|
||||
<h2>Freiwilligkeit</h2>
|
||||
<p>Du bist weder gesetzlich noch vertraglich verpflichtet, uns diese Daten bereitzustellen. Wenn Du ablehnst oder Deine Einwilligung widerrufst, kannst Du die Website trotzdem uneingeschränkt nutzen. Eine automatisierte Entscheidungsfindung findet nicht statt.</p>
|
||||
|
||||
<h2>Einwilligung widerrufen</h2>
|
||||
<p>Du kannst Deine Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen: Klicke auf „Datenschutz-Einstellungen“ (das Keks-Symbol, in der Regel unten rechts auf jeder Seite) und wähle „Ablehnen“. Die Rechtmäßigkeit der Verarbeitung bis zum Widerruf bleibt unberührt.</p>
|
||||
|
||||
<div class="box">
|
||||
<h2>Widerspruchsrecht</h2>
|
||||
<p>Du kannst der Zählung nach Art. 6 Abs. 1 lit. f DSGVO jederzeit aus Gründen widersprechen, die sich aus Deiner besonderen Situation ergeben (Art. 21 DSGVO). Am einfachsten wählst Du im Banner oder über „Datenschutz-Einstellungen“ (das Keks-Symbol) „Ablehnen“ oder aktivierst in Deinem Browser das Signal „Global Privacy Control“. Dann sendet das Skript keine Daten mehr. Du kannst Deinen Widerspruch auch an die oben genannte E-Mail-Adresse richten.</p>
|
||||
</div>
|
||||
|
||||
<h2>Deine Rechte</h2>
|
||||
<p>Du hast das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit (Art. 15 bis 18 und 20 DSGVO). In den Messdaten speichern wir keine Namen, keine IP-Adressen und keine Kennung, die länger als einen Tag gilt, und wir runden Zeitpunkte auf die volle Stunde. Deshalb können wir gespeicherte Messdaten in der Regel nicht Dir zuordnen (Art. 11 DSGVO). Anders kann es sein, solange die Zugriffsprotokolle des Webservers noch vorhanden sind (14 Tage). Wenn Du uns Datum, ungefähre Uhrzeit, besuchte Seiten und, wenn Du möchtest, Deine IP-Adresse nennst, prüfen wir, ob eine Zuordnung möglich ist. Ist das nicht möglich, teilen wir Dir das mit.</p>
|
||||
<p>Mit Deinen Anliegen kannst Du Dich jederzeit an die oben genannte Adresse wenden. Außerdem hast Du das Recht, Dich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO).</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Datenschutz bei der Reichweitenmessung – WEBklar</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #f7f6f3; color: #1d1d1f; font: 16px/1.6 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; }
|
||||
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 12rem; }
|
||||
.brand { font-weight: 800; letter-spacing: 0.12em; font-size: 13px; color: #6d1fd1; }
|
||||
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; }
|
||||
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #e3e0da; }
|
||||
p, li { margin: 6px 0; }
|
||||
ul { padding-left: 24px; }
|
||||
.meta { color: #6b6b70; font-size: 14px; }
|
||||
.box { background: #fff; border: 2px solid #6d1fd1; border-radius: 14px; padding: 16px 20px; margin: 18px 0; }
|
||||
a { color: #6d1fd1; }
|
||||
nav.top { font-size: 14px; margin-bottom: 22px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav class="top"><a href="/">← Zur Startseite</a> · <a href="https://project.webklar.com/datenschutz.html">Datenschutzerklärung</a> · <a href="/impressum">Impressum</a></nav>
|
||||
<div class="brand">WEBKLAR</div>
|
||||
<h1>Datenschutz bei der Reichweitenmessung</h1>
|
||||
<p class="meta">Ergänzung zu unserer Datenschutzerklärung (Abschnitt 4a) · gilt für webklar.com · Stand 17.10.2026</p>
|
||||
<p>Diese Seite beschreibt nur die Reichweitenmessung auf webklar.com. Alle übrigen Hinweise finden Sie in unserer <a href="https://project.webklar.com/datenschutz.html">Datenschutzerklärung</a>.</p>
|
||||
|
||||
<h2>Verantwortlicher</h2>
|
||||
<p>WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler<br>
|
||||
E-Mail: <a href="mailto:support@webklar.com">support@webklar.com</a></p>
|
||||
<p>Einen Datenschutzbeauftragten haben wir nicht benannt, weil wir dazu gesetzlich nicht verpflichtet sind.</p>
|
||||
|
||||
<h2>Worum es geht</h2>
|
||||
<p>Wir möchten verstehen, wie unsere Website genutzt wird, um sie zu verbessern: Welche Seiten werden gelesen, woher kommen die Besucher, wo wird geklickt? Dafür nutzen wir unsere eigene Website-Analyse, dieselbe, die wir auch unseren Kunden anbieten. Sie kommt ohne Cookies und ohne Dienste von Google, Meta oder anderen Werbenetzwerken aus. Wir nutzen die Daten nicht für Werbung und bilden keine Profile einzelner Besucher. Die Messung läuft auf unserem Server in Deutschland.</p>
|
||||
<p>Damit die Messung arbeiten kann, lädt Ihr Browser bei jedem Seitenaufruf ein Skript von unserem Server (project.webklar.com). Dabei erhält dieser Server, wie bei jeder Datei, die eine Website lädt, technisch Ihre IP-Adresse und Ihr Browser-Kennzeichen (User-Agent). Das gilt auch, wenn Sie ablehnen oder „Global Privacy Control“ aktiviert haben. Beim Abruf des Skripts und bei der Messung speichern und protokollieren wir beides nicht.</p>
|
||||
|
||||
<h2>Solange Sie im Banner nichts ausgewählt haben</h2>
|
||||
<p>Dann zählen wir den Aufruf der Seite. Das Skript übermittelt dafür nur die Adresse der Seite und die Angabe, dass das Banner angezeigt wurde. Gespeichert werden:</p>
|
||||
<ul>
|
||||
<li>der Seitenpfad (ohne Suchbegriffe oder Parameter hinter dem „?“),</li>
|
||||
<li>das Land, das wir aus Ihrer IP-Adresse ableiten,</li>
|
||||
<li>die Geräteklasse (Handy, Tablet oder Computer), die wir aus dem Browser-Kennzeichen ableiten,</li>
|
||||
<li>der Zeitpunkt, auf die volle Stunde gerundet, und</li>
|
||||
<li>ein Zähler dafür, dass das Banner angezeigt wurde.</li>
|
||||
</ul>
|
||||
<p>IP-Adresse und Browser-Kennzeichen verwenden wir nur kurz im Arbeitsspeicher: um Land und Geräteklasse zu bestimmen, um automatische Abrufe durch Programme (Bots) auszusortieren und um zu verhindern, dass die Messung durch massenhafte Anfragen verfälscht wird. Für diese Mengenbegrenzung bilden wir aus Ihrer IP-Adresse einen Prüfwert (Hash), den wir nur im Arbeitsspeicher halten und nicht speichern. Eine Kennung, mit der wir Sie wiedererkennen könnten, speichern wir ohne Ihre Einwilligung nicht.</p>
|
||||
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse ist, die Nutzung unserer Website in Zahlen zu kennen, das Banner zu betreiben und die Messung vor Missbrauch zu schützen.</p>
|
||||
|
||||
<h2>Wenn Sie „Ablehnen“ wählen</h2>
|
||||
<p>Dann zählen wir einmal, dass abgelehnt wurde, und das Skript sendet danach nichts mehr, auch keine Seitenaufrufe. Sendet Ihr Browser das Signal „Global Privacy Control“ oder „Do Not Track“, zeigen wir das Banner gar nicht an und zählen nichts.</p>
|
||||
|
||||
<h2>Wenn Sie „Annehmen“ wählen</h2>
|
||||
<p>Dann messen wir zusätzlich:</p>
|
||||
<ul>
|
||||
<li>von welcher Website Sie gekommen sind (nur der Domainname),</li>
|
||||
<li>die Größenklasse Ihres Browserfensters,</li>
|
||||
<li>eine Besucher-Kennung, die täglich wechselt. Wir berechnen sie aus IP-Adresse und Browser-Kennzeichen mit einem Schlüssel, der jeden Tag neu erzeugt und nicht gespeichert wird. Am selben Tag könnten wir die Kennung damit nachrechnen. Nach dem Wechsel des Schlüssels lässt sie sich nicht mehr aus Ihrer IP-Adresse berechnen und nicht mit Kennungen anderer Tage verknüpfen,</li>
|
||||
<li>wie lange Sie auf einer Seite bleiben (ab dem Laden der Seite) und wie weit Sie scrollen,</li>
|
||||
<li>wo auf der Seite geklickt wird: die Klickposition und den Aufbau des angeklickten Elements, ohne Texte und ohne Eingaben in Formulare (für eine „Heatmap“),</li>
|
||||
<li>ob Sie auf eine Telefonnummer, eine E-Mail-Adresse, einen WhatsApp-Link oder einen Link zu einer anderen Website klicken (dann mit deren Domainname, nicht mit der Nummer oder Adresse selbst), und</li>
|
||||
<li>einen Zähler dafür, dass Sie „Annehmen“ gewählt haben.</li>
|
||||
</ul>
|
||||
<p>Rechtsgrundlage ist Ihre Einwilligung nach § 25 Abs. 1 TDDDG und Art. 6 Abs. 1 lit. a DSGVO.</p>
|
||||
|
||||
<h2>Was in Ihrem Browser gespeichert wird</h2>
|
||||
<p>Wir speichern Ihre Auswahl („Annehmen“ oder „Ablehnen“) mit Datum und der Fassung des Banner-Textes im lokalen Speicher Ihres Browsers (Eintrag „wk-consent“), damit wir Sie nicht bei jedem Seitenaufruf erneut fragen. Das ist ohne Einwilligung zulässig (§ 25 Abs. 2 Nr. 2 TDDDG). Die Auswahl gilt 183 Tage, danach fragen wir erneut. Sie können den Eintrag jederzeit selbst löschen, indem Sie die Website-Daten in Ihrem Browser löschen.</p>
|
||||
|
||||
<h2>Speicherdauer</h2>
|
||||
<p>Einzelne Seitenaufrufe, Ereignisse und Klickpositionen löschen wir nach 30 Tagen. Danach bleiben nur zusammengefasste Tageswerte, also Summen je Tag, etwa Aufrufe je Seite oder je Land, ohne Besucher-Kennung. Wir löschen sie nach 25 Monaten. Beenden wir die Messung, löschen wir alle Messdaten spätestens rund 60 Tage danach. Unser Dienstleister sichert den Server täglich; gelöschte Daten bleiben dort bis zu sieben Tage erhalten.</p>
|
||||
|
||||
<h2>Empfänger</h2>
|
||||
<p>Die Messung betreiben wir selbst. Die Daten sehen nur unsere Mitarbeiter. Der Server steht bei der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland; Hetzner ist für uns als Auftragsverarbeiter tätig. Eine Übermittlung in Länder außerhalb der EU bzw. des EWR findet nicht statt.</p>
|
||||
|
||||
<h2>Freiwilligkeit</h2>
|
||||
<p>Sie sind weder gesetzlich noch vertraglich verpflichtet, uns diese Daten bereitzustellen. Wenn Sie ablehnen oder Ihre Einwilligung widerrufen, können Sie unsere Website trotzdem uneingeschränkt nutzen. Eine automatisierte Entscheidungsfindung findet nicht statt.</p>
|
||||
|
||||
<div class="box">
|
||||
<h2 style="border:0;margin-top:0;padding-top:0">Einwilligung widerrufen</h2>
|
||||
<p>Sie können Ihre Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen: Klicken Sie auf „Datenschutz-Einstellungen“ (das Keks-Symbol unten rechts auf jeder Seite) und wählen Sie „Ablehnen“. Die Rechtmäßigkeit der Verarbeitung bis zum Widerruf bleibt unberührt.</p>
|
||||
<h2 style="border:0;padding-top:0">Widerspruchsrecht</h2>
|
||||
<p>Sie können der Zählung nach Art. 6 Abs. 1 lit. f DSGVO jederzeit aus Gründen widersprechen, die sich aus Ihrer besonderen Situation ergeben (Art. 21 DSGVO). Am einfachsten wählen Sie im Banner oder über „Datenschutz-Einstellungen“ (das Keks-Symbol) „Ablehnen“ oder aktivieren in Ihrem Browser das Signal „Global Privacy Control“. Dann sendet das Skript keine Daten mehr. Sie können Ihren Widerspruch auch an die oben genannte E-Mail-Adresse richten.</p>
|
||||
</div>
|
||||
|
||||
<h2>Ihre Rechte</h2>
|
||||
<p>Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit (Art. 15 bis 18 und 20 DSGVO). In den Messdaten speichern wir keine Namen, keine IP-Adressen und keine Kennung, die länger als einen Tag gilt, und wir runden Zeitpunkte auf die volle Stunde. Deshalb können wir gespeicherte Messdaten in der Regel nicht Ihnen zuordnen (Art. 11 DSGVO). Anders kann es sein, solange die Zugriffsprotokolle unseres Webservers noch vorhanden sind (Abschnitt 3 der Datenschutzerklärung). Wenn Sie uns Datum, ungefähre Uhrzeit, besuchte Seiten und, wenn Sie möchten, Ihre IP-Adresse nennen, prüfen wir, ob eine Zuordnung möglich ist. Ist das nicht möglich, teilen wir Ihnen das mit.</p>
|
||||
<p>Mit Ihren Anliegen können Sie sich jederzeit an die oben genannte Adresse wenden. Außerdem haben Sie das Recht, sich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO), für uns zuständig ist der Landesbeauftragte für den Datenschutz und die Informationsfreiheit Rheinland-Pfalz.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,141 @@
|
||||
# Gästetexte des Terminplaners, Fassung 2026-10-02
|
||||
|
||||
Dieses Archiv hält den Wortlaut der Gästetexte fest, die ein Gast in dieser Fassung sieht (AVV Terminplaner Nr. 8 Abs. 5). Der Nachweis einer Buchung speichert die Fassung, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes (SHA-256) der übrigen Texte, nicht die Texte selbst.
|
||||
|
||||
Fassung der Gästetexte: **2026-10-02** (`RECHTSTEXTE_VERSION` in `server/services/termine/rechtstexte.js`). Der Zeitraum der Verwendung ergibt sich aus „gültig ab“ dieser Fassung und der nächsten Fassung in der Tabelle „Terminplaner-Gästetexte“ in `README.md` dieses Ordners.
|
||||
|
||||
Erzeugt mit `node scripts/termine-texte-archiv.mjs`, geprüft mit `--check`. Nicht von Hand ändern: eine Textänderung bekommt eine neue Fassung und eine neue Datei; diese Datei bleibt, wie sie war.
|
||||
|
||||
Die Betriebsangaben stehen als Platzhalter: `{{Name des Betriebs}}`, `{{Anschrift}}`, `{{Telefon}}` und `{{E-Mail}}` stammen aus der Einstellung des Betriebs, `{{Stunden}}` ist die gewählte Vorlaufzeit der Erinnerung (der Nachweis speichert sie), die Aufbewahrung nach dem Termin ist mit der Vorgabe von 90 Tagen dargestellt (einstellbar von 14 bis 365 Tagen).
|
||||
|
||||
## 1. Hinweis unter dem Buchungsformular (Datenschutz)
|
||||
|
||||
### Sie-Fassung
|
||||
|
||||
> Datenschutz. Wir verwenden Ihre Angaben, um Ihren Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO). Betrieben wird der Terminkalender in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland). Wir speichern Ihre Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist. Es werden keine Cookies gesetzt. Bitte machen Sie keine Angaben zu Ihrer Gesundheit. Mehr erfahren Sie in unserer Datenschutzerklärung. Verantwortlich: {{Name des Betriebs}}, {{Anschrift}}.
|
||||
|
||||
### Du-Fassung
|
||||
|
||||
> Datenschutz. Wir verwenden deine Angaben, um deinen Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO). Betrieben wird der Terminkalender in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland). Wir speichern deine Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist. Es werden keine Cookies gesetzt. Bitte mach keine Angaben zu deiner Gesundheit. Mehr erfährst du in unserer Datenschutzerklärung. Verantwortlich: {{Name des Betriebs}}, {{Anschrift}}.
|
||||
|
||||
## 2. Haken
|
||||
|
||||
Die Haken stehen immer in der Sie-Form, auch bei Betrieben, die ihre Gäste duzen.
|
||||
|
||||
### Vorzeitiger Beginn (Pflicht bei einer Leistung mit Preis für Verbraucher, wenn der Termin vor Ablauf der Widerrufsfrist beginnt, bei Terminanfragen immer)
|
||||
|
||||
> Ich verlange ausdrücklich, dass {{Name des Betriebs}} mit der Leistung vor Ablauf der Widerrufsfrist beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).
|
||||
|
||||
### Stornobedingungen (Pflicht, wenn der Betrieb Stornobedingungen hat; der Text der Bedingungen steht beim Betrieb und trägt eine eigene Version)
|
||||
|
||||
> Ich habe die Stornobedingungen gelesen.
|
||||
|
||||
### Erinnerung (freiwillig, nie vorangekreuzt, nur wenn der Betrieb Erinnerungen anbietet)
|
||||
|
||||
> Ich möchte {{Stunden}} Stunden vor dem Termin eine Erinnerung per E-Mail erhalten.
|
||||
|
||||
## 3. Hinweis zu Gesundheitsangaben
|
||||
|
||||
Steht über dem Nachrichtenfeld des Formulars.
|
||||
|
||||
> Bitte keine Gesundheitsangaben.
|
||||
|
||||
## 4. Knopftexte
|
||||
|
||||
| Leistung | Knopf im Widget | Knopf auf der Bestätigungsseite |
|
||||
|---|---|---|
|
||||
| Sofort buchbar, ohne Preis | Termin buchen | Termin bestätigen |
|
||||
| Sofort buchbar, mit Preis | Zahlungspflichtig buchen | Zahlungspflichtig bestätigen |
|
||||
| Terminanfrage | Termin anfragen | Anfrage absenden |
|
||||
|
||||
Der Text des Buchungsknopfs im Widget steht im Nachweis (`knopf`).
|
||||
|
||||
## 5. Widerrufsbelehrung mit Muster-Widerrufsformular für Gäste
|
||||
|
||||
Sie-Form, mit den Betriebsangaben als Platzhalter.
|
||||
|
||||
> Widerrufsbelehrung
|
||||
>
|
||||
> Widerrufsrecht
|
||||
>
|
||||
> Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
|
||||
>
|
||||
> Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag des Vertragsabschlusses.
|
||||
>
|
||||
> Um Ihr Widerrufsrecht auszuüben, müssen Sie uns ({{Name des Betriebs}}, {{Anschrift}}, Telefon: {{Telefon}}, E-Mail: {{E-Mail}}) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
|
||||
>
|
||||
> Sie können Ihr Widerrufsrecht auch online unter „Vertrag widerrufen“ (Schaltfläche im Terminbereich auf unserer Website sowie Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail) ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.
|
||||
>
|
||||
> Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
|
||||
>
|
||||
> Folgen des Widerrufs
|
||||
>
|
||||
> Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
|
||||
>
|
||||
> Haben Sie verlangt, dass die Dienstleistungen während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht.
|
||||
>
|
||||
> Muster-Widerrufsformular
|
||||
>
|
||||
> (Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
|
||||
>
|
||||
> – An {{Name des Betriebs}}, {{Anschrift}}, {{E-Mail}}:
|
||||
> – Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)
|
||||
> – Bestellt am (*)/erhalten am (*)
|
||||
> – Name des/der Verbraucher(s)
|
||||
> – Anschrift des/der Verbraucher(s)
|
||||
> – Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
|
||||
> – Datum
|
||||
>
|
||||
> (*) Unzutreffendes streichen.
|
||||
|
||||
## 6. Datenschutz-Absatz „Online-Terminbuchung“ für die Datenschutzerklärung der Website des Betriebs
|
||||
|
||||
Hier mit allen bedingten Sätzen (Telefon-Feld, Nachricht oder Zusatzfelder, Erinnerung, Widerruf, Telefonnummer des Betriebs); ein Betrieb ohne die jeweilige Funktion hat den Satz nicht.
|
||||
|
||||
### Sie-Fassung
|
||||
|
||||
> Online-Terminbuchung
|
||||
>
|
||||
> Auf unserer Website können Sie Termine online buchen. Dafür verarbeiten wir die folgenden Daten:
|
||||
>
|
||||
> Welche Daten? Ihren Namen, Ihre E-Mail-Adresse, den gewählten Termin, die gewählte Leistung und die gewählte Person oder den gewählten Platz, auf Wunsch Ihre Telefonnummer und die Angaben, die Sie uns im Formular mitteilen. Außerdem speichern wir, wann Sie den Termin bestätigt haben, welche Fassung der Hinweise Sie bei der Buchung gesehen haben und welche Zustimmungen Sie erteilt haben, zum Beispiel ob Sie eine Erinnerung gewünscht haben.
|
||||
>
|
||||
> Wofür und auf welcher Grundlage? Wir brauchen die Daten, um Ihren Termin zu verwalten, Ihnen den Termin zu bestätigen und Sie über Änderungen oder Absagen zu informieren. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag über den gebuchten Termin, bei einer Terminanfrage vorvertragliche Maßnahmen auf Ihre Anfrage). Wenn Sie eine Erinnerung vor dem Termin wünschen, beruht diese auf Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO. Sie können sie jederzeit mit Wirkung für die Zukunft widerrufen, indem Sie uns eine kurze Nachricht senden oder den Termin über den Link in der E-Mail absagen. Widerrufen Sie einen Vertrag über die Schaltfläche „Vertrag widerrufen“, speichern wir Ihre Erklärung mit Datum und Uhrzeit und senden Ihnen eine Eingangsbestätigung (Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit § 356a BGB). Zum Schutz vor Missbrauch (zum Beispiel automatisierten Buchungen) und um die Zahl der Anfragen zu begrenzen, merken wir uns Ihre IP-Adresse kurzzeitig nur im Arbeitsspeicher des Servers, nicht dauerhaft (Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere Betrieb der Terminbuchung).
|
||||
>
|
||||
> E-Mails. Sie erhalten eine E-Mail, in der Sie Ihre E-Mail-Adresse und den Termin per Link bestätigen, danach eine Bestätigung und, wenn Sie das angekreuzt haben, eine Erinnerung. Sagen wir den Termin ab oder verschieben ihn, informieren wir Sie ebenfalls per E-Mail. Die E-Mails enthalten keine Werbung und keine Tracking-Pixel. Absender ist {{Name des Betriebs}}, Ihre Antworten gehen an uns. Bis zum Versand liegt die E-Mail in einer Warteschlange; nach dem Versand wird dort die Empfängeradresse samt Inhalt gelöscht, nicht zustellbare E-Mails nach 48 Stunden.
|
||||
>
|
||||
> Wer verarbeitet die Daten in unserem Auftrag? Den Terminkalender betreibt die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, als unser Auftragsverarbeiter nach Art. 28 DSGVO. Die Daten liegen auf Servern der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland (Rechenzentren Falkenstein und Nürnberg). Der E-Mail-Versand läuft ebenfalls über Hetzner. Eine Übermittlung in Drittländer findet nicht statt.
|
||||
>
|
||||
> Beim Aufruf der Terminbuchung. Beim Aufruf dieser Seite lädt Ihr Browser das Programm der Terminbuchung vom Server der WEBklar GbR (project.webklar.com), beim Öffnen der Terminbuchung zusätzlich die Termine und Texte. Dabei werden technisch bedingt Ihre IP-Adresse und Angaben zu Ihrem Browser übertragen. Der Server speichert sie kurzzeitig in Protokollen und löscht sie nach 14 Tagen (Art. 6 Abs. 1 lit. f DSGVO; berechtigtes Interesse ist der sichere und funktionsfähige Betrieb). Wir setzen dafür keine Cookies und speichern nichts auf Ihrem Gerät.
|
||||
>
|
||||
> Wie lange speichern wir? Nicht bestätigte Buchungen (der Bestätigungslink gilt 30 Minuten) löschen wir 48 Stunden nach Ablauf dieser Frist. Bestätigte Termine löschen wir 90 Tage nach dem Termin, abgesagte oder abgelehnte Termine 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die wir bis zum Terminbeginn nicht beantworten, behandeln wir als abgelehnt. Wird die Online-Terminbuchung abgeschaltet, löschen wir alle Termindaten 30 Tage nach dem Abschalten. Gesetzliche Aufbewahrungspflichten für Geschäftsunterlagen (zum Beispiel Rechnungen) bleiben davon unberührt; sie erfüllen wir außerhalb des Terminkalenders.
|
||||
>
|
||||
> Pflicht zur Angabe. Ohne Name, E-Mail-Adresse und Terminwahl können wir keinen Termin online vergeben. Sie können uns auch telefonisch erreichen: {{Telefon}}. Eine automatisierte Entscheidungsfindung findet nicht statt. Bitte machen Sie bei der Buchung keine Angaben zu Ihrer Gesundheit.
|
||||
>
|
||||
> Ihre Rechte. Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit sowie das Recht, der Verarbeitung auf Grundlage berechtigter Interessen zu widersprechen (Art. 15 bis 21 DSGVO). Eine erteilte Einwilligung können Sie jederzeit widerrufen. Sie können sich bei einer Datenschutzaufsichtsbehörde beschweren. Verantwortlich ist {{Name des Betriebs}}, {{Anschrift}}, E-Mail {{E-Mail}}.
|
||||
|
||||
### Du-Fassung
|
||||
|
||||
> Online-Terminbuchung
|
||||
>
|
||||
> Auf unserer Website kannst du Termine online buchen. Dafür verarbeiten wir die folgenden Daten:
|
||||
>
|
||||
> Welche Daten? Deinen Namen, deine E-Mail-Adresse, den gewählten Termin, die gewählte Leistung und die gewählte Person oder den gewählten Platz, auf Wunsch deine Telefonnummer und die Angaben, die du uns im Formular mitteilst. Außerdem speichern wir, wann du den Termin bestätigt hast, welche Fassung der Hinweise du bei der Buchung gesehen hast und welche Zustimmungen du erteilt hast, zum Beispiel ob du eine Erinnerung gewünscht hast.
|
||||
>
|
||||
> Wofür und auf welcher Grundlage? Wir brauchen die Daten, um deinen Termin zu verwalten, dir den Termin zu bestätigen und dich über Änderungen oder Absagen zu informieren. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag über den gebuchten Termin, bei einer Terminanfrage vorvertragliche Maßnahmen auf deine Anfrage). Wenn du eine Erinnerung vor dem Termin wünschst, beruht diese auf deiner Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO. Du kannst sie jederzeit mit Wirkung für die Zukunft widerrufen, indem du uns eine kurze Nachricht sendest oder den Termin über den Link in der E-Mail absagst. Widerrufst du einen Vertrag über die Schaltfläche „Vertrag widerrufen“, speichern wir deine Erklärung mit Datum und Uhrzeit und senden dir eine Eingangsbestätigung (Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit § 356a BGB). Zum Schutz vor Missbrauch (zum Beispiel automatisierten Buchungen) und um die Zahl der Anfragen zu begrenzen, merken wir uns deine IP-Adresse kurzzeitig nur im Arbeitsspeicher des Servers, nicht dauerhaft (Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere Betrieb der Terminbuchung).
|
||||
>
|
||||
> E-Mails. Du erhältst eine E-Mail, in der du deine E-Mail-Adresse und den Termin per Link bestätigst, danach eine Bestätigung und, wenn du das angekreuzt hast, eine Erinnerung. Sagen wir den Termin ab oder verschieben ihn, informieren wir dich ebenfalls per E-Mail. Die E-Mails enthalten keine Werbung und keine Tracking-Pixel. Absender ist {{Name des Betriebs}}, deine Antworten gehen an uns. Bis zum Versand liegt die E-Mail in einer Warteschlange; nach dem Versand wird dort die Empfängeradresse samt Inhalt gelöscht, nicht zustellbare E-Mails nach 48 Stunden.
|
||||
>
|
||||
> Wer verarbeitet die Daten in unserem Auftrag? Den Terminkalender betreibt die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, als unser Auftragsverarbeiter nach Art. 28 DSGVO. Die Daten liegen auf Servern der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland (Rechenzentren Falkenstein und Nürnberg). Der E-Mail-Versand läuft ebenfalls über Hetzner. Eine Übermittlung in Drittländer findet nicht statt.
|
||||
>
|
||||
> Beim Aufruf der Terminbuchung. Beim Aufruf dieser Seite lädt dein Browser das Programm der Terminbuchung vom Server der WEBklar GbR (project.webklar.com), beim Öffnen der Terminbuchung zusätzlich die Termine und Texte. Dabei werden technisch bedingt deine IP-Adresse und Angaben zu deinem Browser übertragen. Der Server speichert sie kurzzeitig in Protokollen und löscht sie nach 14 Tagen (Art. 6 Abs. 1 lit. f DSGVO; berechtigtes Interesse ist der sichere und funktionsfähige Betrieb). Wir setzen dafür keine Cookies und speichern nichts auf deinem Gerät.
|
||||
>
|
||||
> Wie lange speichern wir? Nicht bestätigte Buchungen (der Bestätigungslink gilt 30 Minuten) löschen wir 48 Stunden nach Ablauf dieser Frist. Bestätigte Termine löschen wir 90 Tage nach dem Termin, abgesagte oder abgelehnte Termine 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die wir bis zum Terminbeginn nicht beantworten, behandeln wir als abgelehnt. Wird die Online-Terminbuchung abgeschaltet, löschen wir alle Termindaten 30 Tage nach dem Abschalten. Gesetzliche Aufbewahrungspflichten für Geschäftsunterlagen (zum Beispiel Rechnungen) bleiben davon unberührt; sie erfüllen wir außerhalb des Terminkalenders.
|
||||
>
|
||||
> Pflicht zur Angabe. Ohne Name, E-Mail-Adresse und Terminwahl können wir keinen Termin online vergeben. Du kannst uns auch telefonisch erreichen: {{Telefon}}. Eine automatisierte Entscheidungsfindung findet nicht statt. Bitte mach bei der Buchung keine Angaben zu deiner Gesundheit.
|
||||
>
|
||||
> Deine Rechte. Du hast das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit sowie das Recht, der Verarbeitung auf Grundlage berechtigter Interessen zu widersprechen (Art. 15 bis 21 DSGVO). Eine erteilte Einwilligung kannst du jederzeit widerrufen. Du kannst dich bei einer Datenschutzaufsichtsbehörde beschweren. Verantwortlich ist {{Name des Betriebs}}, {{Anschrift}}, E-Mail {{E-Mail}}.
|
||||
|
||||
## 7. Nachweis
|
||||
|
||||
Der Nachweis speichert Hashes der Texte mit den Betriebsangaben zum Buchungszeitpunkt (⚖ Q26).
|
||||
@@ -0,0 +1,141 @@
|
||||
# Gästetexte des Terminplaners, Fassung 2026-10-05
|
||||
|
||||
Dieses Archiv hält den Wortlaut der Gästetexte fest, die ein Gast in dieser Fassung sieht (AVV Terminplaner Nr. 8 Abs. 5). Der Nachweis einer Buchung speichert die Fassung, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes (SHA-256) der übrigen Texte, nicht die Texte selbst.
|
||||
|
||||
Fassung der Gästetexte: **2026-10-05** (`RECHTSTEXTE_VERSION` in `server/services/termine/rechtstexte.js`). Der Zeitraum der Verwendung ergibt sich aus „gültig ab“ dieser Fassung und der nächsten Fassung in der Tabelle „Terminplaner-Gästetexte“ in `README.md` dieses Ordners.
|
||||
|
||||
Erzeugt mit `node scripts/termine-texte-archiv.mjs`, geprüft mit `--check`. Nicht von Hand ändern: eine Textänderung bekommt eine neue Fassung und eine neue Datei; diese Datei bleibt, wie sie war.
|
||||
|
||||
Die Betriebsangaben stehen als Platzhalter: `{{Name des Betriebs}}`, `{{Anschrift}}`, `{{Telefon}}` und `{{E-Mail}}` stammen aus der Einstellung des Betriebs, `{{Stunden}}` ist die gewählte Vorlaufzeit der Erinnerung (der Nachweis speichert sie), die Aufbewahrung nach dem Termin ist mit der Vorgabe von 90 Tagen dargestellt (einstellbar von 14 bis 365 Tagen).
|
||||
|
||||
## 1. Hinweis unter dem Buchungsformular (Datenschutz)
|
||||
|
||||
### Sie-Fassung
|
||||
|
||||
> Datenschutz. Wir verwenden Ihre Angaben, um Ihren Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO). Betrieben wird der Terminkalender in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland). Wir speichern Ihre Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist. Es werden keine Cookies gesetzt. Bitte machen Sie keine Angaben zu Ihrer Gesundheit. Mehr erfahren Sie in unserer Datenschutzerklärung. Verantwortlich: {{Name des Betriebs}}, {{Anschrift}}.
|
||||
|
||||
### Du-Fassung
|
||||
|
||||
> Datenschutz. Wir verwenden deine Angaben, um deinen Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO). Betrieben wird der Terminkalender in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland). Wir speichern deine Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist. Es werden keine Cookies gesetzt. Bitte mach keine Angaben zu deiner Gesundheit. Mehr erfährst du in unserer Datenschutzerklärung. Verantwortlich: {{Name des Betriebs}}, {{Anschrift}}.
|
||||
|
||||
## 2. Haken
|
||||
|
||||
Die Haken stehen immer in der Sie-Form, auch bei Betrieben, die ihre Gäste duzen.
|
||||
|
||||
### Vorzeitiger Beginn (Pflicht bei einer Leistung mit Preis für Verbraucher, wenn der Termin vor Ablauf der Widerrufsfrist beginnt, bei Terminanfragen immer)
|
||||
|
||||
> Ich verlange ausdrücklich, dass {{Name des Betriebs}} mit der Leistung vor Ablauf der Widerrufsfrist beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).
|
||||
|
||||
### Stornobedingungen (Pflicht bei einer Leistung mit Preis, wenn der Betrieb Stornobedingungen hat; bei kostenlosen Leistungen fragt der Terminplaner nicht danach; der Text der Bedingungen steht beim Betrieb und trägt eine eigene Version)
|
||||
|
||||
> Ich habe die Stornobedingungen gelesen.
|
||||
|
||||
### Erinnerung (freiwillig, nie vorangekreuzt, nur wenn der Betrieb Erinnerungen anbietet)
|
||||
|
||||
> Ich möchte {{Stunden}} Stunden vor dem Termin eine Erinnerung per E-Mail erhalten. Diese Einwilligung kann ich jederzeit widerrufen.
|
||||
|
||||
## 3. Hinweis zu Gesundheitsangaben
|
||||
|
||||
Steht über dem Nachrichtenfeld des Formulars.
|
||||
|
||||
> Bitte keine Gesundheitsangaben.
|
||||
|
||||
## 4. Knopftexte
|
||||
|
||||
| Leistung | Knopf im Widget | Knopf auf der Bestätigungsseite |
|
||||
|---|---|---|
|
||||
| Sofort buchbar, ohne Preis | Termin buchen | Termin bestätigen |
|
||||
| Sofort buchbar, mit Preis | Zahlungspflichtig buchen | Zahlungspflichtig bestätigen |
|
||||
| Terminanfrage | Termin anfragen | Anfrage absenden |
|
||||
|
||||
Der Text des Buchungsknopfs im Widget steht im Nachweis (`knopf`).
|
||||
|
||||
## 5. Widerrufsbelehrung mit Muster-Widerrufsformular für Gäste
|
||||
|
||||
Sie-Form, mit den Betriebsangaben als Platzhalter.
|
||||
|
||||
> Widerrufsbelehrung
|
||||
>
|
||||
> Widerrufsrecht
|
||||
>
|
||||
> Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
|
||||
>
|
||||
> Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag des Vertragsabschlusses.
|
||||
>
|
||||
> Um Ihr Widerrufsrecht auszuüben, müssen Sie uns ({{Name des Betriebs}}, {{Anschrift}}, Telefon: {{Telefon}}, E-Mail: {{E-Mail}}) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
|
||||
>
|
||||
> Sie können Ihr Widerrufsrecht auch online unter „Vertrag widerrufen“ (Schaltfläche im Terminbereich auf unserer Website sowie Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail) ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.
|
||||
>
|
||||
> Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
|
||||
>
|
||||
> Folgen des Widerrufs
|
||||
>
|
||||
> Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
|
||||
>
|
||||
> Haben Sie verlangt, dass die Dienstleistungen während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht.
|
||||
>
|
||||
> Muster-Widerrufsformular
|
||||
>
|
||||
> (Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
|
||||
>
|
||||
> – An {{Name des Betriebs}}, {{Anschrift}}, {{E-Mail}}:
|
||||
> – Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)
|
||||
> – Bestellt am (*)/erhalten am (*)
|
||||
> – Name des/der Verbraucher(s)
|
||||
> – Anschrift des/der Verbraucher(s)
|
||||
> – Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
|
||||
> – Datum
|
||||
>
|
||||
> (*) Unzutreffendes streichen.
|
||||
|
||||
## 6. Datenschutz-Absatz „Online-Terminbuchung“ für die Datenschutzerklärung der Website des Betriebs
|
||||
|
||||
Hier mit allen bedingten Sätzen (Telefon-Feld, Nachricht oder Zusatzfelder, Erinnerung, Widerruf, Telefonnummer des Betriebs); ein Betrieb ohne die jeweilige Funktion hat den Satz nicht.
|
||||
|
||||
### Sie-Fassung
|
||||
|
||||
> Online-Terminbuchung
|
||||
>
|
||||
> Auf unserer Website können Sie Termine online buchen. Dafür verarbeiten wir die folgenden Daten:
|
||||
>
|
||||
> Welche Daten? Ihren Namen, Ihre E-Mail-Adresse, den gewählten Termin, die gewählte Leistung und die gewählte Person oder den gewählten Platz, auf Wunsch Ihre Telefonnummer und die Angaben, die Sie uns im Formular mitteilen. Außerdem speichern wir, wann Sie den Termin bestätigt haben, welche Fassung der Hinweise Sie bei der Buchung gesehen haben und welche Zustimmungen Sie erteilt haben, zum Beispiel ob Sie eine Erinnerung gewünscht haben.
|
||||
>
|
||||
> Wofür und auf welcher Grundlage? Wir brauchen die Daten, um Ihren Termin zu verwalten, Ihnen den Termin zu bestätigen und Sie über Änderungen oder Absagen zu informieren. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag über den gebuchten Termin, bei einer Terminanfrage vorvertragliche Maßnahmen auf Ihre Anfrage). Wenn Sie eine Erinnerung vor dem Termin wünschen, beruht diese auf Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO. Sie können sie jederzeit mit Wirkung für die Zukunft widerrufen, indem Sie uns eine kurze Nachricht senden oder den Termin über den Link in der E-Mail absagen. Widerrufen Sie einen Vertrag über die Schaltfläche „Vertrag widerrufen“, speichern wir Ihre Erklärung mit Datum und Uhrzeit und senden Ihnen eine Eingangsbestätigung (Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit § 356a BGB). Zum Schutz vor Missbrauch (zum Beispiel automatisierten Buchungen) und um die Zahl der Anfragen zu begrenzen, merken wir uns Ihre IP-Adresse kurzzeitig nur im Arbeitsspeicher des Servers, nicht dauerhaft (Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere Betrieb der Terminbuchung).
|
||||
>
|
||||
> E-Mails. Sie erhalten eine E-Mail, in der Sie Ihre E-Mail-Adresse und den Termin per Link bestätigen, danach eine Bestätigung und, wenn Sie das angekreuzt haben, eine Erinnerung. Sagen wir den Termin ab oder verschieben ihn, informieren wir Sie ebenfalls per E-Mail. Die E-Mails enthalten keine Werbung und keine Tracking-Pixel. Absender ist {{Name des Betriebs}}, Ihre Antworten gehen an uns. Bis zum Versand liegt die E-Mail in einer Warteschlange; nach dem Versand wird dort die Empfängeradresse samt Inhalt gelöscht, nicht zustellbare E-Mails nach 48 Stunden.
|
||||
>
|
||||
> Wer verarbeitet die Daten in unserem Auftrag? Den Terminkalender betreibt die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, als unser Auftragsverarbeiter nach Art. 28 DSGVO. Die Daten liegen auf Servern der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland (Rechenzentren Falkenstein und Nürnberg). Der E-Mail-Versand läuft ebenfalls über Hetzner. Eine Übermittlung in Drittländer findet nicht statt.
|
||||
>
|
||||
> Beim Aufruf der Terminbuchung. Beim Aufruf dieser Seite lädt Ihr Browser das Programm der Terminbuchung vom Server der WEBklar GbR (project.webklar.com), beim Öffnen der Terminbuchung zusätzlich die Termine und Texte. Dabei werden technisch bedingt Ihre IP-Adresse und Angaben zu Ihrem Browser übertragen. Der Server speichert sie kurzzeitig in Protokollen und löscht sie nach 14 Tagen (Art. 6 Abs. 1 lit. f DSGVO; berechtigtes Interesse ist der sichere und funktionsfähige Betrieb). Wir setzen dafür keine Cookies und speichern nichts auf Ihrem Gerät.
|
||||
>
|
||||
> Wie lange speichern wir? Nicht bestätigte Buchungen (der Bestätigungslink gilt 30 Minuten) löschen wir 48 Stunden nach Ablauf dieser Frist. Bestätigte Termine löschen wir 90 Tage nach dem Termin, abgesagte oder abgelehnte Termine 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die wir bis zum Terminbeginn nicht beantworten, behandeln wir als abgelehnt. Wird die Online-Terminbuchung abgeschaltet, zum Beispiel weil unser Vertrag mit WEBklar endet, werden alle Termindaten 30 Tage nach dem Abschalten gelöscht. Gesetzliche Aufbewahrungspflichten für Geschäftsunterlagen (zum Beispiel Rechnungen) bleiben davon unberührt; sie erfüllen wir außerhalb des Terminkalenders.
|
||||
>
|
||||
> Pflicht zur Angabe. Ohne Name, E-Mail-Adresse und Terminwahl können wir keinen Termin online vergeben. Sie können uns auch telefonisch erreichen: {{Telefon}}. Eine automatisierte Entscheidungsfindung findet nicht statt. Bitte machen Sie bei der Buchung keine Angaben zu Ihrer Gesundheit.
|
||||
>
|
||||
> Ihre Rechte. Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit sowie das Recht, der Verarbeitung auf Grundlage berechtigter Interessen zu widersprechen (Art. 15 bis 21 DSGVO). Eine erteilte Einwilligung können Sie jederzeit widerrufen. Sie können sich bei einer Datenschutzaufsichtsbehörde beschweren. Verantwortlich ist {{Name des Betriebs}}, {{Anschrift}}, E-Mail {{E-Mail}}.
|
||||
|
||||
### Du-Fassung
|
||||
|
||||
> Online-Terminbuchung
|
||||
>
|
||||
> Auf unserer Website kannst du Termine online buchen. Dafür verarbeiten wir die folgenden Daten:
|
||||
>
|
||||
> Welche Daten? Deinen Namen, deine E-Mail-Adresse, den gewählten Termin, die gewählte Leistung und die gewählte Person oder den gewählten Platz, auf Wunsch deine Telefonnummer und die Angaben, die du uns im Formular mitteilst. Außerdem speichern wir, wann du den Termin bestätigt hast, welche Fassung der Hinweise du bei der Buchung gesehen hast und welche Zustimmungen du erteilt hast, zum Beispiel ob du eine Erinnerung gewünscht hast.
|
||||
>
|
||||
> Wofür und auf welcher Grundlage? Wir brauchen die Daten, um deinen Termin zu verwalten, dir den Termin zu bestätigen und dich über Änderungen oder Absagen zu informieren. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag über den gebuchten Termin, bei einer Terminanfrage vorvertragliche Maßnahmen auf deine Anfrage). Wenn du eine Erinnerung vor dem Termin wünschst, beruht diese auf deiner Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO. Du kannst sie jederzeit mit Wirkung für die Zukunft widerrufen, indem du uns eine kurze Nachricht sendest oder den Termin über den Link in der E-Mail absagst. Widerrufst du einen Vertrag über die Schaltfläche „Vertrag widerrufen“, speichern wir deine Erklärung mit Datum und Uhrzeit und senden dir eine Eingangsbestätigung (Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit § 356a BGB). Zum Schutz vor Missbrauch (zum Beispiel automatisierten Buchungen) und um die Zahl der Anfragen zu begrenzen, merken wir uns deine IP-Adresse kurzzeitig nur im Arbeitsspeicher des Servers, nicht dauerhaft (Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere Betrieb der Terminbuchung).
|
||||
>
|
||||
> E-Mails. Du erhältst eine E-Mail, in der du deine E-Mail-Adresse und den Termin per Link bestätigst, danach eine Bestätigung und, wenn du das angekreuzt hast, eine Erinnerung. Sagen wir den Termin ab oder verschieben ihn, informieren wir dich ebenfalls per E-Mail. Die E-Mails enthalten keine Werbung und keine Tracking-Pixel. Absender ist {{Name des Betriebs}}, deine Antworten gehen an uns. Bis zum Versand liegt die E-Mail in einer Warteschlange; nach dem Versand wird dort die Empfängeradresse samt Inhalt gelöscht, nicht zustellbare E-Mails nach 48 Stunden.
|
||||
>
|
||||
> Wer verarbeitet die Daten in unserem Auftrag? Den Terminkalender betreibt die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, als unser Auftragsverarbeiter nach Art. 28 DSGVO. Die Daten liegen auf Servern der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland (Rechenzentren Falkenstein und Nürnberg). Der E-Mail-Versand läuft ebenfalls über Hetzner. Eine Übermittlung in Drittländer findet nicht statt.
|
||||
>
|
||||
> Beim Aufruf der Terminbuchung. Beim Aufruf dieser Seite lädt dein Browser das Programm der Terminbuchung vom Server der WEBklar GbR (project.webklar.com), beim Öffnen der Terminbuchung zusätzlich die Termine und Texte. Dabei werden technisch bedingt deine IP-Adresse und Angaben zu deinem Browser übertragen. Der Server speichert sie kurzzeitig in Protokollen und löscht sie nach 14 Tagen (Art. 6 Abs. 1 lit. f DSGVO; berechtigtes Interesse ist der sichere und funktionsfähige Betrieb). Wir setzen dafür keine Cookies und speichern nichts auf deinem Gerät.
|
||||
>
|
||||
> Wie lange speichern wir? Nicht bestätigte Buchungen (der Bestätigungslink gilt 30 Minuten) löschen wir 48 Stunden nach Ablauf dieser Frist. Bestätigte Termine löschen wir 90 Tage nach dem Termin, abgesagte oder abgelehnte Termine 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die wir bis zum Terminbeginn nicht beantworten, behandeln wir als abgelehnt. Wird die Online-Terminbuchung abgeschaltet, zum Beispiel weil unser Vertrag mit WEBklar endet, werden alle Termindaten 30 Tage nach dem Abschalten gelöscht. Gesetzliche Aufbewahrungspflichten für Geschäftsunterlagen (zum Beispiel Rechnungen) bleiben davon unberührt; sie erfüllen wir außerhalb des Terminkalenders.
|
||||
>
|
||||
> Pflicht zur Angabe. Ohne Name, E-Mail-Adresse und Terminwahl können wir keinen Termin online vergeben. Du kannst uns auch telefonisch erreichen: {{Telefon}}. Eine automatisierte Entscheidungsfindung findet nicht statt. Bitte mach bei der Buchung keine Angaben zu deiner Gesundheit.
|
||||
>
|
||||
> Deine Rechte. Du hast das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit sowie das Recht, der Verarbeitung auf Grundlage berechtigter Interessen zu widersprechen (Art. 15 bis 21 DSGVO). Eine erteilte Einwilligung kannst du jederzeit widerrufen. Du kannst dich bei einer Datenschutzaufsichtsbehörde beschweren. Verantwortlich ist {{Name des Betriebs}}, {{Anschrift}}, E-Mail {{E-Mail}}.
|
||||
|
||||
## 7. Nachweis
|
||||
|
||||
Der Nachweis speichert Hashes der Texte mit den Betriebsangaben zum Buchungszeitpunkt (⚖ Q26).
|
||||
@@ -0,0 +1,145 @@
|
||||
# Auftragsverarbeitungsvertrag Terminplaner (Art. 28 DSGVO)
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
|
||||
|
||||
zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
|
||||
|
||||
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
|
||||
|
||||
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Terminplaner“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Terminplaner“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
|
||||
|
||||
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
|
||||
|
||||
(3) Dieser Vertrag ist ein eigener Vertrag für den Terminplaner. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Terminplaner geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
|
||||
|
||||
## 1. Gegenstand und Dauer
|
||||
|
||||
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Verwaltung im Kundenportal („Terminplaner“) und verarbeitet dabei personenbezogene Daten der Gäste und der Beschäftigten des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
|
||||
|
||||
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Terminplaner schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 8 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
|
||||
|
||||
## 2. Art und Zweck der Verarbeitung
|
||||
|
||||
(1) Art: Erheben über das Widget auf der Website des Kunden, Übermitteln an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen eines Kalender-Links und einer CSV-Datei und Löschen.
|
||||
|
||||
(2) Zwecke: Der Kunde soll Termine seiner Gäste verwalten. Dazu nimmt der Terminplaner Buchungen und Terminanfragen entgegen, verhindert Doppelbuchungen, bestätigt, ändert und sagt Termine ab, erinnert Gäste auf deren Wunsch und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Terminplaner für den Kunden vor Missbrauch (Mengenbegrenzung, Erkennen automatisierter Buchungen).
|
||||
|
||||
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der gelöschten Buchungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
|
||||
|
||||
## 3. Datenarten und Betroffene
|
||||
|
||||
(1) Daten der Gäste: Name und E-Mail-Adresse; auf Einstellung des Kunden außerdem Telefonnummer, eine Nachricht und Zusatzfelder, die der Kunde im Formular festlegt; die gebuchte Leistung, die Person oder der Platz und der Termin; der Status der Buchung mit den Zeitpunkten (Anlegen, Bestätigung, Annahme, Absage, Widerruf); Absagegrund und Vermerke zu einer Absage oder einem Widerruf (Freitext, auch mit dem Namen aus einer Widerrufserklärung); Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde; der Nachweis der Zustimmungen (Fassung der Gästetexte, Zeitpunkt, Text des Buchungsknopfs, Anrede und Hashes (Prüfsummen) der gezeigten Texte); der Wunsch nach einer Erinnerung; der Zustellstatus der E-Mails an den Gast.
|
||||
|
||||
(2) Daten der Beschäftigten des Kunden: Namen oder Bezeichnungen der Personen oder Plätze, deren Arbeitszeiten und Abwesenheiten sowie ein geheimer Kalender-Link je Person oder Platz.
|
||||
|
||||
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Terminplaner ist nicht für den Gesundheitsbereich bestimmt; der Kunde fordert von seinen Gästen keine Gesundheitsangaben (Nummer 11 Abs. 4).
|
||||
|
||||
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse nur im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Buchung wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. In den Server-Protokollen stehen sie 14 Tage (Anlage 2 der Abo-Bedingungen).
|
||||
|
||||
(5) Der Terminplaner setzt keine Cookies und speichert nichts im Browser des Gastes.
|
||||
|
||||
(6) Betroffene: Gäste, die über die Website des Kunden Termine buchen oder anfragen, und Beschäftigte des Kunden, die im Terminplaner geführt werden.
|
||||
|
||||
## 4. Weisungen
|
||||
|
||||
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
|
||||
|
||||
(2) Mit dem Einschalten weist der Kunde WEBklar an, den Terminplaner nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Leistungen, Personen oder Plätze, Zeiten, Felder des Formulars, Aufbewahrungsdauer, Erinnerungen, Einschalten und Ausschalten). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Terminplaner abschalten lassen (Nr. 8 Abs. 6 der Leistungsbeschreibung). Die Löschung auf Weisung nach Nummer 9 Abs. 6 setzt WEBklar immer um.
|
||||
|
||||
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
|
||||
|
||||
## 5. Vertraulichkeit
|
||||
|
||||
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
|
||||
|
||||
## 6. Technische und organisatorische Maßnahmen
|
||||
|
||||
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
|
||||
|
||||
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
|
||||
|
||||
## 7. Unterauftragsverarbeiter
|
||||
|
||||
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Terminplaner läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Buchungs-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
|
||||
|
||||
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Terminplaner abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
|
||||
|
||||
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
|
||||
|
||||
(4) Die Zahlung der Einrichtung über Stripe betrifft nur den Vertrag zwischen dem Kunden und WEBklar. Daten von Gästen oder Beschäftigten des Kunden gehen nicht an Stripe. Stripe ist deshalb kein Unterauftragsverarbeiter dieses Vertrags.
|
||||
|
||||
## 8. Unterstützung des Kunden, Betroffenenrechte
|
||||
|
||||
(1) WEBklar leitet Anfragen von Gästen und Beschäftigten, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
|
||||
|
||||
(2) Dafür bietet das Kundenportal die Aktion „Gastdaten löschen“ und den Export der Termine als CSV-Datei. Mit „Gastdaten löschen“ leert der Kunde Name, E-Mail-Adresse, Telefonnummer, Nachricht und Zusatzfelder einer Buchung; Status und Zeiten bleiben. Dabei löscht WEBklar die E-Mails zu dieser Buchung, die noch auf den Versand warten, und die Links des Gastes werden ungültig. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Eine noch nicht bestätigte Buchung verfällt von selbst (Nummer 9 Abs. 1).
|
||||
|
||||
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
|
||||
|
||||
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
|
||||
|
||||
(5) Die Buchung speichert als Nachweis der Zustimmungen die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der übrigen Texte, die der Gast gesehen hat, nicht diese Texte selbst. Damit sich der Wortlaut nachweisen lässt, bewahrt WEBklar jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz und Widerrufsbelehrung für Gäste) mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende des Terminplaners. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung.
|
||||
|
||||
(6) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
|
||||
|
||||
## 9. Löschung und Rückgabe
|
||||
|
||||
(1) Nicht bestätigte Buchungen löscht WEBklar 48 Stunden nach Ablauf der Bestätigungsfrist von 30 Minuten.
|
||||
|
||||
(2) Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, behandelt der Terminplaner als abgelehnt; die 30 Tage laufen dann ab diesem Zeitpunkt.
|
||||
|
||||
(3) Bestätigte und erledigte Termine löscht WEBklar nach der Aufbewahrungsdauer, gerechnet ab dem Ende des Termins. Der Kunde stellt sie im Kundenportal ein; die Vorgabe sind 90 Tage, einstellbar sind 14 bis 365 Tage.
|
||||
|
||||
(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
|
||||
|
||||
(5) Der Löschlauf läuft täglich; gelöscht wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu löschen, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
|
||||
|
||||
(6) Ist der Terminplaner abgeschaltet oder das Wartungs-Abo beendet, löscht WEBklar alle Termindaten und die Einstellungen 30 Tage danach. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen (zum Beispiel Betriebsangaben, Namen der Leistungen, die ersten acht Namen der Personen oder Plätze und die Wünsche des Kunden zum Einbau) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Bis zur Löschung kann der Kunde die Termine im Kundenportal als CSV-Datei exportieren. Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen wieder ein (Nr. 8 Abs. 4 der Leistungsbeschreibung). Früher löscht der Kunde einzelne Gastdaten selbst mit der Aktion „Gastdaten löschen“ (Nummer 8 Abs. 2); alle Daten löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
|
||||
|
||||
(7) Entwürfe aus dem Fragebogen, aus denen die Funktion nicht eingeschaltet wurde, löscht WEBklar 90 Tage nach der letzten Änderung.
|
||||
|
||||
(8) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
|
||||
|
||||
(9) Gesetzliche Aufbewahrungspflichten bleiben unberührt.
|
||||
|
||||
## 10. Nachweise und Kontrollen
|
||||
|
||||
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
|
||||
|
||||
## 11. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste und seiner Beschäftigten im Terminplaner.
|
||||
|
||||
(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für die Online-Terminbuchung in seine Datenschutzerklärung auf (Nr. 4 Abs. 9 der Leistungsbeschreibung) und lässt den Datenschutz-Hinweis am Buchungsformular unverändert.
|
||||
|
||||
(3) Der Kunde informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt (Nr. 4 Abs. 7 der Leistungsbeschreibung).
|
||||
|
||||
(4) Der Kunde setzt den Terminplaner nicht im Gesundheitsbereich ein und fordert von seinen Gästen keine Angaben zu ihrer Gesundheit, auch nicht über Zusatzfelder oder das Nachrichtenfeld (Nr. 4 Abs. 3 der Leistungsbeschreibung).
|
||||
|
||||
(5) Der Kunde verändert den Haken für die Erinnerung und die Texte der übrigen Haken nicht (Nr. 4 Abs. 6 der Leistungsbeschreibung). Erinnerungen versendet der Terminplaner nur, wenn der Gast den Haken gesetzt hat.
|
||||
|
||||
## 12. Haftung, Änderungen, Schlussbestimmungen
|
||||
|
||||
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
|
||||
|
||||
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
|
||||
|
||||
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Terminplaner für seine Website nach Nr. 9 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 9 Abs. 3 der Leistungsbeschreibung aussetzen. Das Wartungs-Abo bleibt davon unberührt.
|
||||
|
||||
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
|
||||
|
||||
## Anlage: Technische und organisatorische Maßnahmen des Terminplaners (Art. 32 DSGVO)
|
||||
|
||||
| Bereich | Maßnahmen |
|
||||
|---|---|
|
||||
| Übertragung | Das Widget, die Seiten der Gäste und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. |
|
||||
| Speicher im Browser | Es werden keine Cookies gesetzt, und der Terminplaner speichert nichts im Browser des Gastes (kein lokaler Speicher, kein Sitzungsspeicher). Das Widget sendet seine Anfragen ohne Cookies, liest weder den Referrer noch die Zeitzone des Browsers und ruft nur die Schnittstelle des Terminplaners auf. |
|
||||
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen eingeschalteten Terminplaner Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Buchungen nimmt sie nur von Seiten des Kunden und des Portals an. Der Kalender-Link enthält ein geheimes Token, das mit einem Vergleich in konstanter Zeit geprüft wird. |
|
||||
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Buchungen. Grenzen je IP-Adresse, je Website und je E-Mail-Adresse sowie das Mailbudget je Stunde (insgesamt und je Funktion) gelten nur im Arbeitsspeicher; ein Neustart leert sie. |
|
||||
| Datenminimierung | Pflicht sind immer nur Name und E-Mail-Adresse; ob Telefonnummer, Nachricht und Zusatzfelder abgefragt werden, legt der Kunde fest. Mit der Buchung werden keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Der Nachweis der Zustimmungen besteht aus der Fassung der Gästetexte, dem Zeitpunkt, dem Text des Buchungsknopfs, der Anrede und Hashes der übrigen gezeigten Texte, nicht aus diesen Texten. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
|
||||
| Trennung | Alle Buchungen, Belegungen und Warteschlangeneinträge tragen die Kennung der Funktion (eines Terminplaners je Website-Projekt); das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten der jeweiligen Funktion. Die Hintergrundläufe (Löschung, Versand der E-Mails, Erinnerungen) gehen die Dokumente aller Funktionen nacheinander durch und schreiben jeweils nur in das Dokument, um das es geht. Die Einstellungen liegen je Funktion in einem eigenen Dokument. |
|
||||
| Löschung | Täglicher Löschlauf für Buchungen, Belegungen, Warteschlange und Entwürfe nach den Fristen der Nummer 9; Termindaten und Einstellungen abgeschalteter Funktionen 30 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). Vor der Löschung einer Buchung, einer abgeschalteten Funktion oder eines nicht bestellten Fragebogens wird der Zustand frisch gelesen; bei einem Lesefehler wird nichts gelöscht. |
|
||||
| Protokolle | Die Programmprotokolle des Terminplaners und der Tagesbericht enthalten nur Zählungen und Buchungsnummern, keine Namen, E-Mail-Adressen oder Nachrichten von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
|
||||
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
|
||||
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
|
||||
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Archiv der Fassungen der Gästetexte (Nummer 8 Abs. 5), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
|
||||
@@ -0,0 +1,145 @@
|
||||
# Auftragsverarbeitungsvertrag Terminplaner (Art. 28 DSGVO)
|
||||
|
||||
**Fassung 2026-10-05**
|
||||
|
||||
zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
|
||||
|
||||
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
|
||||
|
||||
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Einrichtung des Terminplaners bestellt und dabei per Haken diesem Vertrag und der Leistungsbeschreibung „Terminplaner“ („Leistungsbeschreibung“) in der angezeigten Fassung zustimmt. Das Portal speichert die Fassung, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte und die Widerrufsbelehrung in dieser Fassung als PDF enthält und auf sie verlinkt.
|
||||
|
||||
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
|
||||
|
||||
(3) Dieser Vertrag ist ein eigener Vertrag für den Terminplaner. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Terminplaner geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
|
||||
|
||||
## 1. Gegenstand und Dauer
|
||||
|
||||
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Verwaltung im Kundenportal („Terminplaner“) und verarbeitet dabei personenbezogene Daten der Gäste und der Beschäftigten des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
|
||||
|
||||
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Terminplaner schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 8 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
|
||||
|
||||
## 2. Art und Zweck der Verarbeitung
|
||||
|
||||
(1) Art: Erheben über das Widget auf der Website des Kunden, Übermitteln an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen eines Kalender-Links und einer CSV-Datei und Löschen.
|
||||
|
||||
(2) Zwecke: Der Kunde soll Termine seiner Gäste verwalten. Dazu nimmt der Terminplaner Buchungen und Terminanfragen entgegen, verhindert Doppelbuchungen, bestätigt, ändert und sagt Termine ab, erinnert Gäste auf deren Wunsch und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Terminplaner für den Kunden vor Missbrauch (Mengenbegrenzung, Erkennen automatisierter Buchungen).
|
||||
|
||||
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der gelöschten Buchungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
|
||||
|
||||
## 3. Datenarten und Betroffene
|
||||
|
||||
(1) Daten der Gäste: Name und E-Mail-Adresse; auf Einstellung des Kunden außerdem Telefonnummer, eine Nachricht und Zusatzfelder, die der Kunde im Formular festlegt; die gebuchte Leistung, die Person oder der Platz und der Termin; der Status der Buchung mit den Zeitpunkten (Anlegen, Bestätigung, Annahme, Absage, Widerruf); Absagegrund und Vermerke zu einer Absage oder einem Widerruf (Freitext, auch mit dem Namen aus einer Widerrufserklärung); Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde; der Nachweis der Zustimmungen (Fassung der Gästetexte, Zeitpunkt, Text des Buchungsknopfs, Anrede und Hashes (Prüfsummen) der gezeigten Texte); der Wunsch nach einer Erinnerung; der Zustellstatus der E-Mails an den Gast.
|
||||
|
||||
(2) Daten der Beschäftigten des Kunden: Namen oder Bezeichnungen der Personen oder Plätze, deren Arbeitszeiten und Abwesenheiten sowie ein geheimer Kalender-Link je Person oder Platz.
|
||||
|
||||
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Terminplaner ist nicht für den Gesundheitsbereich bestimmt; der Kunde fordert von seinen Gästen keine Gesundheitsangaben (Nummer 11 Abs. 4).
|
||||
|
||||
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse nur im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Buchung wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. In den Server-Protokollen stehen sie 14 Tage (Anlage 2 der Abo-Bedingungen).
|
||||
|
||||
(5) Der Terminplaner setzt keine Cookies und speichert nichts im Browser des Gastes.
|
||||
|
||||
(6) Betroffene: Gäste, die über die Website des Kunden Termine buchen oder anfragen, und Beschäftigte des Kunden, die im Terminplaner geführt werden.
|
||||
|
||||
## 4. Weisungen
|
||||
|
||||
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
|
||||
|
||||
(2) Mit der Bestellung weist der Kunde WEBklar an, den Terminplaner nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Leistungen, Personen oder Plätze, Zeiten, Felder des Formulars, Aufbewahrungsdauer, Erinnerungen, Einschalten und Ausschalten). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Terminplaner abschalten lassen (Nr. 8 Abs. 6 der Leistungsbeschreibung). Die Löschung auf Weisung nach Nummer 9 Abs. 6 setzt WEBklar immer um.
|
||||
|
||||
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
|
||||
|
||||
## 5. Vertraulichkeit
|
||||
|
||||
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
|
||||
|
||||
## 6. Technische und organisatorische Maßnahmen
|
||||
|
||||
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
|
||||
|
||||
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
|
||||
|
||||
## 7. Unterauftragsverarbeiter
|
||||
|
||||
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Terminplaner läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Buchungs-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
|
||||
|
||||
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Terminplaner abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
|
||||
|
||||
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
|
||||
|
||||
(4) Die Zahlung der Einrichtung über Stripe betrifft nur den Vertrag zwischen dem Kunden und WEBklar. Daten von Gästen oder Beschäftigten des Kunden gehen nicht an Stripe. Stripe ist deshalb kein Unterauftragsverarbeiter dieses Vertrags.
|
||||
|
||||
## 8. Unterstützung des Kunden, Betroffenenrechte
|
||||
|
||||
(1) WEBklar leitet Anfragen von Gästen und Beschäftigten, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
|
||||
|
||||
(2) Dafür bietet das Kundenportal die Aktion „Gastdaten löschen“ und den Export der Termine als CSV-Datei. Mit „Gastdaten löschen“ leert der Kunde Name, E-Mail-Adresse, Telefonnummer, Nachricht und Zusatzfelder einer Buchung; Status und Zeiten bleiben. Dabei löscht WEBklar die E-Mails zu dieser Buchung, die noch auf den Versand warten, und die Links des Gastes werden ungültig. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Eine noch nicht bestätigte Buchung verfällt von selbst (Nummer 9 Abs. 1).
|
||||
|
||||
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
|
||||
|
||||
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
|
||||
|
||||
(5) Die Buchung speichert als Nachweis der Zustimmungen die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der übrigen Texte, die der Gast gesehen hat, nicht diese Texte selbst. Damit sich der Wortlaut nachweisen lässt, bewahrt WEBklar jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz und Widerrufsbelehrung für Gäste) mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende des Terminplaners. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung.
|
||||
|
||||
(6) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
|
||||
|
||||
## 9. Löschung und Rückgabe
|
||||
|
||||
(1) Nicht bestätigte Buchungen löscht WEBklar 48 Stunden nach Ablauf der Bestätigungsfrist von 30 Minuten.
|
||||
|
||||
(2) Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, behandelt der Terminplaner als abgelehnt; die 30 Tage laufen dann ab diesem Zeitpunkt.
|
||||
|
||||
(3) Bestätigte und erledigte Termine löscht WEBklar nach der Aufbewahrungsdauer, gerechnet ab dem Ende des Termins. Der Kunde stellt sie im Kundenportal ein; die Vorgabe sind 90 Tage, einstellbar sind 14 bis 365 Tage.
|
||||
|
||||
(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
|
||||
|
||||
(5) Der Löschlauf läuft täglich; gelöscht wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu löschen, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
|
||||
|
||||
(6) Ist der Terminplaner abgeschaltet oder das Wartungs-Abo beendet, löscht WEBklar alle Termindaten und die Einstellungen 30 Tage danach. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen (zum Beispiel Betriebsangaben, Namen der Leistungen, die ersten acht Namen der Personen oder Plätze und die Wünsche des Kunden zum Einbau) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Bis zur Löschung kann der Kunde die Termine im Kundenportal als CSV-Datei exportieren. Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen wieder ein (Nr. 8 Abs. 4 der Leistungsbeschreibung). Früher löscht der Kunde einzelne Gastdaten selbst mit der Aktion „Gastdaten löschen“ (Nummer 8 Abs. 2); alle Daten löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
|
||||
|
||||
(7) Entwürfe aus dem Fragebogen, aus denen keine Bestellung geworden ist, löscht WEBklar 90 Tage nach der letzten Änderung.
|
||||
|
||||
(8) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
|
||||
|
||||
(9) Gesetzliche Aufbewahrungspflichten bleiben unberührt.
|
||||
|
||||
## 10. Nachweise und Kontrollen
|
||||
|
||||
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
|
||||
|
||||
## 11. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste und seiner Beschäftigten im Terminplaner.
|
||||
|
||||
(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für die Online-Terminbuchung in seine Datenschutzerklärung auf (Nr. 4 Abs. 9 der Leistungsbeschreibung) und lässt den Datenschutz-Hinweis am Buchungsformular unverändert.
|
||||
|
||||
(3) Der Kunde informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt (Nr. 4 Abs. 7 der Leistungsbeschreibung).
|
||||
|
||||
(4) Der Kunde setzt den Terminplaner nicht im Gesundheitsbereich ein und fordert von seinen Gästen keine Angaben zu ihrer Gesundheit, auch nicht über Zusatzfelder oder das Nachrichtenfeld (Nr. 4 Abs. 3 der Leistungsbeschreibung).
|
||||
|
||||
(5) Der Kunde verändert den Haken für die Erinnerung und die Texte der übrigen Haken nicht (Nr. 4 Abs. 6 der Leistungsbeschreibung). Erinnerungen versendet der Terminplaner nur, wenn der Gast den Haken gesetzt hat.
|
||||
|
||||
## 12. Haftung, Änderungen, Schlussbestimmungen
|
||||
|
||||
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
|
||||
|
||||
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
|
||||
|
||||
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Terminplaner für seine Website nach Nr. 9 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 9 Abs. 3 der Leistungsbeschreibung aussetzen. Das Wartungs-Abo bleibt davon unberührt.
|
||||
|
||||
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
|
||||
|
||||
## Anlage: Technische und organisatorische Maßnahmen des Terminplaners (Art. 32 DSGVO)
|
||||
|
||||
| Bereich | Maßnahmen |
|
||||
|---|---|
|
||||
| Übertragung | Das Widget, die Seiten der Gäste und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. |
|
||||
| Speicher im Browser | Es werden keine Cookies gesetzt, und der Terminplaner speichert nichts im Browser des Gastes (kein lokaler Speicher, kein Sitzungsspeicher). Das Widget sendet seine Anfragen ohne Cookies, liest weder den Referrer noch die Zeitzone des Browsers und ruft nur die Schnittstelle des Terminplaners auf. |
|
||||
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen eingeschalteten Terminplaner Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Buchungen nimmt sie nur von Seiten des Kunden und des Portals an. Der Kalender-Link enthält ein geheimes Token, das mit einem Vergleich in konstanter Zeit geprüft wird. |
|
||||
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Buchungen. Grenzen je IP-Adresse, je Website und je E-Mail-Adresse sowie das Mailbudget je Stunde (insgesamt und je Funktion) gelten nur im Arbeitsspeicher; ein Neustart leert sie. |
|
||||
| Datenminimierung | Pflicht sind immer nur Name und E-Mail-Adresse; ob Telefonnummer, Nachricht und Zusatzfelder abgefragt werden, legt der Kunde fest. Mit der Buchung werden keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Der Nachweis der Zustimmungen besteht aus der Fassung der Gästetexte, dem Zeitpunkt, dem Text des Buchungsknopfs, der Anrede und Hashes der übrigen gezeigten Texte, nicht aus diesen Texten. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
|
||||
| Trennung | Alle Buchungen, Belegungen und Warteschlangeneinträge tragen die Kennung der Funktion (eines Terminplaners je Website-Projekt); das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten der jeweiligen Funktion. Die Hintergrundläufe (Löschung, Versand der E-Mails, Erinnerungen) gehen die Dokumente aller Funktionen nacheinander durch und schreiben jeweils nur in das Dokument, um das es geht. Die Einstellungen liegen je Funktion in einem eigenen Dokument. |
|
||||
| Löschung | Täglicher Löschlauf für Buchungen, Belegungen, Warteschlange und Entwürfe nach den Fristen der Nummer 9; Termindaten und Einstellungen abgeschalteter Funktionen 30 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). Vor der Löschung einer Buchung, einer abgeschalteten Funktion oder eines nicht bestellten Fragebogens wird der Zustand frisch gelesen; bei einem Lesefehler wird nichts gelöscht. |
|
||||
| Protokolle | Die Programmprotokolle des Terminplaners und der Tagesbericht enthalten nur Zählungen und Buchungsnummern, keine Namen, E-Mail-Adressen oder Nachrichten von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
|
||||
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
|
||||
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
|
||||
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Archiv der Fassungen der Gästetexte (Nummer 8 Abs. 5), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
|
||||
@@ -0,0 +1,157 @@
|
||||
# Auftragsverarbeitungsvertrag Buchungskalender (Art. 28 DSGVO)
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
|
||||
|
||||
zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Vermieter“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
|
||||
|
||||
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
|
||||
|
||||
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Buchungskalender“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Buchungskalender“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
|
||||
|
||||
(2) Ein Mitarbeiter von WEBklar darf die Funktion auch ohne Bestellung im Portal einschalten (zum Beispiel für einen Kunden, dem WEBklar sie als Teil seines Website-Projekts bereitstellt), aber nur, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
|
||||
|
||||
(3) Dieser Vertrag ist ein eigener Vertrag für den Buchungskalender. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Buchungskalender geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
|
||||
|
||||
(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Gäste im Buchungskalender. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4). Die Plattformen, deren Kalender der Kunde einbindet (zum Beispiel Booking.com oder Airbnb), sind weder Auftragsverarbeiter noch Unterauftragsverarbeiter von WEBklar.
|
||||
|
||||
## 1. Gegenstand und Dauer
|
||||
|
||||
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Buchungsseite und einer Verwaltung im Kundenportal („Buchungskalender“) und verarbeitet dabei personenbezogene Daten der Gäste des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
|
||||
|
||||
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Buchungskalender schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
|
||||
|
||||
## 2. Art und Zweck der Verarbeitung
|
||||
|
||||
(1) Art: Übermitteln der Anfrage und der Buchung vom Widget und von der Buchungsseite an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Berechnen des Preises, Erzeugen von Rechnungen und Stornorechnungen im Namen des Kunden (Nr. 3 Abs. 6 der Leistungsbeschreibung), Abgleichen von Kalendern, Versenden von E-Mails, Übernehmen von Zahlungsstatus aus Stripe, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.
|
||||
|
||||
(2) Zwecke: Der Kunde soll Anfragen und Buchungen seiner Gäste abwickeln. Dazu nimmt der Buchungskalender Anfragen entgegen, reserviert Zeiträume nach der Freigabe durch den Kunden, nimmt die verbindliche Buchung des Gastes entgegen, stellt die Rechnung aus, gleicht Online-Zahlungen mit Stripe ab, bildet Zahlungen vor Ort ab, bildet Stornierungen ab und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Buchungskalender für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der Anfragen je IP-Adresse, Erkennen automatisierter Anfragen).
|
||||
|
||||
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Anfragen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
|
||||
|
||||
## 3. Datenarten und Betroffene
|
||||
|
||||
(1) Daten der Gäste, die WEBklar aus dem Widget, von der Buchungsseite und aus Stripe erhält und speichert:
|
||||
- bei der Anfrage: Name, E-Mail-Adresse, auf Wunsch Firma, Telefonnummer und eine Nachricht, die gewünschte Unterkunft, der Zeitraum, die Zahl der Personen, die Anfragenummer, ein Hashwert der IP-Adresse (Absatz 4) und der Zeitpunkt;
|
||||
- bei der Freigabe: die Posten und Summen des Preises (Text, Menge, Einzelbetrag, Steuersatz), die Reservierungsfrist, eine Nachricht des Kunden an den Gast, die Stornobedingungen, die galten, und die Fassung der Rechtstexte, der der Gast gesehen hat, jeweils als Schnappschuss;
|
||||
- bei der Buchung: die Rechnungsadresse (Name, Firma, Straße, Postleitzahl, Ort, Land), die Zahlart, der Zeitpunkt der Buchung und die Rechnungsnummer;
|
||||
- bei der Zahlung und der Erstattung: Status, Zeitpunkt, Art (online über Stripe oder vor Ort), die Kennungen der Zahlung bei Stripe, erstattete Beträge und der Grund einer Stornierung;
|
||||
- die Rechnung und die Stornorechnung als unveränderlicher Schnappschuss (Angaben des Ausstellers, Empfänger, Unterkunft, Zeitraum, Posten, Summen, Steuerart, Bezug), aus dem WEBklar das PDF bei jedem Abruf erzeugt;
|
||||
- ein Hashwert des geheimen Zugangs zur Buchungsseite (der Link in der E-Mail wird nur als Prüfsumme gespeichert);
|
||||
- der Zustellstatus der E-Mails an den Gast.
|
||||
|
||||
**Kartendaten des Gastes erhält und speichert WEBklar nicht;** die gibt der Gast nur bei Stripe ein. Online-Zahlungen sind derzeit nur per Karte (einschließlich Apple Pay und Google Pay) möglich, nicht per SEPA-Lastschrift. Die Bankverbindung des Kunden selbst (Angabe auf der Rechnung) und seine Firmen- und Steuerangaben gehören nicht zu den Gästedaten.
|
||||
|
||||
(2) Von Kalendern anderer Plattformen übernimmt WEBklar nur die belegten Zeiträume (Datum von und bis), nicht Namen, Beschreibungen oder andere Angaben der dortigen Buchungen, auch wenn der Kalender sie enthält. Die iCal-Adresse, die WEBklar dem Kunden bereitstellt, enthält nur belegte Nächte, keine Angaben zu Gästen. Wer die geheime Adresse kennt, sieht die belegten Nächte; der Kunde kann sie jederzeit erneuern.
|
||||
|
||||
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Kunde fordert von seinen Gästen keine entsprechenden Angaben und bittet sie, solche Angaben nicht in das Nachrichtenfeld zu schreiben (Nummer 11 Abs. 4). Meldedaten nach dem Bundesmeldegesetz (Meldeschein) und Daten zur Kurtaxe verarbeitet der Buchungskalender nicht.
|
||||
|
||||
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Anfrage werden weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein **Hashwert der IP-Adresse („IP-Hash“)**: ein SHA-256-Wert, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz. Er dient nicht dazu, Gäste zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Anfrage. WEBklar entfernt ihn aus dem Datensatz, sobald der Kunde die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 9 Abs. 1). In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).
|
||||
|
||||
(5) Der Buchungskalender setzt keine Cookies. Das Widget speichert nichts im Browser. Auch die Buchungsseite legt weder Cookies noch Daten im lokalen oder im Sitzungsspeicher des Browsers ab.
|
||||
|
||||
(6) Betroffene: Gäste, die über die Website des Kunden Anfragen stellen oder buchen, und Personen, deren Angaben der Gast in der Anfrage oder Buchung macht (zum Beispiel Mitreisende oder abweichende Rechnungsempfänger). Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.
|
||||
|
||||
## 4. Weisungen
|
||||
|
||||
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
|
||||
|
||||
(2) Mit dem Einschalten weist der Kunde WEBklar an, den Buchungskalender nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben, in seinem Namen Rechnungen auszustellen und Zahlungen, die nach einer Stornierung oder nach dem Ablauf einer Reservierung eingehen, sowie zweite Zahlungen auf eine bereits bezahlte Buchung ohne weitere Anweisung in voller Höhe über sein Stripe-Konto zu erstatten (dauerhafte Weisung; der Stand der Buchung ändert sich dabei nicht). Bei einer Stornierung schließt WEBklar eine noch offene Zahlungsseite bei Stripe. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Unterkünfte, Preise, Sperren, Kalenderverbindungen, Freigabe und Ablehnung von Anfragen, Zahlungseingang vor Ort, Stornierungen und Erstattungen, Online-Zahlung, Öffnen und Schließen des Buchungskalenders). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Buchungskalender abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um, soweit Aufbewahrungspflichten dem nicht entgegenstehen (Nummer 9 Abs. 8).
|
||||
|
||||
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
|
||||
|
||||
## 5. Vertraulichkeit
|
||||
|
||||
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
|
||||
|
||||
## 6. Technische und organisatorische Maßnahmen
|
||||
|
||||
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
|
||||
|
||||
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
|
||||
|
||||
## 7. Unterauftragsverarbeiter
|
||||
|
||||
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Buchungskalender läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
|
||||
|
||||
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Buchungskalender abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
|
||||
|
||||
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
|
||||
|
||||
(4) **Stripe ist kein Unterauftragsverarbeiter von WEBklar.** Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 3 der Leistungsbeschreibung); die Zahlung des Gastes fließt direkt auf dieses Konto. Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs- und Kartendaten (derzeit nur Karte, einschließlich Apple Pay und Google Pay), die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden legt WEBklar bei Stripe die Zahlung an, ruft die Angaben zu Zahlungen ab, die der Buchungskalender braucht (Nummer 2 Abs. 1), schließt offene Zahlungsseiten und veranlasst Erstattungen (auch die automatischen nach Nummer 4 Abs. 2). An Stripe gehen dabei von WEBklar nur: die Bezeichnung des Aufenthalts mit Rechnungsnummer als Zahlungsposition, der Betrag, die Kennungen der Buchung und der Zahlung (Metadaten), die E-Mail-Adresse des Gastes zur Vorbelegung der Zahlungsseite, soweit WEBklar sie übergibt, die Erstattung und der **geheime Link zur Buchungsseite mit dem Token als Rücksprungadresse** (bei Erfolg und bei Abbruch). Dieser Link ist damit für Stripe und im Stripe-Dashboard des Kunden sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.
|
||||
|
||||
(5) Plattformen, deren iCal-Kalender der Kunde einbindet oder denen er die iCal-Adresse des Buchungskalenders mitteilt, sind Empfänger nach seiner Entscheidung. WEBklar übermittelt ihnen keine Daten von Gästen.
|
||||
|
||||
## 8. Unterstützung des Kunden, Betroffenenrechte
|
||||
|
||||
(1) WEBklar leitet Anfragen von Gästen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
|
||||
|
||||
(2) Dafür bietet das Kundenportal die Anzeige der Anfragen und Buchungen, mit denen der Kunde Auskunft erteilen kann, und den Abruf der Rechnungen als PDF. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Gästedaten einer Anfrage oder Buchung unverzüglich: Name, Firma, E-Mail-Adresse, Telefonnummer und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt in der Regel längst entfernt); Anfragenummer, Zeitraum, Beträge und Status bleiben. Rechnungen und Rechnungsadresse einer gebuchten Anfrage bleiben, solange eine Aufbewahrungspflicht besteht (Nummer 9 Abs. 3). Dabei löscht WEBklar die E-Mails zu dieser Anfrage, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 5 löscht (48 Stunden nach dem Fehlschlag).
|
||||
|
||||
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
|
||||
|
||||
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
|
||||
|
||||
(5) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
|
||||
|
||||
## 9. Löschung und Rückgabe
|
||||
|
||||
(1) Den IP-Hash entfernt WEBklar aus dem Datensatz einer Anfrage, sobald der Kunde sie freigibt oder ablehnt, spätestens 30 Tage nach dem Anlegen.
|
||||
|
||||
(2) Anfragen, aus denen keine Buchung wurde (abgelehnt, abgelaufen, oder neu und nicht innerhalb von 60 Tagen entschieden), anonymisiert WEBklar nach 6 Monaten: Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht werden gelöscht. Anfragenummer, Zeitraum, Status und, soweit vorhanden, der Preisvorschlag bleiben ohne Personenbezug erhalten.
|
||||
|
||||
(3) Bei gebuchten Aufenthalten anonymisiert WEBklar die Gästedaten (Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht) 3 Jahre nach dem Ende des Kalenderjahres, in dem der Gast abgereist ist (Abreisejahr; maßgeblich ist die Zeit in Berlin). **Rechnungen, Stornorechnungen und die Rechnungsadresse bleiben 8 Jahre erhalten**, gerechnet ab dem Ende des Kalenderjahres der Rechnung, solange der Buchungskalender läuft (Buchungsbelege, § 147 AO); sie sind für den Kunden als Aussteller bestimmt. Anfragenummer, Zeitraum, Beträge und Status bleiben ebenfalls.
|
||||
|
||||
(4) Die Anonymisierung nach den Absätzen 2 und 3 sowie die Entfernung des IP-Hash laufen im täglichen Löschlauf; anonymisiert oder entfernt wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
|
||||
|
||||
(5) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
|
||||
|
||||
(6) Ist der Buchungskalender abgeschaltet oder das Wartungs-Abo beendet, kann der Kunde die Anfragen, Buchungen und Rechnungen 90 Tage lang im Kundenportal lesen und die Rechnungen einzeln als PDF herunterladen; WEBklar weist ihn nach dem Abschalten per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern. Danach löscht WEBklar alle Daten des Buchungskalenders: Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen. Damit endet auch die Aufbewahrung der Rechnungen nach Absatz 3; sie bewahrt der Kunde dann selbst auf. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Buchungskalender auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 6 (bei Abschaltung 90 Tage später); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird. alle Daten des Buchungskalenders löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
|
||||
|
||||
(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
|
||||
|
||||
(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Rechnungen und Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung. Die Aufbewahrung der Rechnungen nach Absatz 3 geschieht als Dienst für den Kunden und für die Dauer des Buchungskalenders. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Löschung nach Absatz 6 durch den Download. Soweit eine gesetzliche Aufbewahrungspflicht besteht, löscht oder anonymisiert WEBklar auch auf Weisung des Kunden nicht früher; stattdessen sperrt es die Daten für die weitere Verarbeitung.
|
||||
|
||||
## 10. Nachweise und Kontrollen
|
||||
|
||||
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
|
||||
|
||||
## 11. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste im Buchungskalender.
|
||||
|
||||
(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Buchungskalender in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.
|
||||
|
||||
(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst, wenn er die Online-Zahlung nutzt, und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).
|
||||
|
||||
(4) Der Kunde fordert von seinen Gästen keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten und weist sie auf der Website darauf hin, solche Angaben nicht in das Nachrichtenfeld zu schreiben.
|
||||
|
||||
(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht (Nr. 5 Abs. 9 der Leistungsbeschreibung).
|
||||
|
||||
(6) Der Kunde erfüllt seine Pflichten zu Meldeschein, Kurtaxe und sonstigen Meldungen an Behörden selbst und verarbeitet die dafür nötigen Daten außerhalb des Buchungskalenders (Nr. 5 Abs. 6 der Leistungsbeschreibung).
|
||||
|
||||
## 12. Haftung, Änderungen, Schlussbestimmungen
|
||||
|
||||
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
|
||||
|
||||
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
|
||||
|
||||
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Buchungskalender für seine Website nach Nr. 12 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 12 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.
|
||||
|
||||
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
|
||||
|
||||
## Anlage: Technische und organisatorische Maßnahmen des Buchungskalenders (Art. 32 DSGVO)
|
||||
|
||||
| Bereich | Maßnahmen |
|
||||
|---|---|
|
||||
| Übertragung | Das Widget, die Buchungsseite und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. Kalender anderer Plattformen ruft WEBklar nur über https ab. |
|
||||
| Speicher im Browser | Es werden keine Cookies gesetzt. Das Widget sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Buchungskalenders auf, keine Dienste Dritter. |
|
||||
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Buchungskalender Daten und ist über den Website-Schlüssel (Site-Key) adressiert; die belegten Nächte nennt sie ohne Quelle und ohne Daten von Gästen. Die Buchungsseite ruft der Gast nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert und zeitkonstant verglichen wird. Der Token wird nur als Prüfsumme gespeichert; er wird nur als Teil der Rücksprungadresse an Stripe übergeben (Nummer 7 Abs. 4) und öffnet nur diese eine Buchungsseite; er gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage), danach antwortet die Buchungsseite wie bei einem unbekannten Link. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. Die iCal-Adresse des Buchungskalenders ist ein geheimer, erneuerbarer Zufallswert. |
|
||||
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und Grenzen je IP-Adresse und je Buchungskalender schützen vor automatisierten Anfragen. Die Grenzen gelten im Arbeitsspeicher; ein Neustart leert sie. Mit der Anfrage wird nur ein gesalzener IP-Hash gespeichert (Nummer 3 Abs. 4). Beim Abruf von Kalendern anderer Plattformen sind nur Adressen mit https erlaubt, private und lokale Ziele sind gesperrt, und Größe und Dauer des Abrufs sind begrenzt. |
|
||||
| Datenminimierung | Kartendaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Preisen und Posten, nie aus Angaben des Browsers. Von anderen Kalendern werden nur Zeiträume übernommen. Mit der Anfrage werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert; der IP-Hash wird mit der Entscheidung des Kunden entfernt. Die Gästedaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
|
||||
| Integrität der Rechnungen | Rechnungen und Stornorechnungen werden als unveränderlicher Schnappschuss gespeichert; das PDF entsteht bei jedem Abruf aus diesem Schnappschuss. Rechnungsnummern werden unter Sperre lückenlos je Kunde und Jahr vergeben. Freigabe und Buchung prüfen unter Sperre erneut, dass der Zeitraum frei ist. |
|
||||
| Trennung | Alle Unterkünfte, Anfragen und Rechnungen tragen die Kennung des Kunden, des Projekts und der Funktion; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Buchungskalenders. Die Hintergrundläufe (Ablauf von Reservierungen, Kalenderabgleich, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht. |
|
||||
| Löschung | Täglicher Löschlauf für Anfragen, Buchungen und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Buchungskalender 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). |
|
||||
| Protokolle | Die Programmprotokolle des Buchungskalenders und der Tagesbericht enthalten nur Zählungen und Anfragenummern, keine Namen, E-Mail-Adressen oder Adressen von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
|
||||
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
|
||||
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
|
||||
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
|
||||
@@ -0,0 +1,157 @@
|
||||
# Auftragsverarbeitungsvertrag Buchungskalender (Art. 28 DSGVO)
|
||||
|
||||
**Fassung 2026-10-07** · Entwurf – Prüfung durch Kanzlei offen
|
||||
|
||||
zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Vermieter“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
|
||||
|
||||
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
|
||||
|
||||
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Einrichtung des Buchungskalenders bestellt und dabei per Haken diesem Vertrag und der Leistungsbeschreibung „Buchungskalender“ („Leistungsbeschreibung“) in der angezeigten Fassung zustimmt. Das Portal speichert die Fassung, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte und die Widerrufsbelehrung in dieser Fassung als PDF enthält und auf sie verlinkt.
|
||||
|
||||
(2) Ein Mitarbeiter von WEBklar darf die Funktion auch ohne Bestellung im Portal einschalten (zum Beispiel für einen Kunden, dem WEBklar sie als Teil seines Website-Projekts bereitstellt), aber nur, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
|
||||
|
||||
(3) Dieser Vertrag ist ein eigener Vertrag für den Buchungskalender. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Buchungskalender geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
|
||||
|
||||
(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Gäste im Buchungskalender. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4). Die Plattformen, deren Kalender der Kunde einbindet (zum Beispiel Booking.com oder Airbnb), sind weder Auftragsverarbeiter noch Unterauftragsverarbeiter von WEBklar.
|
||||
|
||||
## 1. Gegenstand und Dauer
|
||||
|
||||
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Buchungsseite und einer Verwaltung im Kundenportal („Buchungskalender“) und verarbeitet dabei personenbezogene Daten der Gäste des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
|
||||
|
||||
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Vertrags, aufgrund dessen WEBklar die Website des Kunden betreibt; den Buchungskalender schaltet WEBklar dann zum Vertragsende ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
|
||||
|
||||
## 2. Art und Zweck der Verarbeitung
|
||||
|
||||
(1) Art: Übermitteln der Anfrage und der Buchung vom Widget und von der Buchungsseite an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Berechnen des Preises, Erzeugen von Rechnungen und Stornorechnungen im Namen des Kunden (Nr. 3 Abs. 6 der Leistungsbeschreibung), Abgleichen von Kalendern, Versenden von E-Mails, Übernehmen von Zahlungsstatus aus Stripe, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.
|
||||
|
||||
(2) Zwecke: Der Kunde soll Anfragen und Buchungen seiner Gäste abwickeln. Dazu nimmt der Buchungskalender Anfragen entgegen, reserviert Zeiträume nach der Freigabe durch den Kunden, nimmt die verbindliche Buchung des Gastes entgegen, stellt die Rechnung aus, gleicht Online-Zahlungen mit Stripe ab, bildet Zahlungen vor Ort ab, bildet Stornierungen ab und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Buchungskalender für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der Anfragen je IP-Adresse, Erkennen automatisierter Anfragen).
|
||||
|
||||
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Anfragen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
|
||||
|
||||
## 3. Datenarten und Betroffene
|
||||
|
||||
(1) Daten der Gäste, die WEBklar aus dem Widget, von der Buchungsseite und aus Stripe erhält und speichert:
|
||||
- bei der Anfrage: Name, E-Mail-Adresse, auf Wunsch Firma, Telefonnummer und eine Nachricht, die gewünschte Unterkunft, der Zeitraum, die Zahl der Personen, die Anfragenummer, ein Hashwert der IP-Adresse (Absatz 4) und der Zeitpunkt;
|
||||
- bei der Freigabe: die Posten und Summen des Preises (Text, Menge, Einzelbetrag, Steuersatz), die Reservierungsfrist, eine Nachricht des Kunden an den Gast, die Stornobedingungen, die galten, und die Fassung der Rechtstexte, der der Gast gesehen hat, jeweils als Schnappschuss;
|
||||
- bei der Buchung: die Rechnungsadresse (Name, Firma, Straße, Postleitzahl, Ort, Land), die Zahlart, der Zeitpunkt der Buchung und die Rechnungsnummer;
|
||||
- bei der Zahlung und der Erstattung: Status, Zeitpunkt, Art (online über Stripe oder vor Ort), die Kennungen der Zahlung bei Stripe, erstattete Beträge und der Grund einer Stornierung;
|
||||
- die Rechnung und die Stornorechnung als unveränderlicher Schnappschuss (Angaben des Ausstellers, Empfänger, Unterkunft, Zeitraum, Posten, Summen, Steuerart, Bezug), aus dem WEBklar das PDF bei jedem Abruf erzeugt;
|
||||
- ein Hashwert des geheimen Zugangs zur Buchungsseite (der Link in der E-Mail wird nur als Prüfsumme gespeichert);
|
||||
- der Zustellstatus der E-Mails an den Gast.
|
||||
|
||||
**Kartendaten des Gastes erhält und speichert WEBklar nicht;** die gibt der Gast nur bei Stripe ein. Online-Zahlungen sind derzeit nur per Karte (einschließlich Apple Pay und Google Pay) möglich, nicht per SEPA-Lastschrift. Die Bankverbindung des Kunden selbst (Angabe auf der Rechnung) und seine Firmen- und Steuerangaben gehören nicht zu den Gästedaten.
|
||||
|
||||
(2) Von Kalendern anderer Plattformen übernimmt WEBklar nur die belegten Zeiträume (Datum von und bis), nicht Namen, Beschreibungen oder andere Angaben der dortigen Buchungen, auch wenn der Kalender sie enthält. Die iCal-Adresse, die WEBklar dem Kunden bereitstellt, enthält nur belegte Nächte, keine Angaben zu Gästen. Wer die geheime Adresse kennt, sieht die belegten Nächte; der Kunde kann sie jederzeit erneuern.
|
||||
|
||||
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Kunde fordert von seinen Gästen keine entsprechenden Angaben und bittet sie, solche Angaben nicht in das Nachrichtenfeld zu schreiben (Nummer 11 Abs. 4). Meldedaten nach dem Bundesmeldegesetz (Meldeschein) und Daten zur Kurtaxe verarbeitet der Buchungskalender nicht.
|
||||
|
||||
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Anfrage werden weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein **Hashwert der IP-Adresse („IP-Hash“)**: ein SHA-256-Wert, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz. Er dient nicht dazu, Gäste zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Anfrage. WEBklar entfernt ihn aus dem Datensatz, sobald der Kunde die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 9 Abs. 1). In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).
|
||||
|
||||
(5) Der Buchungskalender setzt keine Cookies. Das Widget speichert nichts im Browser. Auch die Buchungsseite legt weder Cookies noch Daten im lokalen oder im Sitzungsspeicher des Browsers ab.
|
||||
|
||||
(6) Betroffene: Gäste, die über die Website des Kunden Anfragen stellen oder buchen, und Personen, deren Angaben der Gast in der Anfrage oder Buchung macht (zum Beispiel Mitreisende oder abweichende Rechnungsempfänger). Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.
|
||||
|
||||
## 4. Weisungen
|
||||
|
||||
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
|
||||
|
||||
(2) Mit der Bestellung weist der Kunde WEBklar an, den Buchungskalender nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben, in seinem Namen Rechnungen auszustellen und Zahlungen, die nach einer Stornierung oder nach dem Ablauf einer Reservierung eingehen, sowie zweite Zahlungen auf eine bereits bezahlte Buchung ohne weitere Anweisung in voller Höhe über sein Stripe-Konto zu erstatten (dauerhafte Weisung; der Stand der Buchung ändert sich dabei nicht). Bei einer Stornierung schließt WEBklar eine noch offene Zahlungsseite bei Stripe. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Unterkünfte, Preise, Sperren, Kalenderverbindungen, Freigabe und Ablehnung von Anfragen, Zahlungseingang vor Ort, Stornierungen und Erstattungen, Online-Zahlung, Öffnen und Schließen des Buchungskalenders). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Buchungskalender abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um, soweit Aufbewahrungspflichten dem nicht entgegenstehen (Nummer 9 Abs. 8).
|
||||
|
||||
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
|
||||
|
||||
## 5. Vertraulichkeit
|
||||
|
||||
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
|
||||
|
||||
## 6. Technische und organisatorische Maßnahmen
|
||||
|
||||
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
|
||||
|
||||
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
|
||||
|
||||
## 7. Unterauftragsverarbeiter
|
||||
|
||||
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Buchungskalender läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
|
||||
|
||||
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Buchungskalender abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
|
||||
|
||||
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
|
||||
|
||||
(4) **Stripe ist kein Unterauftragsverarbeiter von WEBklar.** Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 3 der Leistungsbeschreibung); die Zahlung des Gastes fließt direkt auf dieses Konto. Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs- und Kartendaten (derzeit nur Karte, einschließlich Apple Pay und Google Pay), die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden legt WEBklar bei Stripe die Zahlung an, ruft die Angaben zu Zahlungen ab, die der Buchungskalender braucht (Nummer 2 Abs. 1), schließt offene Zahlungsseiten und veranlasst Erstattungen (auch die automatischen nach Nummer 4 Abs. 2). An Stripe gehen dabei von WEBklar nur: die Bezeichnung des Aufenthalts mit Rechnungsnummer als Zahlungsposition, der Betrag, die Kennungen der Buchung und der Zahlung (Metadaten), die E-Mail-Adresse des Gastes zur Vorbelegung der Zahlungsseite, soweit WEBklar sie übergibt, die Erstattung und der **geheime Link zur Buchungsseite mit dem Token als Rücksprungadresse** (bei Erfolg und bei Abbruch). Dieser Link ist damit für Stripe und im Stripe-Dashboard des Kunden sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.
|
||||
|
||||
(5) Plattformen, deren iCal-Kalender der Kunde einbindet oder denen er die iCal-Adresse des Buchungskalenders mitteilt, sind Empfänger nach seiner Entscheidung. WEBklar übermittelt ihnen keine Daten von Gästen.
|
||||
|
||||
## 8. Unterstützung des Kunden, Betroffenenrechte
|
||||
|
||||
(1) WEBklar leitet Anfragen von Gästen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
|
||||
|
||||
(2) Dafür bietet das Kundenportal die Anzeige der Anfragen und Buchungen, mit denen der Kunde Auskunft erteilen kann, und den Abruf der Rechnungen als PDF. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Gästedaten einer Anfrage oder Buchung unverzüglich: Name, Firma, E-Mail-Adresse, Telefonnummer und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt in der Regel längst entfernt); Anfragenummer, Zeitraum, Beträge und Status bleiben. Rechnungen und Rechnungsadresse einer gebuchten Anfrage bleiben, solange eine Aufbewahrungspflicht besteht (Nummer 9 Abs. 3). Dabei löscht WEBklar die E-Mails zu dieser Anfrage, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 5 löscht (48 Stunden nach dem Fehlschlag).
|
||||
|
||||
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
|
||||
|
||||
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
|
||||
|
||||
(5) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
|
||||
|
||||
## 9. Löschung und Rückgabe
|
||||
|
||||
(1) Den IP-Hash entfernt WEBklar aus dem Datensatz einer Anfrage, sobald der Kunde sie freigibt oder ablehnt, spätestens 30 Tage nach dem Anlegen.
|
||||
|
||||
(2) Anfragen, aus denen keine Buchung wurde (abgelehnt, abgelaufen, oder neu und nicht innerhalb von 60 Tagen entschieden), anonymisiert WEBklar nach 6 Monaten: Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht werden gelöscht. Anfragenummer, Zeitraum, Status und, soweit vorhanden, der Preisvorschlag bleiben ohne Personenbezug erhalten.
|
||||
|
||||
(3) Bei gebuchten Aufenthalten anonymisiert WEBklar die Gästedaten (Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht) 3 Jahre nach dem Ende des Kalenderjahres, in dem der Gast abgereist ist (Abreisejahr; maßgeblich ist die Zeit in Berlin). **Rechnungen, Stornorechnungen und die Rechnungsadresse bleiben 8 Jahre erhalten**, gerechnet ab dem Ende des Kalenderjahres der Rechnung, solange der Buchungskalender läuft (Buchungsbelege, § 147 AO); sie sind für den Kunden als Aussteller bestimmt. Anfragenummer, Zeitraum, Beträge und Status bleiben ebenfalls.
|
||||
|
||||
(4) Die Anonymisierung nach den Absätzen 2 und 3 sowie die Entfernung des IP-Hash laufen im täglichen Löschlauf; anonymisiert oder entfernt wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
|
||||
|
||||
(5) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
|
||||
|
||||
(6) Ist der Buchungskalender abgeschaltet oder der Vertrag über den Betrieb der Website beendet, kann der Kunde die Anfragen, Buchungen und Rechnungen 90 Tage lang im Kundenportal lesen und die Rechnungen einzeln als PDF herunterladen; WEBklar weist ihn nach dem Abschalten per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern. Danach löscht WEBklar alle Daten des Buchungskalenders: Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen. Damit endet auch die Aufbewahrung der Rechnungen nach Absatz 3; sie bewahrt der Kunde dann selbst auf. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Vertrag über den Betrieb der Website, schaltet WEBklar den Buchungskalender auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 6 (bei Abschaltung 90 Tage später); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird. alle Daten des Buchungskalenders löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
|
||||
|
||||
(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
|
||||
|
||||
(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Rechnungen und Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung. Die Aufbewahrung der Rechnungen nach Absatz 3 geschieht als Dienst für den Kunden und für die Dauer des Buchungskalenders. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Löschung nach Absatz 6 durch den Download. Soweit eine gesetzliche Aufbewahrungspflicht besteht, löscht oder anonymisiert WEBklar auch auf Weisung des Kunden nicht früher; stattdessen sperrt es die Daten für die weitere Verarbeitung.
|
||||
|
||||
## 10. Nachweise und Kontrollen
|
||||
|
||||
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
|
||||
|
||||
## 11. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste im Buchungskalender.
|
||||
|
||||
(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Buchungskalender in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.
|
||||
|
||||
(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst, wenn er die Online-Zahlung nutzt, und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).
|
||||
|
||||
(4) Der Kunde fordert von seinen Gästen keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten und weist sie auf der Website darauf hin, solche Angaben nicht in das Nachrichtenfeld zu schreiben.
|
||||
|
||||
(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht (Nr. 5 Abs. 9 der Leistungsbeschreibung).
|
||||
|
||||
(6) Der Kunde erfüllt seine Pflichten zu Meldeschein, Kurtaxe und sonstigen Meldungen an Behörden selbst und verarbeitet die dafür nötigen Daten außerhalb des Buchungskalenders (Nr. 5 Abs. 6 der Leistungsbeschreibung).
|
||||
|
||||
## 12. Haftung, Änderungen, Schlussbestimmungen
|
||||
|
||||
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
|
||||
|
||||
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
|
||||
|
||||
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Buchungskalender für seine Website nach Nr. 12 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 12 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.
|
||||
|
||||
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
|
||||
|
||||
## Anlage: Technische und organisatorische Maßnahmen des Buchungskalenders (Art. 32 DSGVO)
|
||||
|
||||
| Bereich | Maßnahmen |
|
||||
|---|---|
|
||||
| Übertragung | Das Widget, die Buchungsseite und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. Kalender anderer Plattformen ruft WEBklar nur über https ab. |
|
||||
| Speicher im Browser | Es werden keine Cookies gesetzt. Das Widget sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Buchungskalenders auf, keine Dienste Dritter. |
|
||||
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Buchungskalender Daten und ist über den Website-Schlüssel (Site-Key) adressiert; die belegten Nächte nennt sie ohne Quelle und ohne Daten von Gästen. Die Buchungsseite ruft der Gast nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert und zeitkonstant verglichen wird. Der Token wird nur als Prüfsumme gespeichert; er wird nur als Teil der Rücksprungadresse an Stripe übergeben (Nummer 7 Abs. 4) und öffnet nur diese eine Buchungsseite; er gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage), danach antwortet die Buchungsseite wie bei einem unbekannten Link. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. Die iCal-Adresse des Buchungskalenders ist ein geheimer, erneuerbarer Zufallswert. |
|
||||
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und Grenzen je IP-Adresse und je Buchungskalender schützen vor automatisierten Anfragen. Die Grenzen gelten im Arbeitsspeicher; ein Neustart leert sie. Mit der Anfrage wird nur ein gesalzener IP-Hash gespeichert (Nummer 3 Abs. 4). Beim Abruf von Kalendern anderer Plattformen sind nur Adressen mit https erlaubt, private und lokale Ziele sind gesperrt, und Größe und Dauer des Abrufs sind begrenzt. |
|
||||
| Datenminimierung | Kartendaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Preisen und Posten, nie aus Angaben des Browsers. Von anderen Kalendern werden nur Zeiträume übernommen. Mit der Anfrage werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert; der IP-Hash wird mit der Entscheidung des Kunden entfernt. Die Gästedaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
|
||||
| Integrität der Rechnungen | Rechnungen und Stornorechnungen werden als unveränderlicher Schnappschuss gespeichert; das PDF entsteht bei jedem Abruf aus diesem Schnappschuss. Rechnungsnummern werden unter Sperre lückenlos je Kunde und Jahr vergeben. Freigabe und Buchung prüfen unter Sperre erneut, dass der Zeitraum frei ist. |
|
||||
| Trennung | Alle Unterkünfte, Anfragen und Rechnungen tragen die Kennung des Kunden, des Projekts und der Funktion; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Buchungskalenders. Die Hintergrundläufe (Ablauf von Reservierungen, Kalenderabgleich, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht. |
|
||||
| Löschung | Täglicher Löschlauf für Anfragen, Buchungen und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Buchungskalender 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). |
|
||||
| Protokolle | Die Programmprotokolle des Buchungskalenders und der Tagesbericht enthalten nur Zählungen und Anfragenummern, keine Namen, E-Mail-Adressen oder Adressen von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
|
||||
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
|
||||
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
|
||||
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
|
||||
@@ -0,0 +1,145 @@
|
||||
# Auftragsverarbeitungsvertrag Webshop (Art. 28 DSGVO)
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
|
||||
|
||||
zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Händler“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
|
||||
|
||||
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
|
||||
|
||||
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Webshop“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Webshop“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
|
||||
|
||||
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
|
||||
|
||||
(3) Dieser Vertrag ist ein eigener Vertrag für den Webshop. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Webshop geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
|
||||
|
||||
(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Käufer im Webshop. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4).
|
||||
|
||||
## 1. Gegenstand und Dauer
|
||||
|
||||
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Shop-Widget mit einer Bestellverwaltung im Kundenportal („Webshop“) und verarbeitet dabei personenbezogene Daten der Käufer des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
|
||||
|
||||
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Webshop schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
|
||||
|
||||
## 2. Art und Zweck der Verarbeitung
|
||||
|
||||
(1) Art: Übermitteln der Bestellung vom Widget an den Server von WEBklar, Übernehmen der Zahlungs- und Adressangaben aus der Checkout-Sitzung bei Stripe, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen einer CSV-Datei, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.
|
||||
|
||||
(2) Zwecke: Der Kunde soll Bestellungen seiner Käufer abwickeln. Dazu nimmt der Webshop Bestellungen entgegen, reserviert den Bestand, gleicht Zahlungen mit Stripe ab, bestätigt Bestellungen (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, zunächst nur mit einer Eingangsmail ohne Vertragsschluss), bildet Versand und Abholung ab, nimmt Widerrufe entgegen, erstattet Zahlungen im Auftrag des Kunden und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Webshop für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der offenen Bestellungen je IP-Adresse, Erkennen automatisierter Bestellungen und Widerrufe).
|
||||
|
||||
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Käufer. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Bestellungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
|
||||
|
||||
## 3. Datenarten und Betroffene
|
||||
|
||||
(1) Daten der Käufer, die WEBklar aus der Checkout-Sitzung bei Stripe und aus dem Widget erhält und speichert: Name, E-Mail-Adresse, Lieferadresse und Rechnungsadresse (die Rechnungsadresse nur, soweit die Zahlungsseite von Stripe sie für die gewählte Zahlungsart abfragt); die Bestellung (Bestellnummer, Positionen mit Namen, Mengen und Preisen zum Zeitpunkt der Bestellung, Versandkosten, Lieferart, Steuerausweis, Gesamtbetrag); der Status der Bestellung mit den Zeitpunkten (bestellt, bezahlt, versendet, abholbereit, abgeschlossen, storniert, erstattet); Zahlungsstatus, Zahlungsart (zum Beispiel Karte oder SEPA-Lastschrift) und Kennungen der Zahlung bei Stripe; Versanddienst und Sendungsnummer, wenn der Kunde sie einträgt; Erstattungen mit Betrag, Zeitpunkt und Grund; die Prüfsumme (Hash) der Rechtstexte des Kunden, die der Käufer bei der Bestellung gesehen hat (den Wortlaut der Fassung verwahrt das Archiv nach Nummer 8 Abs. 5); ein Hashwert der IP-Adresse (Absatz 4); der Zustellstatus der E-Mails an den Käufer, auch der Eingangsmail bei ausstehender Zahlung. **Kartendaten und Kontodaten (IBAN) erhält und speichert WEBklar nicht;** die gibt der Käufer nur bei Stripe ein. **Eine Telefonnummer des Käufers erhebt und speichert WEBklar nicht:** Das Widget hat kein Telefonfeld, die Zahlungsseite von Stripe ist so eingestellt, dass sie keine Telefonnummer verlangt, und eine Telefonnummer, die Stripe in den Kundendaten der Sitzung dennoch übermitteln sollte, übernimmt WEBklar nicht in die Bestellung. Die Telefonnummer des Kunden selbst (Anbieterangaben in seinen Einstellungen) gehört nicht zu den Käuferdaten.
|
||||
|
||||
(2) Daten von Personen, die über die Widerrufsfunktion einen Vertrag widerrufen (Käufer und Interessenten, auch ohne passende Bestellung): Name, E-Mail-Adresse, Bestellnummer, die Erklärung als Freitext und der Zeitpunkt des Eingangs, außerdem der Zeitpunkt der Eingangsbestätigung und der Bearbeitung durch den Kunden.
|
||||
|
||||
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Waren, aus deren Kauf sich besondere Kategorien ergeben können, etwa Arzneimittel, sind nach Nr. 3 der Leistungsbeschreibung ausgeschlossen; der Kunde fordert von seinen Käufern keine entsprechenden Angaben (Nummer 11 Abs. 4).
|
||||
|
||||
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit dem Widerruf wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Mit der Bestellung werden ebenfalls weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein **Hashwert der IP-Adresse („IP-Hash“)**: die ersten 16 Hexadezimalzeichen eines SHA-256-Werts, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz: Je IP-Hash und Webshop sind höchstens drei offene (noch nicht bezahlte) Bestellungen innerhalb von 30 Minuten zulässig. Er dient nicht dazu, Käufer zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Bestellung. WEBklar löscht ihn aus dem Datensatz der Bestellung, sobald die Bestellung bezahlt oder storniert wird, spätestens mit der Löschung nicht bezahlter Bestellungen nach 30 Tagen (Nummer 9 Abs. 1); bei bezahlten Bestellungen bleibt er nicht bis zur Anonymisierung (Nummer 9 Abs. 2) erhalten. In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).
|
||||
|
||||
(5) Der Webshop setzt keine Cookies. Das Widget speichert im Browser des Besuchers nur den Warenkorb (Kennungen der Artikel und Mengen, ohne Personenbezug) im Sitzungsspeicher (sessionStorage) des Tabs, solange der Tab geöffnet ist; das ist für den Warenkorb technisch erforderlich. Andere Daten legt das Widget im Browser nicht ab.
|
||||
|
||||
(6) Betroffene: Käufer, die über die Website des Kunden Waren bestellen, und Personen, die über die Widerrufsfunktion einen Vertrag widerrufen. Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.
|
||||
|
||||
## 4. Weisungen
|
||||
|
||||
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
|
||||
|
||||
(2) Mit dem Einschalten weist der Kunde WEBklar an, den Webshop nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Artikel, Versand, Abholung, Zahlungsarten, Rechtstexte, Bestellstatus, Erstattungen, Öffnen und Schließen des Webshops, Export). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Webshop abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um.
|
||||
|
||||
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
|
||||
|
||||
## 5. Vertraulichkeit
|
||||
|
||||
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
|
||||
|
||||
## 6. Technische und organisatorische Maßnahmen
|
||||
|
||||
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
|
||||
|
||||
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
|
||||
|
||||
## 7. Unterauftragsverarbeiter
|
||||
|
||||
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Webshop läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Bestell-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
|
||||
|
||||
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Webshop abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
|
||||
|
||||
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
|
||||
|
||||
(4) **Stripe ist kein Unterauftragsverarbeiter von WEBklar.** Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 4 der Leistungsbeschreibung). Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs-, Karten- und Kontodaten, die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden ruft WEBklar bei Stripe die Angaben zu Zahlungen und Bestellungen ab, die der Webshop braucht (Nummer 2 Abs. 1), und veranlasst Erstattungen. Daten von Käufern gehen dabei nicht von WEBklar an Stripe, außer der Erstattung selbst und der Kennung der Zahlung. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.
|
||||
|
||||
## 8. Unterstützung des Kunden, Betroffenenrechte
|
||||
|
||||
(1) WEBklar leitet Anfragen von Käufern, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
|
||||
|
||||
(2) Dafür bietet das Kundenportal den Export der Bestellungen als CSV-Datei und die Anzeige der Bestellungen und Widerrufe, mit denen der Kunde Auskunft erteilen kann. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Käuferdaten einer Bestellung oder eines Widerrufs unverzüglich: Name, E-Mail-Adresse, Liefer- und Rechnungsadresse und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt längst gelöscht); Bestellnummer, Positionen, Beträge und Status bleiben. Dabei löscht WEBklar die E-Mails zu dieser Bestellung, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Gesetzliche Aufbewahrungspflichten des Kunden (Nummer 9 Abs. 8) prüft der Kunde vor seiner Weisung selbst.
|
||||
|
||||
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
|
||||
|
||||
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
|
||||
|
||||
(5) Bestätigt der Kunde seine Rechtstexte im Kundenportal (Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular, Versand und Zahlung, Gewährleistungshinweis sowie die Adressen von Impressum und Datenschutzerklärung), legt WEBklar jede bestätigte Fassung einmal im Archiv der Rechtstexte ab: mit ihrer Prüfsumme (Hash), dem Zeitpunkt und der Angabe, welche angemeldete Person des Kunden (Name oder E-Mail-Adresse) sie bestätigt hat. Die Bestellung speichert die Prüfsumme der Fassung, die der Käufer bei der Bestellung gesehen hat. Die Bestellbestätigung (G09) zeigt die archivierte Fassung zu dieser Prüfsumme, nicht die Fassung, die beim Versand der E-Mail gerade gilt; fehlt die archivierte Fassung ausnahmsweise, verwendet WEBklar die aktuell bestätigte Fassung. Das Archiv enthält keine Daten von Käufern. WEBklar löscht es mit den übrigen Daten des Webshops (Nummer 9 Abs. 6), nicht früher. Das Archiv hilft dem Kunden, die Fassung nachzuweisen, die ein Käufer erhalten hat; es ersetzt nicht seine eigene Aufbewahrung (Nummer 9 Abs. 8). Die Bestellbestätigung enthält diese Texte; WEBklar löscht ihren Inhalt nach dem Versand aus der Versandwarteschlange (Nummer 9 Abs. 4).
|
||||
|
||||
(6) Die Käufer informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
|
||||
|
||||
## 9. Löschung und Rückgabe
|
||||
|
||||
(1) Eine Bestellung, die nicht bezahlt wird, verfällt 30 Minuten nach dem Anlegen und gilt als storniert. WEBklar löscht nicht bezahlte Bestellungen (offen oder storniert) 30 Tage nach dem Anlegen. Den IP-Hash löscht WEBklar aus dem Datensatz, sobald die Bestellung bezahlt oder storniert wird, spätestens mit dieser Löschung.
|
||||
|
||||
(2) Bei bezahlten Bestellungen anonymisiert WEBklar die Käuferdaten 3 Jahre nach dem Ende des Kalenderjahres, in dem die Bestellung angelegt wurde (Bestelljahr; maßgeblich ist die Zeit in Berlin): Name, E-Mail-Adresse sowie Liefer- und Rechnungsadresse werden gelöscht (der IP-Hash ist mit der Zahlung bereits gelöscht; eine Telefonnummer des Käufers wird nicht gespeichert). Bestellnummer, Positionen, Beträge, Steuerausweis, Status, Versanddienst und Sendungsnummer bleiben für die Buchhaltung des Kunden erhalten.
|
||||
|
||||
(3) Widerrufe nach Nummer 3 Abs. 2 behandelt WEBklar wie bezahlte Bestellungen: Name, E-Mail-Adresse und Erklärung werden 3 Jahre nach dem Ende des Kalenderjahres des Eingangs gelöscht; die Bestellnummer, die die Person angegeben hat, und der Zeitpunkt des Eingangs bleiben.
|
||||
|
||||
(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
|
||||
|
||||
(5) Der Löschlauf läuft täglich; gelöscht oder anonymisiert wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
|
||||
|
||||
(6) Ist der Webshop abgeschaltet oder das Wartungs-Abo beendet, kann der Kunde die Bestellungen und Widerrufe 90 Tage lang im Kundenportal lesen und als CSV-Datei exportieren. Danach löscht WEBklar alle Daten des Webshops: Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen der Rechtstexte (Nummer 8 Abs. 5). Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Käufern; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Webshop auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); alle Daten des Webshops löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
|
||||
|
||||
(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
|
||||
|
||||
(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung, und das Archiv der Rechtstexte dient nur dem Nachweis der Texte. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Anonymisierung oder Löschung durch den Export und die Belege von Stripe.
|
||||
|
||||
## 10. Nachweise und Kontrollen
|
||||
|
||||
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
|
||||
|
||||
## 11. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Käufer im Webshop.
|
||||
|
||||
(2) Der Kunde informiert seine Käufer nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Webshop in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.
|
||||
|
||||
(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).
|
||||
|
||||
(4) Der Kunde verkauft keine ausgeschlossenen Waren (Nr. 3 der Leistungsbeschreibung) und fordert von seinen Käufern keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten, auch nicht im Freitext der Widerrufserklärung.
|
||||
|
||||
(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht und entfernt den Link „Vertrag widerrufen“ nicht (Nr. 5 Abs. 6 und 9 der Leistungsbeschreibung).
|
||||
|
||||
## 12. Haftung, Änderungen, Schlussbestimmungen
|
||||
|
||||
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
|
||||
|
||||
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
|
||||
|
||||
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Webshop für seine Website nach Nr. 11 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 11 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.
|
||||
|
||||
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
|
||||
|
||||
## Anlage: Technische und organisatorische Maßnahmen des Webshops (Art. 32 DSGVO)
|
||||
|
||||
| Bereich | Maßnahmen |
|
||||
|---|---|
|
||||
| Übertragung | Das Widget, die Seiten der Käufer und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. |
|
||||
| Speicher im Browser | Es werden keine Cookies gesetzt. Das Widget speichert nur den Warenkorb (Kennungen der Artikel und Mengen) im Sitzungsspeicher des Tabs. Es sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Webshops auf, keine Dienste Dritter. |
|
||||
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Webshop Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Bestellungen und Widerrufe nimmt sie nur von Seiten des Kunden und des Portals an. Den Stand einer Bestellung ruft der Käufer nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert wird. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. |
|
||||
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Bestellungen und Widerrufen. Grenzen je IP-Adresse und je Webshop gelten im Arbeitsspeicher; ein Neustart leert sie. Die Obergrenze offener Bestellungen je IP-Adresse (drei in 30 Minuten) arbeitet mit einem gesalzenen Hashwert der IP-Adresse in der Bestellung (Nummer 3 Abs. 4). |
|
||||
| Datenminimierung | Kartendaten und Kontodaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Artikeln, nie aus Angaben des Browsers. Eine Telefonnummer wird weder abgefragt noch gespeichert, die Rechnungsadresse nur, soweit Stripe sie für die Zahlungsart braucht. Mit der Bestellung und mit dem Widerruf werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert, bei der Bestellung nur der gesalzene IP-Hash, und der nur, solange die Bestellung offen ist (gelöscht mit Zahlung oder Stornierung). Die Käuferdaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
|
||||
| Trennung | Alle Artikel, Bestellungen und Widerrufe tragen die Kennung des Webshops und des Kunden; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Webshops. Artikelbilder liegen in einem eigenen Ordner je Webshop. Die Hintergrundläufe (Abgleich offener Bestellungen, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht. |
|
||||
| Löschung | Täglicher Löschlauf für Bestellungen, Widerrufe und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Webshops einschließlich des Archivs der bestätigten Rechtstexte 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6). |
|
||||
| Protokolle | Die Programmprotokolle des Webshops und der Tagesbericht enthalten nur Zählungen und Bestellnummern, keine Namen, E-Mail-Adressen oder Adressen von Käufern. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
|
||||
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
|
||||
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
|
||||
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
|
||||
@@ -0,0 +1,145 @@
|
||||
# Auftragsverarbeitungsvertrag Webshop (Art. 28 DSGVO)
|
||||
|
||||
**Fassung 2026-10-05**
|
||||
|
||||
zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Händler“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
|
||||
|
||||
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
|
||||
|
||||
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Einrichtung des Webshops bestellt und dabei per Haken diesem Vertrag und der Leistungsbeschreibung „Webshop“ („Leistungsbeschreibung“) in der angezeigten Fassung zustimmt. Das Portal speichert die Fassung, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte und die Widerrufsbelehrung in dieser Fassung als PDF enthält und auf sie verlinkt.
|
||||
|
||||
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
|
||||
|
||||
(3) Dieser Vertrag ist ein eigener Vertrag für den Webshop. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Webshop geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
|
||||
|
||||
(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Käufer im Webshop. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4).
|
||||
|
||||
## 1. Gegenstand und Dauer
|
||||
|
||||
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Shop-Widget mit einer Bestellverwaltung im Kundenportal („Webshop“) und verarbeitet dabei personenbezogene Daten der Käufer des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
|
||||
|
||||
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Vertrags, aufgrund dessen WEBklar die Website des Kunden betreibt; den Webshop schaltet WEBklar dann zum Vertragsende ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
|
||||
|
||||
## 2. Art und Zweck der Verarbeitung
|
||||
|
||||
(1) Art: Übermitteln der Bestellung vom Widget an den Server von WEBklar, Übernehmen der Zahlungs- und Adressangaben aus der Checkout-Sitzung bei Stripe, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen einer CSV-Datei, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.
|
||||
|
||||
(2) Zwecke: Der Kunde soll Bestellungen seiner Käufer abwickeln. Dazu nimmt der Webshop Bestellungen entgegen, reserviert den Bestand, gleicht Zahlungen mit Stripe ab, bestätigt Bestellungen (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, zunächst nur mit einer Eingangsmail ohne Vertragsschluss), bildet Versand und Abholung ab, nimmt Widerrufe entgegen, erstattet Zahlungen im Auftrag des Kunden und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Webshop für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der offenen Bestellungen je IP-Adresse, Erkennen automatisierter Bestellungen und Widerrufe).
|
||||
|
||||
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Käufer. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Bestellungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
|
||||
|
||||
## 3. Datenarten und Betroffene
|
||||
|
||||
(1) Daten der Käufer, die WEBklar aus der Checkout-Sitzung bei Stripe und aus dem Widget erhält und speichert: Name, E-Mail-Adresse, Lieferadresse und Rechnungsadresse (die Rechnungsadresse nur, soweit die Zahlungsseite von Stripe sie für die gewählte Zahlungsart abfragt); die Bestellung (Bestellnummer, Positionen mit Namen, Mengen und Preisen zum Zeitpunkt der Bestellung, Versandkosten, Lieferart, Steuerausweis, Gesamtbetrag); der Status der Bestellung mit den Zeitpunkten (bestellt, bezahlt, versendet, abholbereit, abgeschlossen, storniert, erstattet); Zahlungsstatus, Zahlungsart (zum Beispiel Karte oder SEPA-Lastschrift) und Kennungen der Zahlung bei Stripe; Versanddienst und Sendungsnummer, wenn der Kunde sie einträgt; Erstattungen mit Betrag, Zeitpunkt und Grund; die Prüfsumme (Hash) der Rechtstexte des Kunden, die der Käufer bei der Bestellung gesehen hat (den Wortlaut der Fassung verwahrt das Archiv nach Nummer 8 Abs. 5); ein Hashwert der IP-Adresse (Absatz 4); der Zustellstatus der E-Mails an den Käufer, auch der Eingangsmail bei ausstehender Zahlung. **Kartendaten und Kontodaten (IBAN) erhält und speichert WEBklar nicht;** die gibt der Käufer nur bei Stripe ein. **Eine Telefonnummer des Käufers erhebt und speichert WEBklar nicht:** Das Widget hat kein Telefonfeld, die Zahlungsseite von Stripe ist so eingestellt, dass sie keine Telefonnummer verlangt, und eine Telefonnummer, die Stripe in den Kundendaten der Sitzung dennoch übermitteln sollte, übernimmt WEBklar nicht in die Bestellung. Die Telefonnummer des Kunden selbst (Anbieterangaben in seinen Einstellungen) gehört nicht zu den Käuferdaten.
|
||||
|
||||
(2) Daten von Personen, die über die Widerrufsfunktion einen Vertrag widerrufen (Käufer und Interessenten, auch ohne passende Bestellung): Name, E-Mail-Adresse, Bestellnummer, die Erklärung als Freitext und der Zeitpunkt des Eingangs, außerdem der Zeitpunkt der Eingangsbestätigung und der Bearbeitung durch den Kunden.
|
||||
|
||||
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Waren, aus deren Kauf sich besondere Kategorien ergeben können, etwa Arzneimittel, sind nach Nr. 3 der Leistungsbeschreibung ausgeschlossen; der Kunde fordert von seinen Käufern keine entsprechenden Angaben (Nummer 11 Abs. 4).
|
||||
|
||||
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit dem Widerruf wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Mit der Bestellung werden ebenfalls weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein **Hashwert der IP-Adresse („IP-Hash“)**: die ersten 16 Hexadezimalzeichen eines SHA-256-Werts, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz: Je IP-Hash und Webshop sind höchstens drei offene (noch nicht bezahlte) Bestellungen innerhalb von 30 Minuten zulässig. Er dient nicht dazu, Käufer zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Bestellung. WEBklar löscht ihn aus dem Datensatz der Bestellung, sobald die Bestellung bezahlt oder storniert wird, spätestens mit der Löschung nicht bezahlter Bestellungen nach 30 Tagen (Nummer 9 Abs. 1); bei bezahlten Bestellungen bleibt er nicht bis zur Anonymisierung (Nummer 9 Abs. 2) erhalten. In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).
|
||||
|
||||
(5) Der Webshop setzt keine Cookies. Das Widget speichert im Browser des Besuchers nur den Warenkorb (Kennungen der Artikel und Mengen, ohne Personenbezug) im Sitzungsspeicher (sessionStorage) des Tabs, solange der Tab geöffnet ist; das ist für den Warenkorb technisch erforderlich. Andere Daten legt das Widget im Browser nicht ab.
|
||||
|
||||
(6) Betroffene: Käufer, die über die Website des Kunden Waren bestellen, und Personen, die über die Widerrufsfunktion einen Vertrag widerrufen. Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.
|
||||
|
||||
## 4. Weisungen
|
||||
|
||||
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
|
||||
|
||||
(2) Mit der Bestellung weist der Kunde WEBklar an, den Webshop nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Artikel, Versand, Abholung, Zahlungsarten, Rechtstexte, Bestellstatus, Erstattungen, Öffnen und Schließen des Webshops, Export). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Webshop abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um.
|
||||
|
||||
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
|
||||
|
||||
## 5. Vertraulichkeit
|
||||
|
||||
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
|
||||
|
||||
## 6. Technische und organisatorische Maßnahmen
|
||||
|
||||
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
|
||||
|
||||
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
|
||||
|
||||
## 7. Unterauftragsverarbeiter
|
||||
|
||||
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Webshop läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Bestell-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
|
||||
|
||||
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Webshop abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
|
||||
|
||||
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
|
||||
|
||||
(4) **Stripe ist kein Unterauftragsverarbeiter von WEBklar.** Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 4 der Leistungsbeschreibung). Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs-, Karten- und Kontodaten, die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden ruft WEBklar bei Stripe die Angaben zu Zahlungen und Bestellungen ab, die der Webshop braucht (Nummer 2 Abs. 1), und veranlasst Erstattungen. Daten von Käufern gehen dabei nicht von WEBklar an Stripe, außer der Erstattung selbst und der Kennung der Zahlung. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.
|
||||
|
||||
## 8. Unterstützung des Kunden, Betroffenenrechte
|
||||
|
||||
(1) WEBklar leitet Anfragen von Käufern, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
|
||||
|
||||
(2) Dafür bietet das Kundenportal den Export der Bestellungen als CSV-Datei und die Anzeige der Bestellungen und Widerrufe, mit denen der Kunde Auskunft erteilen kann. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Käuferdaten einer Bestellung oder eines Widerrufs unverzüglich: Name, E-Mail-Adresse, Liefer- und Rechnungsadresse und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt längst gelöscht); Bestellnummer, Positionen, Beträge und Status bleiben. Dabei löscht WEBklar die E-Mails zu dieser Bestellung, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Gesetzliche Aufbewahrungspflichten des Kunden (Nummer 9 Abs. 8) prüft der Kunde vor seiner Weisung selbst.
|
||||
|
||||
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
|
||||
|
||||
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
|
||||
|
||||
(5) Bestätigt der Kunde seine Rechtstexte im Kundenportal (Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular, Versand und Zahlung, Gewährleistungshinweis sowie die Adressen von Impressum und Datenschutzerklärung), legt WEBklar jede bestätigte Fassung einmal im Archiv der Rechtstexte ab: mit ihrer Prüfsumme (Hash), dem Zeitpunkt und der Angabe, welche angemeldete Person des Kunden (Name oder E-Mail-Adresse) sie bestätigt hat. Die Bestellung speichert die Prüfsumme der Fassung, die der Käufer bei der Bestellung gesehen hat. Die Bestellbestätigung (G09) zeigt die archivierte Fassung zu dieser Prüfsumme, nicht die Fassung, die beim Versand der E-Mail gerade gilt; fehlt die archivierte Fassung ausnahmsweise, verwendet WEBklar die aktuell bestätigte Fassung. Das Archiv enthält keine Daten von Käufern. WEBklar löscht es mit den übrigen Daten des Webshops (Nummer 9 Abs. 6), nicht früher. Das Archiv hilft dem Kunden, die Fassung nachzuweisen, die ein Käufer erhalten hat; es ersetzt nicht seine eigene Aufbewahrung (Nummer 9 Abs. 8). Die Bestellbestätigung enthält diese Texte; WEBklar löscht ihren Inhalt nach dem Versand aus der Versandwarteschlange (Nummer 9 Abs. 4).
|
||||
|
||||
(6) Die Käufer informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
|
||||
|
||||
## 9. Löschung und Rückgabe
|
||||
|
||||
(1) Eine Bestellung, die nicht bezahlt wird, verfällt 30 Minuten nach dem Anlegen und gilt als storniert. WEBklar löscht nicht bezahlte Bestellungen (offen oder storniert) 30 Tage nach dem Anlegen. Den IP-Hash löscht WEBklar aus dem Datensatz, sobald die Bestellung bezahlt oder storniert wird, spätestens mit dieser Löschung.
|
||||
|
||||
(2) Bei bezahlten Bestellungen anonymisiert WEBklar die Käuferdaten 3 Jahre nach dem Ende des Kalenderjahres, in dem die Bestellung angelegt wurde (Bestelljahr; maßgeblich ist die Zeit in Berlin): Name, E-Mail-Adresse sowie Liefer- und Rechnungsadresse werden gelöscht (der IP-Hash ist mit der Zahlung bereits gelöscht; eine Telefonnummer des Käufers wird nicht gespeichert). Bestellnummer, Positionen, Beträge, Steuerausweis, Status, Versanddienst und Sendungsnummer bleiben für die Buchhaltung des Kunden erhalten.
|
||||
|
||||
(3) Widerrufe nach Nummer 3 Abs. 2 behandelt WEBklar wie bezahlte Bestellungen: Name, E-Mail-Adresse und Erklärung werden 3 Jahre nach dem Ende des Kalenderjahres des Eingangs gelöscht; die Bestellnummer, die die Person angegeben hat, und der Zeitpunkt des Eingangs bleiben.
|
||||
|
||||
(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
|
||||
|
||||
(5) Der Löschlauf läuft täglich; gelöscht oder anonymisiert wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
|
||||
|
||||
(6) Ist der Webshop abgeschaltet oder der Vertrag über den Betrieb der Website beendet, kann der Kunde die Bestellungen und Widerrufe 90 Tage lang im Kundenportal lesen und als CSV-Datei exportieren. Danach löscht WEBklar alle Daten des Webshops: Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen der Rechtstexte (Nummer 8 Abs. 5). Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Käufern; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Vertrag über den Betrieb der Website, schaltet WEBklar den Webshop auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); alle Daten des Webshops löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
|
||||
|
||||
(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
|
||||
|
||||
(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung, und das Archiv der Rechtstexte dient nur dem Nachweis der Texte. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Anonymisierung oder Löschung durch den Export und die Belege von Stripe.
|
||||
|
||||
## 10. Nachweise und Kontrollen
|
||||
|
||||
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
|
||||
|
||||
## 11. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Käufer im Webshop.
|
||||
|
||||
(2) Der Kunde informiert seine Käufer nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Webshop in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.
|
||||
|
||||
(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).
|
||||
|
||||
(4) Der Kunde verkauft keine ausgeschlossenen Waren (Nr. 3 der Leistungsbeschreibung) und fordert von seinen Käufern keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten, auch nicht im Freitext der Widerrufserklärung.
|
||||
|
||||
(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht und entfernt den Link „Vertrag widerrufen“ nicht (Nr. 5 Abs. 6 und 9 der Leistungsbeschreibung).
|
||||
|
||||
## 12. Haftung, Änderungen, Schlussbestimmungen
|
||||
|
||||
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
|
||||
|
||||
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
|
||||
|
||||
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Webshop für seine Website nach Nr. 11 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 11 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.
|
||||
|
||||
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
|
||||
|
||||
## Anlage: Technische und organisatorische Maßnahmen des Webshops (Art. 32 DSGVO)
|
||||
|
||||
| Bereich | Maßnahmen |
|
||||
|---|---|
|
||||
| Übertragung | Das Widget, die Seiten der Käufer und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. |
|
||||
| Speicher im Browser | Es werden keine Cookies gesetzt. Das Widget speichert nur den Warenkorb (Kennungen der Artikel und Mengen) im Sitzungsspeicher des Tabs. Es sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Webshops auf, keine Dienste Dritter. |
|
||||
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Webshop Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Bestellungen und Widerrufe nimmt sie nur von Seiten des Kunden und des Portals an. Den Stand einer Bestellung ruft der Käufer nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert wird. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. |
|
||||
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Bestellungen und Widerrufen. Grenzen je IP-Adresse und je Webshop gelten im Arbeitsspeicher; ein Neustart leert sie. Die Obergrenze offener Bestellungen je IP-Adresse (drei in 30 Minuten) arbeitet mit einem gesalzenen Hashwert der IP-Adresse in der Bestellung (Nummer 3 Abs. 4). |
|
||||
| Datenminimierung | Kartendaten und Kontodaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Artikeln, nie aus Angaben des Browsers. Eine Telefonnummer wird weder abgefragt noch gespeichert, die Rechnungsadresse nur, soweit Stripe sie für die Zahlungsart braucht. Mit der Bestellung und mit dem Widerruf werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert, bei der Bestellung nur der gesalzene IP-Hash, und der nur, solange die Bestellung offen ist (gelöscht mit Zahlung oder Stornierung). Die Käuferdaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
|
||||
| Trennung | Alle Artikel, Bestellungen und Widerrufe tragen die Kennung des Webshops und des Kunden; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Webshops. Artikelbilder liegen in einem eigenen Ordner je Webshop. Die Hintergrundläufe (Abgleich offener Bestellungen, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht. |
|
||||
| Löschung | Täglicher Löschlauf für Bestellungen, Widerrufe und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Webshops einschließlich des Archivs der bestätigten Rechtstexte 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6). |
|
||||
| Protokolle | Die Programmprotokolle des Webshops und der Tagesbericht enthalten nur Zählungen und Bestellnummern, keine Namen, E-Mail-Adressen oder Adressen von Käufern. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
|
||||
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
|
||||
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
|
||||
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
|
||||
@@ -0,0 +1,138 @@
|
||||
# Auftragsverarbeitungsvertrag Website-Analyse (Art. 28 DSGVO)
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
|
||||
|
||||
zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
|
||||
|
||||
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
|
||||
|
||||
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Website-Analyse“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Website-Analyse“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
|
||||
|
||||
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält die E-Mail nach Absatz 1.
|
||||
|
||||
(3) Dieser Vertrag ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für die Website-Analyse geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung „Website-Analyse“ vor.
|
||||
|
||||
## 1. Gegenstand und Dauer
|
||||
|
||||
(1) WEBklar betreibt für die öffentliche Website des Kunden eine Reichweitenmessung mit Einwilligungsbanner („Website-Analyse“) und verarbeitet dabei personenbezogene Daten der Besucher der Website im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung „Website-Analyse“.
|
||||
|
||||
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Ausschalten, spätestens mit dem Ende des Abos, das die Website-Analyse enthält (Hosting-Abo oder Wartungs-Abo); endet nur das Wartungs-Abo und läuft ein Hosting-Abo weiter, läuft dieser Vertrag weiter. Den Messdienst schaltet WEBklar bei Vertragsende spätestens am folgenden Tag ab. Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
|
||||
|
||||
## 2. Art und Zweck der Verarbeitung
|
||||
|
||||
(1) Art: Erheben über ein Skript, das WEBklar beim Veröffentlichen in die Website des Kunden einfügt, Übermitteln an den Messdienst von WEBklar, Speichern, Verdichten zu Tageswerten, Auswerten im Kundenportal (Kennzahlen, Heatmap) und Löschen.
|
||||
|
||||
(2) Zwecke: Der Kunde soll erkennen, wie seine Website genutzt wird, um sie zu verbessern. Außerdem schützt WEBklar den Messdienst für den Kunden vor Missbrauch und verfälschten Zahlen (Mengenbegrenzung, Erkennen von Bots).
|
||||
|
||||
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Besucher. Die Besucher-Kennung dient nur dazu, die Besucher eines Tages zu zählen.
|
||||
|
||||
## 3. Datenarten und Betroffene
|
||||
|
||||
(1) Solange der Besucher im Banner keine Wahl getroffen hat und sein Browser kein Signal nach Absatz 2 Satz 2 sendet: Pfad der aufgerufenen Seite (ohne Suchparameter und Anker), auch bei einem Seitenwechsel ohne Neuladen; Land (aus der IP-Adresse abgeleitet); Geräteklasse (Handy, Tablet oder Computer; aus dem Browser-Kennzeichen abgeleitet); Zeitpunkt (auf die volle Stunde gerundet); ein Zähler dafür, dass das Banner angezeigt wurde.
|
||||
|
||||
(2) Nach „Ablehnen“ sendet das Skript einmalig einen Zähler „abgelehnt“ und danach nichts mehr. Sendet der Browser die Signale „Global Privacy Control“ oder „Do Not Track“, zeigt das Skript kein Banner und sendet nichts.
|
||||
|
||||
(3) Nach „Annehmen“ zusätzlich: Domain der Website, von der der Besucher kam; Größenklasse des Browserfensters; eine täglich wechselnde pseudonyme Besucher-Kennung; Verweildauer (ab dem Laden der Seite) und Scrolltiefe je Seite; Klickpositionen mit einem Elementpfad aus Tag-Namen und Position (ohne Texte, Kennungen und Klassennamen); die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp oder Link auf eine andere Website, dann mit deren Domain); ein Zähler „angenommen“.
|
||||
|
||||
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Skripts und bei jeder Messanfrage, auch nach „Ablehnen“. Der Messdienst verarbeitet sie nur kurz im Arbeitsspeicher, um Land, Geräteklasse, Bots, die Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung zu bestimmen. Er speichert und protokolliert sie nicht. Nicht erhoben werden Cookies, Formularinhalte, Seitentexte, Mausbewegungen, Tastatureingaben und Sitzungsaufzeichnungen.
|
||||
|
||||
(5) Tageswerte sind Summen je Tag und Merkmal (etwa Aufrufe je Seite, je Land oder je Geräteklasse), ohne Besucher-Kennung. Enthalten die Pfade der Website personenbezogene Angaben, können auch Tageswerte personenbezogen sein (Nummer 11 Abs. 2).
|
||||
|
||||
(6) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht gezielt verarbeitet. Je nach Inhalt der Website können sich aus Pfaden oder Domains aber Rückschlüsse ergeben (Nummer 11 Abs. 3).
|
||||
|
||||
(7) Betroffene: Besucher der Website des Kunden.
|
||||
|
||||
## 4. Weisungen
|
||||
|
||||
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
|
||||
|
||||
(2) Mit dem Einschalten weist der Kunde WEBklar an, die Website-Analyse in der Standardkonfiguration nach der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Einschalten, Ausschalten, Akzentfarbe des Banners). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die von der Standardkonfiguration abweichen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde die Funktion ausschalten.
|
||||
|
||||
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
|
||||
|
||||
## 5. Vertraulichkeit
|
||||
|
||||
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
|
||||
|
||||
## 6. Technische und organisatorische Maßnahmen
|
||||
|
||||
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
|
||||
|
||||
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
|
||||
|
||||
## 7. Unterauftragsverarbeiter
|
||||
|
||||
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Messdienst läuft, und für dessen Sicherung. Ort der Verarbeitung ist das Rechenzentrum Falkenstein (Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
|
||||
|
||||
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde die Website-Analyse ausschalten und WEBklar sie für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Verlangen des Kunden sofort.
|
||||
|
||||
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
|
||||
|
||||
(4) Die Länderzuordnung nutzt eine Datenbank, die WEBklar auf dem eigenen Server speichert („IP to Country Lite“ von DB-IP.com, Lizenz CC BY 4.0). Daten der Besucher werden dabei nicht an DB-IP übermittelt. DB-IP ist kein Unterauftragsverarbeiter.
|
||||
|
||||
## 8. Unterstützung des Kunden, Betroffenenrechte
|
||||
|
||||
(1) WEBklar leitet Anfragen betroffener Personen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
|
||||
|
||||
(2) Die gespeicherten Messdaten enthalten keine IP-Adresse, keinen Namen und keine Kennung, die länger als einen Tag gilt; Zeitpunkte sind auf die volle Stunde gerundet. Einzelne Besucher kann WEBklar daher in der Regel nicht identifizieren (Art. 11 DSGVO). Ein Abgleich kann aber möglich sein, solange die Zugriffsprotokolle der Website (Anlage 1 und 2 der Abo-Bedingungen, 14 Tage) vorhanden sind. Nennt eine betroffene Person zusätzliche Angaben, etwa Datum, Uhrzeit, besuchte Seiten oder ihre IP-Adresse, prüft WEBklar auf Weisung des Kunden, auch anhand dieser Protokolle, ob eine Zuordnung möglich ist, und teilt ihm das Ergebnis mit.
|
||||
|
||||
(3) Der Messung können Besucher technisch widersprechen (Art. 21 DSGVO): Nach „Ablehnen“ und bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“ sendet das Skript nichts mehr.
|
||||
|
||||
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht, und eine kurze Interessenabwägung für die Zählung nach Art. 6 Abs. 1 lit. f DSGVO, die WEBklar mit jeder Fassung dieses Vertrags bereitstellt.
|
||||
|
||||
(5) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
|
||||
|
||||
(6) Zum Nachweis der Einwilligungen bewahrt WEBklar jede Fassung des Banner-Textes und der Zusatzseite mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende der Website-Analyse. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung. Ein Protokoll je Besucher führt WEBklar nicht.
|
||||
|
||||
(7) Die Besucher informiert der Kunde (Art. 13 DSGVO). WEBklar stellt ihm dafür die Zusatzseite „/datenschutz-analyse.html“ als Muster bereit. Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar dort einen Hinweis auf die Zusatzseite ein; sonst erhält der Kunde den Hinweis als Textbaustein (Leistungsbeschreibung Nr. 4 Abs. 4).
|
||||
|
||||
## 9. Löschung und Rückgabe
|
||||
|
||||
(1) Einzelne Ereignisse und Klickpositionen löscht WEBklar automatisch nach 30 Tagen. Der Löschlauf läuft täglich, die Löschung erfolgt also in der Regel spätestens am 31. Tag; fällt ein Lauf aus, holt der nächste ihn nach.
|
||||
|
||||
(2) Tageswerte löscht WEBklar 25 Monate nach dem jeweiligen Tag. Nach dem Ende der Website-Analyse (Ausschalten oder Ende des Abos, das sie enthält) löscht WEBklar alle Daten der Website am 61. Tag. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse verlangen; WEBklar stellt sie innerhalb von zwei Wochen als CSV-Datei bereit. Auf Weisung des Kunden löscht WEBklar die Daten früher.
|
||||
|
||||
(3) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
|
||||
|
||||
(4) Gesetzliche Aufbewahrungspflichten bleiben unberührt.
|
||||
|
||||
## 10. Nachweise und Kontrollen
|
||||
|
||||
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
|
||||
|
||||
## 11. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist Verantwortlicher für die Messung. Er entfernt, verdeckt oder umgeht das Banner nicht. Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess- oder Tracking-Werkzeuge ein.
|
||||
|
||||
(2) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.
|
||||
|
||||
(3) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren oder lassen ihre Inhalte auf besondere Kategorien personenbezogener Daten schließen (etwa Gesundheit, Religion oder politische Meinung), gilt Nr. 3 Abs. 4 der Leistungsbeschreibung.
|
||||
|
||||
## 12. Haftung, Änderungen, Schlussbestimmungen
|
||||
|
||||
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
|
||||
|
||||
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
|
||||
|
||||
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar die Website-Analyse für seine Website nach Nr. 8 Abs. 2 der Leistungsbeschreibung beenden oder aussetzen. Das Abo bleibt davon unberührt.
|
||||
|
||||
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
|
||||
|
||||
## Anlage: Technische und organisatorische Maßnahmen der Website-Analyse (Art. 32 DSGVO)
|
||||
|
||||
| Bereich | Maßnahmen |
|
||||
|---|---|
|
||||
| Übertragung | Skript und Messdaten werden nur über TLS (HTTPS) übertragen. |
|
||||
| Speicher im Browser | Es werden keine Cookies gesetzt, und der Messdienst wertet keine Cookies aus. Messanfragen sendet das Skript ohne Cookies. Im lokalen Speicher des Browsers liegt nur die Banner-Wahl (Eintrag „wk-consent“ mit Wahl, Zeitpunkt und Fassung des Banner-Textes). Sie gilt 183 Tage; danach und bei einer neuen Fassung des Banner-Textes fragt das Banner erneut. |
|
||||
| Einwilligung | Solange keine Wahl getroffen ist und kein Browser-Signal vorliegt, sendet das Skript nur den Pfad der Seite, auch beim Seitenwechsel ohne Neuladen, und den Zähler „Banner angezeigt“. Nach „Ablehnen“ sendet es einmalig den Zähler „abgelehnt“ und danach nichts mehr, auch nicht aus anderen geöffneten Tabs. Bei „Global Privacy Control“ oder „Do Not Track“ zeigt es kein Banner und sendet nichts. Klick-, Scroll- und Verweildauer-Ereignisse registriert es erst nach „Annehmen“. Die Schaltflächen „Ablehnen“ und „Annehmen“ sind gleich gestaltet. |
|
||||
| Datenminimierung | IP-Adresse und Browser-Kennzeichen werden weder gespeichert noch protokolliert. Der Messdienst verwendet sie nur im Arbeitsspeicher für Land, Geräteklasse, Bot-Erkennung, Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung. Zeitpunkte werden auf die volle Stunde gerundet gespeichert. Pfade werden ohne Suchparameter und Anker gespeichert; Abschnitte, die wie Kennungen aussehen (lange Ziffernfolgen, UUIDs, E-Mail-Adressen), werden vorher ersetzt. Elementpfade bestehen nur aus Tag-Namen und Position. Formulareingaben und Seitentexte werden nicht erfasst. |
|
||||
| Pseudonymisierung | Die Besucher-Kennung (nur nach Einwilligung) ist ein auf 64 Bit gekürzter HMAC-SHA256 aus Website-Schlüssel, IP-Adresse und Browser-Kennzeichen mit einem Tagesschlüssel. Auch der Schlüssel der Mengenbegrenzung wird mit dem Tagesschlüssel gebildet. Der Tagesschlüssel liegt nur im Arbeitsspeicher; die Auslagerung auf die Festplatte ist für den Messdienst gesperrt. Er wird beim ersten Ereignis eines neuen Kalendertags (Europe/Berlin) und bei jedem Neustart neu erzeugt und nie gesichert. Ältere Kennungen lassen sich danach nicht mehr zuordnen. |
|
||||
| Zugriffskontrolle | Die Verwaltungsschnittstelle des Messdienstes (Auswertung, Website-Verwaltung) läuft auf einem internen Port, der nicht öffentlich erreichbar ist, und ist durch ein Zugriffstoken geschützt. Zugriff haben nur das Kundenportal und die Verwaltung von WEBklar. Das Portal zeigt jedem Kunden nur die Daten seiner eigenen Websites. |
|
||||
| Heatmap | Die Heatmap öffnet ein signierter Link, den nur der angemeldete Kunde im Kundenportal erzeugen kann. Der Link gilt 30 Minuten und liefert nur Klickpunkte der gewählten Seite und Ansicht, ohne Zeitpunkte und Kennungen. |
|
||||
| Eingangsprüfung | Messdaten werden nur angenommen, wenn die Website registriert und eingeschaltet ist und Origin bzw. Referer zu ihren Domains passen. Bots werden verworfen. Größe und Menge der Anfragen sind je Website und Absender begrenzt (Zähler nur im Arbeitsspeicher). Für ausgeschaltete Websites liefert der Messdienst ein leeres Skript aus. |
|
||||
| Löschung | Täglicher Löschlauf um 02:30 Uhr (Europe/Berlin): Ereignisse und Klicks nach 30 Tagen, Tageswerte nach 25 Monaten, alle Daten einer Website 60 Tage nach dem Ausschalten. Gelöschte Datensätze werden in der Datenbankdatei überschrieben und aus dem Schreibprotokoll (WAL) entfernt. |
|
||||
| Trennung | Der Messdienst läuft in einem eigenen Container mit eigenem Datenverzeichnis und hat keinen Zugriff auf andere Kundendaten. Alle Datensätze tragen den Website-Schlüssel; Abfragen erfolgen nur je Website. |
|
||||
| Protokolle | Der Reverse-Proxy führt kein Zugriffsprotokoll, und der Messdienst protokolliert keine Anfragen. Wird ein Zugriffsprotokoll eingeschaltet, dann nicht für die Adresse des Messdienstes. Meldungen der Container werden nach 14 Tagen gelöscht. |
|
||||
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). Einen gesonderten Datenbank-Export des Messdienstes gibt es nicht. |
|
||||
| Rechenzentrum | Hetzner Online GmbH, Rechenzentrum Falkenstein (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
|
||||
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 5), Archiv der Fassungen von Banner-Text und Zusatzseite (Nummer 8 Abs. 6), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
|
||||
@@ -0,0 +1,138 @@
|
||||
# Auftragsverarbeitungsvertrag Website-Analyse (Art. 28 DSGVO)
|
||||
|
||||
**Fassung 2026-10-02**
|
||||
|
||||
zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
|
||||
|
||||
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
|
||||
|
||||
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Website-Analyse“ einschaltet und dabei per Haken diesem Vertrag und der Leistungsbeschreibung „Website-Analyse“ in der angezeigten Fassung zustimmt. Das Portal speichert die Fassung, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
|
||||
|
||||
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält die E-Mail nach Absatz 1.
|
||||
|
||||
(3) Dieser Vertrag ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für die Website-Analyse geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung „Website-Analyse“ vor.
|
||||
|
||||
## 1. Gegenstand und Dauer
|
||||
|
||||
(1) WEBklar betreibt für die öffentliche Website des Kunden eine Reichweitenmessung mit Einwilligungsbanner („Website-Analyse“) und verarbeitet dabei personenbezogene Daten der Besucher der Website im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung „Website-Analyse“.
|
||||
|
||||
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Ausschalten, spätestens mit dem Ende des Wartungs-Abos; den Messdienst schaltet WEBklar dann spätestens am folgenden Tag ab. Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
|
||||
|
||||
## 2. Art und Zweck der Verarbeitung
|
||||
|
||||
(1) Art: Erheben über ein Skript, das WEBklar beim Veröffentlichen in die Website des Kunden einfügt, Übermitteln an den Messdienst von WEBklar, Speichern, Verdichten zu Tageswerten, Auswerten im Kundenportal (Kennzahlen, Heatmap) und Löschen.
|
||||
|
||||
(2) Zwecke: Der Kunde soll erkennen, wie seine Website genutzt wird, um sie zu verbessern. Außerdem schützt WEBklar den Messdienst für den Kunden vor Missbrauch und verfälschten Zahlen (Mengenbegrenzung, Erkennen von Bots).
|
||||
|
||||
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Besucher. Die Besucher-Kennung dient nur dazu, die Besucher eines Tages zu zählen.
|
||||
|
||||
## 3. Datenarten und Betroffene
|
||||
|
||||
(1) Solange der Besucher im Banner keine Wahl getroffen hat und sein Browser kein Signal nach Absatz 2 Satz 2 sendet: Pfad der aufgerufenen Seite (ohne Suchparameter und Anker), auch bei einem Seitenwechsel ohne Neuladen; Land (aus der IP-Adresse abgeleitet); Geräteklasse (Handy, Tablet oder Computer; aus dem Browser-Kennzeichen abgeleitet); Zeitpunkt (auf die volle Stunde gerundet); ein Zähler dafür, dass das Banner angezeigt wurde.
|
||||
|
||||
(2) Nach „Ablehnen“ sendet das Skript einmalig einen Zähler „abgelehnt“ und danach nichts mehr. Sendet der Browser die Signale „Global Privacy Control“ oder „Do Not Track“, zeigt das Skript kein Banner und sendet nichts.
|
||||
|
||||
(3) Nach „Annehmen“ zusätzlich: Domain der Website, von der der Besucher kam; Größenklasse des Browserfensters; eine täglich wechselnde pseudonyme Besucher-Kennung; Verweildauer (ab dem Laden der Seite) und Scrolltiefe je Seite; Klickpositionen mit einem Elementpfad aus Tag-Namen und Position (ohne Texte, Kennungen und Klassennamen); die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp oder Link auf eine andere Website, dann mit deren Domain); ein Zähler „angenommen“.
|
||||
|
||||
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Skripts und bei jeder Messanfrage, auch nach „Ablehnen“. Der Messdienst verarbeitet sie nur kurz im Arbeitsspeicher, um Land, Geräteklasse, Bots, die Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung zu bestimmen. Er speichert und protokolliert sie nicht. Nicht erhoben werden Cookies, Formularinhalte, Seitentexte, Mausbewegungen, Tastatureingaben und Sitzungsaufzeichnungen.
|
||||
|
||||
(5) Tageswerte sind Summen je Tag und Merkmal (etwa Aufrufe je Seite, je Land oder je Geräteklasse), ohne Besucher-Kennung. Enthalten die Pfade der Website personenbezogene Angaben, können auch Tageswerte personenbezogen sein (Nummer 11 Abs. 2).
|
||||
|
||||
(6) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht gezielt verarbeitet. Je nach Inhalt der Website können sich aus Pfaden oder Domains aber Rückschlüsse ergeben (Nummer 11 Abs. 3).
|
||||
|
||||
(7) Betroffene: Besucher der Website des Kunden.
|
||||
|
||||
## 4. Weisungen
|
||||
|
||||
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
|
||||
|
||||
(2) Mit dem Einschalten weist der Kunde WEBklar an, die Website-Analyse in der Standardkonfiguration nach der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Einschalten, Ausschalten, Akzentfarbe des Banners). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die von der Standardkonfiguration abweichen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde die Funktion ausschalten.
|
||||
|
||||
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
|
||||
|
||||
## 5. Vertraulichkeit
|
||||
|
||||
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
|
||||
|
||||
## 6. Technische und organisatorische Maßnahmen
|
||||
|
||||
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
|
||||
|
||||
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
|
||||
|
||||
## 7. Unterauftragsverarbeiter
|
||||
|
||||
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Messdienst läuft, und für dessen Sicherung. Ort der Verarbeitung ist das Rechenzentrum Falkenstein (Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
|
||||
|
||||
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde die Website-Analyse ausschalten und WEBklar sie für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Verlangen des Kunden sofort.
|
||||
|
||||
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
|
||||
|
||||
(4) Die Länderzuordnung nutzt eine Datenbank, die WEBklar auf dem eigenen Server speichert („IP to Country Lite“ von DB-IP.com, Lizenz CC BY 4.0). Daten der Besucher werden dabei nicht an DB-IP übermittelt. DB-IP ist kein Unterauftragsverarbeiter.
|
||||
|
||||
## 8. Unterstützung des Kunden, Betroffenenrechte
|
||||
|
||||
(1) WEBklar leitet Anfragen betroffener Personen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
|
||||
|
||||
(2) Die gespeicherten Messdaten enthalten keine IP-Adresse, keinen Namen und keine Kennung, die länger als einen Tag gilt; Zeitpunkte sind auf die volle Stunde gerundet. Einzelne Besucher kann WEBklar daher in der Regel nicht identifizieren (Art. 11 DSGVO). Ein Abgleich kann aber möglich sein, solange die Zugriffsprotokolle der Website (Anlage 1 und 2 der Abo-Bedingungen, 14 Tage) vorhanden sind. Nennt eine betroffene Person zusätzliche Angaben, etwa Datum, Uhrzeit, besuchte Seiten oder ihre IP-Adresse, prüft WEBklar auf Weisung des Kunden, auch anhand dieser Protokolle, ob eine Zuordnung möglich ist, und teilt ihm das Ergebnis mit.
|
||||
|
||||
(3) Der Messung können Besucher technisch widersprechen (Art. 21 DSGVO): Nach „Ablehnen“ und bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“ sendet das Skript nichts mehr.
|
||||
|
||||
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht, und eine kurze Interessenabwägung für die Zählung nach Art. 6 Abs. 1 lit. f DSGVO, die WEBklar mit jeder Fassung dieses Vertrags bereitstellt.
|
||||
|
||||
(5) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
|
||||
|
||||
(6) Zum Nachweis der Einwilligungen bewahrt WEBklar jede Fassung des Banner-Textes und der Zusatzseite mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende der Website-Analyse. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung. Ein Protokoll je Besucher führt WEBklar nicht.
|
||||
|
||||
(7) Die Besucher informiert der Kunde (Art. 13 DSGVO). WEBklar stellt ihm dafür die Zusatzseite „/datenschutz-analyse.html“ als Muster bereit. Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar dort einen Hinweis auf die Zusatzseite ein; sonst erhält der Kunde den Hinweis als Textbaustein (Leistungsbeschreibung Nr. 4 Abs. 4).
|
||||
|
||||
## 9. Löschung und Rückgabe
|
||||
|
||||
(1) Einzelne Ereignisse und Klickpositionen löscht WEBklar automatisch nach 30 Tagen. Der Löschlauf läuft täglich, die Löschung erfolgt also in der Regel spätestens am 31. Tag; fällt ein Lauf aus, holt der nächste ihn nach.
|
||||
|
||||
(2) Tageswerte löscht WEBklar 25 Monate nach dem jeweiligen Tag. Nach dem Ende der Website-Analyse (Ausschalten oder Ende des Wartungs-Abos) löscht WEBklar alle Daten der Website am 61. Tag. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse verlangen; WEBklar stellt sie innerhalb von zwei Wochen als CSV-Datei bereit. Auf Weisung des Kunden löscht WEBklar die Daten früher.
|
||||
|
||||
(3) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
|
||||
|
||||
(4) Gesetzliche Aufbewahrungspflichten bleiben unberührt.
|
||||
|
||||
## 10. Nachweise und Kontrollen
|
||||
|
||||
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
|
||||
|
||||
## 11. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist Verantwortlicher für die Messung. Er entfernt, verdeckt oder umgeht das Banner nicht. Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess- oder Tracking-Werkzeuge ein.
|
||||
|
||||
(2) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.
|
||||
|
||||
(3) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren oder lassen ihre Inhalte auf besondere Kategorien personenbezogener Daten schließen (etwa Gesundheit, Religion oder politische Meinung), gilt Nr. 3 Abs. 4 der Leistungsbeschreibung.
|
||||
|
||||
## 12. Haftung, Änderungen, Schlussbestimmungen
|
||||
|
||||
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
|
||||
|
||||
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
|
||||
|
||||
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar die Website-Analyse für seine Website nach Nr. 8 Abs. 2 der Leistungsbeschreibung beenden oder aussetzen. Das Wartungs-Abo bleibt davon unberührt.
|
||||
|
||||
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
|
||||
|
||||
## Anlage: Technische und organisatorische Maßnahmen der Website-Analyse (Art. 32 DSGVO)
|
||||
|
||||
| Bereich | Maßnahmen |
|
||||
|---|---|
|
||||
| Übertragung | Skript und Messdaten werden nur über TLS (HTTPS) übertragen. |
|
||||
| Speicher im Browser | Es werden keine Cookies gesetzt, und der Messdienst wertet keine Cookies aus. Messanfragen sendet das Skript ohne Cookies. Im lokalen Speicher des Browsers liegt nur die Banner-Wahl (Eintrag „wk-consent“ mit Wahl, Zeitpunkt und Fassung des Banner-Textes). Sie gilt 183 Tage; danach und bei einer neuen Fassung des Banner-Textes fragt das Banner erneut. |
|
||||
| Einwilligung | Solange keine Wahl getroffen ist und kein Browser-Signal vorliegt, sendet das Skript nur den Pfad der Seite, auch beim Seitenwechsel ohne Neuladen, und den Zähler „Banner angezeigt“. Nach „Ablehnen“ sendet es einmalig den Zähler „abgelehnt“ und danach nichts mehr, auch nicht aus anderen geöffneten Tabs. Bei „Global Privacy Control“ oder „Do Not Track“ zeigt es kein Banner und sendet nichts. Klick-, Scroll- und Verweildauer-Ereignisse registriert es erst nach „Annehmen“. Die Schaltflächen „Ablehnen“ und „Annehmen“ sind gleich gestaltet. |
|
||||
| Datenminimierung | IP-Adresse und Browser-Kennzeichen werden weder gespeichert noch protokolliert. Der Messdienst verwendet sie nur im Arbeitsspeicher für Land, Geräteklasse, Bot-Erkennung, Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung. Zeitpunkte werden auf die volle Stunde gerundet gespeichert. Pfade werden ohne Suchparameter und Anker gespeichert; Abschnitte, die wie Kennungen aussehen (lange Ziffernfolgen, UUIDs, E-Mail-Adressen), werden vorher ersetzt. Elementpfade bestehen nur aus Tag-Namen und Position. Formulareingaben und Seitentexte werden nicht erfasst. |
|
||||
| Pseudonymisierung | Die Besucher-Kennung (nur nach Einwilligung) ist ein auf 64 Bit gekürzter HMAC-SHA256 aus Website-Schlüssel, IP-Adresse und Browser-Kennzeichen mit einem Tagesschlüssel. Auch der Schlüssel der Mengenbegrenzung wird mit dem Tagesschlüssel gebildet. Der Tagesschlüssel liegt nur im Arbeitsspeicher; die Auslagerung auf die Festplatte ist für den Messdienst gesperrt. Er wird beim ersten Ereignis eines neuen Kalendertags (Europe/Berlin) und bei jedem Neustart neu erzeugt und nie gesichert. Ältere Kennungen lassen sich danach nicht mehr zuordnen. |
|
||||
| Zugriffskontrolle | Die Verwaltungsschnittstelle des Messdienstes (Auswertung, Website-Verwaltung) läuft auf einem internen Port, der nicht öffentlich erreichbar ist, und ist durch ein Zugriffstoken geschützt. Zugriff haben nur das Kundenportal und die Verwaltung von WEBklar. Das Portal zeigt jedem Kunden nur die Daten seiner eigenen Websites. |
|
||||
| Heatmap | Die Heatmap öffnet ein signierter Link, den nur der angemeldete Kunde im Kundenportal erzeugen kann. Der Link gilt 30 Minuten und liefert nur Klickpunkte der gewählten Seite und Ansicht, ohne Zeitpunkte und Kennungen. |
|
||||
| Eingangsprüfung | Messdaten werden nur angenommen, wenn die Website registriert und eingeschaltet ist und Origin bzw. Referer zu ihren Domains passen. Bots werden verworfen. Größe und Menge der Anfragen sind je Website und Absender begrenzt (Zähler nur im Arbeitsspeicher). Für ausgeschaltete Websites liefert der Messdienst ein leeres Skript aus. |
|
||||
| Löschung | Täglicher Löschlauf um 02:30 Uhr (Europe/Berlin): Ereignisse und Klicks nach 30 Tagen, Tageswerte nach 25 Monaten, alle Daten einer Website 60 Tage nach dem Ausschalten. Gelöschte Datensätze werden in der Datenbankdatei überschrieben und aus dem Schreibprotokoll (WAL) entfernt. |
|
||||
| Trennung | Der Messdienst läuft in einem eigenen Container mit eigenem Datenverzeichnis und hat keinen Zugriff auf andere Kundendaten. Alle Datensätze tragen den Website-Schlüssel; Abfragen erfolgen nur je Website. |
|
||||
| Protokolle | Der Reverse-Proxy führt kein Zugriffsprotokoll, und der Messdienst protokolliert keine Anfragen. Wird ein Zugriffsprotokoll eingeschaltet, dann nicht für die Adresse des Messdienstes. Meldungen der Container werden nach 14 Tagen gelöscht. |
|
||||
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). Einen gesonderten Datenbank-Export des Messdienstes gibt es nicht. |
|
||||
| Rechenzentrum | Hetzner Online GmbH, Rechenzentrum Falkenstein (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
|
||||
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 5), Archiv der Fassungen von Banner-Text und Zusatzseite (Nummer 8 Abs. 6), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
|
||||
@@ -0,0 +1,9 @@
|
||||
# Prüfpunkte zur Vorlage datenschutz-analyse.html (intern, nicht veröffentlichen)
|
||||
|
||||
> Entwurf – vor Verwendung anwaltlich prüfen lassen.
|
||||
|
||||
- **P1** Zählung ohne Einwilligung (Seitenaufruf, Referrer-Domain, Land, Gerätegröße): kein Zugriff auf das Endgerät nach § 25 TDDDG? Das Skript liest Referrer und Fensterbreite aus; Einordnung nach strenger DSK-Lesart offen.
|
||||
- **P2** Tages-Kennung nach Einwilligung: pseudonym oder faktisch anonym?
|
||||
- **P3** Rechtsgrundlage des anonymen Banner-Zählers (angezeigt, angenommen, abgelehnt).
|
||||
- **P5** Speicherung der Banner-Wahl (auch der Ablehnung) im lokalen Speicher nach § 25 Abs. 2 Nr. 2 TDDDG; GPC/DNT wie Ablehnung.
|
||||
- **P18** Betroffenenrechte bei nicht identifizierbaren Daten (Art. 11, Art. 12 Abs. 2 DSGVO), Formulierung unter "Deine Rechte".
|
||||
@@ -0,0 +1,23 @@
|
||||
# Datenschutzerklärung: Prüfpunkte für die Kundenansicht neu
|
||||
|
||||
Stand 16.10.2026 · Entwurf – Prüfung durch Kanzlei offen · Entwurf zur Freigabe, keine Rechtsberatung
|
||||
|
||||
Diese Liste ändert die Datenschutzerklärung nicht. `public/datenschutz.html` bleibt unverändert (eingefroren); die Stellen unten sind die, die mit der neuen Kundenansicht und den Texten der Fassung 6 geprüft werden müssen. Die Änderung der Datenschutzerklärung erfolgt erst mit dem Umschalten (Entscheidung R14 in `docs/rechtliches/kundenansicht-entscheidungen.md`), als eigene Fassung, sonst würde sie sofort öffentlich. Zeilenangaben beziehen sich auf den Stand von `public/datenschutz.html` am 16.10.2026.
|
||||
|
||||
Je Prüfpunkt: **Fundstelle**, was dort steht, was sich ändert, und die Frage an den Inhaber oder die Kanzlei.
|
||||
|
||||
**D1** · Fundstelle `public/datenschutz.html:127`. Nachweis bei der Bestellung einer Funktion. Dort steht der „Klick auf ‚Zahlungspflichtig bestellen‘“, die Stripe-Sitzung und eine Funktionsliste nur mit News-Pinnwand und Terminplaner. Künftig löst das Einschalten einer Funktion im Abo keine Zahlung aus und hat keine Stripe-Sitzung; gespeichert werden der Wortlaut der angezeigten Zeilen, die Fassungen, Zeitpunkt des Klicks auf „Einschalten“, IP-Adresse, Browserkennung, Konto, angemeldete Person und die Kundenart (Fassungen: Abo-Bedingungen 2026-10-16-v6, Funktionsunterlagen 2026-10-16). Die Funktionsliste muss alle Funktionen nennen (News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live, Website-Analyse). Frage: Neuer Absatz „Funktion einschalten“ neben dem Absatz für Bestellungen, damit der Bestand (Funktionen, die einzeln gekauft wurden) unverändert beschrieben bleibt?
|
||||
|
||||
**D2** · Fundstelle `public/datenschutz.html:126`. Nachweis bei Abo, Wechsel und Zusatzspeicher. Dort stehen „gegebenenfalls Ihre Angaben zum Leistungsbeginn“ sowie Zeitpunkt, Version, IP-Adresse und Browserkennung. Künftig kommen der Wortlaut der angezeigten Zeilen (Linkzeile, Häkchen für Privatkunden) und die Kundenart zum Klickzeitpunkt hinzu (§ 2 Abs. 6 der Abo-Bedingungen Fassung 6). Frage: Genügt es, die Kundenart in den Aufzählungssatz aufzunehmen, oder braucht sie wegen D3 einen eigenen Hinweis?
|
||||
|
||||
**D3** · Fundstelle `public/datenschutz.html:88`. Abschnitt 6 „Kundenportal“, Kundenkonto-Absatz in der Zeile darunter. Die Kundenart („Betrieb“ oder „privat“) ist ein neues Datum im Kundenkonto. Das Team legt sie nach den Angaben des Kunden fest (Entscheidung E3); ist sie leer, gilt der Kunde als privat (E4). Zu ergänzen sind Zweck (Bestellseiten richten sich danach, Nachweis), Rechtsgrundlage (Art. 6 Abs. 1 lit. b und f DSGVO), Quelle (Angaben des Kunden an das Team) und Dauer (wie das Kundenkonto; im Nachweis wie in D1). Frage: Eigener Satz im Kundenkonto-Absatz oder in Abschnitt 7?
|
||||
|
||||
**D4** · Fundstelle `public/datenschutz.html:128`, ergänzend `public/datenschutz.html:115` und `public/datenschutz.html:121`. Der Stripe-Absatz beschreibt Zahlungen für Abos und Zusatzspeicher; das Einschalten einer Funktion im Abo löst keine Zahlung und keine Übermittlung an Stripe aus. Die Abschnitte zu Stripe Connect beim Webshop und beim Buchungskalender (Zahlungen der Käufer und Gäste auf das eigene Stripe-Konto des Kunden) bleiben inhaltlich wie sie sind. Frage: Genügt ein Satz, dass Funktionen im Abo ohne Zahlung eingeschaltet werden, oder sollen die Abschnitte nur an der Einleitung („bestellen“ statt „einschalten“) angepasst werden?
|
||||
|
||||
**D5** · Fundstelle `public/datenschutz.html:102`. Abschnitt zur Website-Analyse: „Kunden mit Wartungs-Abo“ können die Reichweitenmessung einschalten. Künftig steckt die Website-Analyse im Hosting-Abo und im Wartungs-Abo (Leistungsbeschreibung Website-Analyse Nr. 1 Abs. 1). Der Absatz muss beide Abos nennen und beschreiben, dass die Funktion mit dem Abo endet (Löschfrist 60 Tage, AVV Website-Analyse Nr. 9). Frage: Reicht eine Anpassung der Einleitung?
|
||||
|
||||
**D6** · Fundstellen `public/datenschutz.html:105` (Terminplaner), `public/datenschutz.html:113` (Webshop), `public/datenschutz.html:119` (Buchungskalender), außerdem der Abschnitt zur News-Pinnwand und zu Öffnungszeiten live. Die Funktionsabschnitte sprechen von „Kunden mit Wartungs-Abo“, vom „Freischalten“ und vom Ende mit dem Vertrag. Künftig gilt: Funktionen sind Teil des Wartungs-Abos, enden mit dem Abo (Terminplaner, News-Pinnwand, Webshop und Buchungskalender spätestens am Folgetag, Öffnungszeiten live nach 30 Tagen Weiterlauf) und werden nach den Fristen der Leistungsbeschreibungen gelöscht (30 Tage Terminplaner, News-Pinnwand und Öffnungszeiten live; 90 Tage Webshop und Buchungskalender). Frage: Die Fristen in den Abschnitten und in Abschnitt 7 (Speicherdauer) müssen übereinstimmen; soll Abschnitt 7 die Fristen künftig als Tabelle führen?
|
||||
|
||||
**D7** · Fundstelle `public/datenschutz.html:138`, ergänzend die Beschreibung des Stundenkontingents im Kundenkonto-Absatz. Das Stundenkonto und der Vorschuss für die Einrichtung (§ 4 Abs. 8 der Abo-Bedingungen Fassung 6) sind Abrechnungsdaten: Arbeitszeit, Verbrauch, Vorschuss, Ablaufdaten. Sie stehen heute unter „gegebenenfalls Ihr Stundenkontingent“. Frage: Ist der Vorschuss als Abrechnungsdatum mit demselben Zweck, derselben Rechtsgrundlage (Art. 6 Abs. 1 lit. b DSGVO) und derselben Dauer ausreichend beschrieben, oder braucht er einen eigenen Satz?
|
||||
|
||||
**D8** · Fundstelle `public/datenschutz.html:130`. Abschnitt zu den Seiten Kündigen und Widerrufen. Die Seiten erscheinen in der neuen Kundenansicht in der du-Form (Entscheidung R16) und zeigen angemeldeten Kunden die laufenden Widerrufsfristen aus bereits vorhandenen Daten (Sammelabfrage der Fristen im Kundenportal, `GET /api/fristen`). Das ist ein Lesen vorhandener Daten ohne neues Datum und ohne neuen Zweck; der Absatz bleibt inhaltlich unverändert. Frage: Genügt die Feststellung, dass kein neuer Absatz nötig ist, oder soll die Anzeige der Fristen im Konto erwähnt werden?
|
||||
@@ -0,0 +1,144 @@
|
||||
# Rechtsrunde Fassung 6: Entscheidungen, Kanzlei-Fragen und Ablauf
|
||||
|
||||
Stand 16.10.2026 · Entwurf – Prüfung durch Kanzlei offen · Entwurf zur Freigabe, keine Rechtsberatung
|
||||
|
||||
Dieses Blatt ist die Grundlage für die Freigabe der Rechtstexte der Kundenansicht neu durch den Inhaber (Entscheidung E20). Die Kanzlei-Fragen halten den Livegang nicht auf; die Fragen aus den früheren Rechtsrunden stehen in `/root/reports/kundenansicht-neu-2026-10-15/bestand/recht.md` § 4. Die Entwürfe liegen unter `docs/rechtliches/*-v6.md`, die Seiten unter `public/neu/rechtliches/` (Abo-Bedingungen: `abo-bedingungen.html`, Funktionsunterlagen: `<slug>-2026-10-16.html`). Datenschutz-Prüfpunkte D1 bis D8 stehen in `docs/rechtliches/datenschutz-kundenansicht-pruefpunkte.md`.
|
||||
|
||||
## Zu entscheiden
|
||||
|
||||
| Nr. | Punkt | Vorschlag | Alternative | Kanzlei-Frage |
|
||||
|---|---|---|---|---|
|
||||
| R1 | Fassungsnummer und Datum | Abo `2026-10-16-v6`, Funktionen `2026-10-16`; bei Freigabe auf den Freigabetag setzen | Entwurfstag bleibt (keine Umbenennung der Dateien und Fassungen) | – |
|
||||
| R2 | Geltung für laufende Abos v3 bis v5 (C11) | nur neue Abschlüsse (§ 15 Abs. 2); Funktionen gelten über Linkzeile und Klick, auch mit der Stundenkonto- und Vorschuss-Regel; keine Pflicht-Info-Mail, eine freiwillige Mail später | Info-Mail an laufende Abos mit Bitte um Zustimmung nach § 15 | KQ1 |
|
||||
| R3 | Vorschuss-Regel (E17) | § 4 Abs. 8 und jede Leistungsbeschreibung außer der Analyse: echte Arbeitszeit, nie Mehraufwand, ein Rest bei Vertragsende verfällt zugunsten des Kunden | Obergrenze (zum Beispiel 3 Stunden) mit Rückfrage darüber | KQ2 |
|
||||
| R4 | Einrichtung beim Projektstart (E9, E21) | § 4 Abs. 9 und Baustein „projekt“ der Leistungsbeschreibungen: Abrechnung über die Projektkosten, die Funktion läuft nur mit dem passenden Abo | nur in den Leistungsbeschreibungen, nicht in den Abo-Bedingungen | KQ8 |
|
||||
| R5 | Kundenart als Grundlage (E3, E4) | § 1 Abs. 2: hinterlegt nach Angabe des Kunden, leer gilt als privat, maßgeblich ist der Zweck des Vertrags | Selbstauskunft des Kunden in den Bedingungen | KQ4 |
|
||||
| R6 | Häkchen-Wortlaut für Abo, Wechsel und Einrichtung | Texte aus `WORTLAUT_V6.beginn` ohne Paragrafenzitate; Betriebe setzen nichts | Zitate §§ 356 Abs. 5 Nr. 2, 357a Abs. 2 BGB im Häkchen | KQ5 |
|
||||
| R7 | Einrichtung in der Widerrufsfrist | Teil des Abos, kein eigener Vertrag; Widerruf beendet die Funktion; Wertersatz nach aufgewendeter Einrichtungszeit zum Stundensatz des Abos | WEBklar verzichtet auf Wertersatz für die Einrichtung (wie beim Zusatzspeicher), das Häkchen für die Einrichtung entfällt | KQ3 |
|
||||
| R8 | Ende mit dem Abo, Fristen, kein „Behalten“ (C12); mit P4/P6 abgestimmt | Terminplaner, News, Webshop und Buchungskalender schalten spätestens am Folgetag ab (Tagesjob), die Analyse wie bisher; Öffnungszeiten live läuft nach dem Ende noch 30 Tage unverändert weiter und wird dann abgeschaltet (kein Behalten gegen 30 €); Eingaben danach: Terminplaner, News und Öffnungszeiten 30 Tage, Webshop und Buchungskalender 90, Analyse 60; ein neues Abo in der Frist holt die Funktion zurück | Öffnungszeiten live schalten wie die übrigen spätestens am Folgetag ab (P4 Text Z2, P6 Mail K40 und der Zeiten-Abgleich müssten dafür geändert werden) | KQ8 |
|
||||
| R9 | Analyse im Hosting-Abo (C10) | in beiden Abos, Einrichtung kostenlos (C6): § 3 Abs. 8, § 6 Abs. 6, Leistungsbeschreibung A | nur im Wartungs-Abo, wie heute | KQ8 |
|
||||
| R10 | AVV-Struktur | Option A: die AVV je Funktion bleiben eigene Verträge mit neuer Vorbemerkung, Anlage 1 bleibt der Rahmen (§ 14 Abs. 2) | B: Anlagen 4 bis 7 zu den Abo-Bedingungen; C: Rahmen-AVV mit einer Anlage je Funktion | KQ7 |
|
||||
| R11 | Widerrufsbelehrung der Funktionen | bleibt als Datei für den Bestand, die neuen Texte verlinken sie nicht | bleibt Anlage der Leistungsbeschreibungen | KQ6 |
|
||||
| R12 | Anrede der Rechtsdokumente (E15, Spec 5.7) | bleiben im bisherigen Stil („der Kunde“, in der Übersichtsbox „Ihr“); nur Oberflächentexte stehen in „du“ | Übersichtsbox und Leistungsbeschreibungen auf „du“ | – |
|
||||
| R13 | Webshop nur für Betriebe, Terminplaner nicht im Gesundheitsbereich (C15) | steht in den Leistungsbeschreibungen, das Team prüft bei der Freigabe | Webshop auch für Privatkunden mit Hinweis | KQ4, KQ9 |
|
||||
| R14 | Datenschutzerklärung | Prüfpunkte D1 bis D8; `public/datenschutz.html` wird erst mit dem Umschalten als eigene Fassung geändert (sonst sofort öffentlich) | sofort ändern | – |
|
||||
| R15 | Zusatzspeicher und Übernahmegutschrift | Zusatzspeicher: Linkzeile ohne Häkchen (§ 10 Abs. 3 und Hinweise zum Widerruf Nr. 4: kein Wertersatz); Gutschrift: Satz über dem Knopf (P7) | das Häkchen bleibt | KQ5 |
|
||||
| R16 | Du-Anrede der Seiten Kündigen und Widerrufen (N38) | beide Seiten sprechen ab Fassung 6 „du“; sie speichern keinen Wortlaut und haben keine eigene Version (C14); Belehrung und Muster-Formular folgen R12 | Sie-Form bleibt bis zur Freigabe von Belehrung und Formular | KQ6 |
|
||||
| R17 | Zeilen „Anbieter“ und „Abrechnung“ im Block der Bestellseiten (N37) | beide Zeilen entfallen im Block; der Anbieter steht im Impressum im Konto und in § 1 der Bedingungen, die Abrechnung (monatlich im Voraus per Stripe) in den Zeilen zu Preis und Fälligkeit und in § 7 | beide Zeilen bleiben im Block über dem Knopf | KQ6 |
|
||||
| R18 | Länge des Blocks „Was du bestellst“ am Handy (Abschlussprüfung F1) | Der Block über dem Knopf ist knapp: Leistung als eine Zeile mit Link „Leistungen ansehen“ auf die Leistungsbeschreibung, Preis, Fällig, Laufzeit, Beginn und Zahlung in je ein bis drei Zeilen; die ausführlichen Sätze (Leistung, Laufzeit mit Kündigungsfrist, Zahlungsweg, Gutschrift und Abbuchungstag beim Wechsel) stehen unter dem Knopf („Im Einzelnen“), ohne Klappe. So stehen Kerninfo und Knopf auch auf 375 × 667 zusammen im nutzbaren Bereich (zwischen Kopf und Leiste) | voller Wortlaut im Block über dem Knopf: am Handy dann nicht mehr auf einem Bildschirm mit dem Knopf | KQ6 |
|
||||
|
||||
## Fragen an die Kanzlei
|
||||
|
||||
Jede Frage hat vier Teile wie in `docs/rechtliches/zeiten-kanzlei-fragen.md`: **Frage** (was wir wissen möchten), **Fundstelle im Text** (wo der Wortlaut steht), **Unsere Annahme** (der Stand, mit dem wir arbeiten; bitte bestätigen oder korrigieren), **Alternative** (was wir stattdessen täten). Annahmen sind keine Rechtsurteile.
|
||||
|
||||
### KQ1: Geltung der Fassung 6 und Information an laufende Abos
|
||||
|
||||
**Frage.** Fassung 6 gilt nur für neue Abschlüsse. Wer ein laufendes Abo nach Fassung 3 bis 5 hat und eine Funktion einschaltet, stimmt Leistungsbeschreibung und Auftragsverarbeitung der Funktion in der angezeigten Fassung durch den Klick auf „Einschalten“ zu; dazu gehören die Regeln zur Einrichtung (Stundenkontingent) und zum Vorschuss. Reicht der Klick, um diese Regeln in das laufende Abo einzubeziehen, oder braucht es eine Zustimmung nach dem bisherigen § 15 (Mitteilung mit sechs Wochen Frist, Zustimmung)? Muss WEBklar laufende Kunden über Fassung 6 informieren, obwohl sie für sie nur vorteilhaft ist?
|
||||
|
||||
**Fundstelle im Text.** § 15 Abs. 2 der Abo-Bedingungen Fassung 6; § 3 Abs. 8; § 4 Abs. 8; Nr. 1 Abs. 1 der Leistungsbeschreibungen (Baustein „geltung“).
|
||||
|
||||
**Unsere Annahme.** Der Klick genügt, weil jede Funktion ein zusätzliches Angebot ohne Gebühr ist und die Änderung des Abos nur für neue Abschlüsse gilt. Eine Info-Mail ist keine Pflicht; wir würden später freiwillig informieren.
|
||||
|
||||
**Alternative.** Info-Mail an alle laufenden Abos mit Bitte um Zustimmung nach § 15; Funktionen für Altkunden erst nach dieser Zustimmung.
|
||||
|
||||
### KQ2: Vorschuss-Regel für die Einrichtung
|
||||
|
||||
**Frage.** Die Einrichtung einer Funktion geht mit der tatsächlich aufgewendeten Zeit vom Stundenkontingent ab. Reicht es nicht, entsteht ein Vorschuss, den die folgenden Monatskontingente ausgleichen; ein Rest bei Vertragsende verfällt zugunsten des Kunden, und es wird nie Mehraufwand berechnet. Ist das transparent und wirksam, auch gegenüber Verbrauchern (Überraschungsklausel, Kontingent als Vorleistung)? Braucht es eine Obergrenze oder eine Rückfrage?
|
||||
|
||||
**Fundstelle im Text.** § 4 Abs. 2 und 8 der Abo-Bedingungen Fassung 6; Nr. 5 Abs. 2 (Terminplaner), Nr. 3 Abs. 1 (News-Pinnwand), Nr. 4 Abs. 1 (Öffnungszeiten live), Nr. 9 Abs. 1 (Webshop, Buchungskalender).
|
||||
|
||||
**Unsere Annahme.** Die Regel ist für den Kunden günstig (keine zusätzliche Rechnung, Verfall zu seinen Gunsten) und wird im Kundenportal mit „Vorschuss für Einrichtung“ angezeigt.
|
||||
|
||||
**Alternative.** Obergrenze (zum Beispiel 3 Stunden) mit Rückfrage darüber; oder Einrichtung nur, soweit das Kontingent reicht.
|
||||
|
||||
### KQ3: Einrichtung in der Widerrufsfrist
|
||||
|
||||
**Frage.** Das Einschalten einer Funktion ist nach unserem Text Teil des Abos und kein eigener Vertrag; es verlängert die Widerrufsfrist nicht. Ist das richtig, oder ist die Einrichtung eine eigene Leistung mit eigenem Widerrufsrecht? Erlischt das Widerrufsrecht nach § 356 Abs. 5 Nr. 2 BGB bei einem Dauerschuldverhältnis überhaupt durch „vollständige Erbringung“, und wie bemisst sich der Wertersatz nach § 357a Abs. 2 BGB für Einrichtungszeit innerhalb der Frist?
|
||||
|
||||
**Fundstelle im Text.** Hinweise zum Widerruf Nr. 5 und § 2 Abs. 2 c der Abo-Bedingungen Fassung 6; Baustein „widerruf“ der Leistungsbeschreibungen; `WORTLAUT_V6.beginn.einrichtung` in `server/services/products.js`.
|
||||
|
||||
**Unsere Annahme.** Teil des Abos; Wertersatz nach aufgewendeter Einrichtungszeit zum Stundensatz des Abos; das Häkchen gibt es nur für Privatkunden in der Frist.
|
||||
|
||||
**Alternative.** WEBklar verzichtet auf Wertersatz für die Einrichtung (wie beim Zusatzspeicher nach § 10 Abs. 3) und das Häkchen für die Einrichtung entfällt.
|
||||
|
||||
### KQ4: Belastbarkeit der Kundenart
|
||||
|
||||
**Frage.** Das Team hinterlegt die Kundenart („Betrieb“ oder „privat“) nach den Angaben des Kunden; ist keine hinterlegt, gilt der Kunde als privat; maßgeblich bleibt der Zweck des Vertrags (§ 1 Abs. 2). Wie belastbar ist das, wenn sich später ein Verbraucher als Betrieb geführt herausstellt (Existenzgründer, Vereine, Freiberufler)? Wer trägt das Risiko einer fehlenden Belehrung? Das ist die Frage 5 aus `recht.md` § 4.
|
||||
|
||||
**Fundstelle im Text.** § 1 Abs. 2 und § 2 Abs. 2 c der Abo-Bedingungen Fassung 6; `WORTLAUT_V6.beginn`; Nr. 1 Abs. 1 und Nr. 5 Abs. 1 der Leistungsbeschreibung Webshop.
|
||||
|
||||
**Unsere Annahme.** Unsere Kunden sind fast nur Betriebe (E2). Widerrufsbelehrung, Muster-Formular und die Funktion „Vertrag widerrufen“ bleiben für alle erreichbar; nur das Häkchen und der Beginn nach der Frist hängen an der Kundenart.
|
||||
|
||||
**Alternative.** Selbstauskunft des Kunden bei der Bestellung („Ich bestelle als Unternehmer“) oder das Häkchen für alle Kunden.
|
||||
|
||||
### KQ5: Häkchentexte ohne Normzitat, Wechsel nur mit Häkchen
|
||||
|
||||
**Frage.** Dürfen die Häkchen für den Sofortbeginn auf zwei bis drei Sätze ohne Paragrafenzitate gekürzt werden, ohne die Information nach Art. 246a § 1 Abs. 3 EGBGB über das Erlöschen und den Wertersatz zu verlieren (Frage 2 aus `recht.md` § 4)? Ist es zulässig, dass der Wechsel ins Wartungs-Abo für Privatkunden nur mit Häkchen möglich ist, weil er sofort wirkt? Genügt beim Zusatzspeicher und bei der Übernahmegutschrift eine Linkzeile ohne Häkchen, weil WEBklar auf Wertersatz verzichtet?
|
||||
|
||||
**Fundstelle im Text.** `WORTLAUT_V6.beginn` (Abo, Wechsel, Einrichtung); § 2 Abs. 2 c und § 9 Abs. 6 der Abo-Bedingungen Fassung 6; § 10 Abs. 3 und Hinweise zum Widerruf Nr. 4.
|
||||
|
||||
**Unsere Annahme.** Die Wortlaute der Konstante genügen; für Privatkunden ist der Wechsel ohne Häkchen gesperrt; Zusatzspeicher und Gutschrift brauchen kein Häkchen.
|
||||
|
||||
**Alternative.** Zitate §§ 356 Abs. 5 Nr. 2, 357a Abs. 2 BGB im Häkchen; Wechsel für Privatkunden mit Beginn nach Ablauf der Frist.
|
||||
|
||||
### KQ6: Belehrung und Pflichtinformationen per Link und PDF, Anrede der Seiten Kündigen und Widerrufen
|
||||
|
||||
**Frage.** Genügt der Link auf Widerrufsbelehrung und Muster-Formular vor der Bestellung, wenn der Volltext erst im Bestätigungs-PDF steht (Frage 4 aus `recht.md` § 4)? Passen Belehrung und Formular zum Gestaltungshinweis in Anlage 1 EGBGB zu § 356a? Dürfen die Zeilen „Anbieter“ und „Abrechnung“ im Block über dem Knopf entfallen, solange der Anbieter im Impressum und in § 1 steht und die Abrechnung in den Zeilen zu Preis und Fälligkeit? Ändert die Du-Anrede der Seiten Kündigen und Widerrufen etwas an der Wirksamkeit von Kündigung (§ 312k BGB) und Widerruf (§ 356a BGB)?
|
||||
|
||||
**Fundstelle im Text.** § 2 Abs. 2 c, Widerrufsbelehrung, Muster-Widerrufsformular und Hinweise zum Widerruf der Abo-Bedingungen Fassung 6; Seiten `kuendigen.html` und `widerrufen.html` in der neuen Ansicht; Block „Was du bestellst“ der Bestellseiten.
|
||||
|
||||
**Unsere Annahme.** Der Link genügt, das PDF kommt vor Leistungsbeginn per E-Mail (§ 312f BGB); Anbieter und Abrechnung sind an anderer Stelle erreichbar; die Du-Anrede ändert die Wirksamkeit nicht.
|
||||
|
||||
**Alternative.** Volltext der Belehrung auf der Bestellseite; Zeilen „Anbieter“ und „Abrechnung“ bleiben im Block; Sie-Form der beiden Seiten.
|
||||
|
||||
### KQ7: AVV per Klick auf die Linkzeile, Struktur A, B oder C
|
||||
|
||||
**Frage.** Genügt der Klick auf „Einschalten“ unter der Linkzeile als Abschluss des Auftragsverarbeitungsvertrags nach Art. 28 Abs. 3 und 9 DSGVO („schriftlich, auch elektronisch“), ohne eigenes Häkchen (Frage 3 aus `recht.md` § 4)? Welche Struktur ist am sichersten: A (eigene Verträge je Funktion), B (Anlagen zu den Abo-Bedingungen) oder C (Rahmen-AVV mit Anlage je Funktion)?
|
||||
|
||||
**Fundstelle im Text.** § 14 Abs. 2 der Abo-Bedingungen Fassung 6; Vorbemerkung Abs. 1 jedes AVV in `docs/rechtliches/avv-*-v6.md`; Abschnitt „AVV-Struktur“ unten.
|
||||
|
||||
**Unsere Annahme.** Der Klick genügt, weil Wortlaut, Fassung, Zeit, Konto und angemeldete Person gespeichert werden; wir bleiben bei Option A.
|
||||
|
||||
**Alternative.** Ein eigenes Häkchen für den AVV; Option B oder C.
|
||||
|
||||
### KQ8: Funktionen als Teil des Abos, Kopplung an das Abo-Ende, Rückkehr in der Frist
|
||||
|
||||
**Frage.** Sind die Funktionen als Teil der Abo-Leistung wirksam geregelt (§ 3 Abs. 8), einschließlich der Kopplung an das Abo (§ 11 Abs. 4: Ende mit dem Abo, Weiterlauf von Öffnungszeiten live für 30 Tage, Löschfristen von 30, 60 und 90 Tagen)? Ist es zulässig, dass ein neues Abo in der Frist die Funktion mit den vorhandenen Daten ohne neue Zustimmung zurückholt? Tragen die Analyse im Hosting-Abo (C10) und die Abrechnung der Einrichtung beim Projektstart über die Projektkosten (E9, E21) ohne weitere Hinweise?
|
||||
|
||||
**Fundstelle im Text.** § 3 Abs. 8, § 4 Abs. 9 und § 11 Abs. 4 der Abo-Bedingungen Fassung 6; Abschnitt „Ende der Leistung“ jeder Leistungsbeschreibung; Nr. 10 Abs. 5 der Leistungsbeschreibungen Webshop und Buchungskalender („auf Wunsch in Textform“).
|
||||
|
||||
**Unsere Annahme.** Die Regeln sind als Leistungsbeschreibung wirksam; die Rückkehr in der Frist holt die Funktion zurück. Offen ist für uns, ob die Rückkehr bei Webshop und Buchungskalender wie heute „auf Wunsch in Textform“ oder automatisch geschehen soll.
|
||||
|
||||
**Alternative.** Öffnungszeiten live wie die übrigen Funktionen am Folgetag abschalten (R8); Rückkehr nur auf Wunsch in Textform; einheitliche Löschfrist von 30 Tagen.
|
||||
|
||||
### KQ9: Ausschlüsse als Vertragsklausel
|
||||
|
||||
**Frage.** Sind die Ausschlüsse in den Leistungsbeschreibungen als Leistungsgrenzen wirksam: Terminplaner nicht im Gesundheitsbereich, Webshop nur für Betriebe und ohne die ausgeschlossenen Waren, Buchungskalender nur für Unternehmer? Gilt das auch, wenn die Prüfung beim Einschalten nur durch das Team oder durch eine Selbstangabe im Fragebogen erfolgt? Darf WEBklar bei Verstößen abschalten?
|
||||
|
||||
**Fundstelle im Text.** Nr. 4 Abs. 3 der Leistungsbeschreibung Terminplaner; Nr. 1 Abs. 1, Nr. 3 und Nr. 5 Abs. 1 der Leistungsbeschreibung Webshop; Nr. 1 Abs. 1 der Leistungsbeschreibung Buchungskalender.
|
||||
|
||||
**Unsere Annahme.** Die Ausschlüsse sind als Leistungsbeschreibung wirksam; das Team prüft bei der Freigabe (R13).
|
||||
|
||||
**Alternative.** Webshop auch für Privatkunden mit Hinweis (C15 verschoben); keine automatische Abschaltung, nur ein Hinweis in Textform.
|
||||
|
||||
## AVV-Struktur
|
||||
|
||||
Die Auftragsverarbeitung der Funktionen ist in Fassung 6 so geregelt, dass der Kunde sie mit dem Klick auf „Einschalten“ unter der Linkzeile abschließt. Drei Aufbauten sind möglich; empfohlen wird Option A.
|
||||
|
||||
**Option A, empfohlen: eigene Verträge je Funktion, Anlage 1 bleibt der Rahmen.** Die AVV Terminplaner, Webshop, Buchungskalender und Website-Analyse bleiben eigene Dokumente mit einer neuen Vorbemerkung (Abschluss per Klick) und gelten neben Anlage 1 der Abo-Bedingungen (§ 14 Abs. 2). Vorteil: jede Funktion hat ihre eigenen Datenarten, Fristen und Unterauftragsverarbeiter, und die bestehenden Verträge ändern sich kaum. Nachteil: vier Einzelverträge, deren Gleichlauf mit Anlage 1 bei jeder Änderung geprüft werden muss.
|
||||
|
||||
**Option B: Anlagen 4 bis 7 zu den Abo-Bedingungen.** Die AVV der Funktionen werden Anlagen der Abo-Bedingungen. Vorteil: ein Dokument mit einer Fassung, der Kunde stimmt den Bedingungen einmal zu. Nachteil: die Abo-Bedingungen werden lang und müssen bei jeder Änderung einer Funktion neu versioniert werden; Funktionen, die ein Kunde nie nutzt, stehen trotzdem in seinem Vertrag.
|
||||
|
||||
**Option C: Rahmen-AVV mit einer Anlage je Funktion.** Ein neuer Rahmenvertrag regelt das Gemeinsame (Weisungen, Sicherheit, Löschung), die Anlagen je Funktion nennen nur Daten und Unterauftragsverarbeiter. Vorteil: einheitliche Regeln, bei Änderungen ändert sich nur die Anlage. Nachteil: ein neuer Vertrag neben Anlage 1, abzustimmen mit der Kanzlei, und mehr Arbeit als jetzt nötig.
|
||||
|
||||
## Ablauf nach der Freigabe
|
||||
|
||||
Die Reihenfolge ist fest; jeder Schritt erst, wenn der vorige grün ist. Die Skripte laufen nur auf ausdrückliche Anweisung des Controllers nach der Freigabe.
|
||||
|
||||
1. Freigabe des Inhabers je Punkt (R1 bis R17). Die Antworten auf die Kanzlei-Fragen KQ1 bis KQ9 halten den Livegang nicht auf (E20). Entschiedene Änderungen werden zuerst in den Texten und Tests nachgezogen.
|
||||
2. Datum der Fassungen auf den Freigabetag setzen: `config.abo.termsVersionNeu` (Abo), `FUNKTIONEN_FASSUNG_V6` in `server/services/products.js`, die Datei- und Testnamen mit dem Datum (`<slug>-2026-10-16`), die Fassungszeilen der Markdown-Dateien, `Version 6 · Stand …` und den Druckkopf in der Abo-Seite.
|
||||
3. Entwurfsmarke aus allen Dateien entfernen (Fassungszeilen der Markdown-Dateien, der Absatz unter der Kopfzeile und das meta robots in der Abo-Seite), `ENTWURF_V6 = false` in `scripts/build-legal-html.mjs`, und `ENTWURF = false` in `test/aboBedingungenV6.test.js`, `test/funktionenRechtV6.test.js` und `test/kundenansichtEntscheidungen.test.js`. Auch der Vermerk im Kopf dieser Datei und der Prüfpunkte-Datei fällt weg.
|
||||
4. `node --max-old-space-size=256 scripts/build-legal-html.mjs funktionen6` erzeugt die zehn Seiten neu; `scripts/build-legal-html.mjs funktionen6 --check` meldet danach nichts.
|
||||
5. `free -m` (mindestens 2000 MB verfügbar), dann `nice -n 10 scripts/build-legal-pdfs.sh abo6` und `nice -n 10 scripts/build-legal-pdfs.sh funktionen6`, einzeln und nie parallel (`FORCE=1` nur nach einer Textänderung). Die PDFs und die `.sha256`-Dateien werden mit committet.
|
||||
6. `node scripts/rechtliches-hochladen.mjs --probe`. Meldet der Vorflug `STOPP: Karte fehlt: …` (laut `setup-legal.mjs` des WOMS fehlen drei der elf Karten: News, Website-Analyse und AVV Website-Analyse), zuerst `node scripts/rechtliches-hochladen.mjs --karten-anlegen`, danach den Vorflug wiederholen. Ist er grün, folgt ein Aufruf je Dokument (elf Aufrufe): `node scripts/rechtliches-hochladen.mjs <documentKey> --freigabe=JJJJ-MM-TT`. Zuletzt `node scripts/rechtliches-hochladen.mjs --pruefen`: Exit 0 heißt, alle elf Dokumente haben eine aktuelle Version mit passender Quelle; sonst weiter bei Schritt 6.
|
||||
7. Erst danach `KUNDENANSICHT_NEU` auf `on` (P10); P10 prüft vorher, dass `WORTLAUT_STATUS` auf `'freigegeben'` steht und die Gleichheitstests grün sind (Nachtrag N48). Die Verdrahtung der Analyse (`config.analyse.avvVersion`, `legalPaths`, die Weiche der Mail K18) folgt der Freigabe (Nachtrag N33); P10 verschiebt Seite und PDF der Abo-Bedingungen nach `public/`.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Leistungsbeschreibung News-Pinnwand
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
|
||||
|
||||
## 1. Geltung
|
||||
|
||||
(1) Funktion im Sinne dieser Leistungsbeschreibung ist die News-Pinnwand. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht.
|
||||
|
||||
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für die News-Pinnwand diese Leistungsbeschreibung vor.
|
||||
|
||||
## 2. Leistung
|
||||
|
||||
(1) WEBklar baut die News-Pinnwand in die Website ein, die der Kunde für das gewählte Projekt bei WEBklar im Kundenportal führt. Die Pinnwand zeigt die Beiträge des Kunden als Zettel mit Titel, Text und Foto; pro Seite erscheinen sechs Beiträge, die neuesten zuerst.
|
||||
|
||||
(2) Die Beiträge schreibt, ändert und löscht der Kunde selbst im Kundenportal im Bereich „Funktionen“. Er wählt je Beitrag Zettelfarbe und Anzeigedauer; danach verschwindet der Beitrag von seiner Website. Hochgeladene Fotos werden verkleinert, alle Metadaten einschließlich Aufnahmeort (GPS) werden entfernt.
|
||||
|
||||
(3) Änderungen an der Website, die über den Einbau der Pinnwand hinausgehen, sind nicht Teil dieser Leistung.
|
||||
|
||||
## 3. Einrichtung
|
||||
|
||||
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.
|
||||
|
||||
(2) Der Einbau ist erfahrungsgemäß meist innerhalb von 1–2 Wochen abgeschlossen. Das ist eine Einschätzung und keine verbindliche Zusage eines Termins. Nach Abschluss des Einbaus schaltet WEBklar die Funktion frei; ab dann erscheinen die Beiträge auf der Website.
|
||||
|
||||
(3) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
|
||||
|
||||
(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen.
|
||||
|
||||
## 4. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist für die Inhalte seiner Beiträge und Fotos verantwortlich. Er versichert, dass er die nötigen Rechte an Texten und Fotos hat und dass die Inhalte keine Rechte Dritter verletzen, etwa Urheberrechte, Persönlichkeitsrechte oder das Recht am eigenen Bild abgebildeter Personen.
|
||||
|
||||
## 5. Ende der Leistung
|
||||
|
||||
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Kunden per E-Mail.
|
||||
|
||||
(2) Auf Wunsch des Kunden schaltet WEBklar die Pinnwand jederzeit ab; für den Widerruf gilt Nr. 6.
|
||||
|
||||
(3) Beiträge und Fotos bleiben nach dem Abschalten 30 Tage gespeichert und werden danach gelöscht; beginnt in dieser Zeit wieder ein Wartungs-Abo, schaltet WEBklar die Pinnwand mit den vorhandenen Beiträgen ohne weitere Kosten wieder ein. Der Kunde kann seine Beiträge und Fotos jederzeit selbst löschen; andernfalls gelten die Fristen der Datenschutzerklärung (Abschnitt 6a).
|
||||
|
||||
## 6. Widerruf
|
||||
|
||||
(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.
|
||||
|
||||
(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
|
||||
@@ -0,0 +1,125 @@
|
||||
# Leistungsbeschreibung Terminplaner
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
|
||||
|
||||
## 1. Geltung
|
||||
|
||||
(1) Funktion im Sinne dieser Leistungsbeschreibung ist der Terminplaner. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Terminplaner.
|
||||
|
||||
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Terminplaner diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Terminplaner.
|
||||
|
||||
(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für den Datenschutz-Absatz nach Nr. 4 Abs. 9 und die Gästetexte nach Nr. 2 Abs. 6. Beides ist Teil des Terminplaners.
|
||||
|
||||
## 2. Leistung
|
||||
|
||||
(1) Der Terminplaner ist ein Buchungs-Widget für die Website des Kunden mit einer Verwaltung im Kundenportal. WEBklar baut je Website-Projekt ein Widget an einer Stelle der Website ein, entweder eingebettet in die Seite oder hinter einem Knopf. Die Gestaltung richtet sich nach dem Fragebogen, den der Kunde vor dem Einschalten im Kundenportal beantwortet.
|
||||
|
||||
(2) Der Kunde legt seine Leistungen, seine Personen oder Plätze, seine Zeiten und seine Regeln für Buchungen fest. Gäste buchen im Widget einen Termin; je nach Einstellung ist die Buchung verbindlich oder eine Anfrage.
|
||||
|
||||
(3) Der Terminplaner versendet im Auftrag des Kunden E-Mails an die Gäste: den Bestätigungslink, die Bestätigung, Änderungen, Absagen, auf Wunsch des Gastes eine Erinnerung (nur mit seinem Haken) und die Eingangsbestätigung eines Widerrufs. Den Kunden benachrichtigt er über neue und geänderte Buchungen.
|
||||
|
||||
(4) Der Kunde sieht und verwaltet seine Termine im Kundenportal und kann sie als CSV-Datei exportieren. Dazu erhält er einen geheimen Kalender-Link (ICS), mit dem er seine Termine in einem Kalenderprogramm abonnieren kann.
|
||||
|
||||
(5) Der Terminplaner enthält einen Missbrauchsschutz gegen automatisierte Buchungen. Die Löschfristen für Buchungsdaten nennt Nummer 9 des AVV Terminplaner.
|
||||
|
||||
(6) Bezahlt wird nur vor Ort. Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz für die Website des Kunden und die Widerrufsbelehrung für Gäste sind Mustertexte der Standardkonfiguration (Nr. 11 Abs. 1).
|
||||
|
||||
## 3. Nicht enthalten
|
||||
|
||||
(1) Nicht enthalten sind SMS, Online-Zahlung, ein Kalender-Abgleich (es gibt nur das Abonnement des Kalender-Links nach Nr. 2 Abs. 4), Konten für Gäste, Marketing-E-Mails, Gruppentermine, Mehrsprachigkeit und der Einsatz im Gesundheitsbereich (Nr. 4 Abs. 3).
|
||||
|
||||
(2) Zusätzliche Wünsche, etwa ein zweites Widget, Knöpfe je Leistung oder ein Sonderdesign, gehören nicht zur Einrichtung nach Nr. 5. WEBklar führt sie nach Stunden aus (§ 4 der Abo-Bedingungen).
|
||||
|
||||
## 4. Voraussetzungen und Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde hat ein laufendes Wartungs-Abo.
|
||||
|
||||
(2) Der Kunde trägt vor dem Einschalten seine Betriebsangaben ein, dazu die Adresse seines Impressums und die Adresse seiner Datenschutzerklärung.
|
||||
|
||||
(3) Der Terminplaner ist nicht für Angebote im Gesundheitsbereich bestimmt, weil dort besondere Kategorien personenbezogener Daten anfallen können (Art. 9 DSGVO). Der Kunde setzt ihn dort nicht ein und fordert von seinen Gästen keine Gesundheitsangaben. WEBklar darf die Einrichtung für eine solche Website ablehnen oder den Terminplaner dort abschalten. Der Fragebogen fragt die Branche ab; wählt der Kunde den Gesundheitsbereich, ist der Terminplaner für ihn nicht verfügbar.
|
||||
|
||||
(4) Richtet sich das Angebot des Kunden gezielt an Kinder unter 16 Jahren, nutzt der Kunde den Terminplaner nicht.
|
||||
|
||||
(5) Der Kunde nennt eine Telefonnummer, unter der Gäste Termine vereinbaren können, wenn das Widget nicht erreichbar ist (Nr. 6 Abs. 2).
|
||||
|
||||
(6) Er lässt die Texte der Haken und Hinweise im Widget unverändert.
|
||||
|
||||
(7) Er informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt.
|
||||
|
||||
(8) Die Pflichten gegenüber seinen Gästen trägt der Kunde: Preisangaben, Allgemeine Geschäftsbedingungen und Stornobedingungen sowie die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung.
|
||||
|
||||
(9) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für die Online-Terminbuchung ein; das gehört zur Einrichtung. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
|
||||
|
||||
## 5. Einrichtung
|
||||
|
||||
(1) Die Einrichtung umfasst die Einrichtung nach dem Fragebogen, den Einbau des Widgets in die Website, den Datenschutz-Absatz nach Nr. 4 Abs. 9, die Abnahme durch den Kunden und die Freischaltung durch WEBklar.
|
||||
|
||||
(2) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.
|
||||
|
||||
(3) Die voraussichtliche Dauer der Einrichtung beträgt meist 1–2 Wochen. Sie ist eine Einschätzung und keine verbindliche Zusage eines Termins.
|
||||
|
||||
(4) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
|
||||
|
||||
(5) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen. Für den Betrieb des Terminplaners berechnet WEBklar neben dem Wartungs-Abo nichts weiter; er läuft, solange das Wartungs-Abo läuft (Nr. 8).
|
||||
|
||||
## 6. Verfügbarkeit und Störungen
|
||||
|
||||
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Terminplaner. Störungen des Terminplaners beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
|
||||
|
||||
(2) Ist der Terminplaner nicht erreichbar, zeigt das Widget einen Ausweichtext mit der Telefonnummer des Kunden (Nr. 4 Abs. 5).
|
||||
|
||||
(3) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
|
||||
|
||||
(4) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
|
||||
|
||||
(5) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
|
||||
|
||||
## 7. Haftung für verpasste Termine
|
||||
|
||||
(1) Der Terminplaner ist ein Hilfsmittel. Die Einhaltung der Termine bleibt Sache des Kunden.
|
||||
|
||||
(2) WEBklar übernimmt keine Gewähr für die Zustellung der E-Mails (Nr. 6 Abs. 4).
|
||||
|
||||
(3) Für entgangenen Gewinn und Folgeschäden aus verpassten Terminen haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
|
||||
|
||||
(4) Der Haftungsrahmen des Kunden gegenüber seinen Gästen bleibt unberührt.
|
||||
|
||||
## 8. Ende der Leistung
|
||||
|
||||
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Kunden per E-Mail.
|
||||
|
||||
(2) Mit dem Abschalten nimmt der Terminplaner keine neuen Buchungen mehr an, und das Widget zeigt den Ausweichtext. Die Termine bleiben im Kundenportal sichtbar, und der Kunde kann sie als CSV-Datei exportieren. Gäste können über ihren Link absagen. Bereits angeforderte Erinnerungen laufen weiter. Der Kunde informiert seine Gäste.
|
||||
|
||||
(3) WEBklar löscht alle Termindaten und die Einstellungen 30 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Terminplaner).
|
||||
|
||||
(4) Beginnt innerhalb dieser 30 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Einrichtung nötig (neuer Fragebogen, Einrichtung nach Nr. 5).
|
||||
|
||||
(5) Endet das Wartungs-Abo, bevor WEBklar den Terminplaner freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 5 Abs. 2 verfällt.
|
||||
|
||||
(6) Auf Wunsch des Kunden in Textform schaltet WEBklar den Terminplaner jederzeit ab; für den Widerruf gilt Nr. 10.
|
||||
|
||||
## 9. Änderungen und Einstellung
|
||||
|
||||
(1) WEBklar darf die Technik des Terminplaners verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Terminplaner.
|
||||
|
||||
(2) WEBklar darf den Terminplaner für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
|
||||
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Terminplaner Nummer 12), oder
|
||||
- eine Sicherheitslücke sich nicht anders beheben lässt.
|
||||
|
||||
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
|
||||
|
||||
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Terminplaner ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
|
||||
|
||||
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
|
||||
|
||||
## 10. Widerruf
|
||||
|
||||
(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.
|
||||
|
||||
(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
|
||||
|
||||
## 11. Hinweise
|
||||
|
||||
(1) **Keine Rechtsberatung.** Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz und die Widerrufsbelehrung für Gäste sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung trägt der Kunde. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht.
|
||||
|
||||
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Kunde als Dienstleistungserbringer unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Kunde.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Leistungsbeschreibung Terminplaner
|
||||
|
||||
**Fassung 2026-10-05** · Zusatzbedingungen zum Wartungs-Abo
|
||||
|
||||
## 1. Geltung
|
||||
|
||||
(1) Diese Leistungsbeschreibung regelt die Funktion „Terminplaner“ im Wartungs-Abo. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Sie wird vereinbart, wenn der Kunde die Einrichtung des Terminplaners im Kundenportal bestellt und dabei dieser Leistungsbeschreibung, dem Auftragsverarbeitungsvertrag Terminplaner („AVV Terminplaner“) und der Widerrufsbelehrung zustimmt. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Terminplaner.
|
||||
|
||||
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Terminplaner diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Terminplaner.
|
||||
|
||||
(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für den Datenschutz-Absatz nach Nr. 4 Abs. 9 und die Gästetexte nach Nr. 2 Abs. 6. Beides ist Teil des Terminplaners.
|
||||
|
||||
## 2. Leistung
|
||||
|
||||
(1) Der Terminplaner ist ein Buchungs-Widget für die Website des Kunden mit einer Verwaltung im Kundenportal. WEBklar baut je Website-Projekt ein Widget an einer Stelle der Website ein, entweder eingebettet in die Seite oder hinter einem Knopf. Die Gestaltung richtet sich nach dem Fragebogen, den der Kunde vor der Bestellung im Kundenportal beantwortet.
|
||||
|
||||
(2) Der Kunde legt seine Leistungen, seine Personen oder Plätze, seine Zeiten und seine Regeln für Buchungen fest. Gäste buchen im Widget einen Termin; je nach Einstellung ist die Buchung verbindlich oder eine Anfrage.
|
||||
|
||||
(3) Der Terminplaner versendet im Auftrag des Kunden E-Mails an die Gäste: den Bestätigungslink, die Bestätigung, Änderungen, Absagen, auf Wunsch des Gastes eine Erinnerung (nur mit seinem Haken) und die Eingangsbestätigung eines Widerrufs. Den Kunden benachrichtigt er über neue und geänderte Buchungen.
|
||||
|
||||
(4) Der Kunde sieht und verwaltet seine Termine im Kundenportal und kann sie als CSV-Datei exportieren. Dazu erhält er einen geheimen Kalender-Link (ICS), mit dem er seine Termine in einem Kalenderprogramm abonnieren kann.
|
||||
|
||||
(5) Der Terminplaner enthält einen Missbrauchsschutz gegen automatisierte Buchungen. Die Löschfristen für Buchungsdaten nennt Nummer 9 des AVV Terminplaner.
|
||||
|
||||
(6) Bezahlt wird nur vor Ort. Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz für die Website des Kunden und die Widerrufsbelehrung für Gäste sind Mustertexte der Standardkonfiguration (Nr. 11 Abs. 1).
|
||||
|
||||
## 3. Nicht enthalten
|
||||
|
||||
(1) Nicht enthalten sind SMS, Online-Zahlung, ein Kalender-Abgleich (es gibt nur das Abonnement des Kalender-Links nach Nr. 2 Abs. 4), Konten für Gäste, Marketing-E-Mails, Gruppentermine, Mehrsprachigkeit und der Einsatz im Gesundheitsbereich (Nr. 4 Abs. 3).
|
||||
|
||||
(2) Zusätzliche Wünsche, etwa ein zweites Widget, Knöpfe je Leistung oder ein Sonderdesign, gehören nicht zur Einrichtung nach Nr. 5. WEBklar führt sie nach Stunden aus (§ 4 der Abo-Bedingungen).
|
||||
|
||||
## 4. Voraussetzungen und Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde hat ein laufendes Wartungs-Abo.
|
||||
|
||||
(2) Der Kunde trägt vor der Bestellung seine Betriebsangaben ein, dazu die Adresse seines Impressums und die Adresse seiner Datenschutzerklärung.
|
||||
|
||||
(3) Der Terminplaner ist nicht für Angebote im Gesundheitsbereich bestimmt, weil dort besondere Kategorien personenbezogener Daten anfallen können (Art. 9 DSGVO). Der Kunde setzt ihn dort nicht ein und fordert von seinen Gästen keine Gesundheitsangaben. WEBklar darf die Einrichtung für eine solche Website ablehnen oder den Terminplaner dort abschalten.
|
||||
|
||||
(4) Richtet sich das Angebot des Kunden gezielt an Kinder unter 16 Jahren, nutzt der Kunde den Terminplaner nicht.
|
||||
|
||||
(5) Der Kunde nennt eine Telefonnummer, unter der Gäste Termine vereinbaren können, wenn das Widget nicht erreichbar ist (Nr. 6 Abs. 2).
|
||||
|
||||
(6) Er lässt die Texte der Haken und Hinweise im Widget unverändert.
|
||||
|
||||
(7) Er informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt.
|
||||
|
||||
(8) Die Pflichten gegenüber seinen Gästen trägt der Kunde: Preisangaben, Allgemeine Geschäftsbedingungen und Stornobedingungen sowie die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung.
|
||||
|
||||
(9) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für die Online-Terminbuchung ein; das gehört zur Einrichtung und wird nicht auf das Stundenkontingent angerechnet. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
|
||||
|
||||
## 5. Einrichtung und Preis
|
||||
|
||||
(1) Die Einrichtung umfasst die Einrichtung nach dem Fragebogen, den Einbau des Widgets in die Website, den Datenschutz-Absatz nach Nr. 4 Abs. 9, die Abnahme durch den Kunden und die Freischaltung durch WEBklar.
|
||||
|
||||
(2) Die Einrichtung kostet 149 € einmalig je Website-Projekt, ohne Umsatzsteuer gemäß § 19 UStG.
|
||||
|
||||
(3) Die voraussichtliche Dauer der Einrichtung nennt die Bestellseite vor der Bestellung. Sie ist eine Einschätzung und keine verbindliche Zusage eines Termins.
|
||||
|
||||
(4) Der Kunde bezahlt bei der Bestellung im Kundenportal über Stripe und erhält eine Rechnung.
|
||||
|
||||
(5) Der Preis steht neben dem Stundenkontingent des Wartungs-Abos: Die Einrichtung wird weder auf das Kontingent angerechnet noch damit bezahlt. Für den Betrieb des Terminplaners berechnet WEBklar neben dem Wartungs-Abo nichts weiter; er läuft, solange das Wartungs-Abo läuft (Nr. 8).
|
||||
|
||||
## 6. Verfügbarkeit und Störungen
|
||||
|
||||
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Terminplaner. Störungen des Terminplaners beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
|
||||
|
||||
(2) Ist der Terminplaner nicht erreichbar, zeigt das Widget einen Ausweichtext mit der Telefonnummer des Kunden (Nr. 4 Abs. 5).
|
||||
|
||||
(3) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
|
||||
|
||||
(4) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
|
||||
|
||||
(5) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
|
||||
|
||||
## 7. Haftung für verpasste Termine
|
||||
|
||||
(1) Der Terminplaner ist ein Hilfsmittel. Die Einhaltung der Termine bleibt Sache des Kunden.
|
||||
|
||||
(2) WEBklar übernimmt keine Gewähr für die Zustellung der E-Mails (Nr. 6 Abs. 4).
|
||||
|
||||
(3) Für entgangenen Gewinn und Folgeschäden aus verpassten Terminen haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
|
||||
|
||||
(4) Der Haftungsrahmen des Kunden gegenüber seinen Gästen bleibt unberührt.
|
||||
|
||||
## 8. Ende der Leistung
|
||||
|
||||
(1) Mit dem Ende des Wartungs-Abos endet der Terminplaner. WEBklar schaltet ihn spätestens am folgenden Tag ab und informiert den Kunden per E-Mail. Das gilt auch bei einem Wechsel ins Hosting-Abo und beim Widerruf des Abschlusses oder eines Wechsels ins Wartungs-Abo.
|
||||
|
||||
(2) Mit dem Abschalten nimmt der Terminplaner keine neuen Buchungen mehr an, und das Widget zeigt den Ausweichtext. Die Termine bleiben im Kundenportal sichtbar, und der Kunde kann sie als CSV-Datei exportieren. Gäste können über ihren Link absagen. Bereits angeforderte Erinnerungen laufen weiter. Der Kunde informiert seine Gäste.
|
||||
|
||||
(3) WEBklar löscht alle Termindaten und die Einstellungen 30 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Terminplaner).
|
||||
|
||||
(4) Beginnt innerhalb dieser 30 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Einrichtung nötig (neuer Fragebogen, neuer Preis nach Nr. 5).
|
||||
|
||||
(5) Endet das Wartungs-Abo, bevor WEBklar den Terminplaner freigeschaltet hat, erstattet WEBklar den Einrichtungspreis vollständig.
|
||||
|
||||
(6) Auf Wunsch des Kunden in Textform schaltet WEBklar den Terminplaner jederzeit ab. Eine Erstattung des Einrichtungspreises folgt daraus nicht; für den Widerruf gilt Nr. 10.
|
||||
|
||||
## 9. Änderungen und Einstellung
|
||||
|
||||
(1) WEBklar darf die Technik des Terminplaners verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Terminplaner.
|
||||
|
||||
(2) WEBklar darf den Terminplaner für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
|
||||
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Terminplaner Nummer 12), oder
|
||||
- eine Sicherheitslücke sich nicht anders beheben lässt.
|
||||
|
||||
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
|
||||
|
||||
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Terminplaner ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
|
||||
|
||||
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
|
||||
|
||||
## 10. Widerruf
|
||||
|
||||
(1) Verbraucher können die Bestellung der Einrichtung nach der Widerrufsbelehrung widerrufen. Belehrung und Muster-Widerrufsformular stehen in der Anlage „Widerrufsbelehrung und Muster-Widerrufsformular (Einrichtung von Funktionen)“.
|
||||
|
||||
(2) Ob das Widerrufsrecht erlischt und ob der Kunde einen angemessenen Betrag für die bis dahin erbrachte Leistung schuldet, richtet sich nach der Zustimmung, die der Kunde bei der Bestellung gibt, und nach den Hinweisen zum Widerruf in der Anlage.
|
||||
|
||||
(3) Mit dem Widerruf endet der Terminplaner.
|
||||
|
||||
## 11. Hinweise
|
||||
|
||||
(1) **Keine Rechtsberatung.** Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz und die Widerrufsbelehrung für Gäste sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung trägt der Kunde. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht.
|
||||
|
||||
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Kunde als Dienstleistungserbringer unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Kunde.
|
||||
@@ -0,0 +1,192 @@
|
||||
# Leistungsbeschreibung Buchungskalender
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
|
||||
|
||||
## 1. Geltung
|
||||
|
||||
(1) Diese Leistungsbeschreibung regelt die Funktion „Buchungskalender“. Mit dem Buchungskalender nimmt der Kunde über seine Website Anfragen für die Vermietung von Unterkünften entgegen (Ferienwohnungen, Monteurzimmer, kleine Pensionen) und rechnet die Aufenthalte ab („Vermieter“); Gäste sind die Besucher seiner Website, die im Buchungskalender anfragen und buchen („Gäste“). Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung vermieten.
|
||||
|
||||
(2) Diese Leistungsbeschreibung gilt, wenn der Vermieter die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Vermieter die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Buchungskalender. Einschalten kann der Vermieter erst, nachdem WEBklar seine Anfrage für den Buchungskalender geprüft und angenommen hat (Nr. 2 Abs. 1). Für einen Vermieter, dem WEBklar die Funktion als Teil seines Website-Projekts bereitstellt, schaltet WEBklar sie nach seiner Zustimmung in Textform frei; die Bedingungen dieser Leistungsbeschreibung gelten dann ebenso, die Einrichtung rechnet WEBklar nach Nr. 9 Abs. 3 ab.
|
||||
|
||||
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Buchungskalender diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Buchungskalender.
|
||||
|
||||
(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Buchungskalender nicht entgegen. Das Widget des Buchungskalenders läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und der Muster-Text zur Stornierung nach Nr. 6 ist Teil des Buchungskalenders.
|
||||
|
||||
## 2. Leistung
|
||||
|
||||
(1) Der Vermieter fragt den Buchungskalender im Kundenportal an und beschreibt seine Unterkünfte (Art der Unterkunft, Zahl der Einheiten, ob er Booking.com oder Airbnb nutzt). WEBklar prüft die Anfrage und nimmt sie an oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Annahme besteht nicht. Erst nach der Annahme kann der Vermieter den Buchungskalender einschalten.
|
||||
|
||||
(2) Die Einrichtung umfasst den Einbau eines Kalender-Widgets an einer Stelle der Website des Vermieters, die Bereitstellung der Verwaltung im Kundenportal, den Einbau des Datenschutz-Absatzes für den Buchungskalender (Nr. 5 Abs. 10) und die Freischaltung durch WEBklar. Für jede Website wird ein Buchungskalender eingerichtet; er kann mehrere Unterkünfte desselben Vermieters enthalten. Zusätzlich gibt es für jeden freigegebenen Aufenthalt eine Buchungsseite bei WEBklar, die der Gast über den Link in seiner E-Mail erreicht.
|
||||
|
||||
(3) So läuft eine Buchung ab:
|
||||
- Das Widget zeigt dem Gast die belegten Nächte. Er wählt Anreise und Abreise, gibt Personenzahl, Namen, E-Mail-Adresse und auf Wunsch Firma, Telefonnummer und Nachricht an und sendet eine **unverbindliche Anfrage**. Er erhält eine Eingangsbestätigung, der Vermieter eine Benachrichtigung.
|
||||
- Der Vermieter prüft die Anfrage im Kundenportal, sieht den vorgerechneten Preis, kann die Posten ändern und gibt die Anfrage mit einer Reservierungsfrist frei oder lehnt sie ab. Gibt er sie frei, reserviert der Buchungskalender die Nächte bis zum Ablauf der Frist, und der Gast erhält eine E-Mail mit Preis, Stornobedingungen und dem Link zur Buchungsseite.
|
||||
- Auf der Buchungsseite sieht der Gast alle Angaben zum Aufenthalt, trägt seine Rechnungsadresse ein, wählt die Zahlart (online über Stripe, wenn der Vermieter sie eingeschaltet hat, oder vor Ort) und bucht mit der Schaltfläche zur zahlungspflichtigen Buchung. Erst damit kommt der Vertrag zwischen Vermieter und Gast zustande, nicht schon mit der Anfrage oder der Freigabe.
|
||||
- Mit der Buchung erzeugt der Buchungskalender eine Rechnung mit lückenloser Nummer und sendet dem Gast eine Buchungsbestätigung mit der Rechnung als PDF; der Vermieter wird benachrichtigt.
|
||||
- Verstreicht die Frist ohne Buchung, läuft die Reservierung ab und die Nächte werden frei; der Gast erhält darüber eine E-Mail.
|
||||
- Der Vermieter kann einen Aufenthalt stornieren. Dann erhält der Gast eine Stornorechnung mit eigener Nummer und, soweit der Vermieter eine Online-Zahlung erstattet, eine Mitteilung über den Erstattungsbetrag. Eine Stornierung durch den Gast gibt es im Widget nicht; er wendet sich an den Vermieter.
|
||||
|
||||
(4) Im Kundenportal verwaltet der Vermieter:
|
||||
- Unterkünfte mit Beschreibung, Höchstzahl der Personen, Mindestnächten und Check-in- und Check-out-Zeiten,
|
||||
- Preise (Staffel nach Aufenthaltsdauer, Endreinigung, Zusatzpersonen, Direktbuchungsrabatt, Steuerart und Steuersätze) und seine Stornobedingungen,
|
||||
- Anfragen und Buchungen mit Status, die Eintragung „bezahlt (vor Ort)“ und Stornierungen,
|
||||
- Kalender mit manuellen Sperren,
|
||||
- die Verbindungen zu anderen Kalendern (Nr. 4),
|
||||
- seine Rechnungen als PDF,
|
||||
- seine Angaben als Aussteller der Rechnungen (Name, Anschrift, Steuernummer oder Umsatzsteuer-Identifikationsnummer, Bankverbindung, Rechnungspräfix) und die Verbindung mit seinem Stripe-Konto (Nr. 3).
|
||||
|
||||
(5) Der Buchungskalender versendet im Namen des Vermieters E-Mails an die Gäste: Eingang der Anfrage, Freigabe mit Buchungslink, Ablehnung, Buchungsbestätigung mit Rechnung, Zahlungseingang, Ablauf der Reservierung und Stornierung. Absender ist die Versandadresse von WEBklar mit dem Namen des Vermieters, als Antwortadresse dient seine E-Mail-Adresse. Den Vermieter benachrichtigt der Buchungskalender über neue Anfragen und neue Buchungen sowie, wenn der Abgleich mit einem anderen Kalender seit mindestens sechs Stunden fehlschlägt.
|
||||
|
||||
(6) Der Buchungskalender enthält einen Missbrauchsschutz gegen automatisierte Anfragen (verstecktes Feld, Mindest-Ausfüllzeit, Obergrenzen je IP-Adresse). Dafür speichert WEBklar mit der Anfrage nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird entfernt, sobald der Vermieter die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 3 Abs. 4 des AVV Buchungskalender).
|
||||
|
||||
(7) Nicht enthalten sind:
|
||||
- der Meldeschein nach dem Bundesmeldegesetz und die Verwaltung der Meldedaten,
|
||||
- Kurtaxe, Gästebeitrag und Beherbergungsabgaben,
|
||||
- Anzahlungen und Teilzahlungen (der Gast zahlt den Gesamtbetrag in einer Zahlung, online oder vor Ort),
|
||||
- Sofortbuchung ohne Freigabe,
|
||||
- saisonale Preise (die Preise richten sich nur nach der Aufenthaltsdauer),
|
||||
- Gutscheine, Channel-Manager-Anbindungen außer dem Kalenderabgleich nach Nr. 4 und Mehrsprachigkeit (der Buchungskalender ist deutschsprachig),
|
||||
- die Buchführung des Vermieters.
|
||||
|
||||
Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Buchungskalender oder eine über das übliche Maß hinausgehende Zahl von Unterkünften, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 4).
|
||||
|
||||
## 3. Zahlungen über Stripe und Rechnungen im Namen des Vermieters
|
||||
|
||||
(1) Online-Zahlungen im Buchungskalender sind optional und laufen über Stripe Connect. Der Vermieter verbindet dazu sein eigenes Stripe-Konto mit dem Buchungskalender. Hat er für den Webshop von WEBklar bereits ein Stripe-Konto verbunden, kann er es auch für den Buchungskalender verwenden. Den Vertrag über dieses Konto schließt der Vermieter unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Vermieters durch Stripe und die Auszahlungen gelten die Bedingungen von Stripe. Ohne verbundenes und freigeschaltetes Stripe-Konto ist nur die Zahlart „vor Ort“ möglich.
|
||||
|
||||
(2) Das Geld der Gäste fließt direkt an den Vermieter (Zahlung unmittelbar auf dem Konto des Vermieters, sogenannte Direct Charge). WEBklar erhält und verwahrt kein Geld der Gäste, leitet keines weiter und beteiligt sich nicht am Umsatz des Vermieters. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Vermieter; sie fallen nur zwischen dem Vermieter und Stripe an.
|
||||
|
||||
(3) Der Vermieter prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt ihm im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Gästen und Zahlungsausfälle regeln der Vermieter und Stripe.
|
||||
|
||||
(4) Erstattungen bei einer Stornierung veranlasst der Vermieter im Kundenportal; WEBklar führt die Erstattung über das Stripe-Konto des Vermieters aus. Er bestimmt den Betrag bis zur Höhe der Zahlung des Gastes. Storniert der Vermieter einen Aufenthalt, schließt WEBklar eine noch offene Zahlungsseite bei Stripe. **Automatische Erstattung:** Geht eine Zahlung ein, nachdem die Buchung storniert oder die Reservierung abgelaufen ist, oder zahlt der Gast eine bereits bezahlte Buchung ein zweites Mal, erstattet WEBklar den Betrag ohne weitere Anweisung im Namen des Vermieters in voller Höhe über dessen Stripe-Konto. Dazu erteilt der Vermieter WEBklar mit der Zustimmung zum Buchungskalender eine dauerhafte Weisung; der Stand der Buchung ändert sich dadurch nicht. Erstattungen aus dem Stripe-Dashboard trägt WEBklar in die Buchung nach, soweit Stripe sie meldet.
|
||||
|
||||
(5) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Online-Zahlungen sind derzeit nur per Karte möglich, einschließlich Apple Pay und Google Pay; eine SEPA-Lastschrift bietet der Buchungskalender nicht an. Kartendaten gibt der Gast nur dort ein; WEBklar erhält sie nicht. Beim Start der Zahlung übergibt WEBklar Stripe als Rücksprungadresse (bei Erfolg und bei Abbruch) den geheimen Link zur Buchungsseite des Gastes samt Token. Dieser Link ist damit für Stripe und im Stripe-Dashboard des Vermieters sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite.
|
||||
|
||||
(6) **Rechnungen im Namen des Vermieters.** Der Vermieter beauftragt WEBklar mit dem Rechnungsauftrag in den Einstellungen des Buchungskalenders, in seinem Namen und für seine Rechnung Rechnungen und Stornorechnungen an seine Gäste auszustellen (Rechnung durch einen Dritten im Namen und im Auftrag des Leistenden, § 14 Abs. 2 Satz 4 UStG). Aussteller der Rechnung ist der Vermieter; sie trägt seinen Namen, seine Anschrift, seine Steuernummer oder Umsatzsteuer-Identifikationsnummer und keinen Hinweis auf WEBklar. WEBklar erzeugt die Rechnung aus den Angaben, die der Vermieter hinterlegt und bei der Freigabe bestätigt hat. Er verantwortet die Richtigkeit seiner Angaben, der Preise, der Steuerart und der Steuersätze; WEBklar prüft sie nicht steuerrechtlich. Die Rechnung wird nach ihrer Ausstellung nicht mehr verändert; Fehler korrigiert der Vermieter durch Stornierung und neue Buchung nach den Möglichkeiten des Kundenportals. Ohne diesen Auftrag stellt WEBklar keine Rechnungen aus. Widerruft er den Auftrag in Textform, stellt der Buchungskalender keine Rechnungen mehr aus und ist für ihn nicht mehr nutzbar.
|
||||
|
||||
(7) Die Rechnungsnummern sind je Vermieter und Jahr lückenlos fortlaufend; sie bestehen aus dem Präfix, das der Vermieter vor der ersten Rechnung festlegt, dem Jahr und einer laufenden Nummer.
|
||||
|
||||
## 4. Verfügbarkeit und Kalenderabgleich (iCal)
|
||||
|
||||
(1) Der Vermieter kann Kalender anderer Plattformen (zum Beispiel Booking.com oder Airbnb) über eine iCal-Adresse einlesen lassen; die eingelesenen Zeiträume blockiert der Buchungskalender. Umgekehrt stellt der Buchungskalender dem Vermieter eine geheime iCal-Adresse bereit, die er bei anderen Plattformen einträgt; sie enthält die belegten Nächte ohne Daten der Gäste. Der Vermieter kann diese Adresse jederzeit erneuern; die bisherige Adresse funktioniert dann nicht mehr.
|
||||
|
||||
(2) Der Abgleich läuft regelmäßig, aber zeitversetzt: WEBklar liest die Kalender anderer Plattformen etwa alle 15 Minuten ein, und die anderen Plattformen lesen die Adresse des Buchungskalenders nach ihrem eigenen Takt, oft nur alle paar Stunden. **Zwischen einer Buchung hier und ihrem Eingang dort können deshalb Minuten bis Stunden vergehen, und umgekehrt.** WEBklar sagt keine Echtzeit-Verfügbarkeit zu und übernimmt keine Gewähr dafür, dass eine Unterkunft nicht doppelt gebucht wird.
|
||||
|
||||
(3) Dieses Risiko mindert die Freigabe: Jede Anfrage im Buchungskalender braucht die Freigabe des Vermieters. Er prüft vor der Freigabe seine anderen Plattformen und seinen Kalender. Beim Freigeben und beim Buchen prüft der Buchungskalender zusätzlich, ob die Nächte nach seinem Stand frei sind. Eine Doppelbuchung entsteht trotzdem, wenn eine andere Plattform eine Buchung noch nicht gemeldet hat oder der Abgleich ausfällt. Der Vermieter löst eine Doppelbuchung selbst (etwa durch Stornierung) und trägt die Folgen gegenüber seinen Gästen und den Plattformen.
|
||||
|
||||
(4) Fehlerhafte oder nicht erreichbare iCal-Adressen kann WEBklar nicht beheben. Schlägt der Abruf fehl, bleibt der zuletzt gelesene Stand erhalten, und der Vermieter wird benachrichtigt, wenn der Abruf seit mindestens sechs Stunden nicht gelungen ist (höchstens einmal je 24 Stunden und Kalender). Eingelesen werden nur Zeiträume von gestern bis 18 Monate in die Zukunft, höchstens 1 Megabyte je Kalender; nur Adressen mit https werden abgerufen.
|
||||
|
||||
## 5. Pflichten des Vermieters
|
||||
|
||||
(1) Der Vermieter hat ein laufendes Wartungs-Abo und ist Unternehmer. Will er Online-Zahlungen nutzen, stellt er sein eigenes Stripe-Konto bereit und schließt die Anmeldung bei Stripe selbst ab. Der Vermieter vermietet in eigenem Namen; für Preise, Stornobedingungen, Rechnungen und Meldepflichten ist er verantwortlich. Online-Zahlungen seiner Gäste laufen, sofern er sie nutzt, über sein eigenes Stripe-Konto zu den Bedingungen von Stripe; WEBklar erhält und verwahrt kein Geld seiner Gäste und erhebt keine Umsatzbeteiligung. Rechnungen an Gäste stellt WEBklar nur aus, wenn der Vermieter in den Einstellungen des Buchungskalenders den Rechnungsauftrag erteilt hat (§ 14 Abs. 2 Satz 4 UStG); für die Richtigkeit von Preisen und Steuerangaben bleibt er verantwortlich.
|
||||
|
||||
(2) Er verantwortet seine Unterkünfte, Preise und Angaben zum Angebot. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer, für die Beherbergung und die Endreinigung nach den dafür geltenden Steuersätzen, oder Hinweis auf § 19 UStG), Check-in- und Check-out-Zeiten, Höchstbelegung, Mindestnächte und alle Angaben, die er seinen Gästen vor der Buchung geben muss. Die Steuerart und die Steuersätze trägt er selbst im Kundenportal ein; WEBklar berät dazu nicht.
|
||||
|
||||
(3) Er legt vor der Freischaltung seine Stornobedingungen fest und verantwortet sie. Der Muster-Text nach Nr. 6 ist nur ein Vorschlag.
|
||||
|
||||
(4) Er verantwortet die Verfügbarkeit seiner Unterkünfte, pflegt Sperren und Kalenderverbindungen im Kundenportal und prüft regelmäßig auf neue Anfragen und Buchungen. Die Folgen von Angaben, die er setzt (zum Beispiel einer fälschlich freien oder gesperrten Zeit), trägt er.
|
||||
|
||||
(5) Er gibt Anfragen nur frei, wenn er die Unterkunft für den Zeitraum zu den angegebenen Bedingungen bereitstellen kann, und erfüllt die gebuchten Aufenthalte. Er trägt alle Pflichten eines Vermieters gegenüber seinen Gästen, auch Gewährleistung, Kundenservice und Beschwerden.
|
||||
|
||||
(6) Er erfüllt seine gesetzlichen Pflichten rund um die Beherbergung selbst. Das betrifft insbesondere den Meldeschein nach dem Bundesmeldegesetz, die Kurtaxe und andere kommunale Abgaben, Anzeige- und Genehmigungspflichten für die Vermietung (zum Beispiel Zweckentfremdungs- oder Gewerberecht) sowie die Pflichten aus seinem Steuerrecht. Der Buchungskalender nimmt ihm davon nichts ab.
|
||||
|
||||
(7) Anzahlungen und Teilzahlungen bietet der Buchungskalender nicht an. Vereinbart der Vermieter außerhalb des Buchungskalenders etwas anderes mit seinem Gast, ist das nicht Gegenstand dieser Leistungsbeschreibung.
|
||||
|
||||
(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem. WEBklar bewahrt die Rechnungen des Vermieters im Buchungskalender 8 Jahre auf, solange der Buchungskalender läuft und nach Nr. 10 bis zur Löschung, hält sie ihm zum Abruf als PDF bereit und anonymisiert personenbezogene Daten der Gäste nach Nummer 9 des AVV Buchungskalender. Der Vermieter lädt seine Rechnungen deshalb rechtzeitig vor der Löschung nach Nr. 10 Abs. 3 herunter und bewahrt sie selbst auf.
|
||||
|
||||
(9) Er hält die Vorgaben dieser Leistungsbeschreibung ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht.
|
||||
|
||||
(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Buchungskalender ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Vermieter den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Vermieter darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
|
||||
|
||||
(11) Der Vermieter informiert seine Gäste nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Buchungskalender: Der Vermieter ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Vermieters.
|
||||
|
||||
## 6. Muster-Text zur Stornierung, keine Rechtsberatung
|
||||
|
||||
(1) WEBklar stellt dem Vermieter einen Muster-Text für Stornobedingungen bereit. Er ist eine **Vorlage ohne Gewähr** und keine Rechtsberatung (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes). Er passt zur Technik des Buchungskalenders, kann aber auf den Betrieb des Vermieters nicht zugeschnitten sein.
|
||||
|
||||
(2) WEBklar empfiehlt, die Stornobedingungen und die Angaben zum Angebot vor der Freischaltung von einer Rechtsanwältin oder einem Rechtsanwalt prüfen zu lassen.
|
||||
|
||||
(3) Der Vermieter bleibt für seine Stornobedingungen und alle seine Texte verantwortlich, auch wenn er den Muster-Text unverändert übernimmt (Nr. 5 Abs. 3). Die Stornobedingungen, die bei der Freigabe galten, speichert der Buchungskalender mit der Anfrage; die Buchungsseite und die Buchungsbestätigung zeigen dem Gast diese Fassung.
|
||||
|
||||
## 7. Kein Widerrufsrecht des Gastes, Hinweise auf der Buchungsseite
|
||||
|
||||
(1) Für die Buchung von Beherbergungsleistungen zu einem bestimmten Zeitpunkt oder in einem bestimmten Zeitraum besteht nach § 312g Abs. 2 Satz 1 Nr. 9 BGB kein gesetzliches Widerrufsrecht. Die Buchungsseite weist den Gast darauf hin; der Vermieter kann ihm vertraglich ein weitergehendes Recht einräumen, der Buchungskalender bietet dafür aber keine Funktion.
|
||||
|
||||
(2) Die Buchungsseite zeigt dem Gast vor der Schaltfläche zur zahlungspflichtigen Buchung die wesentlichen Angaben nach Art. 246a EGBGB, soweit der Buchungskalender sie kennt (Anbieter, Unterkunft, Zeitraum, Leistung, Preis insgesamt, Zahlart, Stornobedingungen, Hinweis auf das fehlende Widerrufsrecht). Der Vermieter ergänzt, was darüber hinausgeht, in seinen Texten selbst (Nr. 5 Abs. 2).
|
||||
|
||||
(3) Die Buchungsbestätigung erhält der Gast als E-Mail mit der Rechnung als PDF; sie enthält die Angaben zum Vertrag und die Stornobedingungen auf einem dauerhaften Datenträger.
|
||||
|
||||
## 8. Verfügbarkeit und Störungen
|
||||
|
||||
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Buchungskalender. WEBklar sagt für den Buchungskalender keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Buchungskalenders beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
|
||||
|
||||
(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Buchungskalender kurzzeitig unterbrechen. Während einer Unterbrechung sind Anfragen und Buchungen nicht möglich; das Widget zeigt dann einen Ausweichtext.
|
||||
|
||||
(3) Zahlungen übernimmt der Buchungskalender über Benachrichtigungen von Stripe und einen Abgleich beim Aufruf der Buchungsseite. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Zahlung verzögern. Eine Zahlung, die nach einer Stornierung oder nach dem Ablauf der Reservierung eingeht, und eine zweite Zahlung auf eine bereits bezahlte Buchung erstattet WEBklar automatisch und in voller Höhe (Nr. 3 Abs. 4); die Buchung bleibt dabei unverändert, und die Erstattung braucht einige Tage, bis sie beim Gast ankommt. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.
|
||||
|
||||
(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
|
||||
|
||||
(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
|
||||
|
||||
(6) Die gesetzlichen Rechte des Vermieters sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
|
||||
|
||||
## 9. Einrichtung, Abrechnung und Widerruf
|
||||
|
||||
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Vermieters. Für den Betrieb des Buchungskalenders berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz oder an den Buchungen.
|
||||
|
||||
(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 1 bis 2 Wochen ab dem Einschalten. Sie hängt davon ab, wie schnell der Vermieter Unterkünfte, Preise und gegebenenfalls das Stripe-Konto bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.
|
||||
|
||||
(3) Hat WEBklar die Funktion auf Wunsch des Vermieters schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
|
||||
|
||||
(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Vermieters in Textform aus; es gilt § 4 der Abo-Bedingungen. Welche Wünsche nicht zur Einrichtung gehören, beschreibt Nr. 2 Abs. 7.
|
||||
|
||||
(5) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion. Hat der Vermieter die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat. Das Widerrufsrecht gegenüber WEBklar ist vom Widerrufsrecht der Gäste gegenüber dem Vermieter (Nr. 7) zu unterscheiden.
|
||||
|
||||
## 10. Laufzeit und Ende
|
||||
|
||||
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Vermieter per E-Mail und weist ihn darauf hin, seine Rechnungen als PDF herunterzuladen.
|
||||
|
||||
(2) Mit dem Abschalten nimmt der Buchungskalender keine neuen Anfragen und Buchungen mehr an, und das Widget zeigt den Ausweichtext. Reservierungen laufen aus. Bereits gebuchte Aufenthalte bleiben Verträge zwischen dem Vermieter und seinen Gästen; der Vermieter erfüllt sie und informiert seine Gäste, wenn er den Buchungskalender nicht mehr anbietet. Anfragen, Buchungen und Rechnungen bleiben im Kundenportal 90 Tage lesbar; Rechnungen kann er einzeln als PDF herunterladen.
|
||||
|
||||
(3) WEBklar löscht alle Daten des Buchungskalenders, also Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Buchungskalender). Die Aufbewahrungsfrist der Rechnungen (Nr. 5 Abs. 8) endet damit mit der Löschung; der Vermieter bewahrt sie danach selbst auf. Nach dem Abschalten weist WEBklar den Vermieter per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern; bis dahin sind sie im Kundenportal abrufbar. Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 3 (90 Tage nach dem Abschalten); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird.
|
||||
|
||||
(4) Das Stripe-Konto bleibt beim Vermieter. Das Ende des Buchungskalenders berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Vermieter kann die Verbindung zwischen dem Konto und dem Buchungskalender im Stripe-Dashboard beenden.
|
||||
|
||||
(5) Beginnt innerhalb der 90 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Buchungskalender auf Wunsch des Vermieters in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (Einrichtung nach Nr. 9).
|
||||
|
||||
(6) Endet das Wartungs-Abo, bevor WEBklar den Buchungskalender freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 9 Abs. 1 verfällt.
|
||||
|
||||
(7) Auf Wunsch des Vermieters in Textform schaltet WEBklar den Buchungskalender jederzeit ab; für den Widerruf gilt Nr. 9 Abs. 5.
|
||||
|
||||
## 11. Haftung
|
||||
|
||||
(1) Der Buchungskalender ist ein Hilfsmittel für die Vermietung des Vermieters. Der Vertrag über die Unterkunft besteht allein zwischen dem Vermieter und dem Gast; WEBklar ist daran nicht beteiligt.
|
||||
|
||||
(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Buchungskalenders, aus Verzögerungen oder Fehlern beim Kalenderabgleich (Nr. 4), aus Doppelbuchungen und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
|
||||
|
||||
(3) WEBklar haftet nicht für Inhalte des Vermieters, insbesondere nicht für seine Unterkünfte, Preise, Steuerangaben, Stornobedingungen und Rechtstexte, auch nicht für den Muster-Text, den er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Gästen und den Behörden (Nr. 5).
|
||||
|
||||
(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 3 Abs. 3), und nicht für Ausfälle, Änderungen und Entscheidungen anderer Plattformen, deren Kalender der Vermieter einbindet.
|
||||
|
||||
(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Vermieters gegenüber seinen Gästen bleibt unberührt.
|
||||
|
||||
## 12. Änderungen und Einstellung
|
||||
|
||||
(1) WEBklar darf die Technik des Buchungskalenders verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Buchungskalender.
|
||||
|
||||
(2) WEBklar darf den Buchungskalender für eine Website mit einer Frist von vier Wochen beenden oder für alle Vermieter mit einer Frist von drei Monaten einstellen, wenn
|
||||
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Vermieter einer dafür nötigen neuen Fassung nicht zustimmt (AVV Buchungskalender Nummer 12),
|
||||
- Stripe die Anbindung nach Nr. 3 beendet oder wesentlich ändert, so dass die Online-Zahlung in der bisherigen Form nicht weiterbetrieben werden kann (dann entfällt nur die Online-Zahlung, soweit der Buchungskalender ohne sie weiterläuft), oder
|
||||
- eine Sicherheitslücke sich nicht anders beheben lässt.
|
||||
|
||||
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
|
||||
|
||||
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Buchungskalender ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Vermieter darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
|
||||
|
||||
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
|
||||
|
||||
## 13. Hinweise
|
||||
|
||||
(1) **Keine Rechtsberatung.** Der Buchungskalender und der Muster-Text sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche und steuerliche Einordnung seiner Vermietung, seiner Preisangaben und seines Angebots trägt der Vermieter.
|
||||
|
||||
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen der Tage, Bedienung mit Pfeiltasten, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Vermieter als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Vermieter.
|
||||
@@ -0,0 +1,194 @@
|
||||
# Leistungsbeschreibung Buchungskalender
|
||||
|
||||
**Fassung 2026-10-07** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Vertrag über den Betrieb der Website
|
||||
|
||||
## 1. Geltung
|
||||
|
||||
(1) Diese Leistungsbeschreibung regelt die Funktion „Buchungskalender“. Mit dem Buchungskalender nimmt der Kunde über seine Website Anfragen für die Vermietung von Unterkünften entgegen (Ferienwohnungen, Monteurzimmer, kleine Pensionen) und rechnet die Aufenthalte ab („Vermieter“); Gäste sind die Besucher seiner Website, die im Buchungskalender anfragen und buchen („Gäste“). Die Leistungsbeschreibung ist eine Zusatzleistung zum Vertrag über den Betrieb der Website: Sie setzt voraus, dass WEBklar die Website des Vermieters aufgrund eines Vertrags betreibt, etwa im Hosting-Abo, im Wartungs-Abo oder aufgrund eines anderen Vertrags über den Betrieb der Website. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung vermieten.
|
||||
|
||||
(2) Sie wird vereinbart, wenn der Vermieter die Einrichtung des Buchungskalenders im Kundenportal bestellt und dabei dieser Leistungsbeschreibung, dem Auftragsverarbeitungsvertrag Buchungskalender („AVV Buchungskalender“) und der Widerrufsbelehrung zustimmt. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Buchungskalender. Bestellen kann der Vermieter erst, nachdem WEBklar seine Anfrage für den Buchungskalender geprüft und angenommen hat (Nr. 2 Abs. 1). Für einen Vermieter, dem WEBklar die Funktion als Teil seines Website-Projekts bereitstellt, schaltet WEBklar sie nach seiner Zustimmung in Textform frei; die Bedingungen dieser Leistungsbeschreibung gelten dann ebenso, ein Einrichtungspreis fällt nur an, wenn er vereinbart ist.
|
||||
|
||||
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Buchungskalender diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Buchungskalender.
|
||||
|
||||
(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Buchungskalender nicht entgegen. Das Widget des Buchungskalenders läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und der Muster-Text zur Stornierung nach Nr. 6 ist Teil des Buchungskalenders. Das gilt auch im Hosting-Abo.
|
||||
|
||||
## 2. Leistung
|
||||
|
||||
(1) Der Vermieter fragt den Buchungskalender im Kundenportal an und beschreibt seine Unterkünfte (Art der Unterkunft, Zahl der Einheiten, ob er Booking.com oder Airbnb nutzt). WEBklar prüft die Anfrage und nimmt sie an oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Annahme besteht nicht. Erst nach der Annahme kann der Vermieter die Einrichtung bestellen.
|
||||
|
||||
(2) Die Einrichtung umfasst den Einbau eines Kalender-Widgets an einer Stelle der Website des Vermieters, die Bereitstellung der Verwaltung im Kundenportal, den Einbau des Datenschutz-Absatzes für den Buchungskalender (Nr. 5 Abs. 10) und die Freischaltung durch WEBklar. Für jede Website wird ein Buchungskalender eingerichtet; er kann mehrere Unterkünfte desselben Vermieters enthalten. Zusätzlich gibt es für jeden freigegebenen Aufenthalt eine Buchungsseite bei WEBklar, die der Gast über den Link in seiner E-Mail erreicht.
|
||||
|
||||
(3) So läuft eine Buchung ab:
|
||||
- Das Widget zeigt dem Gast die belegten Nächte. Er wählt Anreise und Abreise, gibt Personenzahl, Namen, E-Mail-Adresse und auf Wunsch Firma, Telefonnummer und Nachricht an und sendet eine **unverbindliche Anfrage**. Er erhält eine Eingangsbestätigung, der Vermieter eine Benachrichtigung.
|
||||
- Der Vermieter prüft die Anfrage im Kundenportal, sieht den vorgerechneten Preis, kann die Posten ändern und gibt die Anfrage mit einer Reservierungsfrist frei oder lehnt sie ab. Gibt er sie frei, reserviert der Buchungskalender die Nächte bis zum Ablauf der Frist, und der Gast erhält eine E-Mail mit Preis, Stornobedingungen und dem Link zur Buchungsseite.
|
||||
- Auf der Buchungsseite sieht der Gast alle Angaben zum Aufenthalt, trägt seine Rechnungsadresse ein, wählt die Zahlart (online über Stripe, wenn der Vermieter sie eingeschaltet hat, oder vor Ort) und bucht mit der Schaltfläche „Zahlungspflichtig buchen“. Erst damit kommt der Vertrag zwischen Vermieter und Gast zustande, nicht schon mit der Anfrage oder der Freigabe.
|
||||
- Mit der Buchung erzeugt der Buchungskalender eine Rechnung mit lückenloser Nummer und sendet dem Gast eine Buchungsbestätigung mit der Rechnung als PDF; der Vermieter wird benachrichtigt.
|
||||
- Verstreicht die Frist ohne Buchung, läuft die Reservierung ab und die Nächte werden frei; der Gast erhält darüber eine E-Mail.
|
||||
- Der Vermieter kann einen Aufenthalt stornieren. Dann erhält der Gast eine Stornorechnung mit eigener Nummer und, soweit der Vermieter eine Online-Zahlung erstattet, eine Mitteilung über den Erstattungsbetrag. Eine Stornierung durch den Gast gibt es im Widget nicht; er wendet sich an den Vermieter.
|
||||
|
||||
(4) Im Kundenportal verwaltet der Vermieter:
|
||||
- Unterkünfte mit Beschreibung, Höchstzahl der Personen, Mindestnächten und Check-in- und Check-out-Zeiten,
|
||||
- Preise (Staffel nach Aufenthaltsdauer, Endreinigung, Zusatzpersonen, Direktbuchungsrabatt, Steuerart und Steuersätze) und seine Stornobedingungen,
|
||||
- Anfragen und Buchungen mit Status, die Eintragung „bezahlt (vor Ort)“ und Stornierungen,
|
||||
- Kalender mit manuellen Sperren,
|
||||
- die Verbindungen zu anderen Kalendern (Nr. 4),
|
||||
- seine Rechnungen als PDF,
|
||||
- seine Angaben als Aussteller der Rechnungen (Name, Anschrift, Steuernummer oder Umsatzsteuer-Identifikationsnummer, Bankverbindung, Rechnungspräfix) und die Verbindung mit seinem Stripe-Konto (Nr. 3).
|
||||
|
||||
(5) Der Buchungskalender versendet im Namen des Vermieters E-Mails an die Gäste: Eingang der Anfrage, Freigabe mit Buchungslink, Ablehnung, Buchungsbestätigung mit Rechnung, Zahlungseingang, Ablauf der Reservierung und Stornierung. Absender ist die Versandadresse von WEBklar mit dem Namen des Vermieters, als Antwortadresse dient seine E-Mail-Adresse. Den Vermieter benachrichtigt der Buchungskalender über neue Anfragen und neue Buchungen sowie, wenn der Abgleich mit einem anderen Kalender seit mindestens sechs Stunden fehlschlägt.
|
||||
|
||||
(6) Der Buchungskalender enthält einen Missbrauchsschutz gegen automatisierte Anfragen (verstecktes Feld, Mindest-Ausfüllzeit, Obergrenzen je IP-Adresse). Dafür speichert WEBklar mit der Anfrage nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird entfernt, sobald der Vermieter die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 3 Abs. 4 des AVV Buchungskalender).
|
||||
|
||||
(7) Nicht enthalten sind:
|
||||
- der Meldeschein nach dem Bundesmeldegesetz und die Verwaltung der Meldedaten,
|
||||
- Kurtaxe, Gästebeitrag und Beherbergungsabgaben,
|
||||
- Anzahlungen und Teilzahlungen (der Gast zahlt den Gesamtbetrag in einer Zahlung, online oder vor Ort),
|
||||
- Sofortbuchung ohne Freigabe,
|
||||
- saisonale Preise (die Preise richten sich nur nach der Aufenthaltsdauer),
|
||||
- Gutscheine, Channel-Manager-Anbindungen außer dem Kalenderabgleich nach Nr. 4 und Mehrsprachigkeit (der Buchungskalender ist deutschsprachig),
|
||||
- die Buchführung des Vermieters.
|
||||
|
||||
Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Buchungskalender oder eine über das übliche Maß hinausgehende Zahl von Unterkünften, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 5).
|
||||
|
||||
## 3. Zahlungen über Stripe und Rechnungen im Namen des Vermieters
|
||||
|
||||
(1) Online-Zahlungen im Buchungskalender sind optional und laufen über Stripe Connect. Der Vermieter verbindet dazu sein eigenes Stripe-Konto mit dem Buchungskalender. Hat er für den Webshop von WEBklar bereits ein Stripe-Konto verbunden, kann er es auch für den Buchungskalender verwenden. Den Vertrag über dieses Konto schließt der Vermieter unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Vermieters durch Stripe und die Auszahlungen gelten die Bedingungen von Stripe. Ohne verbundenes und freigeschaltetes Stripe-Konto ist nur die Zahlart „vor Ort“ möglich.
|
||||
|
||||
(2) Das Geld der Gäste fließt direkt an den Vermieter (Zahlung unmittelbar auf dem Konto des Vermieters, sogenannte Direct Charge). WEBklar erhält und verwahrt kein Geld der Gäste, leitet keines weiter und beteiligt sich nicht am Umsatz des Vermieters. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Vermieter; sie fallen nur zwischen dem Vermieter und Stripe an.
|
||||
|
||||
(3) Der Vermieter prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt ihm im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Gästen und Zahlungsausfälle regeln der Vermieter und Stripe.
|
||||
|
||||
(4) Erstattungen bei einer Stornierung veranlasst der Vermieter im Kundenportal; WEBklar führt die Erstattung über das Stripe-Konto des Vermieters aus. Er bestimmt den Betrag bis zur Höhe der Zahlung des Gastes. Storniert der Vermieter einen Aufenthalt, schließt WEBklar eine noch offene Zahlungsseite bei Stripe. **Automatische Erstattung:** Geht eine Zahlung ein, nachdem die Buchung storniert oder die Reservierung abgelaufen ist, oder zahlt der Gast eine bereits bezahlte Buchung ein zweites Mal, erstattet WEBklar den Betrag ohne weitere Anweisung im Namen des Vermieters in voller Höhe über dessen Stripe-Konto. Dazu erteilt der Vermieter WEBklar mit der Zustimmung zum Buchungskalender eine dauerhafte Weisung; der Stand der Buchung ändert sich dadurch nicht. Erstattungen aus dem Stripe-Dashboard trägt WEBklar in die Buchung nach, soweit Stripe sie meldet.
|
||||
|
||||
(5) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Online-Zahlungen sind derzeit nur per Karte möglich, einschließlich Apple Pay und Google Pay; eine SEPA-Lastschrift bietet der Buchungskalender nicht an. Kartendaten gibt der Gast nur dort ein; WEBklar erhält sie nicht. Beim Start der Zahlung übergibt WEBklar Stripe als Rücksprungadresse (bei Erfolg und bei Abbruch) den geheimen Link zur Buchungsseite des Gastes samt Token. Dieser Link ist damit für Stripe und im Stripe-Dashboard des Vermieters sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite.
|
||||
|
||||
(6) **Rechnungen im Namen des Vermieters.** Der Vermieter beauftragt WEBklar mit der Zustimmung bei der Bestellung, in seinem Namen und für seine Rechnung Rechnungen und Stornorechnungen an seine Gäste auszustellen (Rechnung durch einen Dritten im Namen und im Auftrag des Leistenden, § 14 Abs. 2 Satz 4 UStG). Aussteller der Rechnung ist der Vermieter; sie trägt seinen Namen, seine Anschrift, seine Steuernummer oder Umsatzsteuer-Identifikationsnummer und keinen Hinweis auf WEBklar. WEBklar erzeugt die Rechnung aus den Angaben, die der Vermieter hinterlegt und bei der Freigabe bestätigt hat. Er verantwortet die Richtigkeit seiner Angaben, der Preise, der Steuerart und der Steuersätze; WEBklar prüft sie nicht steuerrechtlich. Die Rechnung wird nach ihrer Ausstellung nicht mehr verändert; Fehler korrigiert der Vermieter durch Stornierung und neue Buchung nach den Möglichkeiten des Kundenportals. Die Zustimmung zu diesem Auftrag gibt der Vermieter bei der Bestellung und bestätigt sie in den Einstellungen des Buchungskalenders. Widerruft er den Auftrag in Textform, stellt der Buchungskalender keine Rechnungen mehr aus und ist für ihn nicht mehr nutzbar.
|
||||
|
||||
(7) Die Rechnungsnummern sind je Vermieter und Jahr lückenlos fortlaufend; sie bestehen aus dem Präfix, das der Vermieter vor der ersten Rechnung festlegt, dem Jahr und einer laufenden Nummer.
|
||||
|
||||
## 4. Verfügbarkeit und Kalenderabgleich (iCal)
|
||||
|
||||
(1) Der Vermieter kann Kalender anderer Plattformen (zum Beispiel Booking.com oder Airbnb) über eine iCal-Adresse einlesen lassen; die eingelesenen Zeiträume blockiert der Buchungskalender. Umgekehrt stellt der Buchungskalender dem Vermieter eine geheime iCal-Adresse bereit, die er bei anderen Plattformen einträgt; sie enthält die belegten Nächte ohne Daten der Gäste. Der Vermieter kann diese Adresse jederzeit erneuern; die bisherige Adresse funktioniert dann nicht mehr.
|
||||
|
||||
(2) Der Abgleich läuft regelmäßig, aber zeitversetzt: WEBklar liest die Kalender anderer Plattformen etwa alle 15 Minuten ein, und die anderen Plattformen lesen die Adresse des Buchungskalenders nach ihrem eigenen Takt, oft nur alle paar Stunden. **Zwischen einer Buchung hier und ihrem Eingang dort können deshalb Minuten bis Stunden vergehen, und umgekehrt.** WEBklar sagt keine Echtzeit-Verfügbarkeit zu und übernimmt keine Gewähr dafür, dass eine Unterkunft nicht doppelt gebucht wird.
|
||||
|
||||
(3) Dieses Risiko mindert die Freigabe: Jede Anfrage im Buchungskalender braucht die Freigabe des Vermieters. Er prüft vor der Freigabe seine anderen Plattformen und seinen Kalender. Beim Freigeben und beim Buchen prüft der Buchungskalender zusätzlich, ob die Nächte nach seinem Stand frei sind. Eine Doppelbuchung entsteht trotzdem, wenn eine andere Plattform eine Buchung noch nicht gemeldet hat oder der Abgleich ausfällt. Der Vermieter löst eine Doppelbuchung selbst (etwa durch Stornierung) und trägt die Folgen gegenüber seinen Gästen und den Plattformen.
|
||||
|
||||
(4) Fehlerhafte oder nicht erreichbare iCal-Adressen kann WEBklar nicht beheben. Schlägt der Abruf fehl, bleibt der zuletzt gelesene Stand erhalten, und der Vermieter wird benachrichtigt, wenn der Abruf seit mindestens sechs Stunden nicht gelungen ist (höchstens einmal je 24 Stunden und Kalender). Eingelesen werden nur Zeiträume von gestern bis 18 Monate in die Zukunft, höchstens 1 Megabyte je Kalender; nur Adressen mit https werden abgerufen.
|
||||
|
||||
## 5. Pflichten des Vermieters
|
||||
|
||||
(1) Der Vermieter hat einen laufenden Vertrag mit WEBklar über den Betrieb seiner Website (Hosting-Abo, Wartungs-Abo oder ein anderer Vertrag über den Betrieb der Website), ist Unternehmer und vermietet in eigenem Namen. Will er Online-Zahlungen nutzen, stellt er sein eigenes Stripe-Konto bereit und schließt die Anmeldung bei Stripe selbst ab.
|
||||
|
||||
(2) Er verantwortet seine Unterkünfte, Preise und Angaben zum Angebot. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer, für die Beherbergung und die Endreinigung nach den dafür geltenden Steuersätzen, oder Hinweis auf § 19 UStG), Check-in- und Check-out-Zeiten, Höchstbelegung, Mindestnächte und alle Angaben, die er seinen Gästen vor der Buchung geben muss. Die Steuerart und die Steuersätze trägt er selbst im Kundenportal ein; WEBklar berät dazu nicht.
|
||||
|
||||
(3) Er legt vor der Freischaltung seine Stornobedingungen fest und verantwortet sie. Der Muster-Text nach Nr. 6 ist nur ein Vorschlag.
|
||||
|
||||
(4) Er verantwortet die Verfügbarkeit seiner Unterkünfte, pflegt Sperren und Kalenderverbindungen im Kundenportal und prüft regelmäßig auf neue Anfragen und Buchungen. Die Folgen von Angaben, die er setzt (zum Beispiel einer fälschlich freien oder gesperrten Zeit), trägt er.
|
||||
|
||||
(5) Er gibt Anfragen nur frei, wenn er die Unterkunft für den Zeitraum zu den angegebenen Bedingungen bereitstellen kann, und erfüllt die gebuchten Aufenthalte. Er trägt alle Pflichten eines Vermieters gegenüber seinen Gästen, auch Gewährleistung, Kundenservice und Beschwerden.
|
||||
|
||||
(6) Er erfüllt seine gesetzlichen Pflichten rund um die Beherbergung selbst. Das betrifft insbesondere den Meldeschein nach dem Bundesmeldegesetz, die Kurtaxe und andere kommunale Abgaben, Anzeige- und Genehmigungspflichten für die Vermietung (zum Beispiel Zweckentfremdungs- oder Gewerberecht) sowie die Pflichten aus seinem Steuerrecht. Der Buchungskalender nimmt ihm davon nichts ab.
|
||||
|
||||
(7) Anzahlungen und Teilzahlungen bietet der Buchungskalender nicht an. Vereinbart der Vermieter außerhalb des Buchungskalenders etwas anderes mit seinem Gast, ist das nicht Gegenstand dieser Leistungsbeschreibung.
|
||||
|
||||
(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem. WEBklar bewahrt die Rechnungen des Vermieters im Buchungskalender 8 Jahre auf, solange der Buchungskalender läuft und nach Nr. 10 bis zur Löschung, hält sie ihm zum Abruf als PDF bereit und anonymisiert personenbezogene Daten der Gäste nach Nummer 9 des AVV Buchungskalender. Der Vermieter lädt seine Rechnungen deshalb rechtzeitig vor der Löschung nach Nr. 10 Abs. 3 herunter und bewahrt sie selbst auf.
|
||||
|
||||
(9) Er hält die Vorgaben dieser Leistungsbeschreibung ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht.
|
||||
|
||||
(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Buchungskalender ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Vermieter den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Vermieter darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
|
||||
|
||||
(11) Der Vermieter informiert seine Gäste nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Buchungskalender: Der Vermieter ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Vermieters.
|
||||
|
||||
## 6. Muster-Text zur Stornierung, keine Rechtsberatung
|
||||
|
||||
(1) WEBklar stellt dem Vermieter einen Muster-Text für Stornobedingungen bereit. Er ist eine **Vorlage ohne Gewähr** und keine Rechtsberatung (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes). Er passt zur Technik des Buchungskalenders, kann aber auf den Betrieb des Vermieters nicht zugeschnitten sein.
|
||||
|
||||
(2) WEBklar empfiehlt, die Stornobedingungen und die Angaben zum Angebot vor der Freischaltung von einer Rechtsanwältin oder einem Rechtsanwalt prüfen zu lassen.
|
||||
|
||||
(3) Der Vermieter bleibt für seine Stornobedingungen und alle seine Texte verantwortlich, auch wenn er den Muster-Text unverändert übernimmt (Nr. 5 Abs. 3). Die Stornobedingungen, die bei der Freigabe galten, speichert der Buchungskalender mit der Anfrage; die Buchungsseite und die Buchungsbestätigung zeigen dem Gast diese Fassung.
|
||||
|
||||
## 7. Kein Widerrufsrecht des Gastes, Hinweise auf der Buchungsseite
|
||||
|
||||
(1) Für die Buchung von Beherbergungsleistungen zu einem bestimmten Zeitpunkt oder in einem bestimmten Zeitraum besteht nach § 312g Abs. 2 Satz 1 Nr. 9 BGB kein gesetzliches Widerrufsrecht. Die Buchungsseite weist den Gast darauf hin; der Vermieter kann ihm vertraglich ein weitergehendes Recht einräumen, der Buchungskalender bietet dafür aber keine Funktion.
|
||||
|
||||
(2) Die Buchungsseite zeigt dem Gast vor der Schaltfläche „Zahlungspflichtig buchen“ die wesentlichen Angaben nach Art. 246a EGBGB, soweit der Buchungskalender sie kennt (Anbieter, Unterkunft, Zeitraum, Leistung, Gesamtpreis, Zahlart, Stornobedingungen, Hinweis auf das fehlende Widerrufsrecht). Der Vermieter ergänzt, was darüber hinausgeht, in seinen Texten selbst (Nr. 5 Abs. 2).
|
||||
|
||||
(3) Die Buchungsbestätigung erhält der Gast als E-Mail mit der Rechnung als PDF; sie enthält die Angaben zum Vertrag und die Stornobedingungen auf einem dauerhaften Datenträger.
|
||||
|
||||
## 8. Verfügbarkeit und Störungen
|
||||
|
||||
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Buchungskalender. WEBklar sagt für den Buchungskalender keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Buchungskalenders beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
|
||||
|
||||
(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Buchungskalender kurzzeitig unterbrechen. Während einer Unterbrechung sind Anfragen und Buchungen nicht möglich; das Widget zeigt dann einen Ausweichtext.
|
||||
|
||||
(3) Zahlungen übernimmt der Buchungskalender über Benachrichtigungen von Stripe und einen Abgleich beim Aufruf der Buchungsseite. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Zahlung verzögern. Eine Zahlung, die nach einer Stornierung oder nach dem Ablauf der Reservierung eingeht, und eine zweite Zahlung auf eine bereits bezahlte Buchung erstattet WEBklar automatisch und in voller Höhe (Nr. 3 Abs. 4); die Buchung bleibt dabei unverändert, und die Erstattung braucht einige Tage, bis sie beim Gast ankommt. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.
|
||||
|
||||
(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
|
||||
|
||||
(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
|
||||
|
||||
(6) Die gesetzlichen Rechte des Vermieters sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
|
||||
|
||||
## 9. Preis, Abrechnung und Widerruf
|
||||
|
||||
(1) Die Einrichtung kostet 199 € einmalig je Website-Projekt, ohne Umsatzsteuer gemäß § 19 UStG. Für den Betrieb des Buchungskalenders berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz oder an den Buchungen. Der Buchungskalender läuft, solange WEBklar die Website betreibt (Nr. 10).
|
||||
|
||||
(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 1 bis 2 Wochen ab der Bestellung. Sie hängt davon ab, wie schnell der Vermieter Unterkünfte, Preise und gegebenenfalls das Stripe-Konto bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.
|
||||
|
||||
(3) Der Vermieter bezahlt bei der Bestellung im Kundenportal über Stripe und erhält eine Rechnung. Für einen Vermieter, dem WEBklar die Funktion als Teil seines Website-Projekts bereitstellt (Nr. 1 Abs. 2), gilt die dafür getroffene Vereinbarung.
|
||||
|
||||
(4) Der Preis steht neben den Leistungen eines Abos: Die Einrichtung wird weder auf ein Stundenkontingent angerechnet noch damit bezahlt.
|
||||
|
||||
(5) Mehraufwand, der nicht zur Einrichtung gehört (Nr. 2 Abs. 7), führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Vermieters in Textform aus. WEBklar nennt vorher den geschätzten Aufwand und berechnet 100 € je Stunde in Schritten von 15 Minuten, ohne Umsatzsteuer gemäß § 19 UStG (§ 4 der Abo-Bedingungen). Der Mehraufwand wird gesondert berechnet und nicht auf ein Stundenkontingent angerechnet.
|
||||
|
||||
(6) Verbraucher können die Bestellung der Einrichtung nach der Widerrufsbelehrung widerrufen. Belehrung und Muster-Widerrufsformular stehen in der Anlage „Widerrufsbelehrung und Muster-Widerrufsformular (Einrichtung von Funktionen)“. Ob das Widerrufsrecht erlischt und ob der Vermieter einen angemessenen Betrag für die bis dahin erbrachte Leistung schuldet, richtet sich nach der Zustimmung, die er bei der Bestellung gibt, und nach den Hinweisen zum Widerruf in der Anlage. Mit dem Widerruf endet der Buchungskalender. Dieses Widerrufsrecht des Vermieters gegenüber WEBklar ist vom Widerrufsrecht der Gäste gegenüber dem Vermieter (Nr. 7) zu unterscheiden.
|
||||
|
||||
## 10. Laufzeit und Ende
|
||||
|
||||
(1) Der Buchungskalender läuft, solange WEBklar die Website des Vermieters betreibt. Endet der Vertrag über den Betrieb der Website, endet auch der Buchungskalender; WEBklar schaltet ihn zum Vertragsende ab und informiert den Vermieter vorher per E-Mail und weist ihn darauf hin, seine Rechnungen als PDF herunterzuladen. Das gilt auch beim Widerruf dieses Vertrags.
|
||||
|
||||
(2) Mit dem Abschalten nimmt der Buchungskalender keine neuen Anfragen und Buchungen mehr an, und das Widget zeigt den Ausweichtext. Reservierungen laufen aus. Bereits gebuchte Aufenthalte bleiben Verträge zwischen dem Vermieter und seinen Gästen; der Vermieter erfüllt sie und informiert seine Gäste, wenn er den Buchungskalender nicht mehr anbietet. Anfragen, Buchungen und Rechnungen bleiben im Kundenportal 90 Tage lesbar; Rechnungen kann er einzeln als PDF herunterladen.
|
||||
|
||||
(3) WEBklar löscht alle Daten des Buchungskalenders, also Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Buchungskalender). Die Aufbewahrungsfrist der Rechnungen (Nr. 5 Abs. 8) endet damit mit der Löschung; der Vermieter bewahrt sie danach selbst auf. Nach dem Abschalten weist WEBklar den Vermieter per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern; bis dahin sind sie im Kundenportal abrufbar. Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 3 (90 Tage nach dem Abschalten); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird.
|
||||
|
||||
(4) Das Stripe-Konto bleibt beim Vermieter. Das Ende des Buchungskalenders berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Vermieter kann die Verbindung zwischen dem Konto und dem Buchungskalender im Stripe-Dashboard beenden.
|
||||
|
||||
(5) Beginnt innerhalb der 90 Tage wieder ein Vertrag über den Betrieb der Website, schaltet WEBklar den Buchungskalender auf Wunsch des Vermieters in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (neuer Preis nach Nr. 9).
|
||||
|
||||
(6) Endet der Vertrag über den Betrieb der Website, bevor WEBklar den Buchungskalender freigeschaltet hat, erstattet WEBklar den gezahlten Einrichtungspreis vollständig.
|
||||
|
||||
(7) Auf Wunsch des Vermieters in Textform schaltet WEBklar den Buchungskalender jederzeit ab. Eine Erstattung des Einrichtungspreises folgt daraus nicht; für den Widerruf gilt Nr. 9 Abs. 6.
|
||||
|
||||
## 11. Haftung
|
||||
|
||||
(1) Der Buchungskalender ist ein Hilfsmittel für die Vermietung des Vermieters. Der Vertrag über die Unterkunft besteht allein zwischen dem Vermieter und dem Gast; WEBklar ist daran nicht beteiligt.
|
||||
|
||||
(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Buchungskalenders, aus Verzögerungen oder Fehlern beim Kalenderabgleich (Nr. 4), aus Doppelbuchungen und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
|
||||
|
||||
(3) WEBklar haftet nicht für Inhalte des Vermieters, insbesondere nicht für seine Unterkünfte, Preise, Steuerangaben, Stornobedingungen und Rechtstexte, auch nicht für den Muster-Text, den er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Gästen und den Behörden (Nr. 5).
|
||||
|
||||
(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 3 Abs. 3), und nicht für Ausfälle, Änderungen und Entscheidungen anderer Plattformen, deren Kalender der Vermieter einbindet.
|
||||
|
||||
(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Vermieters gegenüber seinen Gästen bleibt unberührt.
|
||||
|
||||
## 12. Änderungen und Einstellung
|
||||
|
||||
(1) WEBklar darf die Technik des Buchungskalenders verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Buchungskalender.
|
||||
|
||||
(2) WEBklar darf den Buchungskalender für eine Website mit einer Frist von vier Wochen beenden oder für alle Vermieter mit einer Frist von drei Monaten einstellen, wenn
|
||||
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Vermieter einer dafür nötigen neuen Fassung nicht zustimmt (AVV Buchungskalender Nummer 12),
|
||||
- Stripe die Anbindung nach Nr. 3 beendet oder wesentlich ändert, so dass die Online-Zahlung in der bisherigen Form nicht weiterbetrieben werden kann (dann entfällt nur die Online-Zahlung, soweit der Buchungskalender ohne sie weiterläuft), oder
|
||||
- eine Sicherheitslücke sich nicht anders beheben lässt.
|
||||
|
||||
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
|
||||
|
||||
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Buchungskalender ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Vermieter darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
|
||||
|
||||
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
|
||||
|
||||
## 13. Hinweise
|
||||
|
||||
(1) **Keine Rechtsberatung.** Der Buchungskalender und der Muster-Text sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche und steuerliche Einordnung seiner Vermietung, seiner Preisangaben und seines Angebots trägt der Vermieter.
|
||||
|
||||
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen der Tage, Bedienung mit Pfeiltasten, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Vermieter als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Vermieter.
|
||||
@@ -0,0 +1,179 @@
|
||||
# Leistungsbeschreibung Webshop
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
|
||||
|
||||
## 1. Geltung
|
||||
|
||||
(1) Diese Leistungsbeschreibung regelt die Funktion „Webshop“. Mit dem Webshop verkauft der Kunde Waren über seine Website („Händler“); Käufer sind die Besucher seiner Website, die im Webshop bestellen („Käufer“). Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung verkaufen.
|
||||
|
||||
(2) Diese Leistungsbeschreibung gilt, wenn der Händler die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Händler die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Webshop. Einschalten kann der Händler erst, nachdem WEBklar seine Anfrage für den Webshop geprüft und freigegeben hat (Nr. 2 Abs. 1).
|
||||
|
||||
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Webshop diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Webshop.
|
||||
|
||||
(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Webshop nicht entgegen. Das Widget des Webshops läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und die Muster-Rechtstexte nach Nr. 6 sind Teil des Webshops.
|
||||
|
||||
## 2. Leistung
|
||||
|
||||
(1) Der Händler fragt den Webshop im Kundenportal an und beschreibt, was er verkauft. WEBklar prüft die Anfrage, vor allem nach Nr. 3, und gibt sie frei oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Freigabe besteht nicht. Erst nach der Freigabe kann der Händler den Webshop einschalten.
|
||||
|
||||
(2) Die Einrichtung umfasst den Einbau eines Shop-Widgets an einer Stelle der Website des Händlers, die Seite des Webshops mit einem Verweis im Menü und den Link „Vertrag widerrufen“ in der Fußzeile der Seiten der Website, außerdem die Freischaltung durch WEBklar. Für jede Website wird ein Webshop eingerichtet. Zusätzlich steht dem Händler eine eigenständige Seite des Webshops bei WEBklar zur Verfügung (Standalone-Seite), etwa als Vorschau.
|
||||
|
||||
(3) Im Kundenportal verwaltet der Händler:
|
||||
- Artikel mit Beschreibung, Bildern, Preis, Varianten, Bestand und den Pflichtangaben zum Artikel,
|
||||
- Versand (nur nach Deutschland und Österreich) und Abholung mit Versandkosten und Lieferzeiten,
|
||||
- Bestellungen mit Status (zum Beispiel bezahlt, versendet, abholbereit, abgeschlossen), die er als CSV-Datei exportieren kann,
|
||||
- Erstattungen, ganz oder teilweise, über sein Stripe-Konto (Nr. 4),
|
||||
- die Widerrufe, die Käufer über die Widerrufsfunktion erklärt haben,
|
||||
- die Verbindung mit seinem Stripe-Konto, seine Einstellungen und seine Rechtstexte, deren bestätigte Fassungen WEBklar archiviert (Nr. 6 Abs. 5).
|
||||
|
||||
(4) Der Webshop enthält die Widerrufsfunktion nach § 356a BGB: Das Widget zeigt in seiner Fußzeile und die Standalone-Seite zeigt unter einer eigenen Adresse die Schaltfläche „Vertrag widerrufen“. Sie führt zu einem Formular und danach zur Bestätigungsschaltfläche „Widerruf bestätigen“. WEBklar speichert den Eingang, sendet dem Erklärenden sofort eine Eingangsbestätigung mit Inhalt, Datum und Uhrzeit und benachrichtigt den Händler. Die Funktion ist ohne Anmeldung erreichbar, auch wenn zur Erklärung keine Bestellung gefunden wird. Der Händler bearbeitet jeden Widerruf selbst (Nr. 5 Abs. 6).
|
||||
|
||||
(5) Der Webshop versendet im Namen des Händlers E-Mails an die Käufer: Eingang der Bestellung (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, vor dem Vertragsschluss), Bestellbestätigung, Versand, Abholbereitschaft, Eingangsbestätigung eines Widerrufs, Erstattung und Hinweis auf eine fehlgeschlagene Zahlung. Absender ist die Versandadresse von WEBklar mit dem Namen des Shops, als Antwortadresse dient die E-Mail-Adresse des Händlers. Den Händler benachrichtigt der Webshop über neue Bestellungen und eingegangene Widerrufe.
|
||||
|
||||
(6) Der Webshop enthält einen Missbrauchsschutz gegen automatisierte Bestellungen und Widerrufe, darunter eine Obergrenze von drei offenen Bestellungen je IP-Adresse in 30 Minuten. Dafür speichert WEBklar mit der offenen Bestellung nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird gelöscht, sobald die Bestellung bezahlt oder storniert wird (Nummer 3 Abs. 4 des AVV Webshop). Eine Telefonnummer des Käufers wird nicht erhoben und nicht gespeichert. Die Löschfristen für Bestelldaten nennt Nummer 9 des AVV Webshop.
|
||||
|
||||
(7) Nicht enthalten sind der Verkauf an Verbraucher außerhalb von Deutschland und Österreich, Rechnungen (Nr. 5 Abs. 7), die Verwaltung mehrerer Lager, Rabattcodes, Marktplatzfunktionen, die Anbindung an Warenwirtschaft oder Versanddienstleister, Mehrsprachigkeit und die Zahlung per PayPal (Nr. 4 Abs. 5). Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Webshop oder eine größere Zahl von Artikeln als vorgesehen, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 4).
|
||||
|
||||
## 3. Ausgeschlossene Waren und Leistungen
|
||||
|
||||
(1) Über den Webshop dürfen nicht verkauft werden:
|
||||
- alkoholische Getränke,
|
||||
- Tabakwaren, E-Zigaretten und Zubehör,
|
||||
- Arzneimittel und Medizinprodukte,
|
||||
- Waffen, Munition, Feuerwerk und Sprengstoffe,
|
||||
- Waren mit erotischem oder pornografischem Inhalt,
|
||||
- lebende Tiere,
|
||||
- digitale Inhalte und Downloads,
|
||||
- Gutscheine, Geschenkkarten und Guthaben,
|
||||
- Abonnements und andere wiederkehrende Zahlungen,
|
||||
- Waren und Leistungen, die nach der Liste der verbotenen und eingeschränkten Geschäfte von Stripe nicht erlaubt sind, und
|
||||
- Waren, deren Verkauf oder Versand nach deutschem, österreichischem oder europäischem Recht verboten ist.
|
||||
|
||||
(2) Der Händler liefert nur nach Deutschland und Österreich. Eine Lieferung in andere Länder bietet der Webshop nicht an.
|
||||
|
||||
(3) Der Händler bestätigt bei der Anfrage und beim Einschalten, dass er keine dieser Waren und Leistungen verkauft. WEBklar darf eine Anfrage ablehnen, wenn der Händler Waren nach Absatz 1 verkaufen will oder die Angaben dazu nicht ausreichen. Verkauft der Händler später Waren nach Absatz 1, darf WEBklar den Webshop nach einem Hinweis in Textform abschalten; bei einem Verstoß, der Rechte Dritter oder Käufer gefährdet, darf WEBklar ihn auch ohne vorherigen Hinweis abschalten und informiert den Händler unverzüglich. Für das Abschalten gilt Nr. 10 Abs. 2 und 3. Die bereits auf das Stundenkontingent angerechnete Einrichtungszeit wird daraus nicht gutgeschrieben, wenn der Händler den Verstoß zu vertreten hat.
|
||||
|
||||
## 4. Zahlungen über Stripe
|
||||
|
||||
(1) Zahlungen im Webshop laufen über Stripe Connect. Der Händler verbindet dazu sein eigenes Stripe-Konto mit dem Webshop. Den Vertrag über dieses Konto schließt der Händler unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Händlers durch Stripe (zum Beispiel zur Identität) und die Auszahlungen gelten die Bedingungen von Stripe.
|
||||
|
||||
(2) Das Geld der Käufer fließt direkt an den Händler. WEBklar erhält und verwahrt kein Geld der Käufer, leitet keines weiter und beteiligt sich nicht am Umsatz des Händlers. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Händler; sie fallen nur zwischen dem Händler und Stripe an.
|
||||
|
||||
(3) Der Händler prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt dem Händler im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Käufern und Zahlungsausfälle regeln der Händler und Stripe.
|
||||
|
||||
(4) Der Händler erstattet Zahlungen im Kundenportal oder im Stripe-Dashboard. WEBklar trägt Erstattungen aus dem Stripe-Dashboard in die Bestellung nach. Eine Erstattung führt der Händler rechtzeitig aus (Nr. 5 Abs. 6).
|
||||
|
||||
(5) Der Webshop bietet die Zahlungsarten an, die Stripe im Checkout für das Konto des Händlers bereitstellt und der Händler in seinen Einstellungen wählt, zum Beispiel Karte und SEPA-Lastschrift. PayPal ist über Stripe für diese Art der Anbindung nicht verfügbar und deshalb nicht Teil des Webshops.
|
||||
|
||||
(6) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Kartendaten und Kontodaten gibt der Käufer nur dort ein; WEBklar erhält sie nicht.
|
||||
|
||||
## 5. Pflichten des Händlers
|
||||
|
||||
(1) Der Händler hat ein laufendes Wartungs-Abo, ist Unternehmer und stellt sein eigenes Stripe-Konto bereit; die Anmeldung bei Stripe schließt er selbst ab. Der Webshop steht nur Kunden zur Verfügung, deren Kundenart „Betrieb“ ist. Der Händler verkauft in eigenem Namen. Für seine Produkte, Preise, Produktangaben und Rechtstexte (Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Impressum, Datenschutzerklärung) ist er selbst verantwortlich; WEBklar stellt Muster bereit und berät nicht rechtlich. Er verkauft keine der in Nr. 3 ausgeschlossenen Waren. Die Zahlungen seiner Käufer laufen über sein eigenes Stripe-Konto zu den Bedingungen von Stripe; WEBklar erhält und verwahrt kein Geld seiner Käufer und erhebt keine Umsatzbeteiligung.
|
||||
|
||||
(2) Er verantwortet seine Rechtstexte: Impressum, Datenschutzerklärung, Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular sowie die Angaben zu Versand und Zahlung. Die Mustertexte nach Nr. 6 prüft er, passt sie an seinen Betrieb an und bestätigt sie im Kundenportal, bevor er den Webshop öffnet. Er trägt vor dem Einschalten die Adressen von Impressum und Datenschutzerklärung seiner Website ein.
|
||||
|
||||
(3) Er verantwortet seine Artikel, Preise und Produktangaben. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer oder Hinweis auf § 19 UStG), Grundpreise, Versandkosten und Lieferzeiten, die Pflichtangaben für Lebensmittel (zum Beispiel Zutaten, Allergene, Füllmenge, verantwortlicher Lebensmittelunternehmer), die Angaben zur Produktsicherheit (zum Beispiel Hersteller, Warn- und Sicherheitshinweise), Widerrufsausschlüsse bei einzelnen Artikeln und die Rechte an seinen Texten und Bildern.
|
||||
|
||||
(4) Er liefert die Waren, wie bestellt und beschrieben, und trägt alle Pflichten eines Verkäufers gegenüber seinen Käufern, auch Gewährleistung, Kundenservice und Beschwerden.
|
||||
|
||||
(5) Er pflegt Bestand, Bestellstatus und Versandstatus im Kundenportal. Die Folgen falscher Angaben oder Statusmeldungen, die der Händler setzt, trägt er.
|
||||
|
||||
(6) Er bearbeitet Widerrufe und Erstattungen selbst, fristgerecht und nach dem Gesetz. Er entfernt den Link „Vertrag widerrufen“ nicht und verändert die Widerrufsfunktion nicht. Er prüft das Kundenportal regelmäßig auf neue Bestellungen und Widerrufe und lässt sich über die Benachrichtigungen informieren.
|
||||
|
||||
(7) Rechnungen und Belege an die Käufer erstellt der Händler selbst; WEBklar stellt sie nicht aus. Die Quittung von Stripe und die Bestellbestätigung des Webshops ersetzen keine Rechnung nach dem Umsatzsteuergesetz, soweit der Händler eine ausstellen muss.
|
||||
|
||||
(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem (das Archiv der Rechtstexte nach Nr. 6 Abs. 5 dient nur dem Nachweis der Texte): Personenbezogene Käuferdaten werden nach Nummer 9 des AVV Webshop anonymisiert und nach dem Ende des Webshops gelöscht. Der Händler exportiert seine Bestellungen deshalb rechtzeitig als CSV-Datei und bewahrt sie mit den Belegen von Stripe auf.
|
||||
|
||||
(9) Er hält die Vorgaben nach Nr. 3 ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht.
|
||||
|
||||
(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Webshop ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Händler den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Händler darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
|
||||
|
||||
(11) Der Händler informiert seine Käufer nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Webshop: Der Händler ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Händlers.
|
||||
|
||||
## 6. Muster-Rechtstexte, keine Rechtsberatung
|
||||
|
||||
(1) WEBklar stellt dem Händler Mustertexte bereit: eine Muster-Widerrufsbelehrung, ein Muster-Widerrufsformular, Allgemeine Geschäftsbedingungen und den Text zu Versand und Zahlung. Das Widget zeigt außerdem die Hinweise, die für die Kasse und die Bestellbestätigung vorgesehen sind, darunter den Hinweis auf die gesetzliche Gewährleistung.
|
||||
|
||||
(2) Die Mustertexte sind **Muster ohne Gewähr**. Sie passen zur Standardkonfiguration des Webshops und können auf den Betrieb des Händlers nicht zugeschnitten sein. WEBklar sorgt dafür, dass sie die Technik des Webshops richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes).
|
||||
|
||||
(3) WEBklar empfiehlt, die Rechtstexte vor der Öffnung des Webshops durch eine Rechtsanwältin oder einen Rechtsanwalt prüfen zu lassen, besonders bei besonderen Waren (zum Beispiel Lebensmitteln, Kosmetik, Textilien mit Pflegekennzeichnung, Elektrogeräten).
|
||||
|
||||
(4) Der Händler bleibt für seine Rechtstexte verantwortlich, auch wenn er Mustertexte unverändert übernimmt (Nr. 5 Abs. 2).
|
||||
|
||||
(5) Bestätigt der Händler seine Rechtstexte im Kundenportal, legt WEBklar jede bestätigte Fassung mit Prüfsumme, Zeitpunkt und der bestätigenden Person im Archiv ab. Die Bestellbestätigung zeigt dem Käufer die archivierte Fassung, die zum Zeitpunkt seiner Bestellung galt. WEBklar löscht das Archiv mit den übrigen Daten des Webshops (Nr. 10 Abs. 3; Nummer 8 Abs. 5 des AVV Webshop). Der Händler bewahrt die Fassungen, die er für eigene Aufbewahrungspflichten braucht, selbst auf (Nr. 5 Abs. 8).
|
||||
|
||||
## 7. Verfügbarkeit und Störungen
|
||||
|
||||
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Webshop. WEBklar sagt für den Webshop keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Webshops beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
|
||||
|
||||
(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Webshop kurzzeitig unterbrechen. Während einer Unterbrechung sind Bestellungen im Widget nicht möglich; das Widget zeigt dann einen Ausweichtext.
|
||||
|
||||
(3) Zahlungen übernimmt der Webshop über Benachrichtigungen von Stripe und einen regelmäßigen Abgleich in die Bestellung. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Bestellung verzögern. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.
|
||||
|
||||
(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
|
||||
|
||||
(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
|
||||
|
||||
(6) Die gesetzlichen Rechte des Händlers sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
|
||||
|
||||
## 8. Haftung
|
||||
|
||||
(1) Der Webshop ist ein Hilfsmittel für den Verkauf des Händlers. Der Vertrag über die Ware besteht allein zwischen dem Händler und dem Käufer; WEBklar ist daran nicht beteiligt.
|
||||
|
||||
(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Webshops, aus Fehlern bei Bestand oder Bestellstatus und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
|
||||
|
||||
(3) WEBklar haftet nicht für Inhalte des Händlers, insbesondere nicht für seine Artikel, Preise, Produktangaben und Rechtstexte, auch nicht für Mustertexte, die er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Käufern (Nr. 5).
|
||||
|
||||
(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 4 Abs. 3).
|
||||
|
||||
(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Händlers gegenüber seinen Käufern bleibt unberührt.
|
||||
|
||||
## 9. Einrichtung, Abrechnung und Widerruf
|
||||
|
||||
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Händlers. Für den Betrieb des Webshops berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz.
|
||||
|
||||
(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 2 bis 3 Wochen ab dem Einschalten. Sie hängt davon ab, wie schnell der Händler Stripe-Konto, Artikel und Rechtstexte bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.
|
||||
|
||||
(3) Hat WEBklar die Funktion auf Wunsch des Händlers schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
|
||||
|
||||
(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Händlers in Textform aus; es gilt § 4 der Abo-Bedingungen. Welche Wünsche nicht zur Einrichtung gehören, beschreibt Nr. 2 Abs. 7.
|
||||
|
||||
(5) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion. Hat der Händler die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
|
||||
|
||||
## 10. Laufzeit und Ende
|
||||
|
||||
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Händler per E-Mail.
|
||||
|
||||
(2) Mit dem Abschalten nimmt der Webshop keine neuen Bestellungen mehr an, und das Widget zeigt den Ausweichtext. Bestellungen, die noch nicht bezahlt sind, laufen aus. Die Bestellungen und Widerrufe bleiben im Kundenportal 90 Tage lesbar, und der Händler kann sie als CSV-Datei exportieren. Die Widerrufsfunktion bleibt bis zur Löschung nach Absatz 3 erreichbar, soweit das Widget noch eingebunden ist. Der Händler erfüllt seine Pflichten gegenüber den Käufern weiter (Nr. 5) und informiert sie, wenn er den Webshop nicht mehr anbietet.
|
||||
|
||||
(3) WEBklar löscht alle Daten des Webshops, also Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6 des AVV Webshop).
|
||||
|
||||
(4) Das Stripe-Konto bleibt beim Händler. Das Ende des Webshops berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Händler kann die Verbindung zwischen dem Konto und dem Webshop im Stripe-Dashboard beenden.
|
||||
|
||||
(5) Beginnt innerhalb der 90 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Webshop auf Wunsch des Händlers in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (Einrichtung nach Nr. 9).
|
||||
|
||||
(6) Endet das Wartungs-Abo, bevor WEBklar den Webshop freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 9 Abs. 1 verfällt.
|
||||
|
||||
(7) Auf Wunsch des Händlers in Textform schaltet WEBklar den Webshop jederzeit ab; für den Widerruf gilt Nr. 9 Abs. 5.
|
||||
|
||||
## 11. Änderungen und Einstellung
|
||||
|
||||
(1) WEBklar darf die Technik des Webshops verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Webshop.
|
||||
|
||||
(2) WEBklar darf den Webshop für eine Website mit einer Frist von vier Wochen beenden oder für alle Händler mit einer Frist von drei Monaten einstellen, wenn
|
||||
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Händler einer dafür nötigen neuen Fassung nicht zustimmt (AVV Webshop Nummer 12),
|
||||
- Stripe die Anbindung nach Nr. 4 beendet oder wesentlich ändert, so dass der Webshop in der bisherigen Form nicht weiterbetrieben werden kann, oder
|
||||
- eine Sicherheitslücke sich nicht anders beheben lässt.
|
||||
|
||||
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
|
||||
|
||||
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Webshop ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Händler darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
|
||||
|
||||
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
|
||||
|
||||
## 12. Hinweise
|
||||
|
||||
(1) **Keine Rechtsberatung.** Der Webshop und die Mustertexte sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche Einordnung seiner Waren, seiner Preisangaben und seines Angebots trägt der Händler.
|
||||
|
||||
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Händler als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Händler.
|
||||
@@ -0,0 +1,181 @@
|
||||
# Leistungsbeschreibung Webshop
|
||||
|
||||
**Fassung 2026-10-05** · Zusatzbedingungen zum Vertrag über den Betrieb der Website
|
||||
|
||||
## 1. Geltung
|
||||
|
||||
(1) Diese Leistungsbeschreibung regelt die Funktion „Webshop“. Mit dem Webshop verkauft der Kunde Waren über seine Website („Händler“); Käufer sind die Besucher seiner Website, die im Webshop bestellen („Käufer“). Die Leistungsbeschreibung ist eine Zusatzleistung zum Vertrag über den Betrieb der Website: Sie setzt voraus, dass WEBklar die Website des Händlers aufgrund eines Vertrags betreibt, etwa im Hosting-Abo, im Wartungs-Abo oder aufgrund eines anderen Vertrags über den Betrieb der Website. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung verkaufen.
|
||||
|
||||
(2) Sie wird vereinbart, wenn der Händler die Einrichtung des Webshops im Kundenportal bestellt und dabei dieser Leistungsbeschreibung, dem Auftragsverarbeitungsvertrag Webshop („AVV Webshop“) und der Widerrufsbelehrung zustimmt. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Webshop. Bestellen kann der Händler erst, nachdem WEBklar seine Anfrage für den Webshop geprüft und freigegeben hat (Nr. 2 Abs. 1).
|
||||
|
||||
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Webshop diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Webshop.
|
||||
|
||||
(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Webshop nicht entgegen. Das Widget des Webshops läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und die Muster-Rechtstexte nach Nr. 6 sind Teil des Webshops. Das gilt auch im Hosting-Abo.
|
||||
|
||||
## 2. Leistung
|
||||
|
||||
(1) Der Händler fragt den Webshop im Kundenportal an und beschreibt, was er verkauft. WEBklar prüft die Anfrage, vor allem nach Nr. 3, und gibt sie frei oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Freigabe besteht nicht. Erst nach der Freigabe kann der Händler die Einrichtung bestellen.
|
||||
|
||||
(2) Die Einrichtung umfasst den Einbau eines Shop-Widgets an einer Stelle der Website des Händlers, die Seite des Webshops mit einem Verweis im Menü und den Link „Vertrag widerrufen“ in der Fußzeile der Seiten der Website, außerdem die Freischaltung durch WEBklar. Für jede Website wird ein Webshop eingerichtet. Zusätzlich steht dem Händler eine eigenständige Seite des Webshops bei WEBklar zur Verfügung (Standalone-Seite), etwa als Vorschau.
|
||||
|
||||
(3) Im Kundenportal verwaltet der Händler:
|
||||
- Artikel mit Beschreibung, Bildern, Preis, Varianten, Bestand und den Pflichtangaben zum Artikel,
|
||||
- Versand (nur nach Deutschland und Österreich) und Abholung mit Versandkosten und Lieferzeiten,
|
||||
- Bestellungen mit Status (zum Beispiel bezahlt, versendet, abholbereit, abgeschlossen), die er als CSV-Datei exportieren kann,
|
||||
- Erstattungen, ganz oder teilweise, über sein Stripe-Konto (Nr. 4),
|
||||
- die Widerrufe, die Käufer über die Widerrufsfunktion erklärt haben,
|
||||
- die Verbindung mit seinem Stripe-Konto, seine Einstellungen und seine Rechtstexte, deren bestätigte Fassungen WEBklar archiviert (Nr. 6 Abs. 5).
|
||||
|
||||
(4) Der Webshop enthält die Widerrufsfunktion nach § 356a BGB: Das Widget zeigt in seiner Fußzeile und die Standalone-Seite zeigt unter einer eigenen Adresse die Schaltfläche „Vertrag widerrufen“. Sie führt zu einem Formular und danach zur Bestätigungsschaltfläche „Widerruf bestätigen“. WEBklar speichert den Eingang, sendet dem Erklärenden sofort eine Eingangsbestätigung mit Inhalt, Datum und Uhrzeit und benachrichtigt den Händler. Die Funktion ist ohne Anmeldung erreichbar, auch wenn zur Erklärung keine Bestellung gefunden wird. Der Händler bearbeitet jeden Widerruf selbst (Nr. 5 Abs. 6).
|
||||
|
||||
(5) Der Webshop versendet im Namen des Händlers E-Mails an die Käufer: Eingang der Bestellung (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, vor dem Vertragsschluss), Bestellbestätigung, Versand, Abholbereitschaft, Eingangsbestätigung eines Widerrufs, Erstattung und Hinweis auf eine fehlgeschlagene Zahlung. Absender ist die Versandadresse von WEBklar mit dem Namen des Shops, als Antwortadresse dient die E-Mail-Adresse des Händlers. Den Händler benachrichtigt der Webshop über neue Bestellungen und eingegangene Widerrufe.
|
||||
|
||||
(6) Der Webshop enthält einen Missbrauchsschutz gegen automatisierte Bestellungen und Widerrufe, darunter eine Obergrenze von drei offenen Bestellungen je IP-Adresse in 30 Minuten. Dafür speichert WEBklar mit der offenen Bestellung nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird gelöscht, sobald die Bestellung bezahlt oder storniert wird (Nummer 3 Abs. 4 des AVV Webshop). Eine Telefonnummer des Käufers wird nicht erhoben und nicht gespeichert. Die Löschfristen für Bestelldaten nennt Nummer 9 des AVV Webshop.
|
||||
|
||||
(7) Nicht enthalten sind der Verkauf an Verbraucher außerhalb von Deutschland und Österreich, Rechnungen (Nr. 5 Abs. 7), die Verwaltung mehrerer Lager, Rabattcodes, Marktplatzfunktionen, die Anbindung an Warenwirtschaft oder Versanddienstleister, Mehrsprachigkeit und die Zahlung per PayPal (Nr. 4 Abs. 5). Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Webshop oder eine größere Zahl von Artikeln als vorgesehen, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 5).
|
||||
|
||||
## 3. Ausgeschlossene Waren und Leistungen
|
||||
|
||||
(1) Über den Webshop dürfen nicht verkauft werden:
|
||||
- alkoholische Getränke,
|
||||
- Tabakwaren, E-Zigaretten und Zubehör,
|
||||
- Arzneimittel und Medizinprodukte,
|
||||
- Waffen, Munition, Feuerwerk und Sprengstoffe,
|
||||
- Waren mit erotischem oder pornografischem Inhalt,
|
||||
- lebende Tiere,
|
||||
- digitale Inhalte und Downloads,
|
||||
- Gutscheine, Geschenkkarten und Guthaben,
|
||||
- Abonnements und andere wiederkehrende Zahlungen,
|
||||
- Waren und Leistungen, die nach der Liste der verbotenen und eingeschränkten Geschäfte von Stripe nicht erlaubt sind, und
|
||||
- Waren, deren Verkauf oder Versand nach deutschem, österreichischem oder europäischem Recht verboten ist.
|
||||
|
||||
(2) Der Händler liefert nur nach Deutschland und Österreich. Eine Lieferung in andere Länder bietet der Webshop nicht an.
|
||||
|
||||
(3) Der Händler bestätigt bei der Anfrage und bei der Bestellung, dass er keine dieser Waren und Leistungen verkauft. WEBklar darf eine Anfrage ablehnen, wenn der Händler Waren nach Absatz 1 verkaufen will oder die Angaben dazu nicht ausreichen. Verkauft der Händler später Waren nach Absatz 1, darf WEBklar den Webshop nach einem Hinweis in Textform abschalten; bei einem Verstoß, der Rechte Dritter oder Käufer gefährdet, darf WEBklar ihn auch ohne vorherigen Hinweis abschalten und informiert den Händler unverzüglich. Für das Abschalten gilt Nr. 10 Abs. 2 und 3. Eine Erstattung des Einrichtungspreises folgt daraus nicht, wenn der Händler den Verstoß zu vertreten hat.
|
||||
|
||||
## 4. Zahlungen über Stripe
|
||||
|
||||
(1) Zahlungen im Webshop laufen über Stripe Connect. Der Händler verbindet dazu sein eigenes Stripe-Konto mit dem Webshop. Den Vertrag über dieses Konto schließt der Händler unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Händlers durch Stripe (zum Beispiel zur Identität) und die Auszahlungen gelten die Bedingungen von Stripe.
|
||||
|
||||
(2) Das Geld der Käufer fließt direkt an den Händler. WEBklar erhält und verwahrt kein Geld der Käufer, leitet keines weiter und beteiligt sich nicht am Umsatz des Händlers. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Händler; sie fallen nur zwischen dem Händler und Stripe an.
|
||||
|
||||
(3) Der Händler prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt dem Händler im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Käufern und Zahlungsausfälle regeln der Händler und Stripe.
|
||||
|
||||
(4) Der Händler erstattet Zahlungen im Kundenportal oder im Stripe-Dashboard. WEBklar trägt Erstattungen aus dem Stripe-Dashboard in die Bestellung nach. Eine Erstattung führt der Händler rechtzeitig aus (Nr. 5 Abs. 6).
|
||||
|
||||
(5) Der Webshop bietet die Zahlungsarten an, die Stripe im Checkout für das Konto des Händlers bereitstellt und der Händler in seinen Einstellungen wählt, zum Beispiel Karte und SEPA-Lastschrift. PayPal ist über Stripe für diese Art der Anbindung nicht verfügbar und deshalb nicht Teil des Webshops.
|
||||
|
||||
(6) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Kartendaten und Kontodaten gibt der Käufer nur dort ein; WEBklar erhält sie nicht.
|
||||
|
||||
## 5. Pflichten des Händlers
|
||||
|
||||
(1) Der Händler hat einen laufenden Vertrag mit WEBklar über den Betrieb seiner Website (Hosting-Abo, Wartungs-Abo oder ein anderer Vertrag über den Betrieb der Website), ist Unternehmer und verkauft in eigenem Namen. Er stellt sein eigenes Stripe-Konto bereit und schließt die Anmeldung bei Stripe selbst ab.
|
||||
|
||||
(2) Er verantwortet seine Rechtstexte: Impressum, Datenschutzerklärung, Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular sowie die Angaben zu Versand und Zahlung. Die Mustertexte nach Nr. 6 prüft er, passt sie an seinen Betrieb an und bestätigt sie im Kundenportal, bevor er den Webshop öffnet. Er trägt vor der Bestellung die Adressen von Impressum und Datenschutzerklärung seiner Website ein.
|
||||
|
||||
(3) Er verantwortet seine Artikel, Preise und Produktangaben. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer oder Hinweis auf § 19 UStG), Grundpreise, Versandkosten und Lieferzeiten, die Pflichtangaben für Lebensmittel (zum Beispiel Zutaten, Allergene, Füllmenge, verantwortlicher Lebensmittelunternehmer), die Angaben zur Produktsicherheit (zum Beispiel Hersteller, Warn- und Sicherheitshinweise), Widerrufsausschlüsse bei einzelnen Artikeln und die Rechte an seinen Texten und Bildern.
|
||||
|
||||
(4) Er liefert die Waren, wie bestellt und beschrieben, und trägt alle Pflichten eines Verkäufers gegenüber seinen Käufern, auch Gewährleistung, Kundenservice und Beschwerden.
|
||||
|
||||
(5) Er pflegt Bestand, Bestellstatus und Versandstatus im Kundenportal. Die Folgen falscher Angaben oder Statusmeldungen, die der Händler setzt, trägt er.
|
||||
|
||||
(6) Er bearbeitet Widerrufe und Erstattungen selbst, fristgerecht und nach dem Gesetz. Er entfernt den Link „Vertrag widerrufen“ nicht und verändert die Widerrufsfunktion nicht. Er prüft das Kundenportal regelmäßig auf neue Bestellungen und Widerrufe und lässt sich über die Benachrichtigungen informieren.
|
||||
|
||||
(7) Rechnungen und Belege an die Käufer erstellt der Händler selbst; WEBklar stellt sie nicht aus. Die Quittung von Stripe und die Bestellbestätigung des Webshops ersetzen keine Rechnung nach dem Umsatzsteuergesetz, soweit der Händler eine ausstellen muss.
|
||||
|
||||
(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem (das Archiv der Rechtstexte nach Nr. 6 Abs. 5 dient nur dem Nachweis der Texte): Personenbezogene Käuferdaten werden nach Nummer 9 des AVV Webshop anonymisiert und nach dem Ende des Webshops gelöscht. Der Händler exportiert seine Bestellungen deshalb rechtzeitig als CSV-Datei und bewahrt sie mit den Belegen von Stripe auf.
|
||||
|
||||
(9) Er hält die Vorgaben nach Nr. 3 ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht.
|
||||
|
||||
(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Webshop ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Händler den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Händler darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
|
||||
|
||||
(11) Der Händler informiert seine Käufer nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Webshop: Der Händler ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Händlers.
|
||||
|
||||
## 6. Muster-Rechtstexte, keine Rechtsberatung
|
||||
|
||||
(1) WEBklar stellt dem Händler Mustertexte bereit: eine Muster-Widerrufsbelehrung, ein Muster-Widerrufsformular, Allgemeine Geschäftsbedingungen und den Text zu Versand und Zahlung. Das Widget zeigt außerdem die Hinweise, die für die Kasse und die Bestellbestätigung vorgesehen sind, darunter den Hinweis auf die gesetzliche Gewährleistung.
|
||||
|
||||
(2) Die Mustertexte sind **Muster ohne Gewähr**. Sie passen zur Standardkonfiguration des Webshops und können auf den Betrieb des Händlers nicht zugeschnitten sein. WEBklar sorgt dafür, dass sie die Technik des Webshops richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes).
|
||||
|
||||
(3) WEBklar empfiehlt, die Rechtstexte vor der Öffnung des Webshops durch eine Rechtsanwältin oder einen Rechtsanwalt prüfen zu lassen, besonders bei besonderen Waren (zum Beispiel Lebensmitteln, Kosmetik, Textilien mit Pflegekennzeichnung, Elektrogeräten).
|
||||
|
||||
(4) Der Händler bleibt für seine Rechtstexte verantwortlich, auch wenn er Mustertexte unverändert übernimmt (Nr. 5 Abs. 2).
|
||||
|
||||
(5) Bestätigt der Händler seine Rechtstexte im Kundenportal, legt WEBklar jede bestätigte Fassung mit Prüfsumme, Zeitpunkt und der bestätigenden Person im Archiv ab. Die Bestellbestätigung zeigt dem Käufer die archivierte Fassung, die zum Zeitpunkt seiner Bestellung galt. WEBklar löscht das Archiv mit den übrigen Daten des Webshops (Nr. 10 Abs. 3; Nummer 8 Abs. 5 des AVV Webshop). Der Händler bewahrt die Fassungen, die er für eigene Aufbewahrungspflichten braucht, selbst auf (Nr. 5 Abs. 8).
|
||||
|
||||
## 7. Verfügbarkeit und Störungen
|
||||
|
||||
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Webshop. WEBklar sagt für den Webshop keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Webshops beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
|
||||
|
||||
(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Webshop kurzzeitig unterbrechen. Während einer Unterbrechung sind Bestellungen im Widget nicht möglich; das Widget zeigt dann einen Ausweichtext.
|
||||
|
||||
(3) Zahlungen übernimmt der Webshop über Benachrichtigungen von Stripe und einen regelmäßigen Abgleich in die Bestellung. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Bestellung verzögern. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.
|
||||
|
||||
(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
|
||||
|
||||
(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
|
||||
|
||||
(6) Die gesetzlichen Rechte des Händlers sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
|
||||
|
||||
## 8. Haftung
|
||||
|
||||
(1) Der Webshop ist ein Hilfsmittel für den Verkauf des Händlers. Der Vertrag über die Ware besteht allein zwischen dem Händler und dem Käufer; WEBklar ist daran nicht beteiligt.
|
||||
|
||||
(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Webshops, aus Fehlern bei Bestand oder Bestellstatus und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
|
||||
|
||||
(3) WEBklar haftet nicht für Inhalte des Händlers, insbesondere nicht für seine Artikel, Preise, Produktangaben und Rechtstexte, auch nicht für Mustertexte, die er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Käufern (Nr. 5).
|
||||
|
||||
(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 4 Abs. 3).
|
||||
|
||||
(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Händlers gegenüber seinen Käufern bleibt unberührt.
|
||||
|
||||
## 9. Preis, Abrechnung und Widerruf
|
||||
|
||||
(1) Die Einrichtung kostet 299 € einmalig je Website-Projekt, ohne Umsatzsteuer gemäß § 19 UStG. Für den Betrieb des Webshops berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz. Der Webshop läuft, solange WEBklar die Website betreibt (Nr. 10).
|
||||
|
||||
(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 2 bis 3 Wochen ab der Bestellung. Sie hängt davon ab, wie schnell der Händler Stripe-Konto, Artikel und Rechtstexte bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.
|
||||
|
||||
(3) Der Händler bezahlt bei der Bestellung im Kundenportal über Stripe und erhält eine Rechnung.
|
||||
|
||||
(4) Der Preis steht neben den Leistungen eines Abos: Die Einrichtung wird weder auf ein Stundenkontingent angerechnet noch damit bezahlt.
|
||||
|
||||
(5) Mehraufwand, der nicht zur Einrichtung gehört (Nr. 2 Abs. 7), führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Händlers in Textform aus. WEBklar nennt vorher den geschätzten Aufwand und berechnet 100 € je Stunde in Schritten von 15 Minuten, ohne Umsatzsteuer gemäß § 19 UStG (§ 4 der Abo-Bedingungen). Der Mehraufwand wird gesondert berechnet und nicht auf ein Stundenkontingent angerechnet.
|
||||
|
||||
(6) Verbraucher können die Bestellung der Einrichtung nach der Widerrufsbelehrung widerrufen. Belehrung und Muster-Widerrufsformular stehen in der Anlage „Widerrufsbelehrung und Muster-Widerrufsformular (Einrichtung von Funktionen)“. Ob das Widerrufsrecht erlischt und ob der Händler einen angemessenen Betrag für die bis dahin erbrachte Leistung schuldet, richtet sich nach der Zustimmung, die er bei der Bestellung gibt, und nach den Hinweisen zum Widerruf in der Anlage. Mit dem Widerruf endet der Webshop.
|
||||
|
||||
## 10. Laufzeit und Ende
|
||||
|
||||
(1) Der Webshop läuft, solange WEBklar die Website des Händlers betreibt. Endet der Vertrag über den Betrieb der Website, endet auch der Webshop; WEBklar schaltet ihn zum Vertragsende ab und informiert den Händler vorher per E-Mail. Das gilt auch beim Widerruf dieses Vertrags.
|
||||
|
||||
(2) Mit dem Abschalten nimmt der Webshop keine neuen Bestellungen mehr an, und das Widget zeigt den Ausweichtext. Bestellungen, die noch nicht bezahlt sind, laufen aus. Die Bestellungen und Widerrufe bleiben im Kundenportal 90 Tage lesbar, und der Händler kann sie als CSV-Datei exportieren. Die Widerrufsfunktion bleibt bis zur Löschung nach Absatz 3 erreichbar, soweit das Widget noch eingebunden ist. Der Händler erfüllt seine Pflichten gegenüber den Käufern weiter (Nr. 5) und informiert sie, wenn er den Webshop nicht mehr anbietet.
|
||||
|
||||
(3) WEBklar löscht alle Daten des Webshops, also Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6 des AVV Webshop).
|
||||
|
||||
(4) Das Stripe-Konto bleibt beim Händler. Das Ende des Webshops berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Händler kann die Verbindung zwischen dem Konto und dem Webshop im Stripe-Dashboard beenden.
|
||||
|
||||
(5) Beginnt innerhalb der 90 Tage wieder ein Vertrag über den Betrieb der Website, schaltet WEBklar den Webshop auf Wunsch des Händlers in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (neuer Preis nach Nr. 9).
|
||||
|
||||
(6) Endet der Vertrag über den Betrieb der Website, bevor WEBklar den Webshop freigeschaltet hat, erstattet WEBklar den Einrichtungspreis vollständig.
|
||||
|
||||
(7) Auf Wunsch des Händlers in Textform schaltet WEBklar den Webshop jederzeit ab. Eine Erstattung des Einrichtungspreises folgt daraus nicht; für den Widerruf gilt Nr. 9 Abs. 6.
|
||||
|
||||
## 11. Änderungen und Einstellung
|
||||
|
||||
(1) WEBklar darf die Technik des Webshops verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Webshop.
|
||||
|
||||
(2) WEBklar darf den Webshop für eine Website mit einer Frist von vier Wochen beenden oder für alle Händler mit einer Frist von drei Monaten einstellen, wenn
|
||||
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Händler einer dafür nötigen neuen Fassung nicht zustimmt (AVV Webshop Nummer 12),
|
||||
- Stripe die Anbindung nach Nr. 4 beendet oder wesentlich ändert, so dass der Webshop in der bisherigen Form nicht weiterbetrieben werden kann, oder
|
||||
- eine Sicherheitslücke sich nicht anders beheben lässt.
|
||||
|
||||
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
|
||||
|
||||
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Webshop ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Händler darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
|
||||
|
||||
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
|
||||
|
||||
## 12. Hinweise
|
||||
|
||||
(1) **Keine Rechtsberatung.** Der Webshop und die Mustertexte sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche Einordnung seiner Waren, seiner Preisangaben und seines Angebots trägt der Händler.
|
||||
|
||||
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Händler als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Händler.
|
||||
@@ -0,0 +1,109 @@
|
||||
# Leistungsbeschreibung Website-Analyse
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Hosting-Abo und zum Wartungs-Abo
|
||||
|
||||
## 1. Geltung
|
||||
|
||||
(1) Funktion im Sinne dieser Leistungsbeschreibung ist die Website-Analyse. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist in beiden Abos enthalten. Sie setzt ein laufendes Hosting-Abo oder Wartungs-Abo voraus. Sie wird zusammen mit dem Auftragsverarbeitungsvertrag Website-Analyse („AVV Website-Analyse“) vereinbart. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Website-Analyse.
|
||||
|
||||
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für die Website-Analyse diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Website-Analyse.
|
||||
|
||||
(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für die Zusatzseite nach Nr. 2 Abs. 2 und den Hinweis in der Datenschutzerklärung nach Nr. 4 Abs. 4. Beides ist Teil der Website-Analyse.
|
||||
|
||||
(4) Das Einschalten der Website-Analyse ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen.
|
||||
|
||||
## 2. Leistung
|
||||
|
||||
(1) Die Website-Analyse misst, wie die öffentliche Website des Kunden genutzt wird. Sie ist im Hosting-Abo und im Wartungs-Abo ohne Aufpreis enthalten. Die Einrichtung der Website-Analyse ist für den Kunden kostenlos. WEBklar rechnet sie weder auf ein Stundenkontingent an noch berechnet WEBklar sie gesondert.
|
||||
|
||||
(2) Beim Veröffentlichen der Website fügt WEBklar Folgendes ein:
|
||||
- ein Messskript in jede HTML-Datei der Website,
|
||||
- ein Einwilligungsbanner am unteren Rand mit den gleich gestalteten Schaltflächen „Ablehnen“ und „Annehmen“ in der Akzentfarbe, die der Kunde beim Einschalten wählt (ist sie für weiße Schrift zu hell, nimmt das Banner Schwarz),
|
||||
- eine Schaltfläche „Datenschutz-Einstellungen“ unten rechts, über die Besucher ihre Wahl jederzeit ändern können (hat die Website ein eigenes Element mit dem Attribut `data-webklar-consent`, übernimmt dieses Element die Aufgabe),
|
||||
- eine Zusatzseite „/datenschutz-analyse.html“ mit den Datenschutzhinweisen zur Messung (Verantwortlicher: der Kunde).
|
||||
|
||||
(3) Gemessen wird:
|
||||
- **solange der Besucher keine Wahl getroffen hat und sein Browser kein Signal nach dem nächsten Spiegelstrich sendet:** Seitenaufrufe mit Pfad (ohne Suchparameter), Land (aus der IP-Adresse, die nicht gespeichert wird), Geräteklasse und ein Zähler dafür, dass das Banner angezeigt wurde;
|
||||
- **bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“:** nichts, und es erscheint kein Banner;
|
||||
- **nach „Ablehnen“:** nichts mehr, außer einmal dem Zähler „abgelehnt“;
|
||||
- **nach „Annehmen“ zusätzlich:** die Domain der Website, von der der Besucher kam, die Größenklasse des Browserfensters, eine täglich wechselnde pseudonyme Besucher-Kennung, Verweildauer, Scrolltiefe, Klickpositionen (Heatmap), die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp, Link auf eine andere Website mit deren Domain) und ein Zähler „angenommen“.
|
||||
|
||||
Die einzelnen Datenarten nennt der AVV Website-Analyse in Nummer 3.
|
||||
|
||||
(4) Es werden keine Cookies gesetzt. Gespeichert wird auf dem Gerät des Besuchers nur seine Banner-Wahl im lokalen Speicher des Browsers, für 183 Tage.
|
||||
|
||||
(5) Die Auswertung steht im Kundenportal zur Verfügung: Kennzahlen und Diagramme für die letzten 7, 30 oder 90 Tage und eine Heatmap je Seite und Ansicht (Handy, Tablet, Computer) aus den bis zu 5.000 jüngsten Klicks der letzten 30 Tage. Die Daten liegen auf dem Server von WEBklar bei der Hetzner Online GmbH in Deutschland; sie werden nicht in Drittländer übermittelt. Einzelne Ereignisse und Klicks werden nach 30 Tagen gelöscht, Tageswerte nach 25 Monaten, alle Daten rund 60 Tage nach dem Ende der Website-Analyse (AVV Website-Analyse Nummer 9).
|
||||
|
||||
(6) Die Zahlen sind Näherungswerte. Besucher mit Blockern, abgeschaltetem JavaScript, Browser-Signal oder Ablehnung werden nur eingeschränkt oder gar nicht erfasst. Besucherzahlen, Herkunftsseiten, Verweildauer, Scrolltiefe, Klicks und Kontakt-Klicks stammen nur von Besuchern, die „Annehmen“ gewählt haben.
|
||||
|
||||
(7) Nicht enthalten sind Sitzungsaufzeichnungen, die Auswertung von Formularinhalten, die Messung von Ladezeiten und Fehlern, Kampagnen- und Trichteranalysen sowie ein Datenexport außer nach Nr. 7 Abs. 3.
|
||||
|
||||
(8) Das Banner fragt nur nach der Einwilligung in die Website-Analyse von WEBklar. Andere einwilligungspflichtige Inhalte der Website, etwa eingebettete Karten, Videos oder Schriften von Drittanbietern, deckt es nicht ab.
|
||||
|
||||
## 3. Voraussetzungen
|
||||
|
||||
(1) Der Kunde hat ein laufendes Hosting-Abo oder Wartungs-Abo.
|
||||
|
||||
(2) Die Website ist öffentlich erreichbar und wird bei WEBklar gehostet (Adresse unter project.webklar.com oder eigene Domain, die auf den Server von WEBklar zeigt). Nicht öffentliche Websites und Vorschauen, die nur nach einer Anmeldung erreichbar sind, werden nicht gemessen, ebenso wenig Websites, die im Dev-Modus auf einem Entwicklungsserver laufen. Enthält eine öffentliche Website Bereiche, die erst nach einer Anmeldung zugänglich sind (etwa eine Web-Anwendung mit Kundenkonto), werden auch dort Seitenaufrufe gezählt; dafür gilt Nr. 4 Abs. 5.
|
||||
|
||||
(3) Auf der Zusatzseite erscheinen als Verantwortlicher die Firma (sonst der Name), die Anschrift, soweit sie im Kundenkonto hinterlegt ist, und die E-Mail-Adresse aus dem Kundenkonto. Das Portal zeigt diese Angaben vor dem Einschalten an. Der Kunde prüft sie und teilt Änderungen mit. Fehlen Firma und Name oder fehlt eine gültige E-Mail-Adresse, lässt sich die Funktion nicht einschalten.
|
||||
|
||||
(4) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren, schaltet der Kunde die Website-Analyse nicht ein, weil eine Einwilligung über das Banner dann nicht genügt (Art. 8 DSGVO). Lassen Inhalte oder Adressen der Website auf besondere Kategorien personenbezogener Daten schließen, etwa Gesundheit, Religion oder politische Meinung, teilt der Kunde das WEBklar vor dem Einschalten mit. WEBklar darf das Einschalten für eine solche Website ablehnen oder die Website-Analyse dort ausschalten.
|
||||
|
||||
## 4. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist für die Messung auf seiner Website Verantwortlicher im Sinne der DSGVO.
|
||||
|
||||
(2) Er entfernt, verdeckt oder umgeht das Banner nicht und verändert das Skript nicht. Text und Gestaltung des Banners legt WEBklar fest.
|
||||
|
||||
(3) Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess-, Tracking- oder Marketing-Werkzeuge ein (zum Beispiel Google Analytics oder Meta Pixel), weil sie eine eigene Einwilligung erfordern können.
|
||||
|
||||
(4) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einschalten dort einen kurzen Hinweis mit Link auf die Zusatzseite ein und entfernt ihn beim Ausschalten wieder; das wird nicht auf das Stundenkontingent angerechnet. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Hinweis als Textbaustein und fügt ihn selbst ein und entfernt ihn nach dem Ausschalten selbst. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf (etwa „Wir setzen keine Analyse-Werkzeuge ein“), weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
|
||||
|
||||
(5) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.
|
||||
|
||||
(6) Er nutzt die Auswertung nur für eigene Zwecke, versucht nicht, einzelne Besucher zu identifizieren, und gibt Heatmap-Links nicht weiter.
|
||||
|
||||
## 5. Verfügbarkeit und Störungen
|
||||
|
||||
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für die Messung. Störungen der Messung beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen). Daten, die während einer Störung nicht erfasst wurden, lassen sich nicht nachträglich erheben.
|
||||
|
||||
(2) Fällt der Messdienst aus, wird die Website weiter ausgeliefert, nur ohne Banner und Messung.
|
||||
|
||||
(3) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
|
||||
|
||||
## 6. Hinweis zur Rechtslage
|
||||
|
||||
Für die Zählung, solange der Besucher keine Wahl getroffen hat (Nr. 2 Abs. 3), übermittelt das Skript nur die Adresse der aufgerufenen Seite und die Angabe, dass das Banner angezeigt wurde. Land und Geräteklasse bestimmt der Messdienst aus den Daten, die jeder Browser bei einer Anfrage ohnehin mitsendet. Ob auch dafür eine Einwilligung nach § 25 TDDDG nötig ist, haben Gerichte und Aufsichtsbehörden nicht entschieden; die Aufsichtsbehörden legen den Begriff des Zugriffs auf das Endgerät eher weit aus. Ändert sich die Rechtslage, passt WEBklar die Messung an (Nr. 8).
|
||||
|
||||
## 7. Ende der Leistung
|
||||
|
||||
(1) Der Kunde kann die Website-Analyse jederzeit im Kundenportal ausschalten; das Portal bestätigt die Abschaltung. Die Website-Analyse läuft, solange das Abo läuft, das sie enthält. Endet es, schaltet WEBklar sie spätestens am folgenden Tag ab. Wechselt der Kunde vom Wartungs- ins Hosting-Abo, bleibt die Website-Analyse; das Gleiche gilt, wenn er den Wechsel ins Wartungs-Abo widerruft. WEBklar informiert den Kunden per E-Mail.
|
||||
|
||||
(2) Mit dem Ausschalten speichert der Messdienst keine Messdaten mehr, und die Heatmap ist nicht mehr abrufbar. Das Messskript liefert dann kein Banner mehr aus, sobald die Website einmal mit der aktuellen Fassung des Skripts veröffentlicht wurde. Skript, Zusatzseite und, soweit WEBklar ihn eingefügt hat, den Hinweis in der Datenschutzerklärung entfernt WEBklar mit der nächsten Veröffentlichung der Website. WEBklar löst sie in der Regel sofort aus, spätestens innerhalb von fünf Werktagen.
|
||||
|
||||
(3) Die gespeicherten Daten löscht WEBklar nach Nummer 9 des AVV Website-Analyse. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse als CSV-Datei verlangen.
|
||||
|
||||
## 8. Änderungen und Einstellung
|
||||
|
||||
(1) WEBklar darf die Technik der Website-Analyse verbessern, wenn der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Website-Analyse.
|
||||
|
||||
(2) WEBklar darf die Website-Analyse für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
|
||||
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Website-Analyse Nummer 12 Abs. 3), oder
|
||||
- eine Sicherheitslücke sich nicht anders beheben lässt.
|
||||
|
||||
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
|
||||
|
||||
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar die Messung ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt die Messung wieder auf, sobald das zulässig und technisch möglich ist.
|
||||
|
||||
(4) Gegenüber Unternehmern darf WEBklar die Website-Analyse außerdem ohne besonderen Grund mit einer Frist von drei Monaten in Textform für alle Kunden einstellen. Der Kunde kann das Abo dann innerhalb von 30 Tagen nach der Mitteilung zum Zeitpunkt der Einstellung kündigen; die Mindestlaufzeit steht dem nicht entgegen.
|
||||
|
||||
(5) Weil für die Website-Analyse kein Aufpreis berechnet wird, ändert ihre Einstellung den Preis des Abos nicht. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
|
||||
|
||||
## 9. Hinweise
|
||||
|
||||
(1) **Keine Rechtsberatung.** Die Zusatzseite und der Hinweis in der Datenschutzerklärung sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine Prüfung, ob die Website des Kunden insgesamt datenschutzkonform ist, und eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht. Abstimmungen nach dieser Leistungsbeschreibung betreffen nur die Technik.
|
||||
|
||||
(2) **Nachweis der Einwilligung.** Ein Protokoll je Besucher wird nicht geführt. Als Nachweis dienen die Dokumentation der Technik und das Archiv der Fassungen von Banner-Text und Zusatzseite (AVV Website-Analyse Nummer 8 Abs. 6).
|
||||
|
||||
(3) Die Länderzuordnung nutzt die Datenbank „IP to Country Lite“ von DB-IP.com (Lizenz CC BY 4.0). Im Kundenportal steht dazu unter der Auswertung der Hinweis „Länderdaten: IP-Geolocation by DB-IP“ mit Link auf db-ip.com.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Leistungsbeschreibung Website-Analyse
|
||||
|
||||
**Fassung 2026-10-02** · Zusatzbedingungen zum Wartungs-Abo
|
||||
|
||||
## 1. Geltung
|
||||
|
||||
(1) Diese Leistungsbeschreibung regelt die Funktion „Website-Analyse“ im Wartungs-Abo. Sie wird vereinbart, wenn der Kunde die Funktion einschaltet und ihr zusammen mit dem Auftragsverarbeitungsvertrag Website-Analyse („AVV Website-Analyse“) zustimmt. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Website-Analyse.
|
||||
|
||||
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für die Website-Analyse diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Website-Analyse.
|
||||
|
||||
(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für die Zusatzseite nach Nr. 2 Abs. 2 und den Hinweis in der Datenschutzerklärung nach Nr. 4 Abs. 4. Beides ist Teil der Website-Analyse.
|
||||
|
||||
## 2. Leistung
|
||||
|
||||
(1) Die Website-Analyse misst, wie die öffentliche Website des Kunden genutzt wird. Sie ist im Wartungs-Abo ohne Aufpreis enthalten.
|
||||
|
||||
(2) Beim Veröffentlichen der Website fügt WEBklar Folgendes ein:
|
||||
- ein Messskript in jede HTML-Datei der Website,
|
||||
- ein Einwilligungsbanner am unteren Rand mit den gleich gestalteten Schaltflächen „Ablehnen“ und „Annehmen“ in der Akzentfarbe, die der Kunde beim Einschalten wählt (ist sie für weiße Schrift zu hell, nimmt das Banner Schwarz),
|
||||
- eine Schaltfläche „Datenschutz-Einstellungen“ unten rechts, über die Besucher ihre Wahl jederzeit ändern können (hat die Website ein eigenes Element mit dem Attribut `data-webklar-consent`, übernimmt dieses Element die Aufgabe),
|
||||
- eine Zusatzseite „/datenschutz-analyse.html“ mit den Datenschutzhinweisen zur Messung (Verantwortlicher: der Kunde).
|
||||
|
||||
(3) Gemessen wird:
|
||||
- **solange der Besucher keine Wahl getroffen hat und sein Browser kein Signal nach dem nächsten Spiegelstrich sendet:** Seitenaufrufe mit Pfad (ohne Suchparameter), Land (aus der IP-Adresse, die nicht gespeichert wird), Geräteklasse und ein Zähler dafür, dass das Banner angezeigt wurde;
|
||||
- **bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“:** nichts, und es erscheint kein Banner;
|
||||
- **nach „Ablehnen“:** nichts mehr, außer einmal dem Zähler „abgelehnt“;
|
||||
- **nach „Annehmen“ zusätzlich:** die Domain der Website, von der der Besucher kam, die Größenklasse des Browserfensters, eine täglich wechselnde pseudonyme Besucher-Kennung, Verweildauer, Scrolltiefe, Klickpositionen (Heatmap), die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp, Link auf eine andere Website mit deren Domain) und ein Zähler „angenommen“.
|
||||
|
||||
Die einzelnen Datenarten nennt der AVV Website-Analyse in Nummer 3.
|
||||
|
||||
(4) Es werden keine Cookies gesetzt. Gespeichert wird auf dem Gerät des Besuchers nur seine Banner-Wahl im lokalen Speicher des Browsers, für 183 Tage.
|
||||
|
||||
(5) Die Auswertung steht im Kundenportal zur Verfügung: Kennzahlen und Diagramme für die letzten 7, 30 oder 90 Tage und eine Heatmap je Seite und Ansicht (Handy, Tablet, Computer) aus den bis zu 5.000 jüngsten Klicks der letzten 30 Tage. Die Daten liegen auf dem Server von WEBklar bei der Hetzner Online GmbH in Deutschland; sie werden nicht in Drittländer übermittelt. Einzelne Ereignisse und Klicks werden nach 30 Tagen gelöscht, Tageswerte nach 25 Monaten, alle Daten rund 60 Tage nach dem Ende der Website-Analyse (AVV Website-Analyse Nummer 9).
|
||||
|
||||
(6) Die Zahlen sind Näherungswerte. Besucher mit Blockern, abgeschaltetem JavaScript, Browser-Signal oder Ablehnung werden nur eingeschränkt oder gar nicht erfasst. Besucherzahlen, Herkunftsseiten, Verweildauer, Scrolltiefe, Klicks und Kontakt-Klicks stammen nur von Besuchern, die „Annehmen“ gewählt haben.
|
||||
|
||||
(7) Nicht enthalten sind Sitzungsaufzeichnungen, die Auswertung von Formularinhalten, die Messung von Ladezeiten und Fehlern, Kampagnen- und Trichteranalysen sowie ein Datenexport außer nach Nr. 7 Abs. 3.
|
||||
|
||||
(8) Das Banner fragt nur nach der Einwilligung in die Website-Analyse von WEBklar. Andere einwilligungspflichtige Inhalte der Website, etwa eingebettete Karten, Videos oder Schriften von Drittanbietern, deckt es nicht ab.
|
||||
|
||||
## 3. Voraussetzungen
|
||||
|
||||
(1) Der Kunde hat ein laufendes Wartungs-Abo. Im Hosting-Abo ist die Website-Analyse nicht enthalten.
|
||||
|
||||
(2) Die Website ist öffentlich erreichbar und wird bei WEBklar gehostet (Adresse unter project.webklar.com oder eigene Domain, die auf den Server von WEBklar zeigt). Nicht öffentliche Websites und Vorschauen, die nur nach einer Anmeldung erreichbar sind, werden nicht gemessen, ebenso wenig Websites, die im Dev-Modus auf einem Entwicklungsserver laufen. Enthält eine öffentliche Website Bereiche, die erst nach einer Anmeldung zugänglich sind (etwa eine Web-Anwendung mit Kundenkonto), werden auch dort Seitenaufrufe gezählt; dafür gilt Nr. 4 Abs. 5.
|
||||
|
||||
(3) Auf der Zusatzseite erscheinen als Verantwortlicher die Firma (sonst der Name), die Anschrift, soweit sie im Kundenkonto hinterlegt ist, und die E-Mail-Adresse aus dem Kundenkonto. Das Portal zeigt diese Angaben vor dem Einschalten an. Der Kunde prüft sie und teilt Änderungen mit. Fehlen Firma und Name oder fehlt eine gültige E-Mail-Adresse, lässt sich die Funktion nicht einschalten.
|
||||
|
||||
(4) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren, schaltet der Kunde die Website-Analyse nicht ein, weil eine Einwilligung über das Banner dann nicht genügt (Art. 8 DSGVO). Lassen Inhalte oder Adressen der Website auf besondere Kategorien personenbezogener Daten schließen, etwa Gesundheit, Religion oder politische Meinung, teilt der Kunde das WEBklar vor dem Einschalten mit. WEBklar darf das Einschalten für eine solche Website ablehnen oder die Website-Analyse dort ausschalten.
|
||||
|
||||
## 4. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist für die Messung auf seiner Website Verantwortlicher im Sinne der DSGVO.
|
||||
|
||||
(2) Er entfernt, verdeckt oder umgeht das Banner nicht und verändert das Skript nicht. Text und Gestaltung des Banners legt WEBklar fest.
|
||||
|
||||
(3) Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess-, Tracking- oder Marketing-Werkzeuge ein (zum Beispiel Google Analytics oder Meta Pixel), weil sie eine eigene Einwilligung erfordern können.
|
||||
|
||||
(4) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einschalten dort einen kurzen Hinweis mit Link auf die Zusatzseite ein und entfernt ihn beim Ausschalten wieder; das wird nicht auf das Stundenkontingent angerechnet. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Hinweis als Textbaustein und fügt ihn selbst ein und entfernt ihn nach dem Ausschalten selbst. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf (etwa „Wir setzen keine Analyse-Werkzeuge ein“), weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
|
||||
|
||||
(5) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.
|
||||
|
||||
(6) Er nutzt die Auswertung nur für eigene Zwecke, versucht nicht, einzelne Besucher zu identifizieren, und gibt Heatmap-Links nicht weiter.
|
||||
|
||||
## 5. Verfügbarkeit und Störungen
|
||||
|
||||
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für die Messung. Störungen der Messung beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen). Daten, die während einer Störung nicht erfasst wurden, lassen sich nicht nachträglich erheben.
|
||||
|
||||
(2) Fällt der Messdienst aus, wird die Website weiter ausgeliefert, nur ohne Banner und Messung.
|
||||
|
||||
(3) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
|
||||
|
||||
## 6. Hinweis zur Rechtslage
|
||||
|
||||
Für die Zählung, solange der Besucher keine Wahl getroffen hat (Nr. 2 Abs. 3), übermittelt das Skript nur die Adresse der aufgerufenen Seite und die Angabe, dass das Banner angezeigt wurde. Land und Geräteklasse bestimmt der Messdienst aus den Daten, die jeder Browser bei einer Anfrage ohnehin mitsendet. Ob auch dafür eine Einwilligung nach § 25 TDDDG nötig ist, haben Gerichte und Aufsichtsbehörden nicht entschieden; die Aufsichtsbehörden legen den Begriff des Zugriffs auf das Endgerät eher weit aus. Ändert sich die Rechtslage, passt WEBklar die Messung an (Nr. 8).
|
||||
|
||||
## 7. Ende der Leistung
|
||||
|
||||
(1) Der Kunde kann die Website-Analyse jederzeit im Kundenportal ausschalten; das Portal bestätigt die Abschaltung. Mit dem Ende des Wartungs-Abos endet die Website-Analyse; WEBklar schaltet sie spätestens am folgenden Tag ab und informiert den Kunden per E-Mail. Das gilt auch bei einem Wechsel ins Hosting-Abo und beim Widerruf eines Wechsels ins Wartungs-Abo.
|
||||
|
||||
(2) Mit dem Ausschalten speichert der Messdienst keine Messdaten mehr, und die Heatmap ist nicht mehr abrufbar. Das Messskript liefert dann kein Banner mehr aus, sobald die Website einmal mit der aktuellen Fassung des Skripts veröffentlicht wurde. Skript, Zusatzseite und, soweit WEBklar ihn eingefügt hat, den Hinweis in der Datenschutzerklärung entfernt WEBklar mit der nächsten Veröffentlichung der Website. WEBklar löst sie in der Regel sofort aus, spätestens innerhalb von fünf Werktagen.
|
||||
|
||||
(3) Die gespeicherten Daten löscht WEBklar nach Nummer 9 des AVV Website-Analyse. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse als CSV-Datei verlangen.
|
||||
|
||||
## 8. Änderungen und Einstellung
|
||||
|
||||
(1) WEBklar darf die Technik der Website-Analyse verbessern, wenn der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Website-Analyse.
|
||||
|
||||
(2) WEBklar darf die Website-Analyse für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
|
||||
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Website-Analyse Nummer 12 Abs. 3), oder
|
||||
- eine Sicherheitslücke sich nicht anders beheben lässt.
|
||||
|
||||
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
|
||||
|
||||
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar die Messung ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt die Messung wieder auf, sobald das zulässig und technisch möglich ist.
|
||||
|
||||
(4) Gegenüber Unternehmern darf WEBklar die Website-Analyse außerdem ohne besonderen Grund mit einer Frist von drei Monaten in Textform für alle Kunden einstellen. Der Kunde kann das Wartungs-Abo dann innerhalb von 30 Tagen nach der Mitteilung zum Zeitpunkt der Einstellung kündigen; die Mindestlaufzeit steht dem nicht entgegen.
|
||||
|
||||
(5) Weil für die Website-Analyse kein Aufpreis berechnet wird, ändert ihre Einstellung den Preis des Wartungs-Abos nicht. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
|
||||
|
||||
## 9. Hinweise
|
||||
|
||||
(1) **Keine Rechtsberatung.** Die Zusatzseite und der Hinweis in der Datenschutzerklärung sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine Prüfung, ob die Website des Kunden insgesamt datenschutzkonform ist, und eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht. Abstimmungen nach dieser Leistungsbeschreibung betreffen nur die Technik.
|
||||
|
||||
(2) **Nachweis der Einwilligung.** Ein Protokoll je Besucher wird nicht geführt. Als Nachweis dienen die Dokumentation der Technik und das Archiv der Fassungen von Banner-Text und Zusatzseite (AVV Website-Analyse Nummer 8 Abs. 6).
|
||||
|
||||
(3) Die Länderzuordnung nutzt die Datenbank „IP to Country Lite“ von DB-IP.com (Lizenz CC BY 4.0). Im Kundenportal steht dazu unter der Auswertung der Hinweis „Länderdaten: IP-Geolocation by DB-IP“ mit Link auf db-ip.com.
|
||||
@@ -0,0 +1,135 @@
|
||||
# Leistungsbeschreibung Öffnungszeiten live
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
|
||||
|
||||
## 1. Geltung
|
||||
|
||||
(1) Funktion im Sinne dieser Leistungsbeschreibung ist Öffnungszeiten live. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht.
|
||||
|
||||
(2) Die Funktion ist im Wartungs-Abo ohne Aufpreis enthalten (§ 3 Abs. 8 der Abo-Bedingungen). Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Nicht enthalten“: neue Funktionen und Rechtstexte) gilt dafür nicht: Die Funktion und der Datenschutz-Absatz nach Nr. 6 Abs. 4 sind Teil dieser Leistung.
|
||||
|
||||
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für Öffnungszeiten live diese Leistungsbeschreibung vor. Die Haftungsregeln stehen in Nr. 8 ausgeschrieben da, damit sie auch für Abos früherer Fassungen der Abo-Bedingungen gelten.
|
||||
|
||||
## 2. Leistung
|
||||
|
||||
(1) Der Kunde trägt seine Öffnungszeiten einmal im Kundenportal ein: bis zu fünf benannte Zeitpläne, je Wochentag bis zu zwei Zeitfenster („rund um die Uhr“ und „nach Vereinbarung“ sind möglich), Abweichungen an Feiertagen, Betriebsurlaub und Sondertage. Die Website zeigt diese Zeiten überall gleich an und berechnet daraus den Live-Status („Jetzt geöffnet · schließt um 18 Uhr“).
|
||||
|
||||
(2) An den Stellen der Website, die nach Nr. 3 Abs. 2 dafür vorgesehen sind, erscheinen die Zeiten als Tabelle, als Status oder als Kurzanzeige („Geöffnet“, „Geschlossen“, „Nach Vereinbarung“). Dazu gehören, soweit der Kunde sie einrichtet:
|
||||
- eine Hinweis-Leiste am Seitenanfang für befristete Hinweise (Info, Urlaub, Aktion, Wichtig) mit Beginn und Ende und auf Wunsch einem Knopf (Link oder Anruf),
|
||||
- auf dem Handy eine Kontakt-Leiste am unteren Rand mit den Knöpfen „Anrufen“, „Route“ und, wenn der Kunde sie einrichtet, „WhatsApp“,
|
||||
- die Öffnungszeiten im Seitencode in maschinenlesbarer Form (strukturierte Daten), die Suchmaschinen lesen können,
|
||||
- eine E-Mail vor Feiertagen, die den Kunden bittet, seine Zeiten für diese Tage zu prüfen, und
|
||||
- im Kundenportal der Knopf „Für Google kopieren“, der die Zeiten in der Reihenfolge von Google in die Zwischenablage legt, damit der Kunde sie in sein Google-Unternehmensprofil einträgt.
|
||||
|
||||
(3) Die Funktion rechnet die Feiertage des Bundeslands ein, das der Kunde wählt; je Feiertag kann der Kunde eigene Zeiten eintragen. Feiertage, die nur örtlich gelten, sowie Heiligabend und Silvester zeigt das Kundenportal als „bitte prüfen“. Alle Zeiten gelten in der Zeitzone Europe/Berlin.
|
||||
|
||||
(4) Beim Aufruf der Website lädt der Browser des Besuchers ein kleines Programm und die Öffnungszeiten von project.webklar.com, einem Server der WEBklar GbR bei der Hetzner Online GmbH in Falkenstein (Deutschland). Dabei werden keine Cookies gesetzt, und es wird nichts im Browser gespeichert. Das Programm lädt nichts von anderen Anbietern. WhatsApp und Google Maps werden erst nach dem Klick des Besuchers auf „WhatsApp“ oder „Route“ geöffnet; vorher werden keine Daten an diese Dienste übertragen.
|
||||
|
||||
(5) Das Team von WEBklar kann die Hinweis-Leiste und die Kontakt-Leiste für eine Website abschalten, etwa wenn die Seite schon eine eigene Anruf-Leiste hat. Auf Wunsch des Kunden können Mitarbeiter die Öffnungszeiten in der Kundenansicht für ihn ändern; ein Anspruch darauf besteht nicht (Nr. 5 Abs. 1). Den Haken für den WhatsApp-Hinweis (Nr. 6 Abs. 3) setzen sie nicht.
|
||||
|
||||
## 3. Einbau und Freischaltung
|
||||
|
||||
(1) Der Einbau des Programms erfolgt automatisch nur bei Websites, die WEBklar hostet und über den eigenen Veröffentlichungsablauf bereitstellt: Beim nächsten Veröffentlichen setzt WEBklar die Einbau-Zeile in jede Seite ein, und Hinweis-Leiste, Kontakt-Leiste und Angaben für Suchmaschinen erscheinen ohne weiteren Aufwand. In allen anderen Fällen, etwa bei einer Website bei einem anderen Anbieter, setzt das Team die Einbau-Zeile von Hand ein. Hat WEBklar keinen Zugriff auf die Website, erhält der Kunde die Zeile und setzt sie selbst oder durch seinen Anbieter ein.
|
||||
|
||||
(2) Die Tabelle, den Status und die Kurzanzeige setzt das Team an den Stellen ein, an denen auf der Website feste Öffnungszeiten stehen oder stehen sollen, und ersetzt dort die fest eingetragenen Zeiten. In jedem dieser Platzhalter bleibt eine statische Ersatz-Tabelle stehen. Sie erscheint, wenn der Browser kein JavaScript ausführt, wenn die Daten nicht erreichbar oder älter als 24 Stunden sind und nachdem die Funktion abgeschaltet wurde. Sie zeigt den Wochenplan zum Zeitpunkt des Einbaus.
|
||||
|
||||
(3) WEBklar schaltet die Funktion frei, sobald der Einbau abgeschlossen ist und der Kunde Öffnungszeiten eingetragen hat (mindestens ein geöffneter Tag im Hauptplan). Solange der Kunde keine Zeiten eingetragen hat, kann das Team die Tabelle nicht einbauen.
|
||||
|
||||
(4) Der Einbau dauert erfahrungsgemäß meist wenige Tage. Das ist eine Einschätzung und keine verbindliche Zusage eines Termins.
|
||||
|
||||
## 4. Einrichtung
|
||||
|
||||
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.
|
||||
|
||||
(2) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
|
||||
|
||||
(3) Im Umfang der Einrichtung ist ein Einbau je Website enthalten, einschließlich der Aktualisierung der Ersatz-Tabelle beim Abschalten (Nr. 9 Abs. 4). Weitere Wünsche, etwa zusätzliche Stellen für die Anzeige, eine Sondergestaltung oder andere Änderungen an der Website, gehören nicht zur Einrichtung. Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen.
|
||||
|
||||
## 5. Nicht enthalten
|
||||
|
||||
(1) Nicht enthalten sind:
|
||||
- die Übertragung der Öffnungszeiten zu Google: WEBklar schreibt nichts automatisch in das Google-Unternehmensprofil des Kunden; der Kunde trägt die Zeiten dort selbst ein, der Knopf „Für Google kopieren“ und die Erinnerung vor Feiertagen helfen dabei,
|
||||
- Feiertage einer Gemeinde und andere örtliche Besonderheiten (zum Beispiel Brückentage oder Stadtfeste); der Kunde trägt sie als Ausnahmen selbst ein,
|
||||
- eine Zusage, dass Suchmaschinen die Angaben im Seitencode übernehmen oder wie sie sie anzeigen,
|
||||
- die Pflege der Zeiten und der Hinweise durch WEBklar und
|
||||
- Änderungen an der Website, die über den Einbau nach Nr. 3 hinausgehen (Nr. 4 Abs. 3).
|
||||
|
||||
## 6. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde prüft seine Zeiten, Feiertage und Hinweistexte vor dem Speichern und hält sie aktuell. Er wählt das richtige Bundesland und trägt örtliche Feiertage und Abweichungen selbst ein.
|
||||
|
||||
(2) Preise und Rabatte in Hinweisen gibt er rechtmäßig an, zum Beispiel als Endpreise und, bei Preisermäßigungen für Waren, mit dem niedrigsten Preis der letzten 30 Tage. Hinweise zu Preisen sind Angaben des Kunden, nicht von WEBklar.
|
||||
|
||||
(3) Die Kontaktangaben (Telefonnummer, Anschrift, WhatsApp-Nummer) sind auf der Website öffentlich sichtbar. Der Kunde trägt nur Angaben ein, die er veröffentlichen darf. Die WhatsApp-Schaltfläche schaltet er erst ein, nachdem er den WhatsApp-Hinweis in seine Datenschutzerklärung aufgenommen hat, und bestätigt das im Kundenportal mit einem Haken.
|
||||
|
||||
(4) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz zu Öffnungszeiten live ein; das gehört zum Einbau. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
|
||||
|
||||
(5) Der Kunde prüft nach dem Einbau, ob die Anzeige auf seiner Website stimmt, und meldet Fehler unverzüglich im Chat des Kundenportals.
|
||||
|
||||
(6) Er entfernt oder verändert das Programm von WEBklar nicht. Wünscht er die Hinweis-Leiste oder die Kontakt-Leiste nicht, teilt er das dem Team mit (Nr. 2 Abs. 5).
|
||||
|
||||
## 7. Verfügbarkeit und Aktualität
|
||||
|
||||
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für die Live-Anzeige. Störungen der Anzeige behebt WEBklar in angemessener Frist.
|
||||
|
||||
(2) Ist die Anzeige nicht erreichbar oder sind die Daten älter als 24 Stunden, bleibt auf der Website die Ersatz-Tabelle stehen (Nr. 3 Abs. 2); Hinweis-Leiste und Kontakt-Leiste erscheinen dann nicht.
|
||||
|
||||
(3) Änderungen, die der Kunde im Kundenportal speichert, erscheinen nicht sofort auf der Website, sondern mit einer Verzögerung von einigen Minuten (bis zu etwa zehn Minuten). Der Status folgt der Zeitzone Europe/Berlin; geht die Uhr des Geräts des Besuchers um mehr als fünf Minuten falsch, rechnet das Programm mit der Serverzeit von WEBklar.
|
||||
|
||||
## 8. Haftung
|
||||
|
||||
(1) Die Live-Anzeige ist ein Hilfsmittel. Die Richtigkeit der Zeiten, Ausnahmen, Hinweise und Kontaktangaben liegt beim Kunden: WEBklar zeigt die Angaben des Kunden an und prüft sie nicht auf Richtigkeit.
|
||||
|
||||
(2) Die Feiertage stammen aus einer Feiertagsliste für das gewählte Bundesland. WEBklar beseitigt Fehler in den Feiertagsdaten und andere Softwarefehler nach Meldung in angemessener Frist (Nr. 7 Abs. 1). Für Schäden daraus gelten die Absätze 3 bis 5.
|
||||
|
||||
(3) Für entgangenen Gewinn und Folgeschäden, etwa weil ein Besucher wegen einer falschen oder fehlenden Angabe vor verschlossener Tür steht, haftet WEBklar nur im Rahmen der Absätze 4 und 5.
|
||||
|
||||
(4) WEBklar haftet unbeschränkt bei Vorsatz und grober Fahrlässigkeit, bei Verletzung von Leben, Körper oder Gesundheit, nach dem Produkthaftungsgesetz und soweit WEBklar eine Garantie übernommen hat.
|
||||
|
||||
(5) Bei leicht fahrlässiger Verletzung einer wesentlichen Vertragspflicht haftet WEBklar nur für den vertragstypischen, vorhersehbaren Schaden. Wesentlich sind Pflichten, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Kunde regelmäßig vertrauen darf. Im Übrigen ist die Haftung für leichte Fahrlässigkeit ausgeschlossen.
|
||||
|
||||
(6) Diese Regeln gelten auch zugunsten der Mitarbeiter und Beauftragten von WEBklar. Dieser Abschnitt entspricht § 13 Abs. 1 und 2 der Abo-Bedingungen und gilt unabhängig davon, nach welcher Fassung der Abo-Bedingungen das Wartungs-Abo besteht.
|
||||
|
||||
(7) Gesetzliche Rechte des Kunden, die sich nicht ausschließen lassen, bleiben unberührt. Der Haftungsrahmen des Kunden gegenüber seinen Besuchern und Kunden bleibt ebenfalls unberührt.
|
||||
|
||||
## 9. Ende der Leistung
|
||||
|
||||
(1) Auf Wunsch des Kunden in Textform (im Chat des Kundenportals oder per E-Mail) schaltet WEBklar Öffnungszeiten live jederzeit ab; für den Widerruf gilt Nr. 11.
|
||||
|
||||
(2) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo, läuft die Funktion noch 30 Tage unverändert weiter; danach schaltet WEBklar sie ab. Das Gleiche gilt beim Widerruf eines Wechsels ins Wartungs-Abo. WEBklar informiert den Kunden per E-Mail. Die Eingaben des Kunden bleiben nach dem Abschalten 30 Tage gespeichert und werden danach gelöscht; beginnt schon in den 30 Tagen Weiterlauf wieder ein Wartungs-Abo, läuft die Funktion einfach weiter, beginnt es in den 30 Tagen danach, schaltet WEBklar die Funktion mit den vorhandenen Eingaben ohne weitere Kosten wieder ein.
|
||||
|
||||
(3) Sobald ein neues Wartungs-Abo beginnt, solange die Eingaben des Kunden noch gespeichert sind (Absatz 4), läuft die Funktion mit den vorhandenen Eingaben weiter oder WEBklar schaltet sie ohne weitere Kosten wieder ein.
|
||||
|
||||
(4) Mit dem Abschalten verschwinden Hinweis-Leiste, Kontakt-Leiste, Live-Status und die Angaben für Suchmaschinen; auf der Website steht wieder die Ersatz-Tabelle (Nr. 3 Abs. 2). Hat sich der Wochenplan seit dem Einbau geändert, aktualisiert WEBklar die Ersatz-Tabelle einmal beim Abschalten; das gehört zum Einbau nach Nr. 4 Abs. 3. Hat WEBklar keinen Zugriff auf die Website, erhält der Kunde den aktuellen Text und setzt ihn selbst ein. Nach dem Abschalten wegen des Endes des Abos bleiben die Eingaben des Kunden noch weitere 30 Tage gespeichert und werden danach gelöscht; bis dahin kann der Kunde sie im Kundenportal ansehen.
|
||||
|
||||
(5) Schaltet WEBklar die Funktion auf Wunsch des Kunden oder aus einem anderen Grund als dem Ende des Abos ab, löscht WEBklar die Eingaben des Kunden auf sein Verlangen in Textform, spätestens mit dem Löschen des Kundenkontos.
|
||||
|
||||
## 10. Änderungen und Einstellung
|
||||
|
||||
(1) WEBklar darf die Technik von Öffnungszeiten live verbessern, wenn der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten die Funktion verarbeitet, passt WEBklar die Datenschutzerklärung an und informiert den Kunden, weil sich dann sein Datenschutz-Absatz (Nr. 6 Abs. 4) ändern kann.
|
||||
|
||||
(2) WEBklar darf die Funktion für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
|
||||
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung dieser Leistungsbeschreibung nicht zustimmt, oder
|
||||
- eine Sicherheitslücke sich nicht anders beheben lässt.
|
||||
|
||||
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
|
||||
|
||||
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar die Funktion ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
|
||||
|
||||
(4) Ist der Kunde Unternehmer, darf WEBklar die Funktion außerdem ohne besonderen Grund mit einer Frist von drei Monaten in Textform für alle Kunden einstellen. Der Kunde kann das Wartungs-Abo dann innerhalb von 30 Tagen nach der Mitteilung zum Zeitpunkt der Einstellung kündigen; die Mindestlaufzeit steht dem nicht entgegen.
|
||||
|
||||
(5) Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
|
||||
|
||||
## 11. Widerruf
|
||||
|
||||
(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.
|
||||
|
||||
(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
|
||||
|
||||
## 12. Hinweise
|
||||
|
||||
(1) **Keine Rechtsberatung.** Der Datenschutz-Absatz, der WhatsApp-Hinweis und die Hinweise des Kundenportals zu Preisangaben sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine rechtliche Beratung im Einzelfall, etwa zu Preisangaben, zum Wettbewerbsrecht oder zum Datenschutz der Website des Kunden, schuldet WEBklar nicht.
|
||||
|
||||
(2) **Datenschutz.** Wie WEBklar im Kundenportal Daten zu Öffnungszeiten live verarbeitet, steht in der Datenschutzerklärung von WEBklar (Abschnitt 6e). Was beim Aufruf der Website des Kunden übertragen wird, beschreibt Nr. 2 Abs. 4; die Information der Besucher gehört in die Datenschutzerklärung der Website (Nr. 6 Abs. 4).
|
||||
|
||||
Anbieter: WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler. Es gelten ergänzend die Abo-Bedingungen und die Datenschutzerklärung.
|
||||
@@ -0,0 +1,63 @@
|
||||
Quelle: https://www.gesetze-im-internet.de/bgbeg/art_253anlage_1.html (Anlage 1), abgerufen 2026-10-02
|
||||
|
||||
(Fundstelle: BGBl. I 2013, S. 3663 – 3664;
|
||||
bzgl. der einzelnen Änderungen vgl. Fußnote)
|
||||
|
||||
Widerrufsbelehrung
|
||||
Widerrufsrecht
|
||||
Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
|
||||
Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag .
|
||||
Um Ihr Widerrufsrecht auszuüben, müssen Sie uns ( ) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
|
||||
Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
|
||||
Folgen des Widerrufs
|
||||
Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
|
||||
|
||||
Gestaltungshinweise:Fügen Sie einen der folgenden in Anführungszeichen gesetzten Textbausteine ein:
|
||||
a)
|
||||
im Falle eines Dienstleistungsvertrags oder eines Vertrags über die Lieferung von Wasser, Gas oder Strom, wenn sie nicht in einem begrenzten Volumen oder in einer bestimmten Menge zum Verkauf angeboten werden, von Fernwärme oder von digitalen Inhalten, die nicht auf einem körperlichen Datenträger geliefert werden: „des Vertragsabschlusses.“;
|
||||
|
||||
b)
|
||||
im Falle eines Kaufvertrags: „, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die Waren in Besitz genommen haben bzw. hat.“;
|
||||
|
||||
c)
|
||||
im Falle eines Vertrags über mehrere Waren, die der Verbraucher im Rahmen einer einheitlichen Bestellung bestellt hat und die getrennt geliefert werden: „, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die letzte Ware in Besitz genommen haben bzw. hat.“;
|
||||
|
||||
d)
|
||||
im Falle eines Vertrags über die Lieferung einer Ware in mehreren Teilsendungen oder Stücken: „, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die letzte Teilsendung oder das letzte Stück in Besitz genommen haben bzw. hat.“;
|
||||
|
||||
e)
|
||||
im Falle eines Vertrags zur regelmäßigen Lieferung von Waren über einen festgelegten Zeitraum hinweg: „, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die erste Ware in Besitz genommen haben bzw. hat.“
|
||||
|
||||
Fügen Sie Ihren Namen, Ihre Anschrift, Ihre Telefonnummer und Ihre E-Mail-Adresse ein.
|
||||
|
||||
Wenn Sie dazu verpflichtet sind, eine Funktion bereitzustellen, mit der der Verbraucher den online geschlossenen Vertrag widerrufen kann, fügen Sie Folgendes ein: „Sie können Ihr Widerrufsrecht auch online unter … [Internetadresse oder anderen geeigneten Hinweis darüber eingeben, wo die Widerrufsfunktion verfügbar ist] ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs. “ Wenn Sie dem Verbraucher die Wahl einräumen, die Information über seinen Widerruf des Vertrags auf Ihrer Webseite elektronisch auszufüllen und zu übermitteln, fügen Sie Folgendes ein: „Sie können das Muster-Widerrufsformular oder eine andere eindeutige Erklärung auch auf unserer Webseite … [Internet-Adresse einfügen] elektronisch ausfüllen und übermitteln. Machen Sie von dieser Möglichkeit Gebrauch, so werden wir Ihnen unverzüglich (z. B. per E-Mail) eine Bestätigung über den Eingang eines solchen Widerrufs übermitteln. “
|
||||
|
||||
Im Falle von Kaufverträgen, in denen Sie nicht angeboten haben, im Falle des Widerrufs die Waren selbst abzuholen, fügen Sie Folgendes ein: „Wir können die Rückzahlung verweigern, bis wir die Waren wieder zurückerhalten haben oder bis Sie den Nachweis erbracht haben, dass Sie die Waren zurückgesandt haben, je nachdem, welches der frühere Zeitpunkt ist.“
|
||||
|
||||
Wenn der Verbraucher Waren im Zusammenhang mit dem Vertrag erhalten hat:
|
||||
a)
|
||||
Fügen Sie ein:
|
||||
–
|
||||
„Wir holen die Waren ab.“ oder
|
||||
|
||||
–
|
||||
„Sie haben die Waren unverzüglich und in jedem Fall spätestens binnen vierzehn Tagen ab dem Tag, an dem Sie uns über den Widerruf dieses Vertrags unterrichten, an … uns oder an [hier sind gegebenenfalls der Name und die Anschrift der von Ihnen zur Entgegennahme der Waren ermächtigten Person einzufügen] zurückzusenden oder zu übergeben. Die Frist ist gewahrt, wenn Sie die Waren vor Ablauf der Frist von vierzehn Tagen absenden.“
|
||||
|
||||
b)
|
||||
fügen Sie ein:
|
||||
–
|
||||
„Wir tragen die Kosten der Rücksendung der Waren.“;
|
||||
|
||||
–
|
||||
„Sie tragen die unmittelbaren Kosten der Rücksendung der Waren.“;
|
||||
|
||||
–
|
||||
Wenn Sie bei einem Fernabsatzvertrag nicht anbieten, die Kosten der Rücksendung der Waren zu tragen, und die Waren aufgrund ihrer Beschaffenheit nicht normal mit der Post zurückgesandt werden können: „Sie tragen die unmittelbaren Kosten der Rücksendung der Waren in Höhe von … EUR [Betrag einfügen].“, oder, wenn die Kosten vernünftigerweise nicht im Voraus berechnet werden können: „Sie tragen die unmittelbaren Kosten der Rücksendung der Waren. Die Kosten werden auf höchstens etwa … EUR [Betrag einfügen] geschätzt.“ oder
|
||||
|
||||
–
|
||||
Wenn die Waren bei einem außerhalb von Geschäftsräumen geschlossenen Vertrag aufgrund ihrer Beschaffenheit nicht normal mit der Post zurückgesandt werden können und zum Zeitpunkt des Vertragsschlusses zur Wohnung des Verbrauchers gebracht worden sind: „Wir holen die Waren auf unsere Kosten ab.“ und
|
||||
|
||||
c)
|
||||
fügen Sie ein: „Sie müssen für einen etwaigen Wertverlust der Waren nur aufkommen, wenn dieser Wertverlust auf einen zur Prüfung der Beschaffenheit, Eigenschaften und Funktionsweise der Waren nicht notwendigen Umgang mit ihnen zurückzuführen ist.“
|
||||
|
||||
Im Falle eines Vertrags, der die Erbringung von Dienstleistungen oder die Lieferung von Wasser, Gas oder Strom, wenn sie nicht in einem begrenzten Volumen oder in einer bestimmten Menge zum Verkauf angeboten werden, oder von Fernwärme zum Gegenstand hat, fügen Sie Folgendes ein: „Haben Sie verlangt, dass die Dienstleistungen oder Lieferung von Wasser/Gas/Strom/Fernwärme [Unzutreffendes streichen] während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht. “
|
||||
@@ -0,0 +1,30 @@
|
||||
Quelle: https://www.gesetze-im-internet.de/bgbeg/art_253anlage_2.html (Anlage 2), abgerufen 2026-10-02
|
||||
|
||||
(Fundstelle: BGBl. I 2013, S. 3665;
|
||||
bzgl. der einzelnen Änderungen vgl. Fußnote)
|
||||
|
||||
Muster-Widerrufsformular
|
||||
(Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
|
||||
|
||||
–
|
||||
An [hier ist der Name, die Anschrift und die E-Mail-Adresse des Unternehmers durch den Unternehmer einzufügen]:
|
||||
|
||||
–
|
||||
Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)
|
||||
|
||||
–
|
||||
Bestellt am (*)/erhalten am (*)
|
||||
|
||||
–
|
||||
Name des/der Verbraucher(s)
|
||||
|
||||
–
|
||||
Anschrift des/der Verbraucher(s)
|
||||
|
||||
–
|
||||
Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
|
||||
|
||||
–
|
||||
Datum
|
||||
|
||||
(*) Unzutreffendes streichen.
|
||||
@@ -0,0 +1,103 @@
|
||||
Normen, auf die sich die Texte der Auftragsbestätigung, der Kundenseiten und der Mails stützen (Auszüge, unverändert, abgerufen 2026-10-06 von gesetze-im-internet.de).
|
||||
Die Datei dient als Nachweis der Zählung; test/auftraegeRechtstexte.test.js gleicht die Zitate in den Texten mit ihr ab.
|
||||
|
||||
Prüfung vom 06.10.2026 (zweiter Abruf von § 649 und § 650 BGB): Der Kostenanschlag steht in § 649 BGB (amtliche Überschrift „Kostenanschlag“). Absatz 2 lautet: „Ist eine solche Überschreitung des Anschlags zu erwarten, so hat der Unternehmer dem Besteller unverzüglich Anzeige zu machen.“ § 650 BGB regelt dagegen den Werklieferungsvertrag und Verbraucherverträge über die Herstellung digitaler Produkte; ein Verweis auf „§ 650“ für den Kostenanschlag ist falsch. Der Satz „Zeichnet sich eine deutliche Überschreitung ab, melden wir uns vorher.“ (AB_TEXTE.aufwand) stützt sich auf § 649 Absatz 2.
|
||||
|
||||
Quelle: https://www.gesetze-im-internet.de/bgb/__312f.html (Auszug: § 312f Absatz 1 und 2 BGB)
|
||||
(1) Bei außerhalb von Geschäftsräumen geschlossenen Verträgen ist der Unternehmer verpflichtet, dem Verbraucher alsbald auf Papier zur Verfügung zu stellen
|
||||
1.
|
||||
eine Abschrift eines Vertragsdokuments, das von den Vertragsschließenden so unterzeichnet wurde, dass ihre Identität erkennbar ist, oder
|
||||
2.
|
||||
eine Bestätigung des Vertrags, in der der Vertragsinhalt wiedergegeben ist.
|
||||
Wenn der Verbraucher zustimmt, kann für die Abschrift oder die Bestätigung des Vertrags auch ein anderer dauerhafter Datenträger verwendet werden. Die Bestätigung nach Satz 1 muss die in Artikel 246a des Einführungsgesetzes zum Bürgerlichen Gesetzbuche genannten Angaben nur enthalten, wenn der Unternehmer dem Verbraucher diese Informationen nicht bereits vor Vertragsschluss in Erfüllung seiner Informationspflichten nach § 312d Absatz 1 auf einem dauerhaften Datenträger zur Verfügung gestellt hat.
|
||||
(2) Bei Fernabsatzverträgen ist der Unternehmer verpflichtet, dem Verbraucher eine Bestätigung des Vertrags, in der der Vertragsinhalt wiedergegeben ist, innerhalb einer angemessenen Frist nach Vertragsschluss, spätestens jedoch bei der Lieferung der Ware oder bevor mit der Ausführung der Dienstleistung begonnen wird, auf einem dauerhaften Datenträger zur Verfügung zu stellen. Die Bestätigung nach Satz 1 muss die in Artikel 246a des Einführungsgesetzes zum Bürgerlichen Gesetzbuche genannten Angaben enthalten, es sei denn, der Unternehmer hat dem Verbraucher diese Informationen bereits vor Vertragsschluss in Erfüllung seiner Informationspflichten nach § 312d Absatz 1 auf einem dauerhaften Datenträger zur Verfügung gestellt.
|
||||
|
||||
Quelle: https://www.gesetze-im-internet.de/bgb/__355.html (Auszug: § 355 Absatz 1 BGB)
|
||||
(1) Wird einem Verbraucher durch Gesetz ein Widerrufsrecht nach dieser Vorschrift eingeräumt, so sind der Verbraucher und der Unternehmer an ihre auf den Abschluss des Vertrags gerichteten Willenserklärungen nicht mehr gebunden, wenn der Verbraucher seine Willenserklärung fristgerecht widerrufen hat. Der Widerruf erfolgt durch Erklärung gegenüber dem Unternehmer. Aus der Erklärung muss der Entschluss des Verbrauchers zum Widerruf des Vertrags eindeutig hervorgehen. Der Widerruf muss keine Begründung enthalten. Zur Fristwahrung genügt die rechtzeitige Absendung des Widerrufs.
|
||||
|
||||
Quelle: https://www.gesetze-im-internet.de/bgb/__356.html (Auszug: § 356 Absatz 4 bis 6 BGB)
|
||||
(4) Das Widerrufsrecht erlischt spätestens zwölf Monate und 14 Tage nach dem in Absatz 2 oder § 355 Absatz 2 Satz 2 genannten Zeitpunkt. Bei Verträgen über Finanzdienstleistungen gilt Satz 1 nicht, wenn der Verbraucher nicht nach Artikel 246b § 1 Absatz 1 Nummer 16 des Einführungsgesetzes zum Bürgerlichen Gesetzbuche über sein Widerrufsrecht belehrt wurde.
|
||||
(5) Das Widerrufsrecht erlischt bei Verträgen über die Erbringung von Dienstleistungen auch unter folgenden Voraussetzungen:
|
||||
1.
|
||||
bei einem Vertrag, der den Verbraucher nicht zur Zahlung eines Preises verpflichtet, wenn der Unternehmer die Dienstleistung vollständig erbracht hat,
|
||||
2.
|
||||
bei einem Vertrag, der den Verbraucher zur Zahlung eines Preises verpflichtet, mit der vollständigen Erbringung der Dienstleistung, wenn der Verbraucher vor Beginn der Erbringung
|
||||
a)
|
||||
ausdrücklich zugestimmt hat, dass der Unternehmer mit der Erbringung der Dienstleistung vor Ablauf der Widerrufsfrist beginnt,
|
||||
b)
|
||||
bei einem außerhalb von Geschäftsräumen geschlossenen Vertrag die Zustimmung nach Buchstabe a auf einem dauerhaften Datenträger übermittelt hat und
|
||||
c)
|
||||
seine Kenntnis davon bestätigt hat, dass sein Widerrufsrecht mit vollständiger Vertragserfüllung durch den Unternehmer erlischt,
|
||||
3.
|
||||
bei einem Vertrag, bei dem der Verbraucher den Unternehmer ausdrücklich aufgefordert hat, ihn aufzusuchen, um Reparaturarbeiten auszuführen, mit der vollständigen Erbringung der Dienstleistung, wenn der Verbraucher die in Nummer 2 Buchstabe a und b genannten Voraussetzungen erfüllt hat,
|
||||
4.
|
||||
bei einem Vertrag über die Erbringung von Finanzdienstleistungen, wenn der Vertrag von beiden Seiten auf ausdrücklichen Wunsch des Verbrauchers vollständig erfüllt ist, bevor der Verbraucher sein Widerrufsrecht ausübt.
|
||||
(6) Das Widerrufsrecht erlischt bei Verträgen über die Bereitstellung von nicht auf einem körperlichen Datenträger befindlichen digitalen Inhalten auch unter folgenden Voraussetzungen:
|
||||
1.
|
||||
bei einem Vertrag, der den Verbraucher nicht zur Zahlung eines Preises verpflichtet, wenn der Unternehmer mit der Vertragserfüllung begonnen hat,
|
||||
2.
|
||||
bei einem Vertrag, der den Verbraucher zur Zahlung eines Preises verpflichtet, wenn
|
||||
a)
|
||||
der Unternehmer mit der Vertragserfüllung begonnen hat,
|
||||
b)
|
||||
der Verbraucher ausdrücklich zugestimmt hat, dass der Unternehmer mit der Vertragserfüllung vor Ablauf der Widerrufsfrist beginnt,
|
||||
c)
|
||||
der Verbraucher seine Kenntnis davon bestätigt hat, dass durch seine Zustimmung nach Buchstabe b mit Beginn der Vertragserfüllung sein Widerrufsrecht erlischt, und
|
||||
d)
|
||||
der Unternehmer dem Verbraucher eine Bestätigung gemäß § 312f zur Verfügung gestellt hat.
|
||||
|
||||
Quelle: https://www.gesetze-im-internet.de/bgb/__356a.html (Auszug: § 356a Absatz 2 und 4 BGB)
|
||||
(2) Die Widerrufsfunktion muss dem Verbraucher ermöglichen, eine Widerrufserklärung an den Unternehmer zu übermitteln und dem Unternehmer in oder mit der Widerrufserklärung ohne Weiteres folgende Informationen bereitzustellen oder zu bestätigen:
|
||||
1.
|
||||
den Namen des Verbrauchers,
|
||||
2.
|
||||
Angaben zur Identifizierung des Vertrags oder des Teils des Vertrags, den der Verbraucher widerrufen möchte,
|
||||
3.
|
||||
Angaben zum elektronischen Kommunikationsmittel, mit welchem dem Verbraucher eine Eingangsbestätigung für den Widerruf zu übermitteln ist.
|
||||
(4) Der Unternehmer hat dem Verbraucher, wenn dieser die Bestätigungsfunktion aktiviert hat, auf einem dauerhaften Datenträger unverzüglich eine Eingangsbestätigung zu übermitteln, die zumindest den Inhalt der Widerrufserklärung nach Absatz 2 sowie das Datum und die Uhrzeit ihres Eingangs enthält.
|
||||
|
||||
Quelle: https://www.gesetze-im-internet.de/bgb/__357a.html (Auszug: § 357a Absatz 2 BGB)
|
||||
(2) Der Verbraucher hat Wertersatz für die bis zum Widerruf erbrachten Dienstleistungen, für die der Vertrag die Zahlung eines Preises vorsieht, oder die bis zum Widerruf erfolgte Lieferung von Wasser, Gas oder Strom in nicht bestimmten Mengen oder nicht begrenztem Volumen oder von Fernwärme zu leisten, wenn
|
||||
1.
|
||||
der Verbraucher von dem Unternehmer ausdrücklich verlangt hat, dass mit der Leistung vor Ablauf der Widerrufsfrist begonnen werden soll,
|
||||
2.
|
||||
bei einem außerhalb von Geschäftsräumen geschlossenen Vertrag der Verbraucher das Verlangen nach Nummer 1 auf einem dauerhaften Datenträger übermittelt hat und
|
||||
3.
|
||||
der Unternehmer den Verbraucher nach Artikel 246a § 1 Absatz 2 Satz 1 Nummer 1 und 3 des Einführungsgesetzes zum Bürgerlichen Gesetzbuche ordnungsgemäß informiert hat.
|
||||
Bei der Berechnung des Wertersatzes ist der vereinbarte Gesamtpreis zu Grunde zu legen. Ist der vereinbarte Gesamtpreis unverhältnismäßig hoch, so ist der Wertersatz auf der Grundlage des Marktwerts der erbrachten Leistung zu berechnen.
|
||||
|
||||
Quelle: https://www.gesetze-im-internet.de/bgbeg/art_246a__1.html (Auszug: Artikel 246a § 1 Absatz 3 EGBGB)
|
||||
(3) Der Unternehmer hat den Verbraucher auch zu informieren, wenn
|
||||
1.
|
||||
dem Verbraucher nach § 312g Absatz 2 Nummer 1, 2, 5 und 7 bis 13 des Bürgerlichen Gesetzbuchs ein Widerrufsrecht nicht zusteht, dass der Verbraucher seine Willenserklärung nicht widerrufen kann, oder
|
||||
2.
|
||||
das Widerrufsrecht des Verbrauchers nach § 312g Absatz 2 Nummer 3, 4 und 6 sowie § 356 Absatz 5 und 6 des Bürgerlichen Gesetzbuchs vorzeitig erlöschen kann, über die Umstände, unter denen der Verbraucher ein zunächst bestehendes Widerrufsrecht verliert.
|
||||
|
||||
Quelle: https://www.gesetze-im-internet.de/bgb/__640.html (Auszug: § 640 Absatz 2 BGB)
|
||||
(2) Als abgenommen gilt ein Werk auch, wenn der Unternehmer dem Besteller nach Fertigstellung des Werks eine angemessene Frist zur Abnahme gesetzt hat und der Besteller die Abnahme nicht innerhalb dieser Frist unter Angabe mindestens eines Mangels verweigert hat. Ist der Besteller ein Verbraucher, so treten die Rechtsfolgen des Satzes 1 nur dann ein, wenn der Unternehmer den Besteller zusammen mit der Aufforderung zur Abnahme auf die Folgen einer nicht erklärten oder ohne Angabe von Mängeln verweigerten Abnahme hingewiesen hat; der Hinweis muss in Textform erfolgen.
|
||||
|
||||
Quelle: https://www.gesetze-im-internet.de/bgb/__649.html (§ 649 BGB, erneut abgerufen 2026-10-06; die amtliche Überschrift lautet „Kostenanschlag“)
|
||||
§ 649 Kostenanschlag
|
||||
(1) Ist dem Vertrag ein Kostenanschlag zugrunde gelegt worden, ohne dass der Unternehmer die Gewähr für die Richtigkeit des Anschlags übernommen hat, und ergibt sich, dass das Werk nicht ohne eine wesentliche Überschreitung des Anschlags ausführbar ist, so steht dem Unternehmer, wenn der Besteller den Vertrag aus diesem Grund kündigt, nur der im § 645 Abs. 1 bestimmte Anspruch zu.
|
||||
(2) Ist eine solche Überschreitung des Anschlags zu erwarten, so hat der Unternehmer dem Besteller unverzüglich Anzeige zu machen.
|
||||
|
||||
Quelle: https://www.gesetze-im-internet.de/bgb/__650.html (Auszug: § 650 Absatz 2 BGB, zur Abgrenzung: § 650 ist nicht der Kostenanschlag; er regelt den Werklieferungsvertrag und Verbraucherverträge über digitale Produkte)
|
||||
§ 650 Werklieferungsvertrag; Verbrauchervertrag über die Herstellung digitaler Produkte
|
||||
(2) Auf einen Verbrauchervertrag, bei dem der Unternehmer sich verpflichtet,
|
||||
1.
|
||||
digitale Inhalte herzustellen,
|
||||
2.
|
||||
einen Erfolg durch eine digitale Dienstleistung herbeizuführen oder
|
||||
3.
|
||||
einen körperlichen Datenträger herzustellen, der ausschließlich als Träger digitaler Inhalte dient,
|
||||
sind die §§ 633 bis 639 über die Rechte bei Mängeln sowie § 640 über die Abnahme nicht anzuwenden. An die Stelle der nach Satz 1 nicht anzuwendenden Vorschriften treten die Vorschriften des Abschnitts 3 Titel 2a. Die §§ 641, 644 und 645 sind mit der Maßgabe anzuwenden, dass an die Stelle der Abnahme die Bereitstellung des digitalen Produkts (§ 327b Absatz 3 bis 5) tritt.
|
||||
|
||||
Quelle: https://www.gesetze-im-internet.de/ao_1977/__147.html (Auszug: § 147 Absatz 1 Nummer 2 bis 4, Absatz 3 Satz 1 und Absatz 4 AO)
|
||||
2.
|
||||
die empfangenen Handels- oder Geschäftsbriefe,
|
||||
3.
|
||||
Wiedergaben der abgesandten Handels- oder Geschäftsbriefe,
|
||||
4.
|
||||
Buchungsbelege,
|
||||
(3) Die in Absatz 1 Nummer 1 und 4a aufgeführten Unterlagen sind zehn Jahre, die in Absatz 1 Nummer 4 aufgeführten Unterlagen acht Jahre und die sonstigen in Absatz 1 aufgeführten Unterlagen sechs Jahre aufzubewahren, sofern nicht in anderen Steuergesetzen kürzere Aufbewahrungsfristen zugelassen sind.
|
||||
(4) Die Aufbewahrungsfrist beginnt mit dem Schluss des Kalenderjahrs, in dem die letzte Eintragung in das Buch gemacht, das Inventar, die Eröffnungsbilanz, der Jahresabschluss oder der Lagebericht aufgestellt, der Handels- oder Geschäftsbrief empfangen oder abgesandt worden oder der Buchungsbeleg entstanden ist, ferner die Aufzeichnung vorgenommen worden ist oder die sonstigen Unterlagen entstanden sind.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Datenschutzerklärung von WEBklar: Abschnitt 6c „Online-Terminbuchung auf Websites unserer Kunden“
|
||||
|
||||
Fassung 2026-10-05 (Stand der Datenschutzerklärung 05.10.2026). Markdown-Quelle des Abschnitts 6c in `public/datenschutz.html`; der Abschnitt steht dort wörtlich so (`test/legalTexts.test.js` prüft, dass beide übereinstimmen). Er beschreibt, was WEBklar selbst verarbeitet: als Auftragsverarbeiter für die Gäste der Betriebe und als Verantwortlicher für die Betriebsdaten, den Entwurf des Fragebogens und die Bestellung im Kundenportal. Den Text für die Website des Betriebs enthält `termine-datenschutz-absatz.md`.
|
||||
|
||||
## Abschnitt (HTML)
|
||||
|
||||
```html
|
||||
<h2 id="termine">6c. Online-Terminbuchung auf Websites unserer Kunden</h2>
|
||||
<p>Kunden mit Wartungs-Abo können auf ihrer eigenen Website eine Online-Terminbuchung anbieten („Terminplaner“), sobald wir die Funktion für sie freigeschaltet haben. Dabei verarbeiten wir die Daten der Personen, die dort einen Termin buchen („Gäste“), ausschließlich als Auftragsverarbeiter nach den Weisungen des jeweiligen Kunden, der dafür Verantwortlicher ist. Wir verwenden diese Daten nicht für eigene Zwecke. Verantwortliche sind wir für die Betriebsdaten des Kunden im Kundenportal, für den Entwurf seines Fragebogens und für seine Bestellung. Diese Datenschutzerklärung gilt für unsere eigenen Seiten und das Kundenportal. Wer auf der Website eines Kunden einen Termin bucht, findet die Informationen zur Buchung (Zwecke, Rechtsgrundlagen, Speicherdauer, Rechte) in der Datenschutzerklärung dieser Website. Anfragen von Gästen zu ihren Daten leiten wir an den Kunden weiter.</p>
|
||||
<p>Für den Betrieb des Terminplaners speichern wir im Auftrag des Kunden: Name, E-Mail-Adresse und, wenn der Kunde diese Felder eingeschaltet hat, Telefonnummer, Nachricht und weitere Angaben des Gastes; die gewählte Leistung, Person oder den gewählten Platz und den Termin; den Stand der Buchung (zum Beispiel bestätigt, abgesagt oder widerrufen) mit den Zeitpunkten; als Nachweis den Zeitpunkt der Bestätigung und der Zustimmungen sowie die Fassung der angezeigten Hinweise; gegebenenfalls den Wunsch nach einer Erinnerung. Der Terminplaner ist nicht für Leistungen der Heilkunde und der Gesundheitsversorgung bestimmt; Gäste werden gebeten, keine Gesundheitsangaben zu machen.</p>
|
||||
<p>Der Kunde sieht, ändert und löscht die Termine im Kundenportal. Dort verarbeiten wir als Verantwortliche seine Einstellungen für den Terminplaner (Betriebsangaben, Leistungen, Namen und Zeiten der gebuchten Personen oder Plätze, Öffnungszeiten, Absagefristen und Texte), wie die übrigen Portal-Daten (Abschnitt 6) auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO. Bevor der Kunde den Terminplaner bestellt, füllt er im Kundenportal einen Fragebogen aus (Branche und Betrieb, Personen oder Plätze, Leistungen, Zeiten, Regeln, Angaben von Gästen, Benachrichtigungen und Website); seine Eingaben speichern wir als Entwurf seiner Einstellungen. Bestellt er nicht, löschen wir den Entwurf 90 Tage nach der letzten Änderung, bei der Löschung seines Kundenkontos zugleich mit dem Konto. Die Bestellung und ihren Nachweis beschreibt Abschnitt 7. Für jede Person oder jeden Platz kann der Kunde einen geheimen Link erzeugen, mit dem er die bestätigten Termine in seinem Kalenderprogramm abonniert; wer den Link kennt, sieht diese Termine. Der Kunde kann den Link jederzeit durch einen neuen ersetzen.</p>
|
||||
<p>Im Namen des Kunden senden wir E-Mails an Gäste (Bestätigung der E-Mail-Adresse, Bestätigung, Änderung oder Absage eines Termins, Eingangsbestätigung eines Widerrufs und, wenn der Gast das gewählt hat, eine Erinnerung) und an den Kunden (neue Buchung, Absage, Widerruf). Der Versand läuft über den E-Mail-Dienst der Hetzner Online GmbH (Rechenzentrum Nürnberg, Abschnitt 8); als Absender erscheint der Kunde, Antworten gehen an ihn. Wir verschicken keine Werbung, setzen keine Tracking-Pixel ein und erhalten keine Kopie. Bis zum Versand liegt jede E-Mail in einer Warteschlange. Nach dem Versand löschen wir dort Empfängeradresse und Inhalt, den Eintrag selbst nach sieben Tagen; E-Mails, die nicht zugestellt werden konnten, löschen wir nach 48 Stunden.</p>
|
||||
<p>Beim Aufruf einer Kundenwebsite mit Terminbuchung lädt der Browser das Programm der Terminbuchung von unserem Server (project.webklar.com) und schon beim Laden der Seite die Konfiguration mit den Texten und Leistungen. Ist die Terminbuchung unmittelbar in die Seite eingebaut, holt sie die freien Termine, sobald der Gast durchklickt; sitzt sie hinter einem Knopf, geschieht das erst nach dem Klick auf den Knopf. Dabei gelten die Protokolle in Abschnitt 3. Die Terminbuchung setzt keine Cookies und speichert nichts im Browser des Gastes. Zum Schutz vor Missbrauch und um die Zahl der Anfragen zu begrenzen (höchstens zehn Buchungen je zehn Minuten und je IP-Adresse), merken wir uns die Zeitpunkte der Aufrufe je IP-Adresse nur im Arbeitsspeicher des Servers, nicht dauerhaft; mit der Buchung speichern wir die IP-Adresse nicht. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere und störungsfreie Betrieb.</p>
|
||||
<p>Löschfristen: Nicht bestätigte Buchungen löschen wir 48 Stunden nach Ablauf der Bestätigungsfrist von 30 Minuten, abgesagte oder abgelehnte Termine nach 30 Tagen, bestätigte Termine nach der Frist, die der Kunde eingestellt hat (Vorgabe 90 Tage, einstellbar von 14 bis 365 Tagen nach dem Termin). Nicht bestellte Entwürfe löschen wir 90 Tage nach der letzten Änderung. Endet das Wartungs-Abo oder schalten wir die Funktion ab, schalten wir den Terminplaner spätestens am folgenden Tag ab und löschen alle Termindaten und Einstellungen 30 Tage danach; beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schalten wir ihn mit den vorhandenen Daten wieder ein. Eine Ausnahme ist das Einrichtungs-Ticket, in dem wir die Einrichtung als Arbeitsnachweis festhalten (Abschnitt 6): Es bleibt mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen. In unseren Datensicherungen bleiben gelöschte Daten noch bis zu sieben Tage erhalten und werden dann überschrieben. Die Daten liegen auf unserem Server bei Hetzner im Rechenzentrum Falkenstein (Deutschland), wie die übrigen Portal-Daten (Abschnitt 3).</p>
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
# Datenschutz-Absatz „Online-Terminbuchung“ für die Website des Betriebs
|
||||
|
||||
Fassung 2026-10-05. Grundlage: `recherche-recht.md` Abschnitt 11.1. Der Absatz gehört in die Datenschutzerklärung der Website des **Betriebs** (der Betrieb ist Verantwortlicher, WEBklar Auftragsverarbeiter). Die Einbauanleitung für Mitarbeiter (`docs/funktionen/termine-einbau.md`, TP2) verweist hierher. Für die Datenschutzerklärung von WEBklar selbst gilt `termine-datenschutz-6c.md`.
|
||||
|
||||
Zwei Fassungen weiter unten: Sie und Du. Nimm die Fassung, die zur Datenschutzerklärung der Website passt (in der Regel zur `anrede` des Betriebs).
|
||||
|
||||
## Platzhalter und optionale Sätze
|
||||
|
||||
- `{{Firma}}`, `{{Anschrift}}`, `{{E-Mail}}`, `{{Telefon}}`: Betriebsangaben aus der Einstellung. `{{Tage}}`: Aufbewahrung nach dem Termin (`aufbewahrungTage`, Vorgabe 90, einstellbar 14 bis 365).
|
||||
- `[[wenn …: …]]`: Satz oder Satzteil nur aufnehmen, wenn die genannte Einstellung aktiv ist; sonst samt Klammern streichen. Beginnt der Satzteil mit „und“, steht nach dem Doppelpunkt ein zweites Leerzeichen: Es ist das Leerzeichen zwischen dem Vordertext und „und“ und fällt mit dem Satzteil weg.
|
||||
|
||||
## Löschfristen (Vorgaben des Löschlaufs, im Text unten verwendet)
|
||||
|
||||
| Daten | Frist |
|
||||
|---|---|
|
||||
| Nicht bestätigte Buchung (Status „verfallen“) | 48 Stunden nach Ablauf der Bestätigungsfrist (30 Minuten) |
|
||||
| Abgesagter oder abgelehnter Termin | 30 Tage |
|
||||
| Bestätigter, erledigter oder nicht wahrgenommener Termin | `aufbewahrungTage` (Vorgabe 90) nach dem Ende des Termins |
|
||||
| Terminanfrage, deren Beginn verstrichen ist | gilt als abgelehnt (durch das System), dann 30 Tage |
|
||||
| Funktion abgeschaltet (zum Beispiel Wartungs-Abo beendet) | alle Termindaten 30 Tage nach dem Abschalten |
|
||||
| Versandwarteschlange für E-Mails | Empfängeradresse und Inhalt werden nach dem Versand geleert, der Eintrag nach 7 Tagen gelöscht; nicht zustellbare Einträge nach 48 Stunden |
|
||||
|
||||
## Abweichungen von Abschnitt 11.1 der Recherche
|
||||
|
||||
1. **Server-Protokolle**: 11.1 sagt, IP-Adresse und Browserangaben würden „nicht gespeichert“. Der Server von WEBklar protokolliert sie aber kurzzeitig (Datenschutzerklärung von WEBklar Abschnitt 3: 14 Tage). Der Absatz sagt das jetzt offen.
|
||||
2. **Rechtsgrundlage**: Bei einer sofort verbindlichen Buchung kommt der Vertrag mit der Bestätigung zustande; „vorvertragliche Maßnahmen“ passt nur für Terminanfragen. Beide Fälle stehen im Text. Neu: Widerruf über die Widerrufsfunktion (Art. 6 Abs. 1 lit. c DSGVO, § 356a BGB).
|
||||
3. **Aufruf**: Das Skript der Terminbuchung wird beim Aufruf der Seite geladen, nicht erst beim Klick auf den Knopf; der Text sagt das.
|
||||
4. **Nachweise**: Gespeichert werden auch der Zeitpunkt der Zustimmungen und die Fassung der angezeigten Texte (Spezifikation, Feld `nachweis`).
|
||||
5. **Löschfristen** angepasst (siehe Tabelle); 11.1 kannte nur „nicht bestätigt 48 Stunden“, „abgesagt 30 Tage“ und „bestätigt {{90}} Tage“.
|
||||
|
||||
## Fassung Sie
|
||||
|
||||
> ### Online-Terminbuchung
|
||||
>
|
||||
> Auf unserer Website können Sie Termine online buchen. Dafür verarbeiten wir die folgenden Daten:
|
||||
>
|
||||
> **Welche Daten?** Ihren Namen, Ihre E-Mail-Adresse, den gewählten Termin, die gewählte Leistung und die gewählte Person oder den gewählten Platz[[wenn Telefon-Feld aktiv: , auf Wunsch Ihre Telefonnummer]][[wenn Nachricht oder Zusatzfelder aktiv: und die Angaben, die Sie uns im Formular mitteilen]]. Außerdem speichern wir, wann Sie den Termin bestätigt haben, welche Fassung der Hinweise Sie bei der Buchung gesehen haben und welche Zustimmungen Sie erteilt haben[[wenn Erinnerung angeboten: , zum Beispiel ob Sie eine Erinnerung gewünscht haben]].
|
||||
>
|
||||
> **Wofür und auf welcher Grundlage?** Wir brauchen die Daten, um Ihren Termin zu verwalten, Ihnen den Termin zu bestätigen und Sie über Änderungen oder Absagen zu informieren. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag über den gebuchten Termin, bei einer Terminanfrage vorvertragliche Maßnahmen auf Ihre Anfrage). [[wenn Erinnerung angeboten: Wenn Sie eine Erinnerung vor dem Termin wünschen, beruht diese auf Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO. Sie können sie jederzeit mit Wirkung für die Zukunft widerrufen, indem Sie uns eine kurze Nachricht senden oder den Termin über den Link in der E-Mail absagen.]] [[wenn Gäste Verbraucher sein können: Widerrufen Sie einen Vertrag über die Schaltfläche „Vertrag widerrufen“, speichern wir Ihre Erklärung mit Datum und Uhrzeit und senden Ihnen eine Eingangsbestätigung (Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit § 356a BGB).]] Zum Schutz vor Missbrauch (zum Beispiel automatisierten Buchungen) und um die Zahl der Anfragen zu begrenzen, merken wir uns Ihre IP-Adresse kurzzeitig nur im Arbeitsspeicher des Servers, nicht dauerhaft (Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere Betrieb der Terminbuchung).
|
||||
>
|
||||
> **E-Mails.** Sie erhalten eine E-Mail, in der Sie Ihre E-Mail-Adresse und den Termin per Link bestätigen, danach eine Bestätigung[[wenn Erinnerung angeboten: und, wenn Sie das angekreuzt haben, eine Erinnerung]]. Sagen wir den Termin ab oder verschieben ihn, informieren wir Sie ebenfalls per E-Mail. Die E-Mails enthalten keine Werbung und keine Tracking-Pixel. Absender ist {{Firma}}, Ihre Antworten gehen an uns. Bis zum Versand liegt die E-Mail in einer Warteschlange; nach dem Versand wird dort die Empfängeradresse samt Inhalt gelöscht, nicht zustellbare E-Mails nach 48 Stunden.
|
||||
>
|
||||
> **Wer verarbeitet die Daten in unserem Auftrag?** Den Terminkalender betreibt die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, als unser Auftragsverarbeiter nach Art. 28 DSGVO. Die Daten liegen auf Servern der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland (Rechenzentren Falkenstein und Nürnberg). Der E-Mail-Versand läuft ebenfalls über Hetzner. Eine Übermittlung in Drittländer findet nicht statt.
|
||||
>
|
||||
> **Beim Aufruf der Terminbuchung.** Beim Aufruf dieser Seite lädt Ihr Browser das Programm der Terminbuchung vom Server der WEBklar GbR (project.webklar.com), beim Öffnen der Terminbuchung zusätzlich die Termine und Texte. Dabei werden technisch bedingt Ihre IP-Adresse und Angaben zu Ihrem Browser übertragen. Der Server speichert sie kurzzeitig in Protokollen und löscht sie nach 14 Tagen (Art. 6 Abs. 1 lit. f DSGVO; berechtigtes Interesse ist der sichere und funktionsfähige Betrieb). Wir setzen dafür keine Cookies und speichern nichts auf Ihrem Gerät.
|
||||
>
|
||||
> **Wie lange speichern wir?** Nicht bestätigte Buchungen (der Bestätigungslink gilt 30 Minuten) löschen wir 48 Stunden nach Ablauf dieser Frist. Bestätigte Termine löschen wir {{Tage}} Tage nach dem Termin, abgesagte oder abgelehnte Termine 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die wir bis zum Terminbeginn nicht beantworten, behandeln wir als abgelehnt. Wird die Online-Terminbuchung abgeschaltet, zum Beispiel weil unser Vertrag mit WEBklar endet, werden alle Termindaten 30 Tage nach dem Abschalten gelöscht. Gesetzliche Aufbewahrungspflichten für Geschäftsunterlagen (zum Beispiel Rechnungen) bleiben davon unberührt; sie erfüllen wir außerhalb des Terminkalenders.
|
||||
>
|
||||
> **Pflicht zur Angabe.** Ohne Name, E-Mail-Adresse und Terminwahl können wir keinen Termin online vergeben. [[wenn Telefon hinterlegt: Sie können uns auch telefonisch erreichen: {{Telefon}}.]] Eine automatisierte Entscheidungsfindung findet nicht statt. Bitte machen Sie bei der Buchung keine Angaben zu Ihrer Gesundheit.
|
||||
>
|
||||
> **Ihre Rechte.** Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit sowie das Recht, der Verarbeitung auf Grundlage berechtigter Interessen zu widersprechen (Art. 15 bis 21 DSGVO). Eine erteilte Einwilligung können Sie jederzeit widerrufen. Sie können sich bei einer Datenschutzaufsichtsbehörde beschweren. Verantwortlich ist {{Firma}}, {{Anschrift}}, E-Mail {{E-Mail}}.
|
||||
|
||||
## Fassung Du
|
||||
|
||||
> ### Online-Terminbuchung
|
||||
>
|
||||
> Auf unserer Website kannst du Termine online buchen. Dafür verarbeiten wir die folgenden Daten:
|
||||
>
|
||||
> **Welche Daten?** Deinen Namen, deine E-Mail-Adresse, den gewählten Termin, die gewählte Leistung und die gewählte Person oder den gewählten Platz[[wenn Telefon-Feld aktiv: , auf Wunsch deine Telefonnummer]][[wenn Nachricht oder Zusatzfelder aktiv: und die Angaben, die du uns im Formular mitteilst]]. Außerdem speichern wir, wann du den Termin bestätigt hast, welche Fassung der Hinweise du bei der Buchung gesehen hast und welche Zustimmungen du erteilt hast[[wenn Erinnerung angeboten: , zum Beispiel ob du eine Erinnerung gewünscht hast]].
|
||||
>
|
||||
> **Wofür und auf welcher Grundlage?** Wir brauchen die Daten, um deinen Termin zu verwalten, dir den Termin zu bestätigen und dich über Änderungen oder Absagen zu informieren. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag über den gebuchten Termin, bei einer Terminanfrage vorvertragliche Maßnahmen auf deine Anfrage). [[wenn Erinnerung angeboten: Wenn du eine Erinnerung vor dem Termin wünschst, beruht diese auf deiner Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO. Du kannst sie jederzeit mit Wirkung für die Zukunft widerrufen, indem du uns eine kurze Nachricht sendest oder den Termin über den Link in der E-Mail absagst.]] [[wenn Gäste Verbraucher sein können: Widerrufst du einen Vertrag über die Schaltfläche „Vertrag widerrufen“, speichern wir deine Erklärung mit Datum und Uhrzeit und senden dir eine Eingangsbestätigung (Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit § 356a BGB).]] Zum Schutz vor Missbrauch (zum Beispiel automatisierten Buchungen) und um die Zahl der Anfragen zu begrenzen, merken wir uns deine IP-Adresse kurzzeitig nur im Arbeitsspeicher des Servers, nicht dauerhaft (Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere Betrieb der Terminbuchung).
|
||||
>
|
||||
> **E-Mails.** Du erhältst eine E-Mail, in der du deine E-Mail-Adresse und den Termin per Link bestätigst, danach eine Bestätigung[[wenn Erinnerung angeboten: und, wenn du das angekreuzt hast, eine Erinnerung]]. Sagen wir den Termin ab oder verschieben ihn, informieren wir dich ebenfalls per E-Mail. Die E-Mails enthalten keine Werbung und keine Tracking-Pixel. Absender ist {{Firma}}, deine Antworten gehen an uns. Bis zum Versand liegt die E-Mail in einer Warteschlange; nach dem Versand wird dort die Empfängeradresse samt Inhalt gelöscht, nicht zustellbare E-Mails nach 48 Stunden.
|
||||
>
|
||||
> **Wer verarbeitet die Daten in unserem Auftrag?** Den Terminkalender betreibt die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, als unser Auftragsverarbeiter nach Art. 28 DSGVO. Die Daten liegen auf Servern der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland (Rechenzentren Falkenstein und Nürnberg). Der E-Mail-Versand läuft ebenfalls über Hetzner. Eine Übermittlung in Drittländer findet nicht statt.
|
||||
>
|
||||
> **Beim Aufruf der Terminbuchung.** Beim Aufruf dieser Seite lädt dein Browser das Programm der Terminbuchung vom Server der WEBklar GbR (project.webklar.com), beim Öffnen der Terminbuchung zusätzlich die Termine und Texte. Dabei werden technisch bedingt deine IP-Adresse und Angaben zu deinem Browser übertragen. Der Server speichert sie kurzzeitig in Protokollen und löscht sie nach 14 Tagen (Art. 6 Abs. 1 lit. f DSGVO; berechtigtes Interesse ist der sichere und funktionsfähige Betrieb). Wir setzen dafür keine Cookies und speichern nichts auf deinem Gerät.
|
||||
>
|
||||
> **Wie lange speichern wir?** Nicht bestätigte Buchungen (der Bestätigungslink gilt 30 Minuten) löschen wir 48 Stunden nach Ablauf dieser Frist. Bestätigte Termine löschen wir {{Tage}} Tage nach dem Termin, abgesagte oder abgelehnte Termine 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die wir bis zum Terminbeginn nicht beantworten, behandeln wir als abgelehnt. Wird die Online-Terminbuchung abgeschaltet, zum Beispiel weil unser Vertrag mit WEBklar endet, werden alle Termindaten 30 Tage nach dem Abschalten gelöscht. Gesetzliche Aufbewahrungspflichten für Geschäftsunterlagen (zum Beispiel Rechnungen) bleiben davon unberührt; sie erfüllen wir außerhalb des Terminkalenders.
|
||||
>
|
||||
> **Pflicht zur Angabe.** Ohne Name, E-Mail-Adresse und Terminwahl können wir keinen Termin online vergeben. [[wenn Telefon hinterlegt: Du kannst uns auch telefonisch erreichen: {{Telefon}}.]] Eine automatisierte Entscheidungsfindung findet nicht statt. Bitte mach bei der Buchung keine Angaben zu deiner Gesundheit.
|
||||
>
|
||||
> **Deine Rechte.** Du hast das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit sowie das Recht, der Verarbeitung auf Grundlage berechtigter Interessen zu widersprechen (Art. 15 bis 21 DSGVO). Eine erteilte Einwilligung kannst du jederzeit widerrufen. Du kannst dich bei einer Datenschutzaufsichtsbehörde beschweren. Verantwortlich ist {{Firma}}, {{Anschrift}}, E-Mail {{E-Mail}}.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Hinweise, Haken und Knopftexte für Gäste (Terminplaner)
|
||||
|
||||
Fassung 2026-10-05. Grundlage: `recherche-recht.md` Abschnitt 11.2 (Hinweis unter dem Formular) und 11.3, `recherche-recht-zahlung.md` Abschnitt 7.3 C bis E (Haken, Widerrufshinweis, Widerrufsfunktion). Die Texte in den Abschnitten 1 bis 3 sind die Fassung aus `server/services/termine/rechtstexte.js` (Platzhalter in doppelten geschweiften Klammern); `test/termineRechtstexte.test.js` prüft, dass sie hier wörtlich stehen. Ändere sie deshalb nur in `rechtstexte.js` und übernimm sie hierher. Die Abschnitte 4 bis 6 geben Texte des Widgets und der Mails wieder (Knopftexte: `knopfText` in `server/services/termine/einstellungen.js`; Widerrufshinweis: Mailvorlage `server/mail-templates/G02 Termin bestaetigt.html`; Eingangsbestätigung: `server/mail-templates/G08 Eingang Widerruf.html`); maßgeblich sind die Mailvorlagen und das Widget, nicht dieses Dokument.
|
||||
|
||||
Platzhalter: `{{Firma}}` = Betriebsname, `{{Anschrift}}` = Anschrift des Betriebs, `{{n}}` = Stunden vor dem Termin (`erinnerung.stundenVorher`: 2, 12, 24 oder 48). Die Texte zeigen die Vorgabe von 90 Tagen Aufbewahrung nach dem Termin; der Wert kommt aus `aufbewahrungTage` (14 bis 365) der Einstellung.
|
||||
|
||||
## 1. Datenschutz-Hinweis unter dem Formular (Kurzfassung nach Art. 13 DSGVO)
|
||||
|
||||
Steht in der Zusammenfassung vor dem Absenden. Sie- oder Du-Fassung nach `anrede` des Betriebs. Neben dem Text zeigt das Widget einen Link auf `betrieb.datenschutzUrl` (die Datenschutzerklärung des Betriebs); die Betriebs-E-Mail steht bewusst nicht im Text. Es gibt keinen Haken dafür.
|
||||
|
||||
Fassung Sie:
|
||||
|
||||
```text
|
||||
Datenschutz. Wir verwenden Ihre Angaben, um Ihren Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO). Betrieben wird der Terminkalender in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland). Wir speichern Ihre Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist. Es werden keine Cookies gesetzt. Bitte machen Sie keine Angaben zu Ihrer Gesundheit. Mehr erfahren Sie in unserer Datenschutzerklärung. Verantwortlich: {{Firma}}, {{Anschrift}}.
|
||||
```
|
||||
|
||||
Fassung Du:
|
||||
|
||||
```text
|
||||
Datenschutz. Wir verwenden deine Angaben, um deinen Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO). Betrieben wird der Terminkalender in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland). Wir speichern deine Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist. Es werden keine Cookies gesetzt. Bitte mach keine Angaben zu deiner Gesundheit. Mehr erfährst du in unserer Datenschutzerklärung. Verantwortlich: {{Firma}}, {{Anschrift}}.
|
||||
```
|
||||
|
||||
Gegenüber Abschnitt 11.2 der Recherche: Die Löschfristen sind die des Löschlaufs (nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist, abgesagte Termine 30 Tage, vergangene Termine `aufbewahrungTage` nach dem Termin) und der Verantwortliche nennt zusätzlich die Anschrift.
|
||||
|
||||
## 2. Haken (Zustimmungen)
|
||||
|
||||
Wortlaut immer in der Sie-Form, auch bei Betrieben, die ihre Gäste duzen: Es sind Rechtstexte. Keiner der Haken ist vorangekreuzt.
|
||||
|
||||
| Haken | Wann | Pflicht | Wortlaut |
|
||||
|---|---|---|---|
|
||||
| Vorzeitiger Beginn | Verbraucher, mit Preis: bei sofort buchbaren Leistungen, wenn der Terminbeginn nicht nach dem Ende der Widerrufsfrist liegt (14 Tage ab dem Tag nach dem Vertragsschluss, Berliner Kalender, §§ 187, 188, 193 BGB; gerechnet mit dem Zeitpunkt der Buchung plus Haltezeit; R29); bei bepreisten Anfragen immer, weil der Tag der Annahme nicht feststeht (R31) | ja | Ich verlange ausdrücklich, dass {{Firma}} mit der Leistung vor Ablauf der Widerrufsfrist beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB). |
|
||||
| Stornobedingungen | der Betrieb hat Stornobedingungen hinterlegt und die gebuchte Leistung hat einen Preis (bei kostenlosen Leistungen fragt der Terminplaner nicht danach) | ja | Ich habe die Stornobedingungen gelesen. |
|
||||
| Erinnerung | der Betrieb bietet Erinnerungen an | nein | Ich möchte {{n}} Stunden vor dem Termin eine Erinnerung per E-Mail erhalten. Diese Einwilligung kann ich jederzeit widerrufen. |
|
||||
|
||||
## 3. Hinweis über dem Nachrichtenfeld
|
||||
|
||||
Bitte keine Gesundheitsangaben.
|
||||
|
||||
Der Terminplaner ist nicht für den Gesundheitsbereich bestimmt (Spezifikation, Vorgaben). Der Hinweis steht über jedem Freitextfeld.
|
||||
|
||||
## 4. Knopftexte (exakt)
|
||||
|
||||
| Fall | Knopf im Widget | Knopf auf der Bestätigungsseite |
|
||||
|---|---|---|
|
||||
| verbindlich, mit Preis | Zahlungspflichtig buchen | Zahlungspflichtig bestätigen |
|
||||
| verbindlich, ohne Preis | Termin buchen | Termin bestätigen |
|
||||
| Anfrage | Termin anfragen | Anfrage absenden |
|
||||
| Widerruf (Funktion) | Vertrag widerrufen | Widerruf bestätigen |
|
||||
|
||||
## 5. Widerrufshinweis in der Bestätigungs-E-Mail
|
||||
|
||||
Er ersetzt nicht die Widerrufsbelehrung (`termine-widerrufsbelehrung-muster.md`), sondern führt zu ihr. Gilt für Buchungen von Verbrauchern, auch für Anfragen: Der Vertrag kommt mit der Annahme zustande, ist aber ebenfalls ein Fernabsatzvertrag, und die Frist läuft ab der Annahme (R55, R29). Die Widerrufsfunktion steht im Widget als zweiter Knopf im Fuß und im Knopfmodus dauerhaft neben dem Buchungsknopf (R54).
|
||||
|
||||
**Mit Widerrufsrecht** (Bestätigungs-E-Mail G02, vor der Belehrung; das Widget zeigt dort die ausführliche Belehrung zum Aufklappen):
|
||||
|
||||
> **Widerrufsrecht.** Wenn Sie Verbraucher sind, können Sie diesen Vertrag innerhalb von 14 Tagen ab Vertragsschluss ohne Angabe von Gründen widerrufen. Dafür nutzen Sie die Schaltfläche „Vertrag widerrufen“ im Terminbereich auf unserer Website oder den Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail. Die ausführliche Widerrufsbelehrung und das Muster-Widerrufsformular stehen in Ihrer Bestätigungs-E-Mail.
|
||||
|
||||
Maßgeblich für die Mail ist die Vorlage G02: Bis zum Beginn des Termins geht der Widerruf über den Knopf „Vertrag widerrufen“; danach über das Formular „Vertrag widerrufen“ auf der Website des Betriebs oder per E-Mail an den Betrieb. Die Verwaltungsseite bietet den Knopf nur bis zum Beginn des Termins an.
|
||||
|
||||
**Freizeit-Dienstleistung mit festem Termin** (§ 312g Abs. 2 Nr. 9 BGB; `recht.freizeit`), kein Widerrufsrecht, keine Belehrung, kein Formular, keine Funktion:
|
||||
|
||||
> **Für diese Buchung besteht kein Widerrufsrecht.** Ihre Buchung ist verbindlich; Absagen richten sich nach den Stornobedingungen.
|
||||
|
||||
Gäste, die nur Unternehmen sind (`recht.gaesteSind = unternehmer`): kein Hinweis.
|
||||
|
||||
## 6. Widerrufsfunktion (Texte)
|
||||
|
||||
- Schaltfläche oder Link: **Vertrag widerrufen** (im Terminbereich der Website, auf der Verwaltungsseite hinter „Termin verwalten“).
|
||||
- Formular ohne Anmeldung: Name · Buchungsnummer · E-Mail-Adresse für die Eingangsbestätigung. Schaltfläche: **Widerruf bestätigen**.
|
||||
- Eingangsbestätigung per E-Mail (G08; die Mailvorlage ist maßgeblich, hier nur der Kern. Die Mail enthält keine Eingaben des Gastes, nur die Buchungsnummer und den Eingang):
|
||||
|
||||
> Betreff: Eingangsbestätigung Ihres Widerrufs
|
||||
>
|
||||
> Wir bestätigen den Eingang Ihres Widerrufs. Inhalt Ihrer Erklärung: Widerruf des Vertrags zur Buchung {{nummer}}. Eingegangen am {{eingang}} (Datum und Uhrzeit). {{Nur im Block „zugeordnet“: Ihr Termin ist abgesagt.}} Absender: {{Firma}}.
|
||||
|
||||
Der Eingang muss mit Datum und Uhrzeit bestätigt werden (§ 356a BGB); das ist auch der Text der Belehrung („Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs“).
|
||||
@@ -0,0 +1,966 @@
|
||||
# Kanzlei-Paket Terminplaner (WEBklar)
|
||||
|
||||
Dieses Paket gehört zum Verkaufsweg des Terminplaners der WEBklar GbR (Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein). Es zeigt einer Kanzlei, was verkauft wird, wie der Ablauf ist, welche Texte Kunden und Gäste in Endfassung sehen, wo diese Texte im Programm stehen und was wir wissen möchten.
|
||||
|
||||
Fassungen: Leistungsbeschreibung, Auftragsverarbeitungsvertrag (AVV), Widerrufsbelehrung und Bestelltexte tragen die Fassung **2026-10-05** (Konstante `config.termine.avvVersion`). Die Gästetexte (Hinweise, Haken, Belehrung und Datenschutz-Absatz für die Gäste unserer Kunden) tragen die Fassung **2026-10-05** (Konstante `RECHTSTEXTE_VERSION`). Beide Konstanten haben heute dasselbe Datum, sind aber getrennt und können sich später unterscheiden.
|
||||
|
||||
## 1. Zweck und Umfang
|
||||
|
||||
**Zweck.** Der Inhaber lässt die Texte des Terminplaners durch eine Kanzlei prüfen, bevor der erste fremde Betrieb den Terminplaner nutzt. Bis dahin ist der Terminplaner nicht für fremde Betriebe freigeschaltet. Das Paket soll ohne Rückfragen zeigen, was die Kanzlei prüfen soll und von welchen Annahmen wir ausgehen.
|
||||
|
||||
**Kernpaket.** Der Inhaber hat festgelegt, dass die Kanzlei dieses Kernpaket prüft und alles Übrige WEBklar selbst prüft (Hausrunde). Die Kanzlei prüft:
|
||||
|
||||
- die Widerrufsbelehrung und das Muster-Widerrufsformular der Bestellung der Einrichtung für 149 € (Abschnitt 3.2),
|
||||
- den Zustimmungssatz zum vorzeitigen Beginn mit Erlöschen des Widerrufsrechts und Wertersatz (Abschnitt 3.1, Haken 3),
|
||||
- den Auftragsverarbeitungsvertrag Terminplaner (Anlage A, Auszüge in Abschnitt 3.5),
|
||||
- die Haftungs- und Ende-mit-dem-Abo-Klauseln der Leistungsbeschreibung (Anlage B, Auszüge in Abschnitt 3.4),
|
||||
- die Gästetexte mit Gesetzlichkeitsfiktion: Widerrufsbelehrung für Gäste und die Hinweise und Haken im Widget (Abschnitte 3.6 bis 3.10).
|
||||
|
||||
**Hausrunde.** Die Datenschutzerklärung von WEBklar (Abschnitt 6c und die Ergänzungen in den Abschnitten 6 und 7), die Hinweise im Kundenportal und die Mailtexte prüft WEBklar selbst. Zu einzelnen Punkten daraus stellen wir trotzdem Fragen (Q15 bis Q19 und Q24); was wir nicht fragen, steht in Abschnitt 6.
|
||||
|
||||
**Begriffe.** *Betriebsart* (im Programm „Modus“) des Terminplaners: `off` (abgeschaltet), `pilot` (nur Kunden auf einer Liste von Pilotadressen) oder `on` (alle Kunden mit Wartungs-Abo). *Pilot* ist ein Kunde auf dieser Liste; derzeit sind das nur Konten des Inhabers. `on` setzt der Inhaber erst nach Ihrem Befund und seiner Freigabe. *Kulanz* heißt, dass das Team einem Kunden den Terminplaner ohne Bestellung und ohne Zahlung freischaltet (Abschnitt 2.1). *Fassung* ist ein Stand der Texte mit Datum.
|
||||
|
||||
**Keine Rechtsberatung durch WEBklar.** Die Texte sind Mustertexte für die Standardkonfiguration und keine Rechtsberatung (Leistungsbeschreibung Nr. 11 Abs. 1). Die rechtlichen Aussagen in diesem Paket (vor allem „unsere Annahme“ bei den Fragen) sind ein Arbeitsstand von WEBklar und kein Rechtsurteil.
|
||||
|
||||
**Fassungen und Änderungen.** Eine ausgelieferte Fassung wird nie überschrieben. Ändern wir nach dem Befund einen Text, bekommt er ein neues Fassungsdatum, neue Dateien (HTML und PDF) und ein neues Archiv der Gästetexte; die Zitate in diesem Paket werden dann angepasst. Ein Test prüft jedes Zitat wörtlich gegen den Quelltext, damit Paket und Programm nicht auseinanderlaufen.
|
||||
|
||||
**Aufbau.** Abschnitt 2 beschreibt den Produktablauf, Abschnitt 3 enthält die Texte, Abschnitt 4 unsere Fragen Q1 bis Q26, Abschnitt 5 die Fundstellen im Programm, Abschnitt 6 was nicht Teil der Prüfung ist, Abschnitt 7 die Checkliste für den Inhaber. Hinter jedem Zitat in Abschnitt 3 steht „⚖ Q…“ mit den Fragen, die den Text betreffen. Anlagen: **Anlage A** ist der AVV Terminplaner, **Anlage B** die Leistungsbeschreibung Terminplaner, beide in voller Länge als PDF (Dateien in Abschnitt 3.11). **Anlage C** sind die gerenderten Mails K15 (Bestellbestätigung mit den Zustimmungen) und K16 (Terminplaner bereit); sie folgt nach der Mail-Überarbeitung des Inhabers, weil ihr Wortlaut bis dahin noch nicht endgültig ist.
|
||||
|
||||
## 2. Produktablauf in Kurzform
|
||||
|
||||
Der Terminplaner ist ein Buchungs-Widget für die Website eines Betriebs (zum Beispiel Friseur, Werkstatt oder Studio) mit einer Verwaltung im Kundenportal von WEBklar. Der Betrieb ist Kunde von WEBklar („Kunde“); seine Besucher, die einen Termin buchen, sind „Gäste“. Verkauft wird die Einrichtung für 149 € einmalig je Website-Projekt, ohne Umsatzsteuer gemäß § 19 UStG. Für den Betrieb berechnet WEBklar neben dem Wartungs-Abo nichts; der Terminplaner läuft, solange das Wartungs-Abo läuft. Bezahlt wird beim Terminplaner nur vor Ort, es gibt keine Online-Zahlung der Gäste.
|
||||
|
||||
### 2.1 Bestellung und Einrichtung
|
||||
|
||||
| Schritt | Was geschieht | Frage |
|
||||
|---|---|---|
|
||||
| 1. Abo-Prüfung | Nur Kunden mit laufendem Wartungs-Abo können bestellen; ein Hosting-Abo genügt nicht. Bestellbar ist der Terminplaner nur, solange das Wartungs-Abo aktiv ist und keine Kündigung vorgemerkt ist. | ⚖ Q25 |
|
||||
| 2. Fragebogen-Entwurf | Der Kunde beantwortet im Kundenportal einen Fragebogen in acht Schritten. Jeder Schritt wird sofort als Entwurf der späteren Einstellungen gespeichert; ein Entwurf ohne Bestellung wird 90 Tage nach der letzten Änderung gelöscht. Die Branchenfrage kennt den Gesundheitsbereich; wählt der Kunde ihn, ist die Bestellung gesperrt. Zusätzlich warnt eine Stichwortprüfung über Betriebsname, Leistungen und Felder das Team, das die Einrichtung ablehnen kann. | ⚖ Q11, Q24 |
|
||||
| 3. Bestellung mit vier Haken | „Prüfen und bestellen“: Zusammenfassung, offene Punkte (der Server prüft, ob alles Nötige vorliegt, darunter die Adressen von Impressum und Datenschutzerklärung der Website), volle Widerrufsbelehrung mit Muster-Widerrufsformular, vier Haken und der Knopf „Zahlungspflichtig bestellen · 149 €“ (Abschnitte 3.1 und 3.2). Alle Besteller werden wie Verbraucher behandelt. | ⚖ Q1, Q3, Q19, Q20 |
|
||||
| 4. Nachweis | Der Server legt zuerst die Zahlungssitzung bei Stripe an und speichert dann den Nachweis: die gezeigten Texte, ihre Fassungen, einen Hash, den Zeitpunkt des Klicks, IP-Adresse und Browser-Kennzeichen, Kundenkonto und angemeldete Person. Erst danach erhält der Browser die Adresse der Zahlungsseite. Der Zeitpunkt des Klicks ist der Anker der Widerrufsfrist. | ⚖ Q2, Q23 |
|
||||
| 5. Zahlung | Stripe Checkout (Karte oder SEPA-Lastschrift); Stripe erstellt die Rechnung. | ⚖ Q19 |
|
||||
| 6. Ticket und Bestätigung | Nach dem Zahlungseingang legt das Portal ein Einrichtungs-Ticket mit der Einbauanleitung an. Der Kunde erhält die Bestellbestätigung K15 mit den gezeigten Zustimmungen und, als PDF-Anhang auf einem dauerhaften Datenträger, Leistungsbeschreibung, AVV und Widerrufsbelehrung mit Muster-Widerrufsformular in der Fassung, der er zugestimmt hat. Das Team erhält die Meldung T16. | ⚖ Q3, Q20 |
|
||||
| 7. Einbau und Freischaltung | WEBklar baut das Widget ein, der Kunde nimmt es ab, WEBklar schaltet es frei; der Kunde erhält die Mail K16 („bereit“). | |
|
||||
| 8. Betrieb | Gäste buchen im Widget (Abschnitt 2.2), der Kunde verwaltet seine Termine im Kundenportal. | |
|
||||
|
||||
**Freischaltung ohne Bestellung (Pilot, Kulanz).** Das Team kann einen Terminplaner ohne Zahlung aktivieren. Sobald die Betriebsart `on` gilt (alle Kunden mit Wartungs-Abo), verlangt das Portal dafür einen Nachweis der Zustimmung des Kunden in Textform (Datum, Weg Chat oder E-Mail, Name des Mitarbeiters), außer für Pilotadressen; danach erhält der Kunde die Mail K16 mit Leistungsbeschreibung und AVV als PDF. Solche Dokumente gelten nicht als durch Kauf freigeschaltet; der tägliche Abo-Abgleich nimmt sie aus (⚖ Q7, Q21).
|
||||
|
||||
### 2.2 Gast: Buchung, Widerruf, Daten
|
||||
|
||||
1. Der Gast wählt Leistung, Termin und gegebenenfalls Person oder Platz und gibt Name und E-Mail-Adresse an (je nach Einstellung des Kunden auch Telefon, Nachricht, Zusatzfelder). Er sieht den Datenschutz-Hinweis (Abschnitt 3.8), die Haken (Abschnitt 3.7) und, bei Verbrauchern, die Widerrufsbelehrung zum Aufklappen (Abschnitt 3.6). ⚖ Q12, Q13, Q17
|
||||
2. Die Buchung wird 30 Minuten gehalten. In dieser Zeit bestätigt der Gast per Link in einer E-Mail; erst die Bestätigung macht die Buchung verbindlich, bei einer Terminanfrage nimmt der Kunde später an. Nicht bestätigte Buchungen löscht WEBklar 48 Stunden nach Ablauf der Bestätigungsfrist. ⚖ Q14
|
||||
3. Die Bestätigungs-E-Mail (G02) enthält bei Verbrauchern Widerrufshinweis, Widerrufsbelehrung und Muster-Widerrufsformular; bei einer Freizeit-Dienstleistung mit festem Termin, die der Kunde so einstuft, steht stattdessen der Hinweis, dass kein Widerrufsrecht besteht (Abschnitt 3.9). ⚖ Q15, Q16
|
||||
4. Widerrufen kann der Gast über den Knopf „Vertrag widerrufen“ im Terminbereich der Website oder über den Link „Termin verwalten“ in der Bestätigungs-E-Mail; er erhält eine Eingangsbestätigung mit Datum und Uhrzeit (G08). Die Widerrufsfrist beträgt 14 Tage.
|
||||
5. Der Terminplaner setzt keine Cookies und speichert nichts im Browser des Gastes; die IP-Adresse dient dem Missbrauchsschutz nur im Arbeitsspeicher des Servers. Die Buchung speichert als Nachweis die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der gezeigten Texte; WEBklar archiviert jede Fassung der Gästetexte. ⚖ Q26
|
||||
6. Löschfristen: bestätigte und erledigte Termine 90 Tage nach dem Terminende (einstellbar 14 bis 365 Tage), abgesagte oder abgelehnte Termine 30 Tage nach der Absage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist; in der Versandwarteschlange für E-Mails werden Empfängeradresse und Inhalt nach dem Versand geleert und der Eintrag nach 7 Tagen gelöscht; Sicherungen halten gelöschte Daten bis zu sieben Tage. ⚖ Q9
|
||||
|
||||
### 2.3 Ende des Abos
|
||||
|
||||
1. Ein täglicher Abgleich prüft das Wartungs-Abo. Endet es (auch durch Wechsel ins Hosting-Abo oder durch Widerruf des Abschlusses oder eines Wechsels), schaltet WEBklar den Terminplaner spätestens am folgenden Tag ab und informiert den Kunden mit der Mail K22. Ist etwas nicht lesbar, schaltet der Abgleich nichts ab und versucht es am nächsten Tag erneut. ⚖ Q7
|
||||
2. Nach dem Abschalten nimmt der Terminplaner keine neuen Buchungen mehr an. Die Termine bleiben im Kundenportal sichtbar und als CSV-Datei exportierbar, Gäste können über ihren Link absagen, bereits angeforderte Erinnerungen laufen weiter, der Kunde informiert seine Gäste.
|
||||
3. 30 Tage nach dem Abschalten löscht WEBklar alle Termindaten und die Einstellungen; das Einrichtungs-Ticket des Teams bleibt als Arbeitsnachweis mit einer Kurzfassung der Einstellungen bestehen (keine Daten von Gästen). Beginnt innerhalb dieser 30 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein (Rückkehr, Mail K23). Nach der Löschung sind ein neuer Fragebogen und die Einrichtung für 149 € noch einmal nötig. ⚖ Q9
|
||||
4. Endet das Wartungs-Abo, bevor der Terminplaner freigeschaltet ist, erstattet WEBklar die 149 € vollständig. Der Abgleich schaltet den Terminplaner mit dem Grund „Abo endete vor Einrichtung“ ab und meldet dem Team die Erstattung (T19); die Erstattung löst der Inhaber in Stripe aus, es gibt keine automatische Rückzahlung. In diesem Fall gibt es keine automatische Rückkehr. ⚖ Q6
|
||||
|
||||
### 2.4 Widerruf der Bestellung durch den Kunden
|
||||
|
||||
Der Kunde widerruft auf der Seite „Vertrag widerrufen“ mit der Auswahl „Terminplaner (Einrichtung)“. Jede Bestellung hat ihre eigene Frist von 14 Tagen ab dem Zeitpunkt des Klicks auf „Zahlungspflichtig bestellen“ (bei mehreren Website-Projekten also mehrere Fristen). Die Widerrufsseite ordnet den Widerruf keinem Projekt zu: Sie bewertet den Eingang gegen das späteste Fristende aller bezahlten Terminplaner-Bestellungen des Kunden; das Team erhält in der Meldung T09 jede Bestellung mit Projekt, Zustand und eigenem Fristende und entscheidet. Der Widerruf erzeugt die Mail K04 an den Kunden und die Meldung T09 an das Team; die Abwicklung (Erstattung in Stripe abzüglich Wertersatz für einen schon begonnenen Einbau, Abschalten des Terminplaners) erledigt das Team von Hand. ⚖ Q2, Q5, Q7
|
||||
|
||||
### 2.5 Zahlen
|
||||
|
||||
| Wert | Bedeutung | Quelle im Code |
|
||||
|---|---|---|
|
||||
| 149 € | Einrichtung je Website-Projekt, einmalig, ohne Umsatzsteuer gemäß § 19 UStG | `priceLabel` des Terminplaners, `amountCents` 14900 |
|
||||
| 14 Tage | Widerrufsfrist | `WITHDRAWAL_DAYS` |
|
||||
| 30 Minuten | Haltezeit einer Buchung, Gültigkeit des Bestätigungslinks | `holdMinutes` |
|
||||
| 48 Stunden | Löschung nicht bestätigter Buchungen nach Ablauf der Bestätigungsfrist | `VERFALLEN_BEHALTEN_MS` |
|
||||
| 90 Tage | Aufbewahrung bestätigter Termine nach dem Terminende (Vorgabe) und Aufbewahrung von Entwürfen ohne Bestellung | `AUFBEWAHRUNG_VORGABE`, `ENTWURF_TAGE` |
|
||||
| 7 Tage | Eintrag in der Versandwarteschlange nach dem Versand; Sicherungen des Servers | `GESENDET_BEHALTEN_MS` |
|
||||
| 30 Tage | Löschung nach dem Abschalten; Löschung abgesagter oder abgelehnter Termine | `LOESCHFRIST_TAGE`, `ABGESAGT_BEHALTEN_MS` |
|
||||
|
||||
## 3. Texte in Endfassung
|
||||
|
||||
Alle Zitate stehen wörtlich so im Programm oder in den Dokumenten (Abschnitt 5 nennt die Fundstellen). Platzhalter in doppelten geschweiften Klammern stehen für Angaben des jeweiligen Betriebs. In den Zitaten steht vor dem Sternchen in Klammern (\*) und vor spitzen Klammern ein Rückstrich, damit die Darstellung sie nicht als Hervorhebung oder Markup deutet; gemeint sind die Zeichen selbst.
|
||||
|
||||
### 3.1 Bestellung des Terminplaners: vier Haken und Knopf
|
||||
|
||||
Auf der Bestellseite stehen vor dem Knopf die volle Widerrufsbelehrung mit Muster-Widerrufsformular (Abschnitt 3.2) und diese vier Haken, keiner ist vorangekreuzt. Die Links auf Leistungsbeschreibung, AVV und Widerrufsbelehrung in der Fassung stehen daneben, nicht im Text der Haken. Gespeichert wird genau der gezeigte Text mit Fassung und Zeitpunkt.
|
||||
|
||||
**Haken 1 (`unterlagen`): Zustimmung zu Leistungsbeschreibung und AVV**
|
||||
|
||||
> Ich stimme der Leistungsbeschreibung Terminplaner und dem Auftragsverarbeitungsvertrag Terminplaner in der Fassung 2026-10-05 zu.
|
||||
|
||||
⚖ Q1, Q10
|
||||
|
||||
**Haken 2 (`gesundheit`): kein Gesundheitsbereich**
|
||||
|
||||
> Mein Angebot liegt nicht im Gesundheitsbereich (zum Beispiel keine Arzt-, Zahnarzt-, Psychotherapie-, Heilpraktiker- oder Physiotherapiepraxis).
|
||||
|
||||
⚖ Q11
|
||||
|
||||
**Haken 3 (`beginn`): vorzeitiger Beginn, Erlöschen, Wertersatz**
|
||||
|
||||
> Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit der Einrichtung des Terminplaners beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Einrichtung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).
|
||||
|
||||
⚖ Q3, Q5
|
||||
|
||||
**Haken 4 (`information`): Pflichtinformationen**
|
||||
|
||||
> Ich habe die Pflichtinformationen zu dieser Bestellung gelesen: Leistung (Einrichtung des Terminplaners für diese Website: ein Widget an einer Stelle, gestaltet nach dem Fragebogen, mit Einbau und Datenschutz-Absatz), Gesamtpreis 149 € einmalig (ohne Umsatzsteuer gemäß § 19 UStG), voraussichtliche Dauer meist innerhalb von 1–2 Wochen, der Terminplaner endet mit dem Wartungs-Abo. Die Widerrufsbelehrung mit Muster-Widerrufsformular habe ich gelesen.
|
||||
|
||||
⚖ Q7, Q19, Q20
|
||||
|
||||
**Knopf**
|
||||
|
||||
> Zahlungspflichtig bestellen · 149 €
|
||||
|
||||
⚖ Q2, Q19
|
||||
|
||||
### 3.2 Widerrufsbelehrung und Muster-Widerrufsformular der Funktionen
|
||||
|
||||
WEBklar ist hier der Unternehmer. Die Belehrung steht auf der Bestellseite, im PDF-Anhang der Bestellbestätigung K15 und in der Datei `docs/rechtliches/widerrufsbelehrung-funktionen.md` (Fassung 2026-10-05).
|
||||
|
||||
**Widerrufsbelehrung und Muster-Widerrufsformular (Bestellung der Einrichtung)**
|
||||
|
||||
> Widerrufsbelehrung
|
||||
>
|
||||
> Widerrufsrecht
|
||||
>
|
||||
> Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
|
||||
>
|
||||
> Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag des Vertragsabschlusses.
|
||||
>
|
||||
> Um Ihr Widerrufsrecht auszuüben, müssen Sie uns (WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, Telefon: 0176 23726355, E-Mail: support@webklar.com) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
|
||||
>
|
||||
> Sie können Ihr Widerrufsrecht auch online unter https://project.webklar.com/widerrufen.html ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.
|
||||
>
|
||||
> Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
|
||||
>
|
||||
> Folgen des Widerrufs
|
||||
>
|
||||
> Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
|
||||
>
|
||||
> Haben Sie verlangt, dass die Dienstleistungen während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht.
|
||||
>
|
||||
> Muster-Widerrufsformular
|
||||
>
|
||||
> (Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
|
||||
>
|
||||
> – An WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, E-Mail: support@webklar.com:
|
||||
> – Hiermit widerrufe(n) ich/wir (\*) den von mir/uns (\*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (\*)/die Erbringung der folgenden Dienstleistung (\*)
|
||||
> – Bestellt am (\*)/erhalten am (\*)
|
||||
> – Name des/der Verbraucher(s)
|
||||
> – Anschrift des/der Verbraucher(s)
|
||||
> – Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
|
||||
> – Datum
|
||||
>
|
||||
> (\*) Unzutreffendes streichen.
|
||||
|
||||
⚖ Q1, Q2, Q12
|
||||
|
||||
Das Dokument enthält vor der Belehrung einen Einleitungssatz und nach dem Muster-Widerrufsformular „Hinweise zum Widerruf“, die nicht zur gesetzlichen Belehrung gehören:
|
||||
|
||||
**Einleitung des Dokuments**
|
||||
|
||||
> Das Widerrufsrecht gilt nur für Verbraucher. Wir zeigen die Belehrung und das Muster-Widerrufsformular trotzdem allen, die eine Funktion bestellen. Die Belehrung gilt für die Bestellung der Einrichtung einer Funktion im Kundenportal, zum Beispiel des Terminplaners. Hinweise dazu stehen nach dem Muster-Widerrufsformular.
|
||||
|
||||
⚖ Q1
|
||||
|
||||
**Hinweise zum Widerruf**
|
||||
|
||||
> Diese Hinweise gehören nicht zur gesetzlichen Widerrufsbelehrung. Sie erklären, wie sich das Widerrufsrecht auf die Bestellung einer Funktion auswirkt.
|
||||
>
|
||||
> - Das Widerrufsrecht gilt für jede Bestellung einer Funktion gesondert, also für jedes Website-Projekt, für das der Kunde eine Funktion bestellt. Die Frist von vierzehn Tagen beginnt jeweils mit dem Vertragsschluss.
|
||||
> - Widerruft der Kunde die Einrichtung einer Funktion, endet diese Funktion. Das Wartungs-Abo bleibt davon unberührt.
|
||||
> - Hat WEBklar auf ausdrücklichen Wunsch des Kunden vor Ablauf der Widerrufsfrist mit der Einrichtung begonnen, schuldet der Kunde bei einem Widerruf einen angemessenen Betrag für die bis dahin erbrachte Leistung. Der Betrag bemisst sich als Anteil am Gesamtpreis; Maß ist der im Arbeitsblatt erfasste Aufwand für die Einrichtung.
|
||||
> - Die Rückzahlung erfolgt über das ursprüngliche Zahlungsmittel; die Zahlung lief über Stripe.
|
||||
|
||||
⚖ Q5, Q7
|
||||
|
||||
### 3.3 Zustimmung bei der Bestellung der News-Pinnwand (Altbestand)
|
||||
|
||||
Die News-Pinnwand (30 € einmalig) wird seit 2026-09-30 über denselben Bestellweg verkauft, bisher mit einem Haken und ohne Widerrufsbelehrung. Zum Vergleich der Text, den neue Bestellungen jetzt sehen, und der Haken, der bis zur Korrektur galt (Programmstand 3a0b008 vor der Korrektur). Die Leistungsbeschreibung der News-Pinnwand aus dieser Zeit steht in Frage Q4.
|
||||
|
||||
**Neuer Haken der News-Pinnwand**
|
||||
|
||||
> Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung des Einbaus erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).
|
||||
|
||||
⚖ Q4, Q3
|
||||
|
||||
**Alte Fassung (Stand 3a0b008): früherer Haken der News-Bestellung**
|
||||
|
||||
> Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht erlischt, sobald der Einbau vollständig erbracht ist (§ 356 Abs. 4 BGB).
|
||||
|
||||
⚖ Q4
|
||||
|
||||
### 3.4 Leistungsbeschreibung Terminplaner (Anlage B), Auszüge
|
||||
|
||||
Die Datei ist `docs/rechtliches/leistungsbeschreibung-terminplaner.md`; sie ist als Anlage B vollständig beigefügt. Zitiert sind die Nummern, auf die sich unsere Fragen beziehen; weitere Nummern behandeln Geltung, Leistung, Voraussetzungen und Verfügbarkeit.
|
||||
|
||||
**Nr. 4 Abs. 3: Gesundheitsbereich**
|
||||
|
||||
> (3) Der Terminplaner ist nicht für Angebote im Gesundheitsbereich bestimmt, weil dort besondere Kategorien personenbezogener Daten anfallen können (Art. 9 DSGVO). Der Kunde setzt ihn dort nicht ein und fordert von seinen Gästen keine Gesundheitsangaben. WEBklar darf die Einrichtung für eine solche Website ablehnen oder den Terminplaner dort abschalten.
|
||||
|
||||
⚖ Q11
|
||||
|
||||
**Nr. 4 Abs. 9: Datenschutz-Absatz auf der Website des Kunden**
|
||||
|
||||
> (9) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für die Online-Terminbuchung ein; das gehört zur Einrichtung und wird nicht auf das Stundenkontingent angerechnet. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
|
||||
|
||||
⚖ Q18
|
||||
|
||||
**Nr. 5: Einrichtung und Preis**
|
||||
|
||||
> (1) Die Einrichtung umfasst die Einrichtung nach dem Fragebogen, den Einbau des Widgets in die Website, den Datenschutz-Absatz nach Nr. 4 Abs. 9, die Abnahme durch den Kunden und die Freischaltung durch WEBklar.
|
||||
>
|
||||
> (2) Die Einrichtung kostet 149 € einmalig je Website-Projekt, ohne Umsatzsteuer gemäß § 19 UStG.
|
||||
>
|
||||
> (3) Die voraussichtliche Dauer der Einrichtung nennt die Bestellseite vor der Bestellung. Sie ist eine Einschätzung und keine verbindliche Zusage eines Termins.
|
||||
>
|
||||
> (4) Der Kunde bezahlt bei der Bestellung im Kundenportal über Stripe und erhält eine Rechnung.
|
||||
>
|
||||
> (5) Der Preis steht neben dem Stundenkontingent des Wartungs-Abos: Die Einrichtung wird weder auf das Kontingent angerechnet noch damit bezahlt. Für den Betrieb des Terminplaners berechnet WEBklar neben dem Wartungs-Abo nichts weiter; er läuft, solange das Wartungs-Abo läuft (Nr. 8).
|
||||
|
||||
⚖ Q19, Q20
|
||||
|
||||
**Nr. 7: Haftung für verpasste Termine**
|
||||
|
||||
> (1) Der Terminplaner ist ein Hilfsmittel. Die Einhaltung der Termine bleibt Sache des Kunden.
|
||||
>
|
||||
> (2) WEBklar übernimmt keine Gewähr für die Zustellung der E-Mails (Nr. 6 Abs. 4).
|
||||
>
|
||||
> (3) Für entgangenen Gewinn und Folgeschäden aus verpassten Terminen haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
|
||||
>
|
||||
> (4) Der Haftungsrahmen des Kunden gegenüber seinen Gästen bleibt unberührt.
|
||||
|
||||
⚖ Q8
|
||||
|
||||
**Nr. 8: Ende der Leistung**
|
||||
|
||||
> (1) Mit dem Ende des Wartungs-Abos endet der Terminplaner. WEBklar schaltet ihn spätestens am folgenden Tag ab und informiert den Kunden per E-Mail. Das gilt auch bei einem Wechsel ins Hosting-Abo und beim Widerruf des Abschlusses oder eines Wechsels ins Wartungs-Abo.
|
||||
>
|
||||
> (2) Mit dem Abschalten nimmt der Terminplaner keine neuen Buchungen mehr an, und das Widget zeigt den Ausweichtext. Die Termine bleiben im Kundenportal sichtbar, und der Kunde kann sie als CSV-Datei exportieren. Gäste können über ihren Link absagen. Bereits angeforderte Erinnerungen laufen weiter. Der Kunde informiert seine Gäste.
|
||||
>
|
||||
> (3) WEBklar löscht alle Termindaten und die Einstellungen 30 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Terminplaner).
|
||||
>
|
||||
> (4) Beginnt innerhalb dieser 30 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Einrichtung nötig (neuer Fragebogen, neuer Preis nach Nr. 5).
|
||||
>
|
||||
> (5) Endet das Wartungs-Abo, bevor WEBklar den Terminplaner freigeschaltet hat, erstattet WEBklar den Einrichtungspreis vollständig.
|
||||
>
|
||||
> (6) Auf Wunsch des Kunden in Textform schaltet WEBklar den Terminplaner jederzeit ab. Eine Erstattung des Einrichtungspreises folgt daraus nicht; für den Widerruf gilt Nr. 10.
|
||||
|
||||
⚖ Q6, Q7, Q9
|
||||
|
||||
**Nr. 9 Abs. 4: Einstellung ohne Grund**
|
||||
|
||||
> (4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
|
||||
|
||||
⚖ Q8
|
||||
|
||||
**Nr. 10: Widerruf**
|
||||
|
||||
> (1) Verbraucher können die Bestellung der Einrichtung nach der Widerrufsbelehrung widerrufen. Belehrung und Muster-Widerrufsformular stehen in der Anlage „Widerrufsbelehrung und Muster-Widerrufsformular (Einrichtung von Funktionen)“.
|
||||
>
|
||||
> (2) Ob das Widerrufsrecht erlischt und ob der Kunde einen angemessenen Betrag für die bis dahin erbrachte Leistung schuldet, richtet sich nach der Zustimmung, die der Kunde bei der Bestellung gibt, und nach den Hinweisen zum Widerruf in der Anlage.
|
||||
>
|
||||
> (3) Mit dem Widerruf endet der Terminplaner.
|
||||
|
||||
⚖ Q3, Q5, Q7
|
||||
|
||||
**Nr. 11: Hinweise**
|
||||
|
||||
> (1) **Keine Rechtsberatung.** Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz und die Widerrufsbelehrung für Gäste sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung trägt der Kunde. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht.
|
||||
>
|
||||
> (2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Kunde als Dienstleistungserbringer unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Kunde.
|
||||
|
||||
⚖ Q15, Q18, Q22
|
||||
|
||||
### 3.5 Auftragsverarbeitungsvertrag Terminplaner (Anlage A), Auszüge
|
||||
|
||||
Die Datei ist `docs/rechtliches/avv-terminplaner.md`; sie ist als Anlage A vollständig beigefügt (mit der Anlage der technischen und organisatorischen Maßnahmen).
|
||||
|
||||
**Vorbemerkung Abs. 1 und 2: Abschluss**
|
||||
|
||||
> (1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Einrichtung des Terminplaners bestellt und dabei per Haken diesem Vertrag und der Leistungsbeschreibung „Terminplaner“ („Leistungsbeschreibung“) in der angezeigten Fassung zustimmt. Das Portal speichert die Fassung, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte und die Widerrufsbelehrung in dieser Fassung als PDF enthält und auf sie verlinkt.
|
||||
>
|
||||
> (2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
|
||||
|
||||
⚖ Q10, Q21
|
||||
|
||||
**Nr. 3 Abs. 1: Daten der Gäste**
|
||||
|
||||
> (1) Daten der Gäste: Name und E-Mail-Adresse; auf Einstellung des Kunden außerdem Telefonnummer, eine Nachricht und Zusatzfelder, die der Kunde im Formular festlegt; die gebuchte Leistung, die Person oder der Platz und der Termin; der Status der Buchung mit den Zeitpunkten (Anlegen, Bestätigung, Annahme, Absage, Widerruf); Absagegrund und Vermerke zu einer Absage oder einem Widerruf (Freitext, auch mit dem Namen aus einer Widerrufserklärung); Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde; der Nachweis der Zustimmungen (Fassung der Gästetexte, Zeitpunkt, Text des Buchungsknopfs, Anrede und Hashes (Prüfsummen) der gezeigten Texte); der Wunsch nach einer Erinnerung; der Zustellstatus der E-Mails an den Gast.
|
||||
|
||||
⚖ Q10
|
||||
|
||||
**Nr. 4 Abs. 2: Weisungen**
|
||||
|
||||
> (2) Mit der Bestellung weist der Kunde WEBklar an, den Terminplaner nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Leistungen, Personen oder Plätze, Zeiten, Felder des Formulars, Aufbewahrungsdauer, Erinnerungen, Einschalten und Ausschalten). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Terminplaner abschalten lassen (Nr. 8 Abs. 6 der Leistungsbeschreibung). Die Löschung auf Weisung nach Nummer 9 Abs. 6 setzt WEBklar immer um.
|
||||
|
||||
⚖ Q10
|
||||
|
||||
**Nr. 7: Unterauftragsverarbeiter**
|
||||
|
||||
> (1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Terminplaner läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Buchungs-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
|
||||
>
|
||||
> (2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Terminplaner abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
|
||||
>
|
||||
> (3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
|
||||
>
|
||||
> (4) Die Zahlung der Einrichtung über Stripe betrifft nur den Vertrag zwischen dem Kunden und WEBklar. Daten von Gästen oder Beschäftigten des Kunden gehen nicht an Stripe. Stripe ist deshalb kein Unterauftragsverarbeiter dieses Vertrags.
|
||||
|
||||
⚖ Q10
|
||||
|
||||
**Nr. 8 Abs. 5: Nachweis und Archiv der Gästetexte**
|
||||
|
||||
> (5) Die Buchung speichert als Nachweis der Zustimmungen die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der übrigen Texte, die der Gast gesehen hat, nicht diese Texte selbst. Damit sich der Wortlaut nachweisen lässt, bewahrt WEBklar jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz und Widerrufsbelehrung für Gäste) mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende des Terminplaners. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung.
|
||||
|
||||
⚖ Q26
|
||||
|
||||
**Nr. 9: Löschung und Rückgabe**
|
||||
|
||||
> (1) Nicht bestätigte Buchungen löscht WEBklar 48 Stunden nach Ablauf der Bestätigungsfrist von 30 Minuten.
|
||||
>
|
||||
> (2) Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, behandelt der Terminplaner als abgelehnt; die 30 Tage laufen dann ab diesem Zeitpunkt.
|
||||
>
|
||||
> (3) Bestätigte und erledigte Termine löscht WEBklar nach der Aufbewahrungsdauer, gerechnet ab dem Ende des Termins. Der Kunde stellt sie im Kundenportal ein; die Vorgabe sind 90 Tage, einstellbar sind 14 bis 365 Tage.
|
||||
>
|
||||
> (4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
|
||||
>
|
||||
> (5) Der Löschlauf läuft täglich; gelöscht wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu löschen, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
|
||||
>
|
||||
> (6) Ist der Terminplaner abgeschaltet oder das Wartungs-Abo beendet, löscht WEBklar alle Termindaten und die Einstellungen 30 Tage danach. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen (zum Beispiel Betriebsangaben, Namen der Leistungen, die ersten acht Namen der Personen oder Plätze und die Wünsche des Kunden zum Einbau) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Bis zur Löschung kann der Kunde die Termine im Kundenportal als CSV-Datei exportieren. Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen wieder ein (Nr. 8 Abs. 4 der Leistungsbeschreibung). Früher löscht der Kunde einzelne Gastdaten selbst mit der Aktion „Gastdaten löschen“ (Nummer 8 Abs. 2); alle Daten löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
|
||||
>
|
||||
> (7) Entwürfe aus dem Fragebogen, aus denen keine Bestellung geworden ist, löscht WEBklar 90 Tage nach der letzten Änderung.
|
||||
>
|
||||
> (8) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
|
||||
>
|
||||
> (9) Gesetzliche Aufbewahrungspflichten bleiben unberührt.
|
||||
|
||||
⚖ Q7, Q9, Q10
|
||||
|
||||
### 3.6 Gäste: Widerrufsbelehrung und Muster-Widerrufsformular
|
||||
|
||||
Der Terminplaner erzeugt diese Belehrung mit den Angaben des Betriebs (alle vier Angaben sind Pflicht) und zeigt sie Verbrauchern beim Buchen und in der Bestätigungs-E-Mail. Grundlage ist das Muster der Anlage 1 und das Muster-Widerrufsformular der Anlage 2 zu Art. 246a § 1 Abs. 2 Satz 2 EGBGB (Dateien `docs/rechtliches/quellen/anlage-1.txt` und `anlage-2.txt`, abgerufen 2026-10-02). Gestaltungshinweise für einen Dienstleistungsvertrag sind angewendet: Fristbeginn „des Vertragsabschlusses“, Wertersatz für Dienstleistungen; die Widerrufsfunktion nennt keine Internetadresse, sondern einen anderen geeigneten Hinweis. Ausgelassen sind die Textbausteine zu Waren und der Absatz zum elektronischen Webformular. Die Anrede ist immer „Sie“.
|
||||
|
||||
**Widerrufsbelehrung und Muster-Widerrufsformular für Gäste (Platzhalter für die Betriebsangaben)**
|
||||
|
||||
> Widerrufsbelehrung
|
||||
>
|
||||
> Widerrufsrecht
|
||||
>
|
||||
> Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
|
||||
>
|
||||
> Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag des Vertragsabschlusses.
|
||||
>
|
||||
> Um Ihr Widerrufsrecht auszuüben, müssen Sie uns ({{Name des Betriebs}}, {{Anschrift}}, Telefon: {{Telefon}}, E-Mail: {{E-Mail}}) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
|
||||
>
|
||||
> Sie können Ihr Widerrufsrecht auch online unter „Vertrag widerrufen“ (Schaltfläche im Terminbereich auf unserer Website sowie Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail) ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.
|
||||
>
|
||||
> Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
|
||||
>
|
||||
> Folgen des Widerrufs
|
||||
>
|
||||
> Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
|
||||
>
|
||||
> Haben Sie verlangt, dass die Dienstleistungen während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht.
|
||||
>
|
||||
> Muster-Widerrufsformular
|
||||
>
|
||||
> (Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
|
||||
>
|
||||
> – An {{Name des Betriebs}}, {{Anschrift}}, {{E-Mail}}:
|
||||
> – Hiermit widerrufe(n) ich/wir (\*) den von mir/uns (\*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (\*)/die Erbringung der folgenden Dienstleistung (\*)
|
||||
> – Bestellt am (\*)/erhalten am (\*)
|
||||
> – Name des/der Verbraucher(s)
|
||||
> – Anschrift des/der Verbraucher(s)
|
||||
> – Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
|
||||
> – Datum
|
||||
>
|
||||
> (\*) Unzutreffendes streichen.
|
||||
|
||||
⚖ Q12, Q13
|
||||
|
||||
### 3.7 Gäste: Haken und Hinweis zu Gesundheitsangaben
|
||||
|
||||
Die Haken stehen immer in der Sie-Form. Keiner ist vorangekreuzt.
|
||||
|
||||
**Haken „Vorzeitiger Beginn“** (Pflicht, nur für Verbraucher bei Leistungen mit Preis, wenn der Termin vor Ablauf der Widerrufsfrist liegt; bei Anfragen mit Preis immer)
|
||||
|
||||
> Ich verlange ausdrücklich, dass {{Firma}} mit der Leistung vor Ablauf der Widerrufsfrist beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).
|
||||
|
||||
⚖ Q13, Q3
|
||||
|
||||
**Haken „Stornobedingungen“** (Pflicht, wenn der Betrieb Stornobedingungen hinterlegt hat und die Leistung einen Preis hat)
|
||||
|
||||
> Ich habe die Stornobedingungen gelesen.
|
||||
|
||||
⚖ Q16
|
||||
|
||||
**Haken „Erinnerung“** (freiwillig; nur wenn der Betrieb Erinnerungen anbietet; `{{n}}` ist 2, 12, 24 oder 48)
|
||||
|
||||
> Ich möchte {{n}} Stunden vor dem Termin eine Erinnerung per E-Mail erhalten. Diese Einwilligung kann ich jederzeit widerrufen.
|
||||
|
||||
⚖ Q17
|
||||
|
||||
**Hinweis über jedem Freitextfeld**
|
||||
|
||||
> Bitte keine Gesundheitsangaben.
|
||||
|
||||
⚖ Q11
|
||||
|
||||
### 3.8 Gäste: Datenschutz-Hinweis am Buchungsformular
|
||||
|
||||
Der Hinweis steht in der Zusammenfassung vor dem Absenden, zusammen mit einem Link auf die Datenschutzerklärung des Betriebs; es gibt keinen Haken dafür. Sie- oder Du-Fassung nach der Anrede des Betriebs; die Löschfristen sind die des Löschlaufs.
|
||||
|
||||
**Fassung Sie**
|
||||
|
||||
> Datenschutz. Wir verwenden Ihre Angaben, um Ihren Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO). Betrieben wird der Terminkalender in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland). Wir speichern Ihre Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist. Es werden keine Cookies gesetzt. Bitte machen Sie keine Angaben zu Ihrer Gesundheit. Mehr erfahren Sie in unserer Datenschutzerklärung. Verantwortlich: {{Firma}}, {{Anschrift}}.
|
||||
|
||||
⚖ Q17, Q9
|
||||
|
||||
**Fassung Du**
|
||||
|
||||
> Datenschutz. Wir verwenden deine Angaben, um deinen Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO). Betrieben wird der Terminkalender in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland). Wir speichern deine Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist. Es werden keine Cookies gesetzt. Bitte mach keine Angaben zu deiner Gesundheit. Mehr erfährst du in unserer Datenschutzerklärung. Verantwortlich: {{Firma}}, {{Anschrift}}.
|
||||
|
||||
⚖ Q17, Q9
|
||||
|
||||
### 3.9 Gäste: Hinweise in der Bestätigungs-E-Mail (G02)
|
||||
|
||||
Die Bestätigungs-E-Mail (Vorlage `server/mail-templates/G02 Termin bestaetigt.html`) enthält je nach Buchung einen der beiden folgenden Hinweise (hier die Sie-Fassung); danach folgt bei Widerrufsrecht die Belehrung aus Abschnitt 3.6. Die Du-Fassung ist inhaltlich gleich.
|
||||
|
||||
**Widerrufsrecht besteht (Verbraucher)**
|
||||
|
||||
> Widerrufsrecht. Wenn Sie Verbraucher sind, können Sie diesen Vertrag innerhalb von 14 Tagen ab Vertragsschluss ohne Angabe von Gründen widerrufen. Bis zum Beginn des Termins geht das über den Knopf „Vertrag widerrufen“; danach über das Formular „Vertrag widerrufen“ auf der Website von {{betrieb_name}} oder per E-Mail an {{betrieb_email}}. Die ausführliche Widerrufsbelehrung und das Muster-Widerrufsformular finden Sie weiter unten in dieser E-Mail.
|
||||
|
||||
⚖ Q2, Q12
|
||||
|
||||
**Kein Widerrufsrecht (Freizeit-Dienstleistung mit festem Termin)**
|
||||
|
||||
> Für diese Buchung besteht kein Widerrufsrecht (§ 312g Abs. 2 Nr. 9 BGB). Absagen richten sich nach den Stornobedingungen.
|
||||
|
||||
⚖ Q15, Q16
|
||||
|
||||
### 3.10 Gäste: Knopftexte
|
||||
|
||||
Die Knopftexte hängen davon ab, ob die Buchung verbindlich oder eine Anfrage ist und ob die Leistung einen Preis hat. Im Widget und auf der Bestätigungsseite heißen die Knöpfe:
|
||||
|
||||
**Verbindliche Buchung mit Preis: Widget, dann Bestätigungsseite**
|
||||
|
||||
> Zahlungspflichtig buchen
|
||||
>
|
||||
> Zahlungspflichtig bestätigen
|
||||
|
||||
⚖ Q14
|
||||
|
||||
**Verbindliche Buchung ohne Preis: Widget, dann Bestätigungsseite**
|
||||
|
||||
> Termin buchen
|
||||
>
|
||||
> Termin bestätigen
|
||||
|
||||
⚖ Q14
|
||||
|
||||
**Anfrage: Widget, dann Bestätigungsseite**
|
||||
|
||||
> Termin anfragen
|
||||
>
|
||||
> Anfrage absenden
|
||||
|
||||
⚖ Q14
|
||||
|
||||
**Widerrufsfunktion: Knopf im Terminbereich, dann Bestätigung des Formulars**
|
||||
|
||||
> Vertrag widerrufen
|
||||
>
|
||||
> Widerruf bestätigen
|
||||
|
||||
⚖ Q12, Q14
|
||||
|
||||
### 3.11 Anlagen A und B
|
||||
|
||||
- **Anlage A:** Auftragsverarbeitungsvertrag Terminplaner, Fassung 2026-10-05, als PDF `public/rechtliches/avv-terminplaner-2026-10-05.pdf` (Quelle `docs/rechtliches/avv-terminplaner.md`).
|
||||
- **Anlage B:** Leistungsbeschreibung Terminplaner, Fassung 2026-10-05, als PDF `public/rechtliches/leistungsbeschreibung-terminplaner-2026-10-05.pdf` (Quelle `docs/rechtliches/leistungsbeschreibung-terminplaner.md`).
|
||||
- Die Widerrufsbelehrung mit Muster-Widerrufsformular der Funktionen steht in Abschnitt 3.2 vollständig; ihr PDF ist `public/rechtliches/widerrufsbelehrung-funktionen-2026-10-05.pdf`.
|
||||
- **Anlage C** (gerenderte Mails K15 und K16) wird nach der Mail-Überarbeitung des Inhabers nachgereicht (Abschnitt 7.4).
|
||||
|
||||
## 4. Fragen an die Kanzlei
|
||||
|
||||
Jede Frage hat vier Teile. **Frage:** was wir wissen möchten. **Fundstelle im Text:** wo der Text steht (Abschnitt 3, Anlage A oder B, Programm) und, als Kennung H1 bis C8, welche Punkte der früheren Listen offener Rechtsfragen die Frage mit abdeckt (Abschnitt 6.3 ordnet alle Kennungen zu). **Unsere Annahme:** der Stand, mit dem wir arbeiten; bitte bestätigen oder korrigieren. **Alternative:** was wir stattdessen täten. Die Zählung von § 356 BGB haben wir am 2026-10-05 auf gesetze-im-internet.de abgeglichen; für die übrigen Normen bitten wir um Gegenprüfung (Q3). Einordnungen wie „Annahme“ sind keine Rechtsurteile.
|
||||
|
||||
### Q1: Alle Besteller wie Verbraucher
|
||||
|
||||
**Frage.** Der Inhaber hat entschieden, alle Besteller der Einrichtung (149 €) wie Verbraucher zu behandeln: Alle sehen die volle Widerrufsbelehrung mit Muster-Widerrufsformular und alle setzen die Haken zum vorzeitigen Beginn. Die Besteller sind meist Unternehmer (Betriebe mit eigener Website); ob jemand als Verbraucher (§ 13 BGB) oder Unternehmer (§ 14 BGB) bestellt, kann WEBklar nicht verlässlich prüfen (Existenzgründer, Nebenerwerb). Genügt der Satz am Anfang des Dokuments „Das Widerrufsrecht gilt nur für Verbraucher. Wir zeigen die Belehrung und das Muster-Widerrufsformular trotzdem allen, die eine Funktion bestellen.“, damit Unternehmern kein vertragliches Widerrufsrecht entsteht und die Haken für sie nichts Falsches sagen?
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.1 (Haken 1 bis 4), Abschnitt 3.2 (Einleitung des Dokuments und Belehrung), Leistungsbeschreibung Nr. 10 Abs. 1 („Verbraucher können die Bestellung der Einrichtung nach der Widerrufsbelehrung widerrufen“).
|
||||
|
||||
**Unsere Annahme.** Ja. Verbraucher haben das gesetzliche Widerrufsrecht, die Haken wirken bei ihnen; Unternehmern räumt die Einleitung kein Widerrufsrecht ein, die Haken schaden ihnen nicht. Die Abwicklung eines Widerrufs ist Handarbeit des Teams (Meldung T09); ob ein Unternehmer erstattet bekommt, entscheidet das Team im Einzelfall.
|
||||
|
||||
**Alternative.** Vor den Haken fragen, ob als Verbraucher oder als Unternehmer bestellt wird (Haken 3 und Belehrung nur für Verbraucher), oder den Terminplaner nur an Unternehmer verkaufen (Bestätigung nach § 14 BGB) und die Belehrung als freiwillige Zusage geben.
|
||||
|
||||
### Q2: Beginn der Widerrufsfrist
|
||||
|
||||
**Frage.** Wann beginnt die Widerrufsfrist der Bestellung? Wir rechnen ab dem Zeitpunkt des Klicks auf „Zahlungspflichtig bestellen · 149 €“ (gespeichert im Nachweis); das Fristende wird nach § 193 BGB auf einen Werktag verschoben. Die Belehrung steht vor dem Klick auf der Bestellseite, auf einem dauerhaften Datenträger (PDF-Anhang der Bestellbestätigung K15) erhält der Kunde sie erst nach dem Zahlungseingang. Beginnt die Frist mit dem Klick (Annahme durch WEBklar), erst mit dem Zahlungseingang (bei SEPA-Lastschrift Tage später) oder erst mit dem Zugang von K15 (§ 356 Abs. 3 BGB)? Gilt dasselbe für die Gäste (Vertragsschluss bei verbindlicher Buchung mit der Bestätigung per Link, bei einer Anfrage mit der Annahme durch den Betrieb)?
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.1 (Knopf), Abschnitt 3.2 („ab dem Tag des Vertragsabschlusses“), Abschnitt 3.9 (G02), Abschnitt 2.4; Programm: `withdrawFeature` rechnet mit dem Zeitpunkt des Klicks aus dem Nachweis (ersatzweise mit dem Bestellzeitpunkt), `withdrawalDeadline` mit 14 Tagen.
|
||||
|
||||
**Unsere Annahme.** Der Klick ist der Vertragsschluss: Mit ihm löst der Kunde die Zahlung aus; danach gibt es keine gesonderte Annahmeerklärung von WEBklar. WEBklar kann die Einrichtung nur aus den Gründen von Leistungsbeschreibung Nr. 4 Abs. 3 (Gesundheitsbereich, Q11) ablehnen oder abschalten. Die Belehrung ist vor dem Klick lesbar; K15 bestätigt sie mit den PDFs auf einem dauerhaften Datenträger.
|
||||
|
||||
**Alternative.** Die Frist ab Zahlungseingang oder ab Zugang von K15 rechnen und die Belehrung entsprechend formulieren („ab dem Tag, an dem Sie diese Bestellbestätigung erhalten“).
|
||||
|
||||
### Q3: Zustimmungssatz: Erlöschen, Wertersatz, Kenntnis, dauerhafter Datenträger
|
||||
|
||||
**Frage.** Genügt Haken 3 („Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit der Einrichtung des Terminplaners beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Einrichtung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich … schulde ich einen angemessenen Betrag … (§ 357a Abs. 2 BGB).“)? Bitte prüfen: (a) die Zählung der Norm für das Erlöschen bei Dienstleistungen (§ 356 Abs. 5 Nr. 2 BGB) und für den Wertersatz (§ 357a Abs. 2 BGB); (b) ob das ausdrückliche Verlangen und die Bestätigung der Kenntnis im selben Haken stehen dürfen; (c) ob der Wertersatz-Hinweis im Haken ausreicht oder auch in die Belehrung gehört; (d) ob K15 mit den Texten der Zustimmung, dem Zeitpunkt und den PDFs die nötige Bestätigung auf einem dauerhaften Datenträger ist (die gerenderte Mail folgt als Anlage C nach der Mail-Überarbeitung des Inhabers).
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.1 (Haken 3), Abschnitt 3.2 („Hinweise zum Widerruf“), Abschnitt 3.3 (Haken der News), Leistungsbeschreibung Nr. 10 Abs. 2; Programm: `termineConsent` (Register), `onFeaturePaid` (K15, Block „Zustimmungen“ und Anhänge).
|
||||
|
||||
**Unsere Annahme.** Der Haken erfüllt beides: Verlangen, Kenntnis vom Erlöschen und Hinweis auf den Wertersatz in einem Satz mit Zeitpunkt im Nachweis; K15 gibt den Satz zusammen mit der Belehrung auf einem dauerhaften Datenträger wieder. Die Zählung entspricht der Fassung auf gesetze-im-internet.de vom 2026-10-05.
|
||||
|
||||
**Alternative.** Zwei Haken (Verlangen und Kenntnis getrennt); die Bestätigung als eigener, ausdrücklich so benannter Absatz in K15.
|
||||
|
||||
### Q4: News-Altkunden
|
||||
|
||||
**Frage.** Seit 2026-09-30 verkauft WEBklar die News-Pinnwand (30 € einmalig) über denselben Bestellweg wie jetzt den Terminplaner. Bis zur Korrektur sah die Bestellung so aus („3a0b008“ ist die Kennung des Programmstands vor der Korrektur). Alte Fassung (Stand 3a0b008), der Haken:
|
||||
|
||||
> Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht erlischt, sobald der Einbau vollständig erbracht ist (§ 356 Abs. 4 BGB).
|
||||
|
||||
Alte Fassung (Stand 3a0b008), Leistungsbeschreibung der News-Pinnwand, Abschnitt 6 „Widerrufsrecht“ (nur diese zwei Absätze, keine gesetzliche Musterbelehrung, kein Muster-Widerrufsformular):
|
||||
|
||||
> Sind Sie Verbraucher, können Sie diesen Vertrag innerhalb von 14 Tagen ohne Angabe von Gründen widerrufen. Die Frist beginnt mit dem Vertragsschluss. Zum Widerruf genügt eine eindeutige Erklärung an uns, zum Beispiel über project.webklar.com/widerrufen.html.
|
||||
>
|
||||
> Verlangen Sie ausdrücklich, dass wir vor Ablauf der Widerrufsfrist mit dem Einbau beginnen, müssen Sie uns bei einem Widerruf einen angemessenen Betrag für die bis dahin erbrachte Leistung zahlen. Das Widerrufsrecht erlischt nach § 356 Abs. 4 BGB, wenn wir die Leistung vollständig erbracht haben und Sie vorher ausdrücklich zugestimmt haben, dass wir vor Ablauf der Widerrufsfrist beginnen, und Ihre Kenntnis bestätigt haben, dass Sie Ihr Widerrufsrecht bei vollständiger Vertragserfüllung verlieren.
|
||||
|
||||
Alte Fassung (Stand 3a0b008), Abschnitt 2 derselben Leistungsbeschreibung zum Preis:
|
||||
|
||||
> Der Einbau kostet 30,00 € einmalig, inklusive Umsatzsteuer. Die Nutzung der Pinnwand danach kostet nichts; es entstehen keine laufenden Kosten und kein Abo. Sie bezahlen bei der Bestellung im Kundenportal über Stripe und erhalten eine Rechnung.
|
||||
|
||||
Die Bestellbestätigung K15 enthielt nach unserem Stand weder eine Belehrung noch Anhänge. Der Haken zitierte also § 356 Abs. 4 BGB und enthielt keinen Hinweis auf den Wertersatz (§ 357a Abs. 2 BGB), den die Leistungsbeschreibung nur allgemein nannte; der Preis war „inklusive Umsatzsteuer“ angegeben (richtig ist § 19 UStG, Q19). Lief bei diesen News-Altkunden, soweit sie Verbraucher sind, die Widerrufsfrist nach § 356 Abs. 3 BGB nicht an? Wenn ja: Sollen wir die Belehrung per Mail nachholen (Form, Inhalt, Zeitpunkt, Hinweis zum Wertersatz), oder genügt es, nur künftig die neuen Texte zu verwenden? Was gilt für bereits eingebaute und freigeschaltete News-Pinnwände (Erlöschen des Widerrufsrechts, Wertersatz)? Die Zahl der betroffenen Fälle liefert der Inhaber vor der Übergabe: [Anzahl der Bestellungen seit 2026-09-30 mit dem alten Text].
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.3 (neuer und früherer Haken), Abschnitt 3.2 (Belehrung der Funktionen, die die News künftig ebenfalls nutzt); Programm: `config.features.news` (neuer Haken; bestehende Bestellungen behalten den damals gespeicherten Text und die Fassung). Die alten Texte stammen aus dem Programmstand 3a0b008 (`server/config.js`, `public/vertragsunterlagen/funktionen-leistungsbeschreibung.html`). Die Leistungsbeschreibung der News-Pinnwand vom 30.09.2026 liegt unverändert unter `public/vertragsunterlagen/funktionen-leistungsbeschreibung-2026-09-30.html` (unveränderte Kopie des Standes 3a0b008, durch eine Prüfsumme gesichert), weil Bestellungen seit dem 30.09.2026 auf ihr beruhen; die geltende Datei (Stand 05.10.2026) verweist am Anfang auf sie.
|
||||
|
||||
**Unsere Annahme.** Nach unserer Lesart (kein Rechtsurteil) kann die Frist bei fehlender ordnungsgemäßer Belehrung nach § 356 Abs. 3 BGB nicht angelaufen sein. Der Inhaber entscheidet nach Ihrem Befund: Belehrung nur nachholen, wenn die Frist danach nicht lief; sonst gelten die neuen Texte nur für künftige Bestellungen.
|
||||
|
||||
**Alternative.** Allen News-Altkunden vorsorglich die Belehrung samt Muster-Formular per Mail schicken (mit dem Hinweis, dass die Frist damit beginnt), unabhängig vom Befund; oder, wenn die Kanzlei keinen Handlungsbedarf sieht, nichts tun.
|
||||
|
||||
### Q5: Wertersatz: Anteil am Gesamtpreis oder Stundenaufwand
|
||||
|
||||
**Frage.** Die Muster-Belehrung verlangt beim Widerruf einen angemessenen Betrag, der dem Anteil der bis zur Unterrichtung erbrachten Dienstleistungen im Vergleich zum Gesamtumfang entspricht (§ 357a Abs. 2 BGB). Unsere „Hinweise zum Widerruf“ sagen: „Der Betrag bemisst sich als Anteil am Gesamtpreis; Maß ist der im Arbeitsblatt erfasste Aufwand für die Einrichtung.“ Intern war ursprünglich eine Berechnung „nach Stundenaufwand“ vorgesehen. Ist diese Berechnung (Anteil an 149 € nach dem erfassten Aufwand im Verhältnis zum Gesamtaufwand der Einrichtung dieses Projekts) zulässig, oder muss der Gesamtumfang vorab festgelegt werden, damit der Anteil bestimmbar ist?
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.1 (Haken 3), Abschnitt 3.2 (Muster-Belehrung, Absatz „Haben Sie verlangt …“, und „Hinweise zum Widerruf“, dritter Punkt), Leistungsbeschreibung Nr. 10 Abs. 2; Mails K04 und T09; Programm `withdrawFeature`.
|
||||
|
||||
**Unsere Annahme.** Anteilig am Preis (nicht Stundensatz mal Zeit). Das Maß der erbrachten Leistung ist der im Arbeitsblatt des Einrichtungs-Tickets erfasste Aufwand, der Gesamtumfang der Aufwand der Einrichtung dieses Projekts nach dem Ticket. Eine feste Gesamtstundenzahl steht nicht im Vertrag.
|
||||
|
||||
**Alternative.** Den Gesamtumfang in der Leistungsbeschreibung festlegen (zum Beispiel feste Schritte oder eine Stundenzahl) und den Wertersatz nach erledigten Schritten bemessen; oder bei Widerruf nach Einbaubeginn pauschal einen festen Anteil ansetzen.
|
||||
|
||||
### Q6: Volle Erstattung, wenn das Abo vor der Freischaltung endet
|
||||
|
||||
**Frage.** Leistungsbeschreibung Nr. 8 Abs. 5 lautet: „Endet das Wartungs-Abo, bevor WEBklar den Terminplaner freigeschaltet hat, erstattet WEBklar den Einrichtungspreis vollständig.“ Ist die Klausel wirksam und klar (§ 307 BGB), auch wenn WEBklar den Einbau schon begonnen oder fertiggestellt hat und auch wenn der Kunde das Abo selbst beendet? Außerdem: Nr. 8 Abs. 4 sagt, dass WEBklar den Terminplaner bei einem neuen Wartungs-Abo innerhalb der 30 Tage wieder einschaltet. Das Programm tut das nur für den Abschaltgrund „Abo beendet“ (`abo_ende`, nach Freischaltung), nie für `abo_ende_vor_einrichtung`: Nach der Erstattung bleibt der Terminplaner aus; ob und wie er wieder eingerichtet wird, entscheidet das Team im Einzelfall. Muss der Text das sagen?
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.4 (Nr. 8 Abs. 4 bis 6), Abschnitt 2.3 Punkt 4; Programm: `gleicheAboAb` (Grund `abo_ende_vor_einrichtung`, Meldung T19 an das Team, keine automatische Rückkehr), Mail K22 Block „vor_einrichtung“.
|
||||
|
||||
**Unsere Annahme.** Die Klausel begünstigt den Kunden und ist unproblematisch. Die Rückkehr gilt nur für den Fall nach der Freischaltung; für den Erstattungsfall ist sie nicht gewollt, weil die Erstattung voll geschuldet bleibt.
|
||||
|
||||
**Alternative.** Die Erstattung auf den Fall beschränken, dass WEBklar noch nicht mit dem Einbau begonnen hat (sonst Wertersatz); Nr. 8 Abs. 4 um den Satz ergänzen, dass die Rückkehr nicht gilt, wenn der Einrichtungspreis erstattet wurde.
|
||||
|
||||
### Q7: Ende mit dem Abo, auch bei Wechsel und Widerruf; Ausnahme für Pilot und Kulanz
|
||||
|
||||
**Frage.** (a) Leistungsbeschreibung Nr. 8 Abs. 1 lässt den Terminplaner mit dem Wartungs-Abo enden, auch beim Wechsel ins Hosting-Abo und beim Widerruf des Abschlusses oder eines Wechsels ins Wartungs-Abo. Ist diese Kopplung zulässig, obwohl der Kunde die Einrichtung einmalig bezahlt hat und sie ein eigener Vertrag ist? Wie verhält sich der Widerruf des Abos zum Einrichtungsvertrag, insbesondere wenn der Terminplaner schon freigeschaltet ist und deshalb nichts erstattet wird? (b) Ausnahme: Terminplaner, die das Team ohne Bestellung freigeschaltet hat (Pilot, Kulanz; im Programm `unlockedByPurchase` nicht wahr), nimmt der Abgleich aus und zählt sie im Tagesbericht als „ausgenommen“; sie laufen weiter, bis WEBklar sie abschaltet. Genügt der Satz „WEBklar schaltet ihn spätestens am folgenden Tag ab“ trotzdem, oder braucht es eine Klausel, die diese Kulanzfälle nennt? (c) Der Abgleich schaltet bei Lesefehlern nichts ab und versucht es am nächsten Tag; ein Abo im Status „Zahlungsverzug“ gilt als laufend. Ist „spätestens am folgenden Tag“ dann zu streng gesagt?
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.4 (Nr. 8 Abs. 1 und 6, Nr. 10), Abschnitt 3.1 (Haken 4: „der Terminplaner endet mit dem Wartungs-Abo“), Abschnitt 2.3, Abschnitt 2.1 (Freischaltung ohne Bestellung); Programm: `gleicheAboAb`, `endeTermineFuerKunde`.
|
||||
|
||||
**Unsere Annahme.** Die Kopplung ist zulässig, weil der Terminplaner nur mit einem Wartungs-Abo betrieben wird und der Kunde das in Haken 4 liest. Kulanzfälle sind keine Verträge über die Einrichtung; ihre Ausnahme braucht keine Klausel. „Spätestens am folgenden Tag“ beschreibt den Normalfall.
|
||||
|
||||
**Alternative.** Nr. 8 Abs. 1 auf „in der Regel am folgenden Tag“ abschwächen; eine Kulanzklausel („Terminplaner, die WEBklar ohne Bestellung freigeschaltet hat, schaltet WEBklar nach eigenem Ermessen ab“) aufnehmen; beim Widerruf des Abos nach Freischaltung eine anteilige Erstattung prüfen.
|
||||
|
||||
### Q8: Haftung für verpasste Termine; Erstattung bei Einstellung
|
||||
|
||||
**Frage.** (a) Leistungsbeschreibung Nr. 7 Abs. 3 (Abschnitt 3.4) beschränkt die Haftung für entgangenen Gewinn und Folgeschäden aus verpassten Terminen auf den Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen (unbeschränkt bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit; bei leicht fahrlässiger Verletzung wesentlicher Pflichten begrenzt auf den vertragstypischen, vorhersehbaren Schaden; im Übrigen ausgeschlossen). Ist die Klausel gegenüber Verbrauchern mit § 309 Nr. 7 BGB vereinbar, und ist die Verweisung hinreichend transparent? (b) Braucht es gegenüber Unternehmern einen Höchstbetrag (etwa den Jahresbetrag des Wartungs-Abos)? (c) Nr. 9 Abs. 4 schließt eine Einstellung ohne Grund aus. Wann WEBklar den Terminplaner beenden oder aussetzen darf, regeln Nr. 9 Abs. 2 und 3:
|
||||
|
||||
> (2) WEBklar darf den Terminplaner für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
|
||||
> - ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Terminplaner Nummer 12), oder
|
||||
> - eine Sicherheitslücke sich nicht anders beheben lässt.
|
||||
>
|
||||
> Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
|
||||
>
|
||||
> (3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Terminplaner ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
|
||||
|
||||
Zur Erstattung des Einrichtungspreises, wenn WEBklar den Terminplaner danach beendet, schweigt der Text. Braucht es eine (anteilige) Erstattung?
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.4 (Nr. 7 und Nr. 9 Abs. 4), Leistungsbeschreibung Nr. 9 Abs. 2 und 3 (oben zitiert), Abo-Bedingungen § 13 (`public/abo-bedingungen.html`).
|
||||
|
||||
**Unsere Annahme.** Die Verweisung auf § 13 genügt; ein Höchstbetrag ist nicht nötig, weil § 13 Abs. 2 die Haftung schon auf den vorhersehbaren Schaden begrenzt. Bei Einstellung aus den Gründen von Nr. 9 Abs. 2 erstattet WEBklar nichts, weil der Terminplaner bis dahin lief; die Rechte aus § 327r BGB bleiben.
|
||||
|
||||
**Alternative.** Einen Höchstbetrag gegenüber Unternehmern festlegen; bei Einstellung durch WEBklar den Einrichtungspreis anteilig für die Restlaufzeit erstatten.
|
||||
|
||||
### Q9: 30 Tage nach dem Abschalten: Anker, Rückkehr, Sicherungen
|
||||
|
||||
**Frage.** Der Terminplaner endet mit dem Wartungs-Abo; WEBklar schaltet ihn spätestens am folgenden Tag ab und löscht alle Termindaten und Einstellungen 30 Tage nach dem Abschalten. Bis dahin kann der Kunde die Termine im Kundenportal ansehen und als CSV-Datei exportieren; beginnt ein neues Wartungs-Abo, schaltet WEBklar ohne Kosten mit den vorhandenen Daten wieder ein (Rückkehr, nur beim Abschaltgrund „Abo beendet“, `abo_ende`). Fragen: (a) Ist der Anker (Zeitpunkt des Abschaltens, `disabledAt`, nicht der Tag des Abo-Endes) und die Frist von 30 Tagen mit Art. 28 Abs. 3 lit. g DSGVO vereinbar? (b) Darf die Rückkehr ohne neue Zustimmung zur Fassung automatisch erfolgen? (c) Gelöschte Daten bleiben in der täglichen Vollsicherung bis zu sieben Tage (Rückspielen holt der Löschlauf nach): genügt das? (d) Der tägliche Löschlauf löscht frühestens nach Ablauf der jeweiligen Frist, nicht sekundengenau; „48 Stunden“ und „30 Tage“ sind frühestmögliche Zeitpunkte (H4, A3). (e) Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, gelten ab dann als abgelehnt, und die 30 Tage laufen ab diesem Zeitpunkt (AVV Nr. 9 Abs. 2). (f) Der Satz im Datenschutz-Absatz der Kunden-Website („Wird die Online-Terminbuchung abgeschaltet, zum Beispiel weil unser Vertrag mit WEBklar endet, werden alle Termindaten 30 Tage nach dem Abschalten gelöscht.“) und der Satz in Abschnitt 6c stimmen mit dem Programm überein, seit der tägliche Abgleich das Abschalten übernimmt. (g) Das Einrichtungs-Ticket, in dem das Team die Einrichtung als Arbeitsnachweis festhält, bleibt nach der Löschung mit einer Kurzfassung der Einstellungen bestehen (Betriebsangaben, Namen der Leistungen, die ersten acht Namen der Personen oder Plätze, Wünsche des Kunden zum Einbau; keine Daten von Gästen). Leistungsbeschreibung Nr. 8 Abs. 3 und AVV Nr. 9 Abs. 6 sagen das; die Datenschutzerklärung (Abschnitt 6) nennt die Arbeitsnachweise und die Aufbewahrung bis zum Ablauf der Verjährungsfrist. Ist das neben der Löschpflicht des Art. 28 Abs. 3 lit. g DSGVO zulässig, auch wenn der Kunde die Löschung aller Daten anweist (AVV Nr. 9 Abs. 6)?
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.4 (Nr. 8 Abs. 3 und 4), Abschnitt 3.5 (AVV Nr. 9, besonders Abs. 2, 6 und 8), Abschnitt 2.3; frühere Punkte H4, A3, C4 (Sicherungen), C7 (Satz in 6c); Mail K22 und Datenschutzerklärung 6c (Hausrunde); Programm: `LOESCHFRIST_TAGE`, `loescheAbgeschaltete`, `gleicheAboAb`; Einrichtungs-Ticket: `termineTicketDetails`.
|
||||
|
||||
**Unsere Annahme.** Ja. 30 Tage ab dem Abschalten sind angemessen, weil der Kunde in dieser Zeit Termine ansehen und exportieren kann und eine Rückkehr kostenlos ist. Die Rückkehr stellt den früheren Stand wieder her; die zugestimmte Fassung gilt weiter, eine neue Fassung braucht ihre Zustimmung nach AVV Nr. 12. Der Sicherungsüberhang von sieben Tagen folgt aus der täglichen Vollsicherung bei Hetzner (letzte sieben Tagessicherungen) und steht offen im Vertrag.
|
||||
|
||||
**Alternative.** Das Modell der Website-Analyse: Löschung am 61. Tag nach dem Ende, Kopie der Daten auf Verlangen bis zum 46. Tag (AVV Website-Analyse Nr. 9 Abs. 2); oder 30 Tage ab dem Tag des Abo-Endes statt ab dem Abschalten.
|
||||
|
||||
### Q10: AVV Terminplaner: Aufbau, Hetzner, Drittland, Daten, Weisungen
|
||||
|
||||
**Frage.** Bitte den AVV (Anlage A) prüfen; besonders: (a) der Abschluss durch Haken bei der Bestellung (Textform; Art. 28 Abs. 9 DSGVO) mit gespeicherter Fassung, Zeitpunkt, Konto und Person, und durch Zustimmung in Textform bei Freischaltung durch das Team (Q21); (b) die Wahl eines eigenen Vertrags (Geschwister des AVV der Website-Analyse, Vorrang vor Anlage 1 der Abo-Bedingungen) statt eines Rahmen-AVV oder eines Zusatzes, damit Kunden der Website-Analyse nicht neu zustimmen müssen; (c) Nr. 7: Hetzner Online GmbH, Rechenzentrum Falkenstein (Server und Sicherung) und Nürnberg (SMTP-Dienst des Webhostings) und der Satz „Eine Übermittlung in Drittländer findet nicht statt“, auch wenn ein Kunde den geheimen Kalender-Link in einem Kalenderdienst eines Anbieters wie Google oder Apple abonniert (das Abonnement richtet der Kunde ein, WEBklar übermittelt dorthin nichts) (A2); (d) Nr. 7 Abs. 4: Stripe ist kein Unterauftragsverarbeiter, weil keine Daten von Gästen oder Beschäftigten an Stripe gehen; (e) Aufruf des Widgets: IP-Adresse und Browser-Kennzeichen erhält der Server technisch bei jedem Abruf, Server-Protokolle 14 Tage (Art. 6 Abs. 1 lit. f DSGVO) (A1); im Knopfmodus lädt das Widget die Konfiguration schon beim Laden der Seite (A6, C8), vom selben Server wie das Skript; (f) Beschäftigtendaten: Namen, Zeiten und der geheime Kalender-Link; der Kunde informiert seine Beschäftigten (C3); (g) Nr. 3 Abs. 1: Absagegrund und Vermerke (Freitext, auch mit dem Namen aus einer Widerrufserklärung) sowie Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde; Nr. 2 Abs. 3: Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht, sind keine eigenen Zwecke; (h) Nr. 4 Abs. 2 und Nr. 9 Abs. 6: Löschung auf Weisung in Textform „unverzüglich“, ohne feste Frist.
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.5 (Vorbemerkung Abs. 1 und 2, Nr. 3 Abs. 1, Nr. 4 Abs. 2, Nr. 7, Nr. 9), Anlage A vollständig mit der Anlage der technischen und organisatorischen Maßnahmen; frühere Punkte A1, A2, A6, C3, C8; Programm: `baueNachweis`, `datenschutzAbsatz`.
|
||||
|
||||
**Unsere Annahme.** Der AVV ist vollständig und passt zum Programm (Datenarten, Löschfristen, Unterauftragsverarbeiter sind durch Tests an den Code gebunden). Eine Drittlandübermittlung durch WEBklar findet nicht statt; das Kalender-Abonnement liegt in der Verantwortung des Kunden. „Unverzüglich“ genügt für die Löschung auf Weisung.
|
||||
|
||||
**Alternative.** Ein Rahmen-AVV mit einer Anlage je Funktion (Abschnitt 6.2); im AVV einen Hinweis auf das Kalender-Abonnement bei Drittanbietern aufnehmen; für die Löschung auf Weisung eine feste Frist nennen (zum Beispiel 14 Tage).
|
||||
|
||||
### Q11: Gesundheits-Ausschluss
|
||||
|
||||
**Frage.** Der Terminplaner ist nicht für den Gesundheitsbereich bestimmt (Art. 9 DSGVO). Schutzmaßnahmen: die Branchenfrage im Fragebogen (der Gesundheitsbereich ist wählbar, die Bestellung dann aber gesperrt), Haken 2 mit Aufzählung (Arzt-, Zahnarzt-, Psychotherapie-, Heilpraktiker- oder Physiotherapiepraxis), eine weiche Stichwortprüfung über Betriebsname, Leistungen und Felder, die das Team warnt, das Recht in Leistungsbeschreibung Nr. 4 Abs. 3, die Einrichtung abzulehnen oder den Terminplaner dort abzuschalten, der Hinweis „Bitte keine Gesundheitsangaben“ über jedem Freitextfeld und im Datenschutz-Hinweis (A5), die Pflicht des Kunden in AVV Nr. 11 Abs. 4. Reicht das, damit WEBklar als Auftragsverarbeiter keine besonderen Kategorien verarbeitet? Und: Leistungsbeschreibung Nr. 4 Abs. 3 sagt nichts zur Erstattung, wenn WEBklar die Einrichtung wegen des Gesundheitsbereichs ablehnt oder abschaltet; muss der Text das regeln?
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.1 (Haken 2), Abschnitt 3.4 (Nr. 4 Abs. 3), Abschnitt 3.7 (Hinweis), Anlage A Nr. 3 Abs. 3 und Nr. 11 Abs. 4; früherer Punkt A5; Programm: `BRANCHEN`, `gesundheitsWarnungen`, `bestellpruefung`.
|
||||
|
||||
**Unsere Annahme.** Die Kombination aus Frage, Haken, Warnung und Ablehnungsrecht genügt; die Aufzählung im Haken ist beispielhaft. Lehnt WEBklar die Einrichtung ab, erstattet WEBklar den Preis aus Kulanz (nach Widerruf oder Kulanz, kein Satz im Text).
|
||||
|
||||
**Alternative.** Eine ausdrückliche Erstattungsklausel für die Ablehnung; die Aufzählung im Haken durch „Heilkunde und Gesundheitsversorgung“ ersetzen; eine zweite Bestätigung im Fragebogen.
|
||||
|
||||
### Q12: Gäste-Belehrung: Abweichungen vom Muster und Gesetzlichkeitsfiktion
|
||||
|
||||
**Frage.** Die Belehrung für Gäste (Abschnitt 3.6) beruht auf dem Muster der Anlage 1 zu Art. 246a § 1 Abs. 2 Satz 2 EGBGB, angepasst für einen Dienstleistungsvertrag. Genügt sie für die Gesetzlichkeitsfiktion, obwohl sie abweicht? (a) Die Klammer zu den Lieferkosten im Absatz „Folgen des Widerrufs“ bleibt stehen, weil der Gestaltungshinweis keine Streichung bei Dienstleistungen vorsieht (B1); dasselbe gilt für die Belehrung der Funktionen (Abschnitt 3.2). (b) Die Widerrufsfunktion nennt statt einer Internetadresse einen „anderen geeigneten Hinweis“: die Schaltfläche „Vertrag widerrufen“ im Terminbereich und den Link „Termin verwalten“ in der Bestätigungs-E-Mail; die Adresse der Seite fehlt (B5). (c) „… dass die Dienstleistungen … beginnen soll“ steht im Singular des Gesetzestexts (B2). (d) Das Widget zeigt die Belehrung aufklappbar in der Zusammenfassung vor dem Knopf, die Bestätigungs-E-Mail enthält sie als Text (H1). (e) Die Fiktion gilt nur, wenn das Muster inhaltlich und gestalterisch dem Gesetz entspricht; Zeilenumbrüche, Überschriften und die Aufzählung im Formular sind unsere Darstellung (B6).
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.6, Abschnitt 3.2 (Lieferkosten-Klammer und Widerrufsfunktion mit Adresse der Widerrufsseite bei den Funktionen), Abschnitt 3.9, Abschnitt 3.10 („Vertrag widerrufen“); frühere Punkte H1, B1, B2, B5, B6; Programm: `widerrufsbelehrung`, Muster in `docs/rechtliches/quellen/`.
|
||||
|
||||
**Unsere Annahme.** Ja: Die Abweichungen sind die Ausfüllung der Lücken, die das Muster vorsieht, oder der Gesetzestext selbst; die aufklappbare Darstellung ist zulässig, weil sie vor dem Absenden vollständig erreichbar ist.
|
||||
|
||||
**Alternative.** Die Klammer zu den Lieferkosten streichen; die Widerrufsfunktion zusätzlich mit der Adresse der Seite des Betriebs nennen; die Belehrung ohne Aufklappen offen anzeigen; „beginnen sollen“ im Plural.
|
||||
|
||||
### Q13: Gäste: Haken zum vorzeitigen Beginn und Wertersatz
|
||||
|
||||
**Frage.** Der Haken „Vorzeitiger Beginn“ (Abschnitt 3.7) wird nur Verbrauchern bei Leistungen mit Preis gezeigt, wenn der Termin vor Ablauf der Widerrufsfrist beginnt; bei Anfragen mit Preis immer, weil der Tag der Annahme nicht feststeht. Die Frist rechnen wir mit 14 Tagen ab dem Tag nach dem Vertragsschluss im Berliner Kalender (§§ 187, 188, 193 BGB), gerechnet ab Buchungszeitpunkt plus Haltezeit. (a) Genügt der Haken (Verlangen, Kenntnis, Wertersatz) in dieser Form? (b) Der Absatz zum Wertersatz steht immer in der Belehrung, auch wenn der Gast keinen vorzeitigen Beginn verlangt hat, weil das Muster ihn für Dienstleistungen vorsieht (B4). Ist das unschädlich? (c) Liegt der Termin nach Ablauf der Frist, zeigen wir keinen Haken: stimmt das?
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.7 (Haken „Vorzeitiger Beginn“), Abschnitt 3.6 (Absatz „Haben Sie verlangt …“); früherer Punkt B4; Programm: `ZUSTIMMUNG_VORZEITIG`, `brauchtZustimmungVorzeitig`.
|
||||
|
||||
**Unsere Annahme.** Ja zu (a) bis (c): Der Haken wirkt nur, wo ein Widerrufsrecht noch besteht und die Leistung vor Fristende beginnt; der Muster-Absatz zum Wertersatz bleibt unverändert, um die Fiktion nicht zu gefährden.
|
||||
|
||||
**Alternative.** Den Haken bei jeder Buchung mit Preis zeigen; den Wertersatz-Absatz nur bei gesetztem Haken in die Belehrung aufnehmen (Abweichung vom Muster).
|
||||
|
||||
### Q14: Anfrage mit Festpreis, Bestätigungslink und Seitenknopf als Bestellung
|
||||
|
||||
**Frage.** Wann ist die Buchung eines Gastes eine Bestellung mit Zahlungspflicht im Sinne von § 312j Abs. 3 BGB? Unser Ablauf: Bei einer verbindlichen Buchung mit Preis trägt der Knopf im Widget die Aufschrift „Zahlungspflichtig buchen“; der Gast bestätigt dann per Link in einer E-Mail, und die Bestätigungsseite hat den Knopf „Zahlungspflichtig bestätigen“; erst damit kommt der Vertrag zustande. Bezahlt wird vor Ort. Bei einer Terminanfrage, auch mit Festpreis, heißen die Knöpfe „Termin anfragen“ und „Anfrage absenden“, der Vertrag kommt erst mit der Annahme durch den Betrieb zustande. Ohne Preis lauten die Knöpfe „Termin buchen“ und „Termin bestätigen“. Genügt das? Muss der Knopf einer Anfrage mit Festpreis ebenfalls „zahlungspflichtig“ heißen? Ist der Seitenknopf auf der Bestätigungsseite die maßgebliche Bestellung?
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.10 (Knopftexte), Abschnitt 2.2 Punkt 2; Programm: `knopfText`.
|
||||
|
||||
**Unsere Annahme.** Ja: Die zahlungspflichtige Handlung ist die Bestätigung auf der Bestätigungsseite; bei einer Anfrage entsteht die Zahlungspflicht erst mit der Annahme, die der Betrieb erklärt und nicht der Gast.
|
||||
|
||||
**Alternative.** Auch den Knopf einer Anfrage mit Festpreis „Zahlungspflichtig anfragen“ nennen; bei einem Preis den Preis am Knopf zeigen.
|
||||
|
||||
### Q15: Einstufung als Freizeitleistung und Hinweis im Portal
|
||||
|
||||
**Frage.** Im Kundenportal stuft der Betrieb selbst ein, ob seine Termine Freizeitveranstaltungen zu einem festen Termin sind (§ 312g Abs. 2 Nr. 9 BGB); kreuzt er das an, zeigt der Terminplaner keine Widerrufsbelehrung, kein Formular und keine Widerrufsfunktion, und die Bestätigung nennt den Satz aus Abschnitt 3.9. Der Hinweis dazu im Portal lautet:
|
||||
|
||||
> Nur ankreuzen, wenn deine Termine Freizeitveranstaltungen zu einem festen Termin sind (§ 312g Abs. 2 Nr. 9 BGB), zum Beispiel ein einzelner Tanzabend, ein Kurs oder ein Escape Room. Nach unserer Einschätzung gilt bei Friseur, Kosmetik, Nagel- und Fußpflege, Tattoo, Fotoshootings, Coaching und Beratung in der Regel ein Widerrufsrecht; bei Massage und Wellness ist die Einordnung unklar, im Zweifel gilt ein Widerrufsrecht. Kreuzt du es an, zeigt der Terminplaner keine Widerrufsbelehrung. Kreuzt du es zu Unrecht an, können deine Gäste noch bis zu 12 Monate und 14 Tage widerrufen. Das ist keine Rechtsberatung: Im Zweifel nicht ankreuzen oder dich rechtlich beraten lassen.
|
||||
|
||||
Ist diese Einschätzung vertretbar, vor allem für Fotoshootings, Coaching, Beratung, Tattoo und Massage? Genügt „Das ist keine Rechtsberatung“, oder haftet WEBklar, wenn ein Betrieb zu Unrecht ankreuzt und Gäste noch bis zu 12 Monate und 14 Tage widerrufen können? Ein zweiter Hinweis im Portal (`WIDERRUF_HINWEIS`) erklärt dem Betrieb, dass Privatpersonen eine Buchung 14 Tage nach Vertragsschluss widerrufen können (bei Anfragen zählt die Annahme), dass das Widerrufsrecht seiner Stornoregel vorgeht, dass bei vorzeitigem Beginn Wertersatz geschuldet sein kann (§ 357a Abs. 2 BGB) und dass der Terminplaner Belehrung und Knopf „Vertrag widerrufen“ automatisch zeigt.
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.9 (Satz „kein Widerrufsrecht“), Leistungsbeschreibung Nr. 4 Abs. 8 und Nr. 11 Abs. 1 (die Einstufung trägt der Kunde); Programm: `FREIZEIT_HINWEIS`, `WIDERRUF_HINWEIS`, `zeigtWiderruf`.
|
||||
|
||||
**Unsere Annahme.** Friseur, Kosmetik, Nagel- und Fußpflege, Tattoo, Fotoshootings, Coaching und Beratung fallen in der Regel nicht unter die Ausnahme; bei Massage und Wellness ist die Einordnung unklar, im Zweifel besteht ein Widerrufsrecht. Der Hinweis stellt die Einstufung klar dem Betrieb zu.
|
||||
|
||||
**Alternative.** Die Einstufung nicht dem Betrieb überlassen: Der Terminplaner zeigt die Belehrung immer, die Freizeit-Ausnahme gibt es nicht; oder die Beispiele im Hinweis streichen.
|
||||
|
||||
### Q16: Storno-Muster (50 Prozent, 24 Stunden) und Wegfall bei kostenlosen Leistungen
|
||||
|
||||
**Frage.** Die Vorlagen schlagen dem Betrieb Stornobedingungen vor, zum Beispiel für Salon, Kosmetik und Beratung:
|
||||
|
||||
> Sie können Ihren Termin bis 24 Stunden vorher kostenlos absagen oder verschieben (Link in der Bestätigungsmail). Danach oder bei Nichterscheinen berechnen wir für den freigehaltenen Termin 50 % des Preises. Dem Kunden bleibt der Nachweis vorbehalten, dass uns kein oder ein geringerer Aufwand entstanden ist. Ihr gesetzliches Widerrufsrecht bleibt unberührt.
|
||||
|
||||
(Bei Tattoo und Fotografie 48 Stunden.) Im Portal steht neben dem Textfeld der Hinweis:
|
||||
|
||||
> Das ist ein Muster, kein Rechtstext. Bitte prüfe es und passe es an deine Bedingungen an.
|
||||
|
||||
(a) Hält das Muster einer Kontrolle nach § 307 BGB (und § 309 Nr. 5 BGB gegenüber Verbrauchern) stand, soweit es als Vorschlag für den Betrieb dient? (b) Hat keine Leistung einer Vorlage einen Preis (Vorlagen „handwerk“ und „sonstiges“), gibt es keinen Vorschlag. Bietet ein Betrieb einzelne kostenlose Leistungen neben Leistungen mit Preis an, bleibt sein Stornotext stehen, aber der Pflicht-Haken „Ich habe die Stornobedingungen gelesen.“ entfällt bei den kostenlosen Leistungen. Genügt das? (c) Die Bestätigungs-E-Mail G02 zeigt den Stornotext immer, wenn der Betrieb einen hinterlegt hat; ein Betrieb kann auch dann einen Text eingeben, wenn er kostenlose Leistungen anbietet, und dann steht er auch in der Bestätigung kostenloser Buchungen. Der Satz „Absagen richten sich nach den Stornobedingungen“ im Block „kein_widerruf“ (Abschnitt 3.9) steht dagegen bei jeder Buchung, die der Betrieb als Freizeit-Dienstleistung einstuft, also auch bei kostenlosen Buchungen und auch ohne hinterlegten Stornotext. Die Einbauanleitung für Mitarbeiter nennt den Stornotext unabhängig vom Preis. Ist das bei kostenlosen Leistungen irreführend? Wir wollen für G02 einen neutralen Satz festlegen (Checkliste, Abschnitt 7.4).
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.7 (Haken „Stornobedingungen“), Abschnitt 3.9 (G02); Programm: `stornoText`, `STORNO_PROZENT`, `pruefeEingabe`, `STORNO_MUSTER_HINWEIS`.
|
||||
|
||||
**Unsere Annahme.** Der Vorschlag ist ein Muster mit Nachweisvorbehalt und dem Hinweis auf das Widerrufsrecht; der Betrieb prüft und passt ihn an. Bei kostenlosen Leistungen gibt es nichts zu stornieren, der Haken ist dort nicht nötig. Der Satz in G02 wird für Buchungen ohne Preis neutral gefasst.
|
||||
|
||||
**Alternative.** Keinen Stornovorschlag mitliefern; oder 50 Prozent durch eine feste Gebühr oder einen Staffelsatz ersetzen.
|
||||
|
||||
### Q17: Datenschutz- und Impressum-Link; Widerrufssatz am Erinnerungshaken
|
||||
|
||||
**Frage.** (a) Der Datenschutz-Hinweis am Buchungsformular verweist auf die Datenschutzerklärung des Betriebs; das Widget ließe den Link sonst still weg. Deshalb verlangt das Bestell-Gate vor der Bestellung die Adressen von Impressum und Datenschutzerklärung (H3). Ist die Pflicht ausreichend, oder müssen Links zu Datenschutzerklärung und Impressum direkt am Haken stehen? (b) Der Erinnerungshaken endet mit „Diese Einwilligung kann ich jederzeit widerrufen.“ (Art. 7 Abs. 3 DSGVO), ohne Widerrufsweg und ohne Link am Haken; die Einzelheiten stehen im Datenschutz-Absatz (H2). Genügt das?
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.7 (Haken „Erinnerung“), Abschnitt 3.8 (Datenschutz-Hinweis); frühere Punkte H2, H3; Programm: `ZUSTIMMUNG_ERINNERUNG`, `formularHinweis`, `bestellpruefung`.
|
||||
|
||||
**Unsere Annahme.** Ja: Der Satz am Haken informiert vor der Einwilligung über das Recht zum Widerruf, die Einzelheiten und der Weg stehen im Hinweis und im Datenschutz-Absatz.
|
||||
|
||||
**Alternative.** Den Widerrufsweg („über den Link in der E-Mail oder per Nachricht an {{E-Mail}}“) in den Haken schreiben; Links zu Datenschutzerklärung und Impressum neben den Haken setzen.
|
||||
|
||||
### Q18: Datenschutz-Absatz auf der Kundenseite: Einfügen durch WEBklar im Preis
|
||||
|
||||
**Frage.** Leistungsbeschreibung Nr. 4 Abs. 9: Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau den Datenschutz-Absatz für die Online-Terminbuchung ein, das gehört zur Einrichtung und wird nicht auf das Stundenkontingent angerechnet; sonst erhält der Kunde den Absatz als Textbaustein. Der Kunde bleibt für seine Datenschutzerklärung verantwortlich; fällt WEBklar ein offensichtlicher Widerspruch auf, weist WEBklar darauf hin, eine Prüfpflicht entsteht nicht. (a) Ist das Einfügen eines Mustertextes durch WEBklar eine Rechtsdienstleistung (§ 2 RDG), oder bleibt es Technik und Mustertext (Nr. 11 Abs. 1)? (b) Reicht die Abgrenzung der Verantwortung? (c) Dieselbe Regel steht in der Leistungsbeschreibung der Website-Analyse (Nr. 4 Abs. 4); wir wollen sie für beide gleich halten.
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.4 (Nr. 4 Abs. 9, Nr. 11 Abs. 1); `docs/rechtliches/leistungsbeschreibung-website-analyse.md` Nr. 4 Abs. 4; Programm: `datenschutzAbsatz`.
|
||||
|
||||
**Unsere Annahme.** Das Einfügen eines unveränderten Mustertextes mit Hinweis ist keine Rechtsdienstleistung im Einzelfall; die Verantwortung bleibt beim Kunden.
|
||||
|
||||
**Alternative.** WEBklar fügt nichts ein und liefert nur den Textbaustein; oder WEBklar fügt ein und der Kunde bestätigt den Text ausdrücklich.
|
||||
|
||||
### Q19: Preisangabe nach § 19 UStG; Preis am Knopf
|
||||
|
||||
**Frage.** (a) Die Einrichtung steht überall sinngemäß als „149 € einmalig, ohne Umsatzsteuer gemäß § 19 UStG“, im Wortlaut so: im Haken 4 als „Gesamtpreis 149 € einmalig (ohne Umsatzsteuer gemäß § 19 UStG)“ (Abschnitt 3.1), in Leistungsbeschreibung Nr. 5 Abs. 2:
|
||||
|
||||
> (2) Die Einrichtung kostet 149 € einmalig je Website-Projekt, ohne Umsatzsteuer gemäß § 19 UStG.
|
||||
|
||||
und in der Bestellbestätigung K15 als Zusatz zur Preiszeile:
|
||||
|
||||
> einmalig, ohne Umsatzsteuer gemäß § 19 UStG; der Terminplaner läuft, solange Dein Wartungs-Abo läuft
|
||||
|
||||
Früher stand bei den News „inklusive Umsatzsteuer“ (Q4); das haben wir korrigiert. Ist die Angabe als Endpreis richtig, solange WEBklar Kleinunternehmer bleibt, und was ist bei Überschreiten der Grenze zu tun? (b) Der Knopf trägt den Preis: „Zahlungspflichtig bestellen · 149 €“ (Abschnitt 3.1). Ist der Zusatz mit § 312j Abs. 3 BGB vereinbar und nötig?
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.1 (Haken 4 und Knopf), Abschnitt 3.4 (Nr. 5); Programm: `ustHinweis`, `TERMINE_PREIS`, `termineConsent`.
|
||||
|
||||
**Unsere Annahme.** WEBklar bleibt Kleinunternehmer (der Inhaber bestätigt das, Abschnitt 7.1); der Preis am Knopf schadet nicht, weil die gesetzlichen Worte „zahlungspflichtig bestellen“ unverändert davor stehen.
|
||||
|
||||
**Alternative.** Knopf ohne Preis und den Preis direkt darüber zeigen; bei Überschreiten der Kleinunternehmergrenze Preise und Texte neu fassen (neues Fassungsdatum).
|
||||
|
||||
### Q20: Pflichtinformationen (Art. 246a § 1 EGBGB) als Haken und in K15
|
||||
|
||||
**Frage.** Haken 4 bestätigt, dass der Besteller die Pflichtinformationen gelesen hat: Leistung (Einrichtung des Terminplaners für diese Website: ein Widget an einer Stelle, gestaltet nach dem Fragebogen, mit Einbau und Datenschutz-Absatz), Gesamtpreis, voraussichtliche Dauer, Ende mit dem Wartungs-Abo, und dass er die Widerrufsbelehrung gelesen hat. Genügen Haken 4 zusammen mit der Belehrung, den versionierten Links auf Leistungsbeschreibung und AVV und der Bestellbestätigung K15 (Zustimmungstexte und PDF-Anhänge) den Informationspflichten nach Art. 246a § 1 Abs. 1 EGBGB? Wird zwischen Klick und Zahlung eine neue Fassung veröffentlicht, schickt K15 die Unterlagen der Fassung, der der Kunde zugestimmt hat (der vereinbarten Fassung), nicht die aktuelle; fehlen sie, geht K15 ohne Anhänge und das Team sendet sie von Hand. Ist das ausreichend? Die gerenderte Mail K15 folgt als Anlage C nach der Mail-Überarbeitung des Inhabers.
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.1 (Haken 4), Abschnitt 3.4 (Nr. 5), Abschnitt 2.1 Schritt 6; Programm: `k15Rechtsunterlagen`, `onFeaturePaid`, `legalPdfAttachments`.
|
||||
|
||||
**Unsere Annahme.** Ja: Die Informationen stehen vor der Bestellung im Text der Seite und in den Unterlagen; der Haken macht die Kenntnisnahme nachweisbar; K15 gibt sie auf einem dauerhaften Datenträger wieder.
|
||||
|
||||
**Alternative.** Die Pflichtinformationen als eigenen Textblock auf der Bestellseite und in K15 im Wortlaut wiederholen, statt sie im Haken zu bündeln.
|
||||
|
||||
### Q21: Freischaltung durch das Team mit Zustimmung in Textform; K16 mit PDFs
|
||||
|
||||
**Frage.** Ohne Bestellung (Kulanz, Pilot) schaltet das Team einen Terminplaner in der Betriebsart `on` (alle Kunden mit Wartungs-Abo, Abschnitt 1) nur frei, wenn der Kunde vorher in Textform (Chat im Kundenportal oder E-Mail) zugestimmt hat; Pilotadressen sind davon ausgenommen. Das Portal speichert als Nachweis (ohne Stripe-Sitzung, IP und Browser; Konto ist der Mitarbeiter) diesen Satz, mit Fassung, Datum und Weg des Falls:
|
||||
|
||||
> Der Kunde hat der Leistungsbeschreibung Terminplaner und dem Auftragsverarbeitungsvertrag Terminplaner (Fassung \<Fassung>) am \<TT>.\<MM>.\<JJJJ> per Chat im Kundenportal in Textform zugestimmt und bestätigt, dass sein Angebot nicht im Gesundheitsbereich liegt.
|
||||
|
||||
Bei der Zustimmung per E-Mail steht „per E-Mail“ statt „per Chat im Kundenportal“. Für den Chat liefert das Ticketsystem dem Mitarbeiter diese Textvorlage zum Kopieren (die Adressen sind die versionierten Seiten der beiden Unterlagen):
|
||||
|
||||
> Damit wir den Terminplaner für Dich einschalten können, brauchen wir Deine Zustimmung in Textform. Es gelten die Leistungsbeschreibung Terminplaner und der Auftragsverarbeitungsvertrag Terminplaner in der Fassung \<Fassung>: \<Adresse der Leistungsbeschreibung> · \<Adresse des AVV>. Bitte antworte hier mit „Ich stimme zu“ und bestätige, dass Dein Angebot nicht im Gesundheitsbereich liegt.
|
||||
|
||||
Danach geht K16 mit Leistungsbeschreibung und AVV als PDF an den Kunden. Genügt das? (a) Zustimmung durch eine Chat-Antwort für den AVV (Art. 28 Abs. 9 DSGVO) und die Leistungsbeschreibung; (b) der Nachweis ist der Eintrag des Mitarbeiters, die Chat-Nachricht bleibt im Portal; (c) K16 nennt die Anhänge nicht im Text und ist die Bestätigung auf einem dauerhaften Datenträger (die gerenderte Mail folgt als Anlage C nach der Mail-Überarbeitung des Inhabers); (d) für diesen Weg gibt es keine Widerrufsbelehrung (kein Kauf, kein Preis); (e) auch das Wiedereinschalten eines abgeschalteten Terminplaners, den das Team ohne Bestellung freigeschaltet hatte, verlangt dieselbe Textform (bei einem gekauften oder schon aktiven Terminplaner entfällt sie).
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.5 (Vorbemerkung Abs. 2), Abschnitt 2.1 (Freischaltung ohne Bestellung); Programm: `termineTextformNoetig`, `textformFuerAktivierung`, `adminActivate`, `textformNachweisText`, `meldeFreigeschaltet`; Zusammenhang mit Q7.
|
||||
|
||||
**Unsere Annahme.** Ja: Textform genügt für die Zustimmung zu Leistungsbeschreibung und AVV, der Nachweis des Mitarbeiters mit dem Chatverlauf im Portal ist hinreichend, und K16 liefert den dauerhaften Datenträger.
|
||||
|
||||
**Alternative.** Die Zustimmung durch den Kunden selbst im Portal (Haken) einholen, auch für Kulanzfälle; K16 im Text auf die Anhänge hinweisen lassen.
|
||||
|
||||
### Q22: BFSG-Satz ohne Konformitätszusage
|
||||
|
||||
**Frage.** Leistungsbeschreibung Nr. 11 Abs. 2 sagt: Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region); eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu; ob der Kunde als Dienstleistungserbringer unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Kunde. Ist die Formulierung ohne Konformitätszusage zulässig und für den Kunden klar genug? Nach unserem Stand gab es für das Widget bisher keinen Lauf mit einem automatischen Prüfwerkzeug oder einem Screenreader.
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.4 (Nr. 11 Abs. 2).
|
||||
|
||||
**Unsere Annahme.** Ja. Der Satz sagt nur, was gestaltet wurde, und weist die Prüfung des BFSG dem Kunden zu.
|
||||
|
||||
**Alternative.** Den Satz streichen; oder eine Prüfung des Widgets beauftragen und die Aussage danach anpassen.
|
||||
|
||||
### Q23: Nachweis der Bestellung mit IP-Adresse und Browser; Aufbewahrung drei Jahre
|
||||
|
||||
**Frage.** Beim Klick auf „Zahlungspflichtig bestellen“ speichert das Portal als Nachweis die gezeigten Texte und Fassungen, den Zeitpunkt, die Stripe-Sitzung, IP-Adresse und Browserkennung, das Konto und die angemeldete Person. Die Datenschutzerklärung sagt dazu:
|
||||
|
||||
> Bestellen Sie eine Funktion (News-Pinnwand, Terminplaner), speichern wir als Nachweis die Texte und Fassungen, denen Sie zugestimmt haben, den Zeitpunkt Ihres Klicks auf „Zahlungspflichtig bestellen“, die Stripe-Sitzung, IP-Adresse und Browserkennung sowie Ihr Konto und die angemeldete Person. Rechtsgrundlagen sind Art. 6 Abs. 1 lit. b, c und f DSGVO; unser berechtigtes Interesse ist der Nachweis des Vertragsschlusses und der Zustimmungen, insbesondere für den Widerruf. Diese Angaben löschen wir drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet. Bestellen Sie ohne Abo (zum Beispiel nur die News-Pinnwand), löschen wir diese Angaben spätestens drei Jahre nach Ablauf des Jahres der Bestellung. Den Entwurf Ihres Fragebogens speichern wir bis zu 90 Tage nach Ihrer letzten Änderung und löschen ihn danach, wenn Sie nichts bestellt haben.
|
||||
|
||||
Ist die Speicherung der IP-Adresse und der Browserkennung für den Nachweis des Vertragsschlusses und der Zustimmungen erforderlich (Art. 6 Abs. 1 lit. f DSGVO) und die Aufbewahrung bis drei Jahre nach Ablauf des Jahres, in dem das letzte Abo endet, angemessen? Genügt die Information in der Datenschutzerklärung? Für Bestellungen ohne Abo (nur News) nennt die Erklärung eine eigene Frist: drei Jahre nach Ablauf des Jahres der Bestellung. Das Programm löscht diese Nachweise heute noch nicht; die Löschung wird vor Ablauf der ersten Frist (31.12.2029) nachgerüstet. Ist diese Frist angemessen?
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 2.1 Schritt 4, Datenschutzerklärung § 7 (Hausrunde); Programm: `speichereZustimmung`, `zustimmungFuerSitzung`, `findDueDeletions` (Löschung der Nachweise gemeinsam mit den Abo-Daten des Kunden; für Kunden ohne Abo noch offen, Abschnitt 7.7).
|
||||
|
||||
**Unsere Annahme.** Ja: Der Nachweis dient dem Widerrufsrecht und der Beweisführung; die Frist entspricht der regelmäßigen Verjährung samt Ablauf des Jahres.
|
||||
|
||||
**Alternative.** IP-Adresse und Browserkennung weglassen (nur Zeit, Texte, Konto); oder eine kürzere Frist, etwa bis zum Ablauf der Widerrufsfrist und der Gewährleistung.
|
||||
|
||||
### Q24: Rollen in Abschnitt 6c; Entwurfsaufbewahrung 90 Tage
|
||||
|
||||
**Frage.** Die Datenschutzerklärung von WEBklar (Abschnitt 6c) beschreibt WEBklar als Auftragsverarbeiter für die Daten der Gäste und als Verantwortlichen für die Betriebsdaten im Kundenportal, den Entwurf des Fragebogens und die Bestellung (C1). (a) Stimmt die Rollenverteilung? (b) Der Satz „mit der Buchung speichern wir die IP-Adresse nicht“ ist durch einen Test belegt (Buchung ohne IP und Browser-Kennzeichen); für den Missbrauchsschutz merkt sich der Server Zeitpunkte je IP-Adresse nur im Arbeitsspeicher (C2). Genügt die Darstellung? (c) Anfragen von Gästen leitet WEBklar an den Kunden weiter (A4). (d) Entwürfe aus dem Fragebogen ohne Bestellung löscht WEBklar 90 Tage nach der letzten Änderung: Ist diese Frist zweckgebunden und angemessen?
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 2.1 Schritt 2, Anlage A Nr. 8 und Nr. 9 Abs. 7; Datenschutzerklärung 6c (`docs/rechtliches/termine-datenschutz-6c.md`, Hausrunde); frühere Punkte C1, C2, A4; Programm: `ENTWURF_TAGE`.
|
||||
|
||||
**Unsere Annahme.** Die Rollen sind richtig verteilt (wie beim Abschnitt 6b der Website-Analyse); 90 Tage Entwurf sind angemessen, weil Kunden Fragebögen über Wochen ausfüllen.
|
||||
|
||||
**Alternative.** Entwürfe kürzer aufbewahren (30 Tage) oder nur auf ausdrückliches Speichern des Kunden.
|
||||
|
||||
### Q25: Mindestlaufzeit des Wartungs-Abos und Terminplaner
|
||||
|
||||
**Frage.** Das Wartungs-Abo hat eine Mindestlaufzeit von zwölf Monaten (§ 8 der Abo-Bedingungen) und ist danach zum Monatsende kündbar. Der Terminplaner hat keine eigene Laufzeit und kein Sonderkündigungsrecht; er endet mit dem Abo. Bestellbar ist er nur bei aktivem Abo ohne vorgemerkte Kündigung (Abschnitt 2.1 Schritt 1). (a) Ist es unbedenklich, dass der Kunde 149 € einmalig für etwas zahlt, das mit dem Abo enden kann, zum Beispiel wenn die Mindestlaufzeit bald endet? (b) Reicht Haken 4 („der Terminplaner endet mit dem Wartungs-Abo“) für die Transparenz, oder braucht die Bestellseite einen Hinweis auf das Ende der laufenden Mindestlaufzeit?
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 3.1 (Haken 4), Abschnitt 3.4 (Nr. 8 Abs. 1 und 5), Abo-Bedingungen § 8 (`public/abo-bedingungen.html`); Programm: `bestellbar`, `laeuft`.
|
||||
|
||||
**Unsere Annahme.** Ja: Ohne vorgemerkte Kündigung läuft das Abo weiter; wird es später gekündigt, greift die Erstattung nur vor der Freischaltung, danach hat der Kunde den Terminplaner genutzt.
|
||||
|
||||
**Alternative.** Auf der Bestellseite das Ende der Mindestlaufzeit anzeigen; bei Beendigung in den ersten Monaten nach der Freischaltung anteilig erstatten.
|
||||
|
||||
### Q26: Nachweis der Gäste: Hash und Archiv der Textfassung
|
||||
|
||||
**Frage.** Die Buchung eines Gastes speichert als Nachweis die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der gezeigten Texte, nicht diese Texte selbst. WEBklar archiviert jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz, Belehrung für Gäste) mit Platzhaltern für die Betriebsangaben bis drei Jahre nach dem Ende des Terminplaners. Die Betriebsangaben (Name, Anschrift, Stornotext, Stunden der Erinnerung, Aufbewahrung) ändern sich später; der Stornotext hat eine eigene Version. Genügt der Hash zusammen mit dem Archiv der Textfassung, um die Zustimmung eines Gastes nachzuweisen? Der Nachweis beruht auf dem Textstand des Servers zum Zeitpunkt der Buchung, nicht auf dem, was das Widget angezeigt hat; wird die Fassung während der Haltezeit von 30 Minuten gewechselt, können beide abweichen (Änderungen sollen außerhalb der Stoßzeiten eingespielt werden).
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 2.2 Punkt 5, Abschnitt 3.5 (AVV Nr. 8 Abs. 5); Archiv `docs/rechtliches/archiv/termine-gaestetexte-2026-10-05.md`; Programm: `baueNachweis`, `baueArchiv`.
|
||||
|
||||
**Unsere Annahme.** Ja: Mit der Fassung, dem Archiv und den Betriebsangaben des Zeitpunkts lässt sich der gezeigte Wortlaut rekonstruieren; das genügt für den Nachweis, ohne Volltexte je Buchung zu speichern.
|
||||
|
||||
**Alternative.** Den vollständigen Text je Buchung speichern (mehr Daten, mehr Speicher); oder den Text je Betrieb und Fassung einmal als Schnappschuss ablegen.
|
||||
|
||||
## 5. Fundstellen im Code
|
||||
|
||||
Pfade gelten im Repository des Kundenportals (`Webklar-Kundenbereich`); Bezeichner sind Funktionen oder Konstanten und stehen in der genannten Datei. Zeilennummern nennen wir nicht, weil sie sich ändern.
|
||||
|
||||
| Thema | Datei | Funktion/Konstante |
|
||||
|---|---|---|
|
||||
| Fassung der Vertragsunterlagen und Bestelltexte | `server/config.js` | `avvVersion` |
|
||||
| Bestell-Haken und Knopf des Terminplaners (Register) | `server/config.js` | `termineConsent`, `TERMINE_PREIS` |
|
||||
| Preisangabe und Umsatzsteuerhinweis | `server/config.js` | `ustHinweis` |
|
||||
| Preis im Produktkatalog (14 900 Cent) | `server/config.js` | `termineSetup`, `amountCents` |
|
||||
| Widerrufsbelehrung der Funktionen (WEBklar als Unternehmer) | `server/services/termine/rechtstexte.js` | `widerrufsbelehrungFunktion`, `WEBKLAR_KONTAKT` |
|
||||
| Gästetexte: Belehrung, Haken, Hinweis, Datenschutz-Absatz | `server/services/termine/rechtstexte.js` | `widerrufsbelehrung`, `ZUSTIMMUNG_VORZEITIG`, `ZUSTIMMUNG_STORNO`, `ZUSTIMMUNG_ERINNERUNG`, `GESUNDHEIT_HINWEIS`, `formularHinweis`, `datenschutzAbsatz` |
|
||||
| Fassung der Gästetexte | `server/services/termine/rechtstexte.js` | `RECHTSTEXTE_VERSION` |
|
||||
| Knopftexte der Gäste, Widerrufsfunktion | `server/services/termine/einstellungen.js` | `knopfText`, `zeigtWiderruf` |
|
||||
| Wann der Haken zum vorzeitigen Beginn gilt | `server/services/termine/einstellungen.js` | `brauchtZustimmungVorzeitig` |
|
||||
| Eingabeprüfung der Buchung, Storno-Haken nur bei Preis, Nachweis mit Hashes | `server/services/termine/ablauf.js` | `pruefeEingabe`, `baueNachweis` |
|
||||
| Storno-Vorschlag der Vorlagen (50 %, 24 Stunden) | `server/services/termine/vorlagen.js` | `stornoText`, `STORNO_PROZENT` |
|
||||
| Zustimmungsnachweis der Bestellung | `server/services/featureConsents.js` | `speichereZustimmung`, `zustimmungFuerSitzung` |
|
||||
| Bestellung: Reihenfolge Zahlungssitzung, Nachweis, Weiterleitung | `server/routes/funktionen.js` | `/:key/checkout`, `speichereNachweis`, `createFeatureCheckout` |
|
||||
| Bestellbestätigung K15 mit PDF-Anhängen der vereinbarten Fassung | `server/services/siteFeatures.js` | `onFeaturePaid`, `k15Rechtsunterlagen` |
|
||||
| Adressen und Dateien der Rechtsunterlagen | `server/services/legalDocs.js` | `legalPaths`, `legalPdfAttachments`, `featureLegalAttachments` |
|
||||
| Abo-Prüfung (laufend, bestellbar) | `server/services/aboStatus.js` | `laeuft`, `bestellbar` |
|
||||
| Täglicher Abo-Abgleich, Mail K22/K23, Erstattung T19 | `server/services/termine/aboAbgleich.js` | `gleicheAboAb`, `meldeAboEnde` |
|
||||
| Abschalten, Wiedereinschalten, Abschaltgründe | `server/services/siteFeatures.js` | `disable`, `activate`, `DISABLED_REASONS` |
|
||||
| Löschfrist nach dem Abschalten (30 Tage) | `server/services/termine/frist.js` | `LOESCHFRIST_TAGE` |
|
||||
| Löschlauf, Fristen der Buchungen | `server/services/termine/jobs.js` | `loescheAbgeschaltete`, `VERFALLEN_BEHALTEN_MS`, `ABGESAGT_BEHALTEN_MS`, `AUFBEWAHRUNG_VORGABE` |
|
||||
| Aufbewahrung der Entwürfe (90 Tage) | `server/services/termine/entwuerfe.js` | `ENTWURF_TAGE` |
|
||||
| Versandwarteschlange für E-Mails | `server/services/termine/outbox.js` | `GESENDET_BEHALTEN_MS`, `FEHLER_BEHALTEN_MS` |
|
||||
| Aufbewahrung der Zustimmungsnachweise der Bestellung | `server/services/retention.js` | `findDueDeletions`, `applyDeletions` |
|
||||
| Gesundheits-Ausschluss: Branchen, Stichwortwarnung, Bestell-Gate | `server/services/termine/fragebogen.js` | `BRANCHEN`, `gesundheitsWarnungen`, `bestellpruefung` |
|
||||
| Freischaltung ohne Bestellung: Textform nötig? | `server/services/termine/schalter.js` | `termineTextformNoetig` |
|
||||
| Freischaltung ohne Bestellung: Prüfung von Datum und Weg, PDFs | `server/routes/admin/funktionenAdmin.js` | `textformFuerAktivierung`, `unterlagenAlsAnhang` |
|
||||
| Freischaltung ohne Bestellung: Nachweis und Aktivierung | `server/services/siteFeatures.js` | `adminActivate`, `textformNachweisText` |
|
||||
| Widerruf der Bestellung: Fristende je Bestellung, spätestes Fristende entscheidet über „rechtzeitig“ | `server/routes/abo.js` | `withdrawFeature` |
|
||||
| Widerrufsfrist (14 Tage), Fristende | `server/services/deadlines.js` | `WITHDRAWAL_DAYS`, `withdrawalDeadline` |
|
||||
| Widerruf durch Gäste (Widerrufsfunktion) | `server/routes/publicTermine.js` | `/:siteKey/widerruf`, `/termin/widerruf`, `widerrufeGast`, `widerrufeOhneLink` |
|
||||
| Bestätigungs-E-Mail an Gäste, Hinweis kein Widerrufsrecht | `server/mail-templates/G02 Termin bestaetigt.html` | `kein_widerruf`, `widerrufsbelehrung` |
|
||||
| Versand der Bestätigungs-E-Mail | `server/services/termine/mails.js` | `gastG02` |
|
||||
| Eingangsbestätigung des Widerrufs | `server/mail-templates/G08 Eingang Widerruf.html` | `{{eingang}}`, `{{nummer}}`, `nach_frist` |
|
||||
| Mail „Terminplaner beendet“ (30 Tage, Rückkehr) | `server/mail-templates/K22 Terminplaner beendet.html` | `loeschdatum`, `Rückkehr` |
|
||||
| Mail „Terminplaner läuft wieder“ | `server/mail-templates/K23 Terminplaner laeuft wieder.html` | `{{kunde_name}}`, `innerhalb der Löschfrist` |
|
||||
| Hinweise im Kundenportal (Widerruf, Freizeit, Storno-Muster) | `public/termine-tab-einstellungen.js` | `WIDERRUF_HINWEIS`, `FREIZEIT_HINWEIS`, `STORNO_MUSTER_HINWEIS` |
|
||||
| Archiv der Gästetexte je Fassung | `scripts/termine-texte-archiv.mjs` | `baueArchiv` |
|
||||
| HTML-Seiten der Vertragsunterlagen | `scripts/build-legal-html.mjs` | `termineDokumente` |
|
||||
| PDFs der Vertragsunterlagen | `scripts/build-legal-pdfs.sh` | `termine_version`, `TERMINE_VERSION` |
|
||||
| Muster der Widerrufsbelehrung (Anlage 1) | `docs/rechtliches/quellen/anlage-1.txt` | `Folgen des Widerrufs` |
|
||||
| Muster-Widerrufsformular (Anlage 2) | `docs/rechtliches/quellen/anlage-2.txt` | `Muster-Widerrufsformular` |
|
||||
| Datenschutzerklärung von WEBklar: Abschnitt 6c, § 6, § 7 | `public/datenschutz.html` | `id="termine"` |
|
||||
|
||||
## 6. Nicht Teil der Kanzleiprüfung
|
||||
|
||||
Dieser Abschnitt nennt, was wir der Kanzlei nicht zur Prüfung vorlegen, und warum.
|
||||
|
||||
### 6.1 Hausrunde
|
||||
|
||||
Diese Texte prüft WEBklar selbst (wie bei der Website-Analyse), weil sie keine der Kernfragen aus Abschnitt 1 betreffen:
|
||||
|
||||
- die Datenschutzerklärung von WEBklar mit Abschnitt 6c „Online-Terminbuchung auf Websites unserer Kunden“ und den Ergänzungen in § 6 (Kundenansicht) und § 7 (Nachweise der Funktionsbestellung); Quelle `docs/rechtliches/termine-datenschutz-6c.md` und `public/datenschutz.html`. Nur die Rollenverteilung fragen wir (Q24),
|
||||
- die Hinweise im Kundenportal (Widerrufs-, Freizeit- und Storno-Hinweise); zu zwei Punkten fragen wir (Q15, Q16),
|
||||
- die Mailtexte (K15, K16, K22, K23, K04, T09, G02, G08); sie gehen durch die Mail-Überarbeitung des Inhabers. Die offene Formulierung in G02 steht in Q16 und in Abschnitt 7,
|
||||
- die Einbauanleitung und das Einrichtungs-Ticket für Mitarbeiter.
|
||||
|
||||
### 6.2 Später
|
||||
|
||||
Diese Themen gehören nicht zu diesem Verkaufsweg und stehen für spätere Pakete bereit:
|
||||
|
||||
- Abo-Bedingungen in der Fassung 6 bzw. ein Rahmen-AVV mit einer Anlage je Funktion,
|
||||
- Themen der Online-Zahlung der Gäste (ein Satz zu Stripe in Abschnitt 6c, Zahlungsdienste, Vorauszahlung in den AGB, Preisangabenverordnung),
|
||||
- Pflichtangaben auf Geschäftsbriefen (§ 37a HGB),
|
||||
- Rechtsmissbrauch bei spätem Widerruf,
|
||||
- Minderjährige,
|
||||
- Mitgliedschaften,
|
||||
- wiederholtes Versenden der Mails G08 und K21 nach Fristende,
|
||||
- Widerruf nur vor Terminbeginn im Verwaltungslink.
|
||||
|
||||
### 6.3 Frühere Listen „Für die Rechtsprüfung offen“
|
||||
|
||||
Beim Fertigstellen der Gästetexte und der Datenschutzerklärung haben wir Listen offener Rechtsfragen aus den Dokumenten entfernt. Jeder Punkt dieser Listen hat eine Kennung: H1 bis H4 (Hinweise-Dokument), A1 bis A6 (Datenschutz-Absatz für die Website der Betriebe), B1 bis B6 (Muster der Widerrufsbelehrung für Gäste), C1 bis C8 (Entwurf von Abschnitt 6c der Datenschutzerklärung). Jeder Punkt wird entweder bei einer Frage in Abschnitt 4 behandelt (die Kennung steht dort in der Fundstelle) oder ist hier mit Grund aufgeführt:
|
||||
|
||||
| Kennung | Punkt | Wo behandelt |
|
||||
|---|---|---|
|
||||
| H1 | Belehrung vor dem Vertragsschluss: aufklappbare Darstellung im Widget | Q12 |
|
||||
| H2 | Widerruf der Einwilligung am Erinnerungshaken | Q17 |
|
||||
| H3 | Link zur Datenschutzerklärung neben dem Hinweis | Q17 |
|
||||
| H4 | Bestätigungsfrist: 48 Stunden sind der früheste Zeitpunkt der Löschung | Q9 |
|
||||
| A1 | Server-Protokolle: 14 Tage und Rechtsgrundlage | Q10 |
|
||||
| A2 | Drittländer: gilt nur, solange Server und Mailversand bei Hetzner in Deutschland bleiben | Q10 |
|
||||
| A3 | Löschlauf: Fristen sind die frühesten Zeitpunkte | Q9 |
|
||||
| A4 | Kontakt der Gäste: Anfragen gehen an den Betrieb, WEBklar leitet weiter | Q24 |
|
||||
| A5 | Gesundheitsbereich: Hinweis „Bitte keine Gesundheitsangaben“ | Q11 |
|
||||
| A6 | Knopfmodus: Konfiguration schon beim Laden der Seite | Q10 |
|
||||
| B1 | Lieferkosten-Klammer in der Belehrung | Q12 |
|
||||
| B2 | „beginnen soll“ im Singular | Q12 |
|
||||
| B3 | Anschrift und Telefon des Betriebs sind Pflicht | hier: technisch erledigt. `widerrufsbelehrung()` wirft einen Fehler, wenn eine der vier Angaben fehlt, und die Einstellungsprüfung verlangt beide Angaben. Keine Rechtsfrage. |
|
||||
| B4 | Wertersatz-Absatz steht immer in der Belehrung | Q13 |
|
||||
| B5 | Hinweis statt Internetadresse bei der Widerrufsfunktion | Q12 |
|
||||
| B6 | Gesetzlichkeitsfiktion des Musters | Q12 |
|
||||
| C1 | Rolle: Auftragsverarbeiter für die Gäste, Verantwortlicher für die Betriebsdaten | Q24 |
|
||||
| C2 | IP-Adresse im Arbeitsspeicher: Satz „mit der Buchung speichern wir die IP-Adresse nicht“ | Q24 |
|
||||
| C3 | Kalender-Abo-Link: Information der Beschäftigten | Q10 |
|
||||
| C4 | Sicherungen: gelöschte Daten bis zu sieben Tage | Q9 |
|
||||
| C5 | Zahlung der Gäste (spätere Online-Zahlung): Satz zu Stripe in Abschnitt 6c | hier: gehört zur späteren Online-Zahlung der Gäste (Abschnitt 6.2); der Terminplaner kennt heute nur die Zahlung vor Ort. |
|
||||
| C6 | Geschäftsbriefe: Bestätigungen mit Preis sind keine Rechnungen | hier: Abschnitt 6.2 (Pflichtangaben auf Geschäftsbriefen); die Aufbewahrung nach Abschnitt 9 der Datenschutzerklärung betrifft den Terminplaner nicht. |
|
||||
| C7 | Ende des Wartungs-Abos: Satz in Abschnitt 6c | Q9 |
|
||||
| C8 | Knopfmodus: Formulierung zum Laden der Konfiguration in Abschnitt 6c | Q10 |
|
||||
|
||||
## 7. Checkliste für den Inhaber
|
||||
|
||||
Diese Punkte erledigt der Inhaber; kein Punkt ist Aufgabe der Kanzlei. Die Reihenfolge entspricht dem Ablauf bis zum ersten fremden Betrieb.
|
||||
|
||||
### 7.1 Vor der Beauftragung der Kanzlei
|
||||
|
||||
- [ ] Die PDFs der Anlagen A und B und der Widerrufsbelehrung einmal am Bildschirm ansehen (Kopfzeile „WEBklar · …“, „Seite x von y“, Tabelle der Anlage des AVV); wir konnten sie nur über ihre HTML-Seiten prüfen.
|
||||
- [ ] Die Zahl der News-Bestellungen seit 2026-09-30 ermitteln, die mit dem alten Zustimmungstext und ohne Widerrufsbelehrung abgeschlossen wurden, und der Kanzlei nennen (Q4).
|
||||
- [ ] Bestätigen, dass WEBklar Kleinunternehmer nach § 19 UStG bleibt (Q19).
|
||||
|
||||
### 7.2 Hetzner (nur im Konto prüfbar)
|
||||
|
||||
- [ ] Auftragsverarbeitungsvertrag mit Hetzner für die Cloud **und** für das Webhosting abgeschlossen (die Buchungs-E-Mails laufen über den SMTP-Dienst des Webhostings).
|
||||
- [ ] Anlage 1 des Vertrags nennt „Terminbucher“, „Beschäftigte“ und „Buchungsdaten“ als Betroffene und Datenarten.
|
||||
- [ ] Die Kontoadresse bei Hetzner stimmt mit dem Impressum überein.
|
||||
- [ ] Die Datensicherung (Backups) ist eingeschaltet; AVV Nr. 9 Abs. 8 und die Abo-Bedingungen versprechen sieben Tagessicherungen.
|
||||
|
||||
### 7.3 Uploads in WOMS „Rechtliches“
|
||||
|
||||
- [ ] Das Setup-Skript von WOMS mit API-Key ausführen: Es legt die drei Karten „Leistungsbeschreibung Terminplaner“, „AVV Terminplaner (Auftragsverarbeitung)“ und „Widerrufsbelehrung und Muster-Widerrufsformular (Funktionen)“ an.
|
||||
- [ ] Für diese drei Karten die Version der Fassung hochladen (PDF oder HTML aus `public/rechtliches/`; Markdown ist im Bucket nicht erlaubt).
|
||||
- [ ] Die neue Datenschutzerklärung (Stand 05.10.2026, `docs/WEBklar-Datenschutzerklaerung.pdf`) als neue Version der Karte „Datenschutzerklärung“ hochladen.
|
||||
- [ ] „Gültig ab“ in `docs/rechtliches/archiv/README.md` eintragen (Fassung der Gästetexte vom 2026-10-05 steht auf „offen bis Livegang“; bei der Fassung vom 2026-10-02 prüfen, ob „gültig ab 2026-10-03 (Pilot)“ stimmt).
|
||||
|
||||
### 7.4 Mailtexte
|
||||
|
||||
- [ ] Die Platzhalter „ENTWURF (Mailrunde): …“ in `mails.danach` und `mails.bereit` des Terminplaners (Register in `server/config.js`) durch den endgültigen Wortlaut ersetzen. Sie stehen heute in K15 und K16 an Kunden.
|
||||
- [ ] Die Kopfkommentare von K22 und K23 („Texte sind Entwurf für die Mail-Überarbeitung“) in der Mail-Überarbeitung abarbeiten.
|
||||
- [ ] K23 (Terminplaner läuft wieder) sagt „Deine Termine und Einstellungen sind unverändert da“. Für die Termine stimmt das nicht immer, weil der Löschlauf auch bei abgeschaltetem Terminplaner abgesagte und erledigte Termine nach ihren Fristen löscht. Den Satz in der Mail-Überarbeitung abschwächen.
|
||||
- [ ] Nach der Mail-Überarbeitung die gerenderten Mails K15 (mit den Zustimmungen) und K16 als **Anlage C** beilegen und der Kanzlei nachreichen; ihre Angemessenheit als Bestätigung auf einem dauerhaften Datenträger fragen Q3, Q20 und Q21.
|
||||
- [ ] G02 zeigt die Stornobedingungen und den Satz „Absagen richten sich nach den Stornobedingungen“ (Block `kein_widerruf`) auch bei kostenlosen Buchungen. Einen neutralen Satz festlegen, der für Buchungen ohne Preis passt (Q16).
|
||||
|
||||
### 7.5 Beauftragung und Freigabe
|
||||
|
||||
- [ ] Die Kanzlei mit der Prüfung des Kernpakets und den Fragen Q1 bis Q26 beauftragen (Anlagen A und B beilegen; Umfang und Frist vereinbaren).
|
||||
- [ ] Den Befund einarbeiten: Texte ändern, neues Fassungsdatum setzen, HTML und PDFs neu erzeugen, Archiv der Gästetexte und Prüfsummen nachziehen. Dieses Paket (`docs/rechtliches/termine-kanzlei-paket.md`) wird dabei nachgezogen; sein Test meldet jedes Zitat, das nicht mehr wörtlich im Programm oder in den Dokumenten steht.
|
||||
- [ ] Über die News-Altfälle entscheiden (Q4): Belehrung per Mail nachholen oder nur künftig die neuen Texte.
|
||||
- [ ] Die Fassung vor dem ersten fremden Betrieb ausdrücklich freigeben; erst danach die Adresse des ersten fremden Betriebs in die Pilotliste aufnehmen.
|
||||
- [ ] Beim Livegang die Reihenfolge in Abschnitt 7.6 einhalten; den Datenschutz-Abschnitt der Test-Website des Terminplaners auf den Endtext bringen.
|
||||
|
||||
### 7.6 Livegang: Reihenfolge
|
||||
|
||||
Diese Schritte gelten für den Tag, an dem die Betriebsart `on` gesetzt wird. Die Reihenfolge ist wichtig, vor allem: beide Schemata vor dem Code-Deploy und `on` als letzter Schritt.
|
||||
|
||||
- [ ] Vor dem Code-Deploy das Schema des Terminplaners anlegen, erst mit `--check`: `node scripts/termine-setup.mjs` ergänzt in `termineConfig` die Spalte `fragebogen`, in `termineBookings` die Indizes `idx_status_ends` und `idx_status_updated` und in `termineOutbox` den Index `idx_booking`. Ohne sie scheitern Schreibzugriffe und Abfragen des neuen Programms (der Fragebogen, der tägliche Löschlauf und das Löschen von Gastdaten).
|
||||
- [ ] Vor dem Code-Deploy das Schema der Funktionen anlegen, erst mit `--check`: `node scripts/feature-setup.mjs` (neue Felder von `portalFeatures` und die Collection `featureConsents` mit ihren Indizes). Der Zeitpunkt ist der Code-Deploy, nicht erst der erste Löschlauf der Aufbewahrungsfristen: Schon die erste Bestellung des neuen Programms schreibt die neuen Felder, und ohne Schema antwortet Appwrite mit „Unknown attribute“.
|
||||
- [ ] Hat sich `public/datenschutz.html` geändert, das PDF der Datenschutzerklärung mit `scripts/build-legal-pdfs.sh datenschutz` neu drucken, nicht mit `all` (das druckt auch die PDFs der Abo-Bedingungen und des Leitfadens neu); die Prüfsumme `docs/WEBklar-Datenschutzerklaerung.pdf.sha256` gehört dazu.
|
||||
- [ ] Den Code zu einer ruhigen Zeit deployen. Der Nachweis einer Buchung speichert die Fassung der Gästetexte, die der Server beim Speichern hat; bucht ein Gast genau während des Deploys, kann sie von der abweichen, die er auf dem Bildschirm sah.
|
||||
- [ ] Nach dem Deploy die Uploads in WOMS „Rechtliches“ erledigen (Abschnitt 7.3).
|
||||
- [ ] Nach dem Deploy und nur mit Freigabe des Inhabers: `scripts/feature-consent-felder-leeren.mjs` zuerst als Probelauf ausführen (ohne Argument oder mit `--dry-run`; schreibt nichts), die Liste prüfen und erst dann mit `--apply` ausführen. Das Skript leert IP-Adresse und Browserkennung der Bestellung an den Funktionsdokumenten (`portalFeatures`), soweit der Nachweis (`featureConsents`) sie trägt; neue Bestellungen speichern sie dort nicht mehr.
|
||||
- [ ] Als allerletzten Schritt `TERMINE_ENABLED=on` setzen, nachdem alle Punkte oben und die Freigabe der Fassung (Abschnitt 7.5) erledigt sind.
|
||||
|
||||
### 7.7 Nach dem Livegang
|
||||
|
||||
- [ ] Die Löschfrist für die Nachweise von Kunden ohne Abo (nur News) umsetzen, bevor die erste Frist abläuft (31.12.2029). Die Datenschutzerklärung (Abschnitt 7) sagt für Bestellungen ohne Abo „spätestens drei Jahre nach Ablauf des Jahres der Bestellung“; der Löschlauf (`findDueDeletions`) erfasst heute nur Kunden mit einem Abo, ein Kunde ohne Abo ist nie fällig. Umsetzung: Kunden ohne Abo-Dokument, aber mit Nachweisen in `featureConsents`, mit dem Ankerjahr der letzten Bestellung in den Plan aufnehmen und nur die Nachweise und die Bestellangaben am Funktionsdokument löschen, nicht die Beiträge.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Widerrufsbelehrung und Muster-Widerrufsformular (Terminplaner, Dienstleistung)
|
||||
|
||||
Fassung 2026-10-05. Quelle des Wortlauts: Anlage 1 und Anlage 2 zu Art. 246a § 1 Abs. 2 Satz 2 EGBGB (Fundstelle BGBl. I 2013, S. 3663 bis 3665), abgerufen am 2026-10-02 von gesetze-im-internet.de; die Textdateien liegen unverändert in `docs/rechtliches/quellen/anlage-1.txt` und `anlage-2.txt`.
|
||||
|
||||
Der Text unten ist die Fassung, die `widerrufsbelehrung(betrieb)` in `server/services/termine/rechtstexte.js` erzeugt, mit Platzhaltern statt der Betriebsdaten. `test/termineRechtstexte.test.js` prüft, dass (a) jeder Absatz des Musters wörtlich aus den Quelldateien im Ergebnis steht und (b) der Textblock unten genau der Ausgabe der Funktion entspricht. Ändere den Text deshalb nur in `rechtstexte.js` und übernimm ihn hierher.
|
||||
|
||||
## Wie das Muster für einen Termin (Dienstleistung) ausgefüllt ist
|
||||
|
||||
| Gestaltungshinweis in Anlage 1 | Umsetzung |
|
||||
|---|---|
|
||||
| Frist: Textbaustein a) für Dienstleistungsverträge | „des Vertragsabschlusses.“ (Fristbeginn ist der Vertragsschluss: bei einer sofort verbindlichen Buchung die Bestätigung per Link, bei einer Anfrage die Annahme durch den Betrieb) |
|
||||
| „Fügen Sie Ihren Namen, Ihre Anschrift, Ihre Telefonnummer und Ihre E-Mail-Adresse ein.“ | Name, Anschrift, Telefon, E-Mail aus den Betriebsangaben der Einstellung (`betrieb.name`, `adresse`, `telefon`, `email`); alle vier sind Pflicht, fehlt oder ist eine Angabe leer, wirft `widerrufsbelehrung()` einen TypeError |
|
||||
| Widerrufsfunktion bei online geschlossenen Verträgen (§ 356a BGB) | Satzrahmen des Gesetzes wörtlich („Sie können Ihr Widerrufsrecht auch online unter … ausüben. Wenn Sie diese Online-Funktion nutzen, …“); in die Lücke „…“ kommt statt einer Internetadresse ein „anderer geeigneter Hinweis“: „Vertrag widerrufen“ (Schaltfläche im Terminbereich auf unserer Website sowie Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail); der Gesetzeswortlaut zur Eingangsbestätigung bleibt unverändert |
|
||||
| Absatz zum elektronisch ausfüllbaren Webformular | ausgelassen: Es gibt keine eigene Webformular-Funktion außer der Widerrufsfunktion |
|
||||
| Textbausteine zu Waren (Besitz, Rückzahlungsverweigerung, Abholung oder Rücksendung, Rücksendekosten, Wertverlust) | ausgelassen: Ein Termin hat keine Waren |
|
||||
| Dienstleistungen: Wertersatz („Haben Sie verlangt, dass die Dienstleistungen …“) | enthalten; „oder Lieferung von Wasser/Gas/Strom/Fernwärme [Unzutreffendes streichen]“ ist gestrichen, wie der Gestaltungshinweis es vorsieht |
|
||||
| Muster-Widerrufsformular (Anlage 2) | enthalten; in der Zeile „An …“ stehen Name, Anschrift und E-Mail des Betriebs; die (*)-Alternativen bleiben unverändert |
|
||||
|
||||
Anrede immer „Sie“, auch bei Betrieben, die ihre Gäste duzen: Es ist der gesetzliche Mustertext.
|
||||
|
||||
## Text (Platzhalter in doppelten geschweiften Klammern)
|
||||
|
||||
```text
|
||||
Widerrufsbelehrung
|
||||
|
||||
Widerrufsrecht
|
||||
|
||||
Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
|
||||
|
||||
Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag des Vertragsabschlusses.
|
||||
|
||||
Um Ihr Widerrufsrecht auszuüben, müssen Sie uns ({{Name des Betriebs}}, {{Anschrift}}, Telefon: {{Telefon}}, E-Mail: {{E-Mail}}) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
|
||||
|
||||
Sie können Ihr Widerrufsrecht auch online unter „Vertrag widerrufen“ (Schaltfläche im Terminbereich auf unserer Website sowie Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail) ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.
|
||||
|
||||
Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
|
||||
|
||||
Folgen des Widerrufs
|
||||
|
||||
Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
|
||||
|
||||
Haben Sie verlangt, dass die Dienstleistungen während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht.
|
||||
|
||||
Muster-Widerrufsformular
|
||||
|
||||
(Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
|
||||
|
||||
– An {{Name des Betriebs}}, {{Anschrift}}, {{E-Mail}}:
|
||||
– Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)
|
||||
– Bestellt am (*)/erhalten am (*)
|
||||
– Name des/der Verbraucher(s)
|
||||
– Anschrift des/der Verbraucher(s)
|
||||
– Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
|
||||
– Datum
|
||||
|
||||
(*) Unzutreffendes streichen.
|
||||
```
|
||||
|
||||
## Wo der Text erscheint
|
||||
|
||||
- G02 (Bestätigung an den Gast), Block `widerruf`, als `{{widerrufsbelehrung}}` (die Mailvorlage setzt den Text HTML-maskiert ein); nur wenn die Buchung ein Widerrufsrecht hat (verbindlich, Verbraucher).
|
||||
- Bei einer Freizeit-Dienstleistung mit festem Termin (§ 312g Abs. 2 Nr. 9 BGB) und bei Gästen, die nur Unternehmen sind, entfallen Belehrung, Formular und Widerrufsfunktion; der Hinweis steht dann in `termine-hinweise.md`.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Entwurf: Abschnitt 6f der Datenschutzerklärung (Buchungskalender)
|
||||
|
||||
Stand 2026-10-07, Fassung der Rechtstexte 2026-10-07. **Entwurf – Prüfung durch Kanzlei offen.** Der Text kommt als Abschnitt `6f` in `public/datenschutz.html`, hinter 6d (Webshop; 6e ist für die Öffnungszeiten reserviert und noch nicht live) und vor Abschnitt 7. Er wird erst mit dem Livegang übernommen (`docs/funktionen/unterkunft-livegang.md`, Schritt 9), danach die PDF-Fassung `scripts/build-legal-pdfs.sh datenschutz` neu drucken. Mit der Übernahme zugleich im Abschnitt 12 „Empfänger im Überblick“ die Zeile zu Stripe und in Abschnitt 3 die Verweise prüfen. Der Text ist an Abschnitt 6d angelehnt und beschreibt, was die Software tut; ändert sich der AVV Buchungskalender, hier mitändern.
|
||||
|
||||
## Text (als HTML-Absätze übernehmen)
|
||||
|
||||
**6f. Buchungskalender auf Websites unserer Kunden**
|
||||
|
||||
Kunden können auf ihrer eigenen Website einen Buchungskalender für ihre Unterkünfte betreiben („Buchungskalender“), sobald wir die Funktion für sie freigeschaltet haben. Dabei verarbeiten wir die Daten der Personen, die dort eine Unterkunft anfragen oder buchen („Gäste“), ausschließlich als Auftragsverarbeiter nach den Weisungen des jeweiligen Kunden, der dafür Verantwortlicher ist (Auftragsverarbeitungsvertrag Buchungskalender). Wir verwenden diese Daten nicht für eigene Zwecke. Verantwortliche sind wir für die Betriebsdaten des Kunden im Kundenportal und für seine Bestellung der Funktion. Diese Datenschutzerklärung gilt für unsere eigenen Seiten und das Kundenportal. Wer auf der Website eines Kunden anfragt oder bucht, findet die Informationen dazu (Zwecke, Rechtsgrundlagen, Speicherdauer, Rechte) in der Datenschutzerklärung dieser Website. Anfragen von Gästen zu ihren Daten leiten wir an den Kunden weiter.
|
||||
|
||||
Für den Betrieb des Buchungskalenders speichern wir im Auftrag des Kunden: bei einer Anfrage Name, E-Mail-Adresse, auf Wunsch Firma, Telefonnummer und Nachricht, die Unterkunft, den Zeitraum und die Zahl der Personen sowie bis zur Entscheidung des Kunden einen Hashwert der IP-Adresse (siehe unten); nach der Freigabe die Posten und den Preis des Aufenthalts, die Stornobedingungen und die Fassung der Rechtstexte, die galten; bei der Buchung die Rechnungsadresse, die Zahlart und die Rechnung (und bei einer Stornierung die Stornorechnung) mit lückenloser Rechnungsnummer, in der der Kunde als Aussteller genannt ist; den Stand der Buchung (zum Beispiel gebucht, bezahlt, storniert) mit den Zeitpunkten, bei einer Online-Zahlung die Kennungen der Zahlung bei Stripe und erstattete Beträge, auch automatische Erstattungen (siehe unten). Zahlungsdaten wie die Kartennummer gibt der Gast nur auf der Zahlungsseite von Stripe ein; wir erhalten sie nicht (Online-Zahlung derzeit nur per Karte, auch mit Apple Pay und Google Pay). Der Kunde sieht und bearbeitet Anfragen, Buchungen und Rechnungen im Kundenportal. Im Namen des Kunden stellen wir die Rechnungen aus (Rechnung im Namen und im Auftrag des Kunden, § 14 Abs. 2 Satz 4 UStG) und senden E-Mails an Gäste (Eingang der Anfrage, Freigabe mit Link zur Buchung, Ablehnung, Buchungsbestätigung mit Rechnung, Zahlungseingang, Ablauf einer Reservierung, Stornierung) und an den Kunden (neue Anfrage, neue Buchung, Hinweis bei fehlgeschlagenem Kalenderabgleich) über den E-Mail-Dienst der Hetzner Online GmbH (Rechenzentrum Nürnberg, Abschnitt 8). Als Absender erscheint der Kunde, Antworten gehen an ihn. Bis zum Versand liegt jede E-Mail in einer Warteschlange; nach dem Versand löschen wir dort Empfängeradresse und Inhalt, den Eintrag selbst nach sieben Tagen; E-Mails, die nicht zugestellt werden konnten, löschen wir nach 48 Stunden. Der Link zur Buchungsseite enthält ein geheimes Kennzeichen, das wir nur als Prüfsumme speichern; bei einer Online-Zahlung übergeben wir den Link mit dem Kennzeichen als Rücksprungadresse an Stripe, sodass er für Stripe und im Stripe-Dashboard des Kunden sichtbar ist (er gilt bis 90 Tage nach der Abreise, bei nicht zustande gekommenen Buchungen 30 Tage, und öffnet nur diese eine Buchungsseite). Das Widget setzt keine Cookies. Der Kunde kann Kalender anderer Plattformen einbinden und die geheime Kalenderadresse des Buchungskalenders bei ihnen eintragen; wir lesen aus fremden Kalendern nur belegte Zeiträume, nicht Angaben zu Gästen, und die Adresse des Buchungskalenders enthält nur belegte Nächte. Zum Schutz vor Missbrauch begrenzen wir die Zahl der Anfragen je IP-Adresse; die Zeitpunkte der Aufrufe merken wir uns nur im Arbeitsspeicher des Servers, nicht dauerhaft. Mit der Anfrage speichern wir nicht die IP-Adresse, sondern einen Hashwert davon, der mit einem geheimen Schlüssel unseres Servers berechnet wird (Pseudonym); ohne den Schlüssel lässt sich daraus keine IP-Adresse ableiten. Wir verwenden ihn nur für den Missbrauchsschutz und entfernen ihn, sobald der Kunde die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen. Im Übrigen gelten die Protokolle in Abschnitt 3.
|
||||
|
||||
Online-Zahlungen im Buchungskalender laufen über Stripe Connect unmittelbar auf das eigene Stripe-Konto des Kunden; wir erhalten und verwahren kein Geld der Gäste und beteiligen uns nicht am Umsatz. Beim Start einer Zahlung übergeben wir Stripe die Bezeichnung des Aufenthalts mit Rechnungsnummer, den Betrag, die Kennungen der Buchung, die E-Mail-Adresse des Gastes zur Vorbelegung und den Rücksprung-Link zur Buchungsseite. Wird ein Aufenthalt storniert, schließen wir eine offene Zahlungsseite. Geht eine Zahlung nach einer Stornierung oder dem Ablauf der Reservierung ein oder zahlt der Gast eine bezahlte Buchung ein zweites Mal, erstatten wir den Betrag im Namen des Kunden und nach seiner dauerhaften Weisung automatisch in voller Höhe über sein Stripe-Konto; der Stand der Buchung ändert sich dadurch nicht. Um dieses Konto zu eröffnen, übermitteln wir, sobald der Kunde im Kundenportal die Verbindung mit Stripe auslöst, seine E-Mail-Adresse, die Adresse seiner Website und den Namen seines Projekts an die Stripe Payments Europe, Limited, 1 Grand Canal Street Lower, Grand Canal Dock, Dublin, D02 H210, Irland („Stripe“), zusammen mit internen Kennzeichen, mit denen wir das Konto seinem Projekt zuordnen. Die weiteren Angaben für das Konto gibt der Kunde direkt bei Stripe ein. Wir speichern die Kennung des Stripe-Kontos und seinen Stand (ob Zahlungen und Auszahlungen möglich sind), den uns Stripe meldet, und zeigen ihn dem Kunden im Kundenportal an. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag mit dem Kunden). Stripe verarbeitet die Daten für sein Konto als eigener Verantwortlicher; dabei können Daten an die Stripe, LLC in den USA übermittelt werden, die nach dem EU-US Data Privacy Framework zertifiziert ist (Abschnitt 7). Es gelten die Datenschutzhinweise von Stripe: stripe.com/de/privacy.
|
||||
|
||||
Löschfristen: Den IP-Hash entfernen wir, sobald der Kunde eine Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen. Anfragen, aus denen keine Buchung wurde, anonymisieren wir nach sechs Monaten. Bei gebuchten Aufenthalten anonymisieren wir Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht drei Jahre nach dem Ende des Jahres der Abreise; die Rechnungen und die Rechnungsadresse bewahren wir für den Kunden acht Jahre auf (Buchungsbelege, § 147 AO), solange der Buchungskalender läuft. Schaltet der Kunde den Buchungskalender ab oder endet der Vertrag über den Betrieb seiner Website, schalten wir den Buchungskalender ab und weisen den Kunden danach per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen herunterzuladen. Der Kunde kann Anfragen, Buchungen und Rechnungen danach 90 Tage lang im Kundenportal lesen; danach löschen wir alle Daten des Buchungskalenders (Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen). Wird das Kundenkonto gelöscht, schalten wir den Buchungskalender ab und löschen seine Daten 90 Tage danach; einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird. Beginnt innerhalb dieser Frist wieder ein Vertrag über den Betrieb der Website, schalten wir den Buchungskalender auf Wunsch des Kunden mit den vorhandenen Daten wieder ein. Eine Ausnahme ist das Einrichtungs-Ticket, in dem wir die Einrichtung als Arbeitsnachweis festhalten (Abschnitt 6): Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen. Löschen wir Daten oder anonymisieren wir sie, bleiben sie in unseren Datensicherungen noch bis zu sieben Tage erhalten und werden dann überschrieben. Die Daten liegen auf unserem Server bei Hetzner im Rechenzentrum Falkenstein (Deutschland), wie die übrigen Portal-Daten (Abschnitt 3). Die Bestellung der Funktion und ihren Nachweis beschreibt Abschnitt 7.
|
||||
|
||||
## Hinweise für die Übernahme
|
||||
|
||||
- Online-Zahlung gibt es derzeit nur per Karte (kein SEPA); bei einer Erweiterung die Texte (6f, AVV Nr. 3 und 7, LB Nr. 3, Absatz) mitändern.
|
||||
- Die Aufbewahrung der Rechnungen für 8 Jahre durch WEBklar ist ein Dienst für den Kunden und endet mit der Löschung 90 Tage nach dem Abschalten (Frage U6 im Kanzlei-Paket).
|
||||
- Abschnitt 12 „Empfänger im Überblick“ der Datenschutzerklärung: Stripe-Zeile um „Buchungskalender“ ergänzen.
|
||||
- Abschnitt 7 (Bestellung der Funktion): Der Satz zu Zustimmungsnachweisen gilt für den Buchungskalender entsprechend; prüfen, ob die Aufzählung der Funktionen dort ergänzt werden muss.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Fragen an die Kanzlei: Buchungskalender
|
||||
|
||||
Stand 2026-10-07. Gegenstand der Prüfung: Leistungsbeschreibung Buchungskalender und AVV Buchungskalender (Fassung 2026-10-07, beide als „Entwurf – Prüfung durch Kanzlei offen“ gekennzeichnet), Abschnitt 6f der Datenschutzerklärung (`unterkunft-datenschutz-6f-entwurf.md`), Muster-Datenschutz-Absatz für die Website des Vermieters (`docs/funktionen/unterkunft-datenschutz-absatz.md`), Muster-Stornobedingungen (`docs/funktionen/unterkunft-storno-muster.md`), die Zustimmungen der Bestellung (Wortlaut in `server/config.js`, Register `unterkunft`) und die Buchungsseite. Der Buchungskalender ist für Ferienwohnungen, Monteurzimmer und kleine Pensionen gedacht; erste Kundin ist eine Vermieterin von Monteurunterkünften (Unternehmer als Gäste, Verbraucher möglich). Die Arbeitsweise der Software steht im Entwurf der Spezifikation `docs/superpowers/specs/2026-10-07-buchungskalender-design.md`; Muster ist das Kanzlei-Paket zum Webshop (`webshop-kanzlei-paket.md`).
|
||||
|
||||
## Fragen
|
||||
|
||||
**U1. Button-Lösung über Mail-Link (§ 312j Abs. 3 BGB).** Die Anfrage des Gastes ist unverbindlich (Schaltfläche „Anfrage senden“). Der Vertrag entsteht erst, wenn der Gast nach der Freigabe durch den Vermieter über den Link in der E-Mail die Buchungsseite öffnet und „Zahlungspflichtig buchen“ klickt; unmittelbar darüber stehen alle Pflichtangaben. Genügt das für die Button-Lösung, auch wenn der Vermieter den Preis vor der Freigabe ändern kann und der Gast ihn erst auf der Buchungsseite verbindlich sieht? Ist die Beschriftung „Zahlungspflichtig buchen“ auch bei der Zahlart „vor Ort“ (keine Zahlung im Moment der Buchung) zulässig, oder braucht es eine andere Beschriftung („Verbindlich buchen“)? Muss die Anfrage-Schaltfläche anders heißen, damit niemand sie für verbindlich hält?
|
||||
|
||||
**U2. Pflichtinformationen (Art. 246a § 1 EGBGB, § 312d BGB) und Rolle des Vermieters.** Welche Angaben muss die Buchungsseite zeigen, welche stehen in der Buchungsbestätigung (dauerhafter Datenträger, § 312f BGB), und welche muss der Vermieter auf seiner Website selbst vorhalten (Impressum, Informationen zum Anbieter, Verbraucherstreitbeilegung nach § 36 VSBG)? Reicht die Buchungsseite (Anbieter, Unterkunft, Zeitraum, Leistung, Gesamtpreis, Zahlart, Stornobedingungen, Hinweis zum Widerrufsrecht), und was fehlt für Beherbergung typischerweise (zum Beispiel Preisangabenverordnung, Pflichtangaben zur Unterkunft, Hausordnung)? Welche Pflichten treffen WEBklar als Betreiber der Seite im Namen des Vermieters? Insbesondere: Die Bestätigungsmail (G19) nennt den Anbieter mit Name und Anschrift aus den Einstellungen, aber ohne Impressum-Link und ohne Steuernummer; muss der Anbieter dort vollständig mit Impressum und Steuernummer genannt werden?
|
||||
|
||||
**U3. Kein Widerrufsrecht (§ 312g Abs. 2 Satz 1 Nr. 9 BGB).** Wir weisen auf der Buchungsseite darauf hin, dass für die Buchung zu einem bestimmten Zeitraum kein Widerrufsrecht besteht. Gilt die Ausnahme auch für Monteurunterkünfte, die langfristig oder für Unternehmer und Verbraucher gemischt vermietet werden, und für Aufenthalte, deren Zeitraum über Wochen reicht? Insbesondere: Ist ein langer Monteursaufenthalt (Wochen bis Monate, eher Wohnzweck als Beherbergung) noch „Beherbergung zu Freizeitzwecken“, oder entfällt die Ausnahme, und was bedeutet das für die Belehrung? Ist die Formulierung („Ein Widerrufsrecht besteht nicht (§ 312g Abs. 2 Satz 1 Nr. 9 BGB).“) ausreichend, und wie ist die Belehrungspflicht zu verstehen, wenn eine Ausnahme vorliegt?
|
||||
|
||||
**U4. Umsatzsteuer der Endreinigung.** Die Software kennt je Vermieter einen Steuersatz für die Übernachtung (Standard 7 %) und einen für die Endreinigung (Standard 7 %) und rechnet die Steuer aus den Bruttobeträgen heraus. Ist es zulässig, die Endreinigung als unselbständige Nebenleistung der Beherbergung mit demselben Satz zu behandeln (§ 12 Abs. 2 Nr. 11 UStG), oder wäre 19 % der sichere Standard? Wie ist es bei Kleinunternehmern (§ 19 UStG, Hinweistext auf der Rechnung) und bei langfristigen Vermietungen, bei denen die Steuerbefreiung nach § 4 Nr. 12 UStG greifen kann? Wie weit darf WEBklar Voreinstellungen vorgeben, ohne dass daraus eine Steuerberatung wird?
|
||||
|
||||
**U5. Rechnung im Namen des Vermieters (§ 14 Abs. 2 Satz 4 UStG).** WEBklar stellt die Rechnungen in Namen und Auftrag des Vermieters aus; er stimmt bei der Bestellung und mit einem Haken in den Einstellungen zu. Genügt das als Zustimmung (Textform?) und ist die Abrechnung durch einen Dritten damit sauber geregelt, auch für die Stornorechnung (Rechnungskorrektur mit eigener Nummer) und die lückenlose Nummerierung je Vermieter und Jahr? Gilt das Konstrukt auch für Rechnungen an Verbraucher (dann ist die Rechnung keine Pflichtrechnung, sondern Beleg) und reichen die Pflichtangaben nach § 14 Abs. 4 UStG, wenn der Vermieter unvollständige Stammdaten hinterlegt? Wer haftet für falsche Steuerangaben auf der Rechnung (§ 14c UStG)?
|
||||
|
||||
**U6. Aufbewahrung durch WEBklar (8 Jahre).** WEBklar speichert die Rechnungen und die Rechnungsadresse der Gäste 8 Jahre lang, solange der Buchungskalender läuft, und löscht sie 90 Tage nach dem Abschalten; der Vermieter wird vorher aufgefordert, sie herunterzuladen. Reicht dieser Dienst, ohne dass WEBklar selbst aufbewahrungspflichtig oder zum Buchführungssystem wird (§ 147 AO, GoBD), und wie ist die Spannung zwischen der 8-Jahres-Frist und der Löschung nach 90 Tagen zu regeln? Die Gästedaten ohne Rechnungsbezug werden 3 Jahre nach dem Ende des Abreisejahres anonymisiert und Anfragen ohne Buchung nach 6 Monaten: Sind die Fristen angemessen (Art. 5 Abs. 1 lit. e DSGVO)?
|
||||
|
||||
**U7. Rolle im Datenschutz, AVV Buchungskalender.** Der Vermieter ist Verantwortlicher, WEBklar Auftragsverarbeiter (Muster AVV Webshop). Ist die Rollenverteilung für die Rechnungserstellung und die Aufbewahrung richtig (kein eigener Zweck von WEBklar)? Ist die Behandlung von Stripe (Vertragspartner des Vermieters, kein Unterauftragsverarbeiter) und der importierten Kalender anderer Plattformen (nur Zeiträume, keine Gästedaten) vertretbar? Genügt die Bezeichnung des IP-Hash als pseudonymes Datum mit Löschung bei der Entscheidung? Der geheime Buchungslink (Token, nur als Prüfsumme gespeichert) gilt bis 90 Tage nach der Abreise, bei abgelehnten oder abgelaufenen Anfragen 30 Tage nach der Entscheidung; er ist für Stripe und im Stripe-Dashboard des Vermieters sichtbar (Rücksprungadresse). Ist die Frist angemessen? Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht die Daten 90 Tage später ohne E-Mail-Hinweis (die Adresse ist dann gelöscht): ausreichend?
|
||||
|
||||
**U8. Haftung bei Doppelbuchung durch iCal-Verzögerung.** Der Abgleich mit Booking.com, Airbnb und ähnlichen Plattformen geschieht über iCal mit Verzögerung von Minuten bis Stunden; jede Anfrage braucht die Freigabe, und beim Freigeben und Buchen prüft die Software erneut. Eine Doppelbuchung ist trotzdem möglich. Ist der Haftungsausschluss in Nr. 4 und Nr. 11 der Leistungsbeschreibung (keine Echtzeit-Verfügbarkeit, kein Ersatz von Folgeschäden außer im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen) wirksam, auch gegenüber Verbrauchern? Die Leistungsbeschreibung richtet sich nur an Vermieter, die Unternehmer sind; für deren Gäste gilt das nicht. Ist eine hervorgehobene Warnung im Portal sinnvoll oder nötig?
|
||||
|
||||
**U9. Zustimmungen der Bestellung.** Genügen die sechs Haken (Unterlagen, Verantwortung, Rechnung im Auftrag, Stripe, Beginn der Leistung und Erlöschen des Widerrufsrechts, Pflichtinformationen) und die Widerrufsbelehrung der Funktionen, wenn der Vermieter in der Praxis Unternehmer ist? Muss der Haken zu Stripe entfallen, wenn der Vermieter keine Online-Zahlung nutzt?
|
||||
|
||||
**U10. Kein Meldeschein, keine Kurtaxe, keine Anzahlung.** Die Leistungsbeschreibung nimmt Meldeschein, Kurtaxe und Anzahlungen aus dem Umfang und weist die Pflichten dem Vermieter zu (Nr. 2 Abs. 7, Nr. 5 Abs. 6). Ist das ausreichend deutlich, und wie ist eine Haftung von WEBklar wegen fehlender Hinweise ausgeschlossen?
|
||||
|
||||
**U11. Muster-Stornobedingungen.** Der Text `unterkunft-storno-muster.md` ist eine Vorlage für Vermieter (Staffel nach Zeit bis zur Anreise, Erstattungen, Verhältnis zu § 537 BGB und § 308 Nr. 7 BGB bei Verbrauchern, Pauschalen nach § 309 Nr. 5 BGB). Ist er als Vorlage mit Hinweis „keine Rechtsberatung“ vertretbar? Welche Pauschalen sind üblich und zulässig?
|
||||
|
||||
**U12. Datenschutz-Absatz für die Vermieter-Website.** Ist der Muster-Absatz `unterkunft-datenschutz-absatz.md` (Verantwortlicher, Rechtsgrundlagen Art. 6 Abs. 1 lit. b und f, Stripe, E-Mails, Speicherdauer) und der Abschnitt 6f der WEBklar-Erklärung in sich schlüssig und vollständig?
|
||||
|
||||
## Prüfpunkte vor dem Livegang
|
||||
|
||||
- Fassung der Rechtstexte: Ändert die Kanzlei etwas, gibt es eine neue Fassung (Datum, `config.unterkunft.fassung`, Zustimmungs-Pin in `test/featureConsentTexte.test.js`); ausgelieferte Fassungen werden nie überschrieben.
|
||||
- Danach Upload in WOMS „Rechtliches“ und Schalter `UNTERKUNFT_ENABLED` von `pilot` auf `on` (Livegang-Schritt 10).
|
||||
@@ -0,0 +1,3 @@
|
||||
# Fragen an die Kanzlei: Webshop
|
||||
|
||||
Die Arbeitsliste der Fragen F1 bis F18 ist in das Kanzlei-Paket übernommen und dort um F19 bis F29 ergänzt worden. Maßgeblich ist jetzt: **`docs/rechtliches/webshop-kanzlei-paket.md`**, Abschnitt 4. Diese Datei bleibt nur als Verweis bestehen.
|
||||
@@ -0,0 +1,394 @@
|
||||
# Kanzlei-Paket Webshop (WEBklar)
|
||||
|
||||
Dieses Paket gehört zum Verkaufsweg des Webshops der WEBklar GbR (Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein). Es zeigt einer Kanzlei, was verkauft wird, wie der Ablauf ist, welche Texte Händler und Käufer in Endfassung sehen, wo diese Texte im Programm stehen und was wir wissen möchten. Es ist wie das Paket des Terminplaners (`docs/rechtliches/termine-kanzlei-paket.md`) aufgebaut; Fragen, die dort schon gestellt sind, nennen wir mit Verweis.
|
||||
|
||||
Fassungen: Leistungsbeschreibung Webshop, Auftragsverarbeitungsvertrag Webshop (AVV) und die Bestelltexte tragen die Fassung **2026-10-05** (Konstante `config.shop.fassung`). Die Widerrufsbelehrung mit Muster-Widerrufsformular ist die gemeinsame der Funktionen (`docs/rechtliches/widerrufsbelehrung-funktionen.md`, Fassung des Terminplaners). Alle Texte sind noch nie ausgeliefert worden; der Webshop bleibt bis zu Ihrem Befund hinter dem Schalter `SHOP_ENABLED` (nur Pilot).
|
||||
|
||||
## 1. Zweck und Umfang
|
||||
|
||||
**Zweck.** Der Inhaber lässt die Texte des Webshops durch eine Kanzlei prüfen, bevor der erste fremde Betrieb ihn nutzt (Schalter `SHOP_ENABLED=on`). Das Paket soll ohne Rückfragen zeigen, was die Kanzlei prüfen soll und von welchen Annahmen wir ausgehen.
|
||||
|
||||
**Was verkauft wird.** WEBklar verkauft nicht Waren, sondern ein Hilfsmittel: ein Shop-Widget für die Website eines Händlers samt Verwaltung im Kundenportal, Einrichtung einmalig 299 €, ohne Umsatzsteuer gemäß § 19 UStG, keine laufende Gebühr, keine Umsatzbeteiligung. Die Käufer bezahlen über Stripe Connect direkt an den Händler (eigenes Stripe-Konto, Direct Charges); WEBklar erhält und verwahrt kein Geld. Vertragspartner der Käufer ist der Händler. Der Webshop läuft, solange WEBklar die Website des Händlers betreibt; er ist nicht an Abo-Daten im Programm gekoppelt (Entscheidung W5, Abschnitt 2.5).
|
||||
|
||||
**Kernpaket.** Wir bitten die Kanzlei, zu prüfen:
|
||||
|
||||
- Leistungsbeschreibung Webshop (Anlage A) und AVV Webshop (Anlage B), vor allem Haftung, Ende mit dem Vertrag über den Betrieb der Website, ausgeschlossene Waren und Rolle bei Zahlungen (F1 bis F18),
|
||||
- die Bestelltexte der Einrichtung: fünf Haken und Knopf (Abschnitt 3.1, F26),
|
||||
- die Muster-Rechtstexte für Händler (Anlage C: AGB, Widerrufsbelehrung inklusive § 356a BGB, Muster-Widerrufsformular, Versand und Zahlung; F25),
|
||||
- die Kasse im Widget: Knopf, Pflichtinformationen, Widerrufsfunktion und Gewährleistungshinweis (Abschnitte 3.3 und 3.4, F19, F20, F24),
|
||||
- die Pflichtangaben am Artikel: Grundpreis, Lebensmittel, Produktsicherheit (F21 bis F23).
|
||||
|
||||
**Hausrunde.** Die Datenschutzerklärung von WEBklar (Abschnitt 6d), den Muster-Absatz für die Datenschutzerklärung des Händlers (Anlage D), die Mailtexte und die Hinweise im Kundenportal prüft WEBklar selbst. Zu einzelnen Punkten daraus stellen wir trotzdem Fragen (F27, F28).
|
||||
|
||||
**Begriffe.** *Händler* ist der Kunde von WEBklar, der den Webshop betreibt; *Käufer* sind die Besucher seiner Website, die bestellen. *Betriebsart* des Webshops ist `off` (abgeschaltet), `pilot` (nur Adressen einer Pilotliste) oder `on` (alle Kunden). Derzeit ist der Webshop nur für Konten des Inhabers freigeschaltet. `on` setzt der Inhaber erst nach Ihrem Befund und seiner Freigabe.
|
||||
|
||||
**Keine Rechtsberatung durch WEBklar.** Die Texte für Händler sind Muster ohne Gewähr (Leistungsbeschreibung Nr. 6). Die rechtlichen Aussagen in diesem Paket (vor allem „unsere Annahme“ bei den Fragen) sind ein Arbeitsstand von WEBklar und kein Rechtsurteil.
|
||||
|
||||
**Fassungen und Änderungen.** Ändern wir nach dem Befund einen Text, bekommt er ein neues Fassungsdatum und neue Dateien (HTML, PDF und Prüfsumme); die Zitate in diesem Paket werden dann angepasst.
|
||||
|
||||
**Aufbau.** Abschnitt 2 beschreibt den Produktablauf, Abschnitt 3 enthält die Texte, Abschnitt 4 unsere Fragen F1 bis F29, Abschnitt 5 die Fundstellen im Programm, Abschnitt 6 was nicht Teil der Prüfung ist, Abschnitt 7 die Checkliste für den Inhaber vor `SHOP_ENABLED=on`. Anlagen: **Anlage A** Leistungsbeschreibung Webshop, **Anlage B** AVV Webshop, **Anlage C** Muster-Rechtstexte der Händler, **Anlage D** Muster-Datenschutzabsatz für die Händler-Website (Pfade in Abschnitt 3.5).
|
||||
|
||||
## 2. Produktablauf in Kurzform
|
||||
|
||||
### 2.1 Anfrage, Freigabe und Bestellung der Einrichtung
|
||||
|
||||
| Schritt | Was geschieht |
|
||||
|---|---|
|
||||
| 1. Anfrage | Der Händler fragt den Webshop im Kundenportal für eine seiner Websites an: was er verkauft (Freitext und Warenart), ungefähre Artikelzahl, Versand und/oder Abholung, zwei Pflicht-Haken (er ist Unternehmer; er verkauft keine der ausgeschlossenen Waren), Nachricht an das Team. |
|
||||
| 2. Prüfung | Das Team prüft die Anfrage (erlaubte Waren, Eignung) und nimmt sie an oder lehnt sie mit Begründung ab. Der Händler erhält eine Mail mit der Entscheidung. Ein Anspruch auf Freigabe besteht nicht (LB Nr. 2 Abs. 1). |
|
||||
| 3. Bestellung | Nach der Freigabe bestellt der Händler die Einrichtung für 299 €: Zusammenfassung, volle Widerrufsbelehrung mit Muster-Widerrufsformular, fünf Haken, Knopf „Zahlungspflichtig bestellen · 299 €“ (Abschnitte 3.1 und 3.2). Eine Bindung an ein Abo besteht nicht. |
|
||||
| 4. Nachweis | Der Server legt zuerst die Zahlungssitzung bei Stripe an und speichert den Nachweis: gezeigte Texte, Fassungen, Hash, Zeitpunkt des Klicks, IP-Adresse und Browser-Kennzeichen, Kundenkonto und angemeldete Person. Der Zeitpunkt des Klicks ist der Anker der Widerrufsfrist. |
|
||||
| 5. Zahlung und Ticket | Stripe Checkout; danach legt das Portal ein Einrichtungs-Ticket mit der Einbauanleitung an, der Händler erhält die Bestellbestätigung mit den Unterlagen (Leistungsbeschreibung, AVV, Widerrufsbelehrung als PDF) in der Fassung, der er zugestimmt hat. |
|
||||
| 6. Einrichtung | Der Händler verbindet sein Stripe-Konto (Onboarding bei Stripe), legt Artikel, Versand, Zahlungsarten und Rechtstexte an und bestätigt die Rechtstexte (Abschnitt 3.2). WEBklar baut das Widget und den Link „Vertrag widerrufen“ in die Website ein (Anleitung `docs/funktionen/shop-einbau.md`), fügt den Datenschutz-Absatz ein (Anlage D), macht mit dem Händler einen Testkauf mit Erstattung und schließt das Ticket; das schaltet die Funktion frei. Schaltet ein Mitarbeiter die Funktion stattdessen ohne Zahlung ein (Betriebsart `on`, Kunde nicht in der Pilotliste), verlangt das Portal vorher die Zustimmung des Kunden in Textform (Datum, Weg) und speichert sie als Zustimmungsnachweis (`featureConsents`, mit Mitarbeiter, ohne IP-Adresse und Browser-Kennzeichen); der Kunde erhält danach Leistungsbeschreibung und AVV als PDF (AVV Vorbemerkung Abs. 2). |
|
||||
| 7. Öffnen | Der Händler öffnet den Shop (Schalter „Shop geöffnet“), sobald Einbau fertig, Stripe-Konto zahlungsfähig, Rechtstexte bestätigt, ein Artikel aktiv und Versand oder Abholung eingerichtet sind. Der Server prüft das bei jeder Bestellung erneut. |
|
||||
|
||||
### 2.2 Käufer: Bestellung
|
||||
|
||||
1. Der Käufer wählt Artikel (mit Grundpreis, Lebensmittel- und Produktsicherheitsangaben, Widerrufsausschluss), legt sie in den Warenkorb (im Sitzungsspeicher des Tabs, kein Cookie) und wählt Versand (nur Deutschland und Österreich) oder Abholung. Ganz oben im Widget stehen Lieferländer und Zahlungsarten.
|
||||
2. In der Kasse stehen unmittelbar über dem Knopf die Pflichtinformationen, der Gewährleistungshinweis (Abschnitt 3.4) und der Knopf „Zahlungspflichtig bestellen“ (Abschnitt 3.3). Der Server rechnet alle Preise neu, prüft und reserviert den Bestand (30 Minuten).
|
||||
3. Der Käufer wird zu Stripe Checkout geleitet und gibt dort Name, Adresse und Zahlung ein (Karte, Apple Pay, Google Pay, Klarna, SEPA-Lastschrift je nach Einstellung des Händlers; kein PayPal). Die Stripe-Kasse verlangt keine Telefonnummer (WEBklar erhebt und speichert auch keine) und fragt die Rechnungsanschrift nur ab, soweit die Zahlungsart sie braucht. SEPA gilt erst bei Eingang als bezahlt; bis dahin erhält der Käufer einmal eine Eingangsmail (G15) ohne Vertragsschluss.
|
||||
4. Nach der Zahlung erhält der Käufer die Bestellbestätigung (G09, Abschnitt 3.3), der Händler die Mail „Neue Bestellung“ (K25).
|
||||
5. Der Händler pflegt Status (versendet, abholbereit, abgeschlossen), erstattet über das Portal (Stripe-Rückzahlung auf dem Konto des Händlers) und exportiert Bestellungen als CSV.
|
||||
|
||||
### 2.3 Käufer: Widerruf (§ 356a BGB)
|
||||
|
||||
Der Käufer widerruft über „Vertrag widerrufen“ im Widget, auf der Standalone-Seite oder über den Link in der Fußzeile jeder Seite der Website: Schaltfläche, Formular (Name, E-Mail-Adresse, Bestellnummer „falls zur Hand“; ohne Bestellnummer ist die Erklärung Pflicht und soll Bestelldatum und Artikel nennen), Schaltfläche „Widerruf bestätigen“. Der Eingang wird gespeichert, der Käufer erhält sofort die Eingangsbestätigung mit Datum und Uhrzeit (G12), der Händler die Mail K26. Der Händler bearbeitet den Widerruf und die Erstattung selbst (LB Nr. 5 Abs. 6). Ohne Anmeldung, auch ohne passende Bestellung, mit Mengenbegrenzung gegen Missbrauch. ⚖ F20
|
||||
|
||||
### 2.4 Daten und Fristen
|
||||
|
||||
Käuferdaten liegen auf dem Server bei Hetzner (Falkenstein), E-Mails laufen über den E-Mail-Dienst von Hetzner (Nürnberg). Nicht bezahlte oder stornierte Bestellungen werden nach 30 Tagen gelöscht; bei bezahlten Bestellungen und Widerrufen werden Name, E-Mail-Adresse und Anschriften drei Jahre nach dem Ende des Bestelljahres anonymisiert (Positionen, Beträge, Bestellnummer und Status bleiben); der IP-Hash (Hashwert der IP-Adresse für den Missbrauchsschutz, nur in der offenen Bestellung) wird gelöscht, sobald die Bestellung bezahlt oder storniert wird, spätestens mit der Löschung nicht bezahlter Bestellungen nach 30 Tagen; eine Telefonnummer des Käufers wird nicht gespeichert. Nach dem Abschalten sind Bestellungen 90 Tage lesbar und als CSV exportierbar, danach löscht der tägliche Job alle Daten des Webshops (Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Archiv der bestätigten Rechtstexte); das Einrichtungs-Ticket bleibt ohne Käuferdaten. Gelöschte Daten bleiben bis zu sieben Tage in den Sicherungen. ⚖ F14
|
||||
|
||||
### 2.5 Ende mit dem Vertrag über den Betrieb der Website
|
||||
|
||||
Der Webshop läuft, solange WEBklar die Website des Händlers betreibt (Hosting-Abo, Wartungs-Abo oder ein anderer Vertrag über den Betrieb der Website; LB Nr. 10 Abs. 1). Das Programm prüft das **nicht**: Es liest keine Abo-Daten für den Webshop und schaltet nicht von selbst ab. Das Team schaltet zum Vertragsende von Hand ab (Ticketsystem, Kunde, Funktionen, Webshop, „Abschalten“) und schreibt dem Händler vorher eine E-Mail. Beginnt innerhalb der 90 Tage wieder ein solcher Vertrag, schaltet das Team auf Wunsch wieder ein; endet der Vertrag, bevor der Webshop freigeschaltet ist, erstattet WEBklar die 299 € vollständig (von Hand in Stripe). ⚖ F9, F29
|
||||
|
||||
### 2.6 Widerruf der Bestellung durch den Händler
|
||||
|
||||
Der Händler (auch ein Existenzgründer oder Verbraucher) widerruft auf der Seite „Vertrag widerrufen“ von WEBklar mit der Auswahl „Webshop (Einrichtung)“. Die Frist von 14 Tagen beginnt mit dem Klick auf „Zahlungspflichtig bestellen“. Das Team erhält die Mitteilung T09 mit Fristende, Preis und Projekt; es wird nichts automatisch geändert. Das Team prüft, erstattet in Stripe und schaltet den Webshop ab; mit dem Widerruf endet der Webshop (LB Nr. 9 Abs. 6). ⚖ F8
|
||||
|
||||
### 2.7 Zahlen
|
||||
|
||||
| Wert | Bedeutung | Quelle im Code |
|
||||
|---|---|---|
|
||||
| 299 € | Einrichtung je Website-Projekt, einmalig, ohne Umsatzsteuer gemäß § 19 UStG | `SHOP_PREIS` in `server/config.js` |
|
||||
| 14 Tage | Widerrufsfrist (Händler wie Käufer) | `WITHDRAWAL_DAYS` |
|
||||
| 30 Minuten | Haltezeit einer offenen Bestellung (Checkout-Sitzung) | `bestellAblaufMinuten` in `server/config.js`, `legeBestellungAn` |
|
||||
| 30 Tage | Löschung nicht bezahlter oder stornierter Bestellungen | `taeglicherJob` |
|
||||
| 3 Jahre nach Ende des Bestelljahres | Anonymisierung der Käuferdaten | `anonymisierenJahre` in `server/config.js` |
|
||||
| 90 Tage | Lesen und Export nach dem Abschalten, danach Löschung | `taeglicherJob` |
|
||||
| 7 Tage | Eintrag in der Versandwarteschlange nach dem Versand; Sicherungen | AVV Nr. 9 |
|
||||
|
||||
## 3. Texte in Endfassung
|
||||
|
||||
Alle Zitate stehen wörtlich so im Programm oder in den Dokumenten (Abschnitt 5 nennt die Fundstellen).
|
||||
|
||||
### 3.1 Bestellung der Einrichtung: fünf Haken und Knopf
|
||||
|
||||
Auf der Bestellseite stehen vor dem Knopf die volle Widerrufsbelehrung mit Muster-Widerrufsformular und diese fünf Haken, keiner ist vorangekreuzt. Die Links auf Leistungsbeschreibung, AVV und Widerrufsbelehrung in der Fassung stehen daneben, nicht im Text der Haken. Gespeichert wird genau der gezeigte Text mit Fassung und Zeitpunkt. ⚖ F26
|
||||
|
||||
**Haken 1 (`unterlagen`)**
|
||||
|
||||
> Ich stimme der Leistungsbeschreibung Webshop und dem Auftragsverarbeitungsvertrag Webshop in der Fassung 2026-10-05 zu.
|
||||
|
||||
**Haken 2 (`verantwortung`)**
|
||||
|
||||
> Ich verkaufe als Unternehmer in eigenem Namen. Für meine Produkte, Preise, Produktangaben und Rechtstexte (AGB, Widerrufsbelehrung, Impressum, Datenschutzerklärung) bin ich selbst verantwortlich; WEBklar stellt Muster bereit und berät nicht rechtlich. Ich verkaufe keine der in der Leistungsbeschreibung ausgeschlossenen Waren.
|
||||
|
||||
**Haken 3 (`stripe`)**
|
||||
|
||||
> Die Zahlungen meiner Käufer laufen über mein eigenes Stripe-Konto zu den Bedingungen von Stripe. WEBklar erhält und verwahrt kein Geld meiner Käufer und erhebt keine Umsatzbeteiligung.
|
||||
|
||||
**Haken 4 (`beginn`)**
|
||||
|
||||
> Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit der Einrichtung des Webshops beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Einrichtung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).
|
||||
|
||||
**Haken 5 (`information`)**
|
||||
|
||||
> Ich habe die Pflichtinformationen zu dieser Bestellung gelesen: Leistung (Einrichtung des Webshops für diese Website: Einbau des Shop-Widgets, Verwaltung im Portal, Muster-Rechtstexte), Gesamtpreis 299 € einmalig (ohne Umsatzsteuer gemäß § 19 UStG), voraussichtliche Dauer meist innerhalb von 2–3 Wochen, keine laufende Gebühr von WEBklar; der Webshop läuft, solange WEBklar die Website betreibt. Die Widerrufsbelehrung mit Muster-Widerrufsformular habe ich gelesen.
|
||||
|
||||
**Knopf**
|
||||
|
||||
> Zahlungspflichtig bestellen · 299 €
|
||||
|
||||
### 3.2 Bestätigung der Rechtstexte durch den Händler
|
||||
|
||||
Bevor der Händler den Shop öffnet, bestätigt er im Portal (Reiter Rechtliches) die Muster oder seine geänderten Texte mit diesem Haken. Zeit, Person und Hash der Texte werden gespeichert und die bestätigte Fassung wird zusätzlich im Archiv der Rechtstexte abgelegt (⚖ F16); jede Änderung der Texte hebt die Bestätigung auf. Neue Bestellungen werden mit der zuletzt bestätigten Fassung geschlossen. ⚖ F25
|
||||
|
||||
> Ich habe die Texte geprüft. Ich bin als Verkäufer für Rechtstexte, Produktangaben und Preise verantwortlich. WEBklar stellt Muster bereit und berät nicht rechtlich.
|
||||
|
||||
### 3.3 Kasse im Widget und Bestellbestätigung
|
||||
|
||||
Über dem Knopf zeigt das Widget (Quelle `public/embed/shop.js`, Zusammenfassung vom Server) unter den Überschriften „Ihre Bestellung“ (Positionen mit Einzel- und Summenpreis, Zwischensumme, Versandkosten oder Lieferart, Gesamtsumme, Steuerhinweis), „Lieferung und Zahlung“ (Lieferart, Lieferzeit, Zahlungsarten, Anbieter) und „Widerrufsrecht“, die Links „AGB lesen“, „Widerrufsbelehrung lesen“, „Versand & Zahlung lesen“, den Gewährleistungshinweis und den Satz „Name, Lieferadresse und Zahlung geben Sie im nächsten Schritt bei unserem Zahlungsdienstleister Stripe ein.“ Der Knopf heißt „Zahlungspflichtig bestellen“. Das Widerrufsrecht steht dort so:
|
||||
|
||||
> Sie haben ein gesetzliches Widerrufsrecht, soweit unten für einzelne Artikel nichts anderes steht. Einzelheiten finden Sie in der Widerrufsbelehrung.
|
||||
|
||||
Die Widerrufsfunktion führt mit diesen Texten:
|
||||
|
||||
> Hier können Sie Ihren Vertrag widerrufen. Bitte geben Sie Ihren Namen, die E-Mail-Adresse Ihrer Bestellung und möglichst die Bestellnummer an. Sie steht in Ihrer Bestätigungs-E-Mail.
|
||||
|
||||
> Bitte prüfen Sie Ihre Angaben. Mit „Widerruf bestätigen“ erklären Sie den Widerruf Ihres Vertrags.
|
||||
|
||||
Die Bestellbestätigung G09 (`server/mail-templates/G09 Bestellbestaetigung.html`) und die Eingangsbestätigung G12 (`G12 Eingang Widerruf.html`) sind (zusammen mit der Eingangsmail G15 bei ausstehender Zahlung) Anlage E in der gerenderten Fassung nach der Mail-Überarbeitung (Abschnitt 7.4). ⚖ F19, F20, F27
|
||||
|
||||
### 3.4 Hinweis auf die gesetzliche Gewährleistung
|
||||
|
||||
Der Hinweis steht hervorgehoben über dem Knopf (als Grafik `public/img/gewaehrleistung-eu.webp`, Wortlaut als Alternativtext) und in der Bestellbestätigung. Wortlaut (`gewaehrleistungsHinweis()`):
|
||||
|
||||
> GESETZLICHE GEWÄHRLEISTUNG
|
||||
>
|
||||
> Mindestens zwei Jahre gesetzliche Gewährleistung der Vertragsmäßigkeit für Waren, die in der Europäischen Union verkauft werden.
|
||||
>
|
||||
> Verbraucherinnen und Verbraucher können ihre Rechte im Rahmen des gesetzlichen Gewährleistungsrechts geltend machen, z. B. wenn die Waren nicht der Beschreibung entsprechen, nicht bestimmungsgemäß funktionieren.
|
||||
>
|
||||
> Verkäufer haften für jede Vertragswidrigkeit, die zum Zeitpunkt der Lieferung der Waren bestand und innerhalb des Zeitraums der gesetzlichen Gewährleistung erkennbar wird. Verkäufer müssen in solchen Fällen Folgendes anbieten: kostenlose Nachbesserung oder kostenlose Ersatzlieferung, in bestimmten Fällen eine Preisminderung oder eine vollständige Erstattung des Kaufpreises.
|
||||
>
|
||||
> In einigen Ländern gilt ein längerer Zeitraum für die gesetzliche Gewährleistung. Für gebrauchte Waren kann ein kürzerer Zeitraum gelten, jedoch nicht weniger als ein Jahr.
|
||||
>
|
||||
> Für weitere Informationen zu Ihren Rechten in einem bestimmten Land scannen Sie den nachstehenden QR-Code oder fragen Sie den Verkäufer. europa.eu/youreurope/garantien
|
||||
>
|
||||
> Was ist zu tun, wenn Sie vertragswidrige Waren erhalten? 1. Melden Sie dem Verkäufer das Problem so bald wie möglich. 2. Legen Sie einen Kaufnachweis vor, z. B. die Quittung, Rechnung oder einen Kontoauszug.
|
||||
>
|
||||
> Verkäufer und Hersteller können auch gewerbliche Garantien gewähren, die unabhängig von der gesetzlichen Gewährleistung gelten. Diese GARAN-Kennzeichnung zeigt beispielsweise, dass der Hersteller eine gewerbliche Haltbarkeitsgarantie ohne zusätzliche Kosten gewährt, die die gesamte Ware abdeckt.
|
||||
|
||||
⚖ F24
|
||||
|
||||
### 3.5 Dokumente und Fundstellen der Texte
|
||||
|
||||
| Anlage | Dokument | Pfad |
|
||||
|---|---|---|
|
||||
| A | Leistungsbeschreibung Webshop (Markdown, HTML, PDF) | `docs/rechtliches/leistungsbeschreibung-webshop.md`, `public/rechtliches/leistungsbeschreibung-webshop-2026-10-05.html` und `.pdf` |
|
||||
| B | Auftragsverarbeitungsvertrag Webshop (Markdown, HTML, PDF) | `docs/rechtliches/avv-webshop.md`, `public/rechtliches/avv-webshop-2026-10-05.html` und `.pdf` |
|
||||
| C | Muster-Rechtstexte der Händler (Belehrung, Formular, AGB, Versand und Zahlung, Gewährleistungshinweis) | `docs/funktionen/shop-muster-rechtstexte.md` (Quelltext der Muster: `server/services/shop/rechtstexte.js`) |
|
||||
| D | Muster-Datenschutzabsatz für die Website des Händlers | `docs/funktionen/shop-datenschutz-absatz.md` |
|
||||
| E | Gerenderte Mails G09 bis G15 und K24 bis K26 (nach der Mail-Überarbeitung nachgereicht) | `server/mail-templates/` |
|
||||
| – | Gemeinsame Widerrufsbelehrung der Funktionen mit Muster-Widerrufsformular | `docs/rechtliches/widerrufsbelehrung-funktionen.md` |
|
||||
| – | Bestelltexte (fünf Haken, Knopf) | `server/config.js`, `shopConsent` |
|
||||
| – | Datenschutzerklärung von WEBklar, Abschnitt 6d (Hausrunde) | `public/datenschutz.html`, `id="webshop"` |
|
||||
| – | Einbauanleitung für das Team, inklusive Abschalten von Hand | `docs/funktionen/shop-einbau.md` |
|
||||
| – | Hinweis auf die gesetzliche Gewährleistung (Grafik und Text) | `public/img/gewaehrleistung-eu.webp`, `server/services/shop/rechtstexte.js`, `gewaehrleistungsHinweis` |
|
||||
| – | Widerruf der Bestellung durch den Händler | `public/widerrufen.html`, `server/routes/abo.js` |
|
||||
|
||||
## 4. Fragen an die Kanzlei
|
||||
|
||||
Die Markierung ⚖ steht nur hier und in den Abschnittsverweisen, nie in den veröffentlichten Texten. „Unsere Annahme“ ist ein Arbeitsstand von WEBklar und kein Rechtsurteil. Kürzel: **LB** = Leistungsbeschreibung Webshop, **AVV** = Auftragsverarbeitungsvertrag Webshop, **AB** = Abo-Bedingungen. Die Fragen F1 bis F18 betreffen Anlagen A und B, F19 bis F29 den Ablauf im Widget und die übrigen Texte.
|
||||
|
||||
## Leistungsbeschreibung (Anlage A)
|
||||
|
||||
### F1: LB Nr. 1 Abs. 4: Webshop im Hosting-Abo
|
||||
**Text.** Leistungsbeschreibung A Nr. 2 Abs. 3 (keine Formulare mit Server) und Nr. 4 (Rechtstexte nicht enthalten) sowie Leistungsbeschreibung B Nr. 3 stehen dem Webshop „nicht entgegen“, auch im Hosting-Abo.
|
||||
**Frage.** Reicht diese Klarstellung, um die statische Hosting-Website mit einem eingebundenen Widget zu vereinbaren, oder muss die Leistungsbeschreibung A selbst geändert werden (Änderungsverfahren § 15 AB)? Ist der Vorrang der LB vor den AB (Abs. 3) wirksam?
|
||||
**Unsere Annahme.** Ja, weil das Widget auf dem Server von WEBklar läuft und die Website statisch bleibt.
|
||||
|
||||
### F2: LB Nr. 3: Ausgeschlossene Waren und Abschaltung
|
||||
**Text.** Katalog ausgeschlossener Waren (Alkohol, Tabak, Arzneimittel, Waffen und Feuerwerk, Erotik, lebende Tiere, digitale Inhalte, Gutscheine, Abonnements, Stripe-Liste); Abschalten nach Hinweis, bei Gefährdung auch ohne Hinweis; dann keine Erstattung des Einrichtungspreises, wenn der Händler den Verstoß zu vertreten hat.
|
||||
**Frage.** Ist der Katalog gegenüber Unternehmern als AGB-Klausel haltbar (§ 307 BGB, auch bei Verweis auf eine fremde Liste wie die von Stripe, die sich ändert)? Ist die Abschaltung ohne vorherigen Hinweis und der Ausschluss der Erstattung angemessen? Sollte der Verweis auf die Stripe-Liste dynamisch bleiben?
|
||||
|
||||
### F3: LB Nr. 4: Zahlungen, Rolle von WEBklar, ZAG
|
||||
**Text.** Stripe Connect mit eigenem Konto des Händlers (Direct Charges), WEBklar erhält und verwahrt kein Geld, keine Umsatzbeteiligung, „WEBklar ist kein Zahlungsdienstleister“; PayPal nicht verfügbar.
|
||||
**Frage.** Trägt diese Konstruktion ohne Erlaubnis nach dem ZAG (Bereichsausnahme für technische Dienstleister, § 2 Abs. 1 Nr. 9 ZAG)? Ist der Satz „WEBklar ist kein Zahlungsdienstleister“ als Aussage in der LB unbedenklich oder sollte er entfallen? Bleibt das so, wenn der Webshop später eine Plattformgebühr oder PayPal bekommt?
|
||||
**Unsere Annahme.** Keine Erlaubnis nötig, solange kein Geld über WEBklar läuft und keine Gebühr einbehalten wird.
|
||||
|
||||
### F4: LB Nr. 5: Pflichten des Händlers und Rolle als Hosting-Anbieter
|
||||
**Text.** Der Händler verantwortet Artikel, Preise, Produktangaben (Lebensmittel, Produktsicherheit, Grundpreis), Rechtstexte, Lieferung, Gewährleistung, Rechnungen und Aufbewahrung.
|
||||
**Frage.** Ist die Verteilung vollständig und vertretbar? Trifft WEBklar als Anbieter des Widgets eigene Pflichten (Verordnung über digitale Dienste für Hosting-Dienste; Produktsicherheitsverordnung für Online-Marktplätze), obwohl WEBklar nicht Verkäufer ist und keinen Marktplatz betreibt? Brauchen wir eine Meldestelle für rechtswidrige Angebote?
|
||||
|
||||
### F5: LB Nr. 6: Muster-Rechtstexte ohne Gewähr
|
||||
**Text.** WEBklar stellt Muster-AGB, Muster-Widerrufsbelehrung und Muster-Widerrufsformular, Versand und Zahlung bereit („Muster ohne Gewähr“, keine Rechtsberatung); das Widget zeigt den Hinweis auf die gesetzliche Gewährleistung (Verordnung (EU) 2025/1960).
|
||||
**Frage.** Ist die Bereitstellung von Mustern als Nebenleistung eines technischen Produkts nach dem Rechtsdienstleistungsgesetz zulässig (§ 2 Abs. 3, § 5 RDG)? Genügt der Hinweis „Muster ohne Gewähr“ mit Prüfempfehlung? Kann WEBklar für Fehler der Muster haften (Abs. 2 verspricht richtige Beschreibung der Technik)?
|
||||
|
||||
### F6: LB Nr. 8: Haftung
|
||||
**Text.** Wie beim Terminplaner: Haftung für entgangenen Gewinn und Folgeschäden nur im Rahmen von § 13 Abs. 1 und 2 AB; keine Haftung für Inhalte, Preise und Rechtstexte des Händlers, Mustertexte, Ausfälle von Stripe, Rückbuchungen und Zahlungsausfälle.
|
||||
**Frage.** Hält die Klausel der Inhaltskontrolle stand (Abs. 3 und 4 sind Ausschlüsse außerhalb des Einflussbereichs, keine Haftungsbegrenzung für eigene Pflichtverletzung)? Reicht der Verweis auf § 13 AB oder braucht der Webshop mit Zahlungsbezug eine eigene Haftungsgrenze (etwa Höchstbetrag)?
|
||||
|
||||
### F7: LB Nr. 9 Abs. 5: Mehraufwand und Stundenkontingent
|
||||
**Text.** Mehraufwand wird nach Freigabe mit 100 € je Stunde berechnet und nicht auf das Stundenkontingent angerechnet.
|
||||
**Frage.** Das ist eine Entscheidung des Inhabers, keine Rechtsfrage; sie ist hier nur zur Kontrolle gelistet: Soll der Händler im Wartungs-Abo den Mehraufwand wahlweise über sein Kontingent abwickeln können (wie beim Terminplaner nach § 4 AB)?
|
||||
|
||||
### F8: LB Nr. 9 Abs. 6: Widerruf der Einrichtung
|
||||
**Text.** Verbraucher können die Bestellung der Einrichtung widerrufen; Verweis auf die gemeinsame Widerrufsbelehrung der Funktionen (Beispiel Terminplaner), Erlöschen nach der Zustimmung bei der Bestellung.
|
||||
**Zitat der gemeinsamen Belehrung (`docs/rechtliches/widerrufsbelehrung-funktionen.md`).**
|
||||
|
||||
> Das Widerrufsrecht gilt nur für Verbraucher. Wir zeigen die Belehrung und das Muster-Widerrufsformular trotzdem allen, die eine Funktion bestellen. Die Belehrung gilt für die Bestellung der Einrichtung einer Funktion im Kundenportal, zum Beispiel des Terminplaners. Hinweise dazu stehen nach dem Muster-Widerrufsformular.
|
||||
|
||||
Die Belehrung nennt als Beispiel nur den Terminplaner, die Bestellung des Webshops verweist auf dieselbe Datei.
|
||||
|
||||
**Frage.** Der Webshop richtet sich nur an Unternehmer (Nr. 1 Abs. 1), die Bestellung kann aber ein Existenzgründer oder ein Verbraucher vor Gewerbebeginn aufgeben. Ist es richtig, allen Bestellern die Belehrung zu zeigen? Muss die gemeinsame Belehrung den Webshop namentlich nennen, oder genügt das „zum Beispiel“ mit dem Terminplaner, weil der Webshop eine Funktion im Sinne des ersten Satzes ist? Eine Änderung der Datei würde auch die Fassung des Terminplaners betreffen (die Kanzlei-Prüfung der Belehrung selbst ist Teil des Pakets Terminplaner).
|
||||
|
||||
### F9: LB Nr. 10: Ende mit dem Vertrag über den Betrieb der Website, Löschfrist, Wiedereinschaltung
|
||||
**Text.** Der Webshop läuft, solange WEBklar die Website des Händlers betreibt (Hosting-Abo, Wartungs-Abo oder ein anderer Vertrag über den Betrieb der Website). Endet dieser Vertrag, endet auch der Webshop; WEBklar schaltet ihn zum Vertragsende ab und informiert den Händler vorher per E-Mail (Abs. 1, gilt auch beim Widerruf des Vertrags). 90 Tage Lesen und Export, danach Löschung; Wiedereinschaltung innerhalb der 90 Tage auf Wunsch ohne Kosten, wenn wieder ein Vertrag über den Betrieb der Website beginnt (Abs. 5); volle Erstattung des Einrichtungspreises, wenn der Vertrag vor der Freischaltung endet (Abs. 6). Technisch ist der Webshop nicht mit den Abo-Daten gekoppelt: Die Bestellung ist nicht an ein Abo gebunden, und das Abschalten zum Vertragsende erledigt das Team von Hand (WOMS, Funktionen, Webshop, „Abschalten“).
|
||||
**Frage.** Ist die Kopplung des Webshops an den Vertrag über den Betrieb der Website wirksam, auch wenn der Händler einen laufenden Betrieb mit offenen Bestellungen und Widerrufen hat? Reicht der Hinweis an den Händler, seine Käufer zu informieren (Abs. 2), und die Vorab-Mail vor dem Abschalten? Sind die Erstattungs- und Wiedereinschaltungsregeln so vereinbar? Ist die Formulierung „anderer Vertrag über den Betrieb der Website“ bestimmt genug (§ 305c, § 307 BGB)?
|
||||
|
||||
### F10: LB Nr. 11 Abs. 2: Beendigung bei Änderungen von Stripe
|
||||
**Text.** WEBklar darf den Webshop (je Website mit vier Wochen, für alle mit drei Monaten Frist) beenden oder einstellen, auch wenn Stripe die Anbindung beendet oder wesentlich ändert.
|
||||
**Frage.** Ist dieser zusätzliche Beendigungsgrund (gegenüber dem Terminplaner) wirksam, oder muss er enger gefasst werden? Was gilt für den bereits gezahlten Einrichtungspreis?
|
||||
|
||||
### F11: LB Nr. 12 Abs. 2: Barrierefreiheit (BFSG)
|
||||
**Text.** Wie beim Terminplaner: keine Zusage einer Konformität (WCAG 2.1 AA, EN 301 549); ob der Händler unter das BFSG fällt, entscheidet er selbst.
|
||||
**Frage.** Ein Online-Shop ist ein Dienstleistungsangebot im elektronischen Geschäftsverkehr. Ist WEBklar mit dem Widget selbst Normadressat oder nur der Händler? Ist die Ausnahme für Kleinstunternehmen (§ 3 Abs. 3 BFSG) richtig dargestellt? Reicht „keine Zusage“, oder sollte WEBklar einen Mindeststandard für das Widget zusagen?
|
||||
|
||||
## Auftragsverarbeitungsvertrag (Anlage B)
|
||||
|
||||
### F12: AVV Vorbemerkung Abs. 4 und Nr. 2: Rollen
|
||||
**Text.** Händler = Verantwortlicher, WEBklar = Auftragsverarbeiter, Stripe = Vertragspartner des Händlers (kein Unterauftragsverarbeiter). WEBklar verarbeitet Bestell- und Widerrufsdaten, versendet Bestell-E-Mails im Namen des Händlers und betreibt den Missbrauchsschutz. Dazu gehört ein gesalzener Hashwert der IP-Adresse in der Bestellung (Obergrenze von drei offenen Bestellungen je IP-Adresse in 30 Minuten; AVV Nr. 3 Abs. 4), gelöscht bei der Löschung oder Anonymisierung der Bestellung.
|
||||
**Frage.** Ist WEBklar für alle diese Vorgänge reiner Auftragsverarbeiter, oder wird es beim Missbrauchsschutz (Arbeitsspeicher, Mengenbegrenzung, IP-Hash in der Bestellung) oder bei der Widerrufsfunktion (Eingangsbestätigung) zum eigenen Verantwortlichen (Art. 4 Nr. 7 oder Art. 26 DSGVO)?
|
||||
|
||||
### F13: AVV Nr. 7 Abs. 4: Stripe und Datenweitergabe
|
||||
**Text.** Stripe ist kein Unterauftragsverarbeiter; Zahlung, Kartendaten und Auszahlung verantworten Händler und Stripe. WEBklar ruft auf Weisung des Händlers die Zahlungs- und Adressangaben der Checkout-Sitzung ab und veranlasst Erstattungen. Daten von Käufern gehen nicht von WEBklar an Stripe.
|
||||
**Frage.** Ist es richtig, den Datenfluss von Stripe zu WEBklar als Verarbeitung im Auftrag des Händlers zu beschreiben? Ist die Abgrenzung (Stripe als Auftragsverarbeiter und teils eigener Verantwortlicher laut DPA von Stripe) für die Datenschutzerklärung des Händlers richtig? Ist der Satz zu Drittlandübermittlungen von Stripe („regeln Kunde und Stripe“) ausreichend?
|
||||
|
||||
### F14: AVV Nr. 9: Fristen für Anonymisierung und Löschung
|
||||
**Text.** Nicht bezahlte Bestellungen (offen oder storniert): Löschung 30 Tage nach dem Anlegen. IP-Hash: gelöscht, sobald die Bestellung bezahlt oder storniert wird, spätestens mit dieser Löschung. Keine Telefonnummer des Käufers. Bezahlte Bestellungen und Widerrufe: Käuferdaten werden 3 Jahre nach dem Ende des Bestelljahres anonymisiert; Positionen, Beträge, Bestellnummer und Status bleiben. Nach dem Abschalten: 90 Tage Lesen und Export, dann Löschung aller Daten (auch des Archivs der Rechtstexte). Versandwarteschlange: Inhalt sofort nach dem Versand leeren, Eintrag nach 7 Tagen löschen, bei Fehlschlag nach 48 Stunden. Sicherung bis zu sieben Tage.
|
||||
**Frage.** Sind 3 Jahre nach Ende des Bestelljahres für die Käuferdaten angemessen (Gewährleistung zwei Jahre, regelmäßige Verjährung drei Jahre ab Jahresende)? Entsteht ein Konflikt mit den Aufbewahrungspflichten des Händlers (zum Beispiel sechs oder acht Jahre nach AO und HGB), wenn WEBklar die Namen schon früher anonymisiert und der Händler den Export nicht gezogen hat? Reicht der Hinweis in Nr. 9 Abs. 8 und LB Nr. 5 Abs. 8, oder soll die Frist einstellbar sein?
|
||||
|
||||
### F15: AVV Nr. 8 Abs. 2: Betroffenenrechte nur auf Weisung im Chat
|
||||
**Text.** Auskunft und Löschung/Anonymisierung einer einzelnen Bestellung geschehen auf Weisung des Händlers in Textform (Chat), nicht über eine Aktion im Portal; der Händler prüft vorher seine Aufbewahrungspflichten.
|
||||
**Frage.** Genügt das als Unterstützung nach Art. 28 Abs. 3 lit. e DSGVO? Welche Reaktionszeit sollte der AVV nennen („unverzüglich“)?
|
||||
|
||||
### F16: AVV Nr. 8 Abs. 5: Archiv der bestätigten Rechtstexte und Prüfsumme
|
||||
**Text.** Bestätigt der Händler seine Rechtstexte, legt WEBklar jede bestätigte Fassung einmal im Archiv ab (Wortlaut, Prüfsumme, Zeitpunkt, bestätigende Person des Händlers; keine Käuferdaten). Die Bestellung speichert die Prüfsumme der Fassung, die der Käufer sah; die Bestellbestätigung G09 zeigt die archivierte Fassung zu dieser Prüfsumme und nicht die beim Versand aktuelle (fehlt sie ausnahmsweise, gilt die aktuell bestätigte). WEBklar löscht das Archiv mit den übrigen Daten des Webshops 90 Tage nach dem Abschalten (AVV Nr. 9 Abs. 6), nicht früher; es ersetzt nicht die Aufbewahrung des Händlers (AVV Nr. 9 Abs. 8). Der Wortlaut bleibt also auch über die Anonymisierung der Bestellung (drei Jahre nach dem Bestelljahr) hinaus erhalten, solange der Webshop betrieben wird.
|
||||
**Frage.** Genügt das als Nachweis des Händlers für den Vertragsinhalt (Bestellbestätigung mit den vollständigen Texten, § 312f Abs. 2 BGB)? Ist es richtig, die Fassungen bis 90 Tage nach dem Abschalten ohne weitere Frist aufzubewahren (Speicherbegrenzung; die Fassungen enthalten keine Käuferdaten, nur die bestätigende Person des Händlers), oder soll der AVV eine kürzere oder längere Frist nennen?
|
||||
|
||||
### F17: AVV Nr. 3 Abs. 5: Warenkorb im Browser
|
||||
**Text.** Kein Cookie; der Warenkorb (Artikelkennungen und Mengen, kein Personenbezug) liegt im Sitzungsspeicher (sessionStorage) des Tabs, „technisch erforderlich“.
|
||||
**Frage.** Ist das nach § 25 Abs. 2 Nr. 2 TDDDG ohne Einwilligung zulässig? Welche Information muss der Händler dazu in seiner Datenschutzerklärung geben (der Musterabsatz dazu entsteht in Aufgabe J2)?
|
||||
|
||||
### F18: AVV insgesamt: Übernahme der Textbausteine des Terminplaners
|
||||
**Text.** Nummern 5, 6, 7 Abs. 1 bis 3, 10, 11 Abs. 1 und 12 sowie die Anlage sind inhaltlich aus dem AVV Terminplaner übernommen.
|
||||
**Frage.** Gilt die Prüfung des AVV Terminplaner für diese Teile unverändert, oder ist wegen der Zahlungsdaten und der Adressdaten der Käufer (Liefer- und Rechnungsadresse, IP-Hash) eine Anpassung der Maßnahmen in der Anlage nötig?
|
||||
|
||||
## Ablauf, Pflichtangaben, Datenschutz und Ende
|
||||
|
||||
### F19: Vertragsschluss im Widget, § 312j BGB: Platz des Knopfes und Stripe nur als Zahlung
|
||||
**Text.** Die verbindliche Bestellung geschieht im Widget des Händlers mit dem Knopf „Zahlungspflichtig bestellen“ (Abschnitt 3.3). Unmittelbar darüber stehen Positionen, Einzel- und Gesamtpreise, Versandkosten, Gesamtsumme, Lieferart und Lieferzeit, Zahlungsarten, Hinweis auf Widerrufsrecht oder -ausschluss je Artikel, Links auf AGB, Widerrufsbelehrung und Versand & Zahlung, der Gewährleistungshinweis (Abschnitt 3.4) und die Anbieterangaben. Name und Lieferadresse gibt der Käufer erst danach auf der Zahlungsseite von Stripe ein (ohne Telefonnummer, Rechnungsanschrift nur soweit nötig); Stripe Checkout ist nur die Zahlung. Bis zur Zahlung (30 Minuten) ist die Bestellung „offen“ und der Bestand reserviert; zahlt der Käufer nicht, verfällt sie.
|
||||
**Frage.** (a) Erfüllt der Knopf mit der Beschriftung „Zahlungspflichtig bestellen“ im Widget § 312j Abs. 3 BGB, obwohl noch Daten (Name, Adresse) und die Zahlung folgen? (b) Erfüllen die Angaben über dem Knopf § 312j Abs. 2 BGB und Art. 246a § 1 Abs. 1 EGBGB, auch wenn die Lieferadresse noch fehlt? (c) Wann kommt der Vertrag zustande: mit dem Klick, mit der Zahlung oder mit der Bestätigung? Passt das zur Bestellbestätigung und zu den Mustertexten? (d) Ist die Informationspflicht zu Beginn des Bestellvorgangs (§ 312j Abs. 1 BGB: Lieferbeschränkungen und Zahlungsmittel) durch die Zeile ganz oben im Widget erfüllt?
|
||||
**Unsere Annahme.** Ja: Der Händler gibt mit dem Klick im Widget das verbindliche Angebot ab; die Zahlung bei Stripe schließt den Vorgang ab; die Hinweise stehen unmittelbar über dem Knopf.
|
||||
**Alternative.** Den Knopf erst auf der letzten Seite zeigen, nachdem Stripe die Daten erhoben hat (nicht möglich, weil Stripe Checkout die Seite betreibt), oder Name und Adresse im Widget abfragen und Stripe nur für die Zahlung nutzen.
|
||||
|
||||
### F20: Widerrufsfunktion nach § 356a BGB: Ausgestaltung
|
||||
**Text.** Die Schaltfläche „Vertrag widerrufen“ steht in der Fußzeile des Widgets, auf der Standalone-Seite unter `/shop/<siteKey>/widerruf` und, vom Team eingebaut, als Link in der Fußzeile jeder Seite der Website des Händlers (Anleitung `docs/funktionen/shop-einbau.md`, Abschnitt 3). Sie führt ohne Anmeldung zu einem Formular (Name, E-Mail-Adresse, Bestellnummer „falls zur Hand“; ohne Bestellnummer muss die Erklärung Bestelldatum und Artikel nennen), danach zur Schaltfläche „Widerruf bestätigen“. WEBklar speichert den Eingang und sendet dem Erklärenden sofort eine Eingangsbestätigung mit Inhalt, Datum und Uhrzeit (G12) auf einem dauerhaften Datenträger; der Händler erhält die Mail K26. Die Funktion ist auch erreichbar, wenn keine Bestellung zur Angabe gefunden wird; der Händler klärt das. Die Bestellnummer ist ein freiwilliges Feld; fehlt sie, verlangt das Formular Bestelldatum und Artikel in der Erklärung (Freitext), damit der Händler zuordnen kann.
|
||||
**Frage.** (a) Genügt diese Ausgestaltung § 356a BGB (Erreichbarkeit „während der gesamten Widerrufsfrist“, Schaltfläche, Bestätigungsschaltfläche, Eingangsbestätigung)? (b) Reichen Name, E-Mail-Adresse und entweder die Bestellnummer oder Bestelldatum und Artikel in der Erklärung zur Zuordnung („Identifikation“ des Vertrags), oder muss die Bestellung zwingend gefunden werden? (c) Ist der Link auf jeder Seite der Website des Händlers die richtige Verortung („Online-Benutzeroberfläche“), obwohl das Widget nur auf einer Seite läuft, und wer trägt die Pflicht, wenn der Händler den Link entfernt (LB Nr. 5 Abs. 6)? (d) Ist der Zeitstempel in Europe/Berlin mit Eingangsbestätigung ausreichend?
|
||||
**Unsere Annahme.** Ja; die Funktion nimmt jede Erklärung an, die Zuordnung ist Sache des Händlers.
|
||||
**Alternative.** Bestellnummer und E-Mail-Adresse als Pflichtfelder; Zuordnung mit Fehlermeldung bei Nichtfinden (würde Widerrufe abweisen und ist nach unserer Einschätzung riskanter).
|
||||
|
||||
### F21: PAngV, Grundpreis immer je 1 kg, 1 l, 1 m, 1 m²
|
||||
**Text.** Das Portal rechnet den Grundpreis immer auf 1 kg, 1 l, 1 m oder 1 m² um und zeigt ihn als „x € / 1 kg“; die Umrechnung auf 100 g oder 100 ml (die für Waren mit einem Nenngewicht bis 250 g erlaubt wäre) ist nicht vorgesehen. Der Händler gibt Menge und Einheit des Artikels an. Der Grundpreis steht neben dem Gesamtpreis im Artikel und in der Kasse.
|
||||
**Frage.** (a) Ist es zulässig, auch bei kleinen Mengen immer den Grundpreis je 1 kg oder 1 l anzugeben? (b) Reicht die Pflege durch den Händler (Menge und Einheit je Artikel) oder braucht es Ausnahmen (Waren nach Stück, lose Ware, Mengen, bei denen ein Grundpreis entfällt)? (c) Wie ist der Grundpreis bei Varianten mit unterschiedlicher Menge zu zeigen?
|
||||
**Unsere Annahme.** Zulässig und eindeutiger; die Ausnahmen kennt der Händler.
|
||||
|
||||
### F22: Lebensmittel im Fernabsatz: lose Backwaren und Allergene
|
||||
**Text.** Der Händler kann einen Artikel als Lebensmittel markieren; dann ist ein Pflichtfeld „Pflichtangaben“ auszufüllen (Bezeichnung, Zutaten, Allergene hervorgehoben, Menge, gegebenenfalls Hersteller), das vor dem Kauf am Artikel angezeigt wird. Ein typischer Kunde ist eine Bäckerei mit losen (nicht vorverpackten) Waren. Das Widerrufsrecht kann der Händler bei schnell verderblichen Waren ausschließen (§ 312g Abs. 2 Nr. 2 BGB). Die Lieferung und Abholung von Speisen ist in der Recherche als offen markiert.
|
||||
**Frage.** (a) Genügt das Freitextfeld, um Art. 14 LMIV (Pflichtangaben im Fernabsatz vor Vertragsschluss) und die Allergenkennzeichnung nicht vorverpackter Ware zu erfüllen? (b) Muss der Shop die Angaben strukturieren (Allergenliste statt Freitext)? (c) Gilt für Lebensmittel, die der Händler selbst ausliefert oder zur Abholung bereitstellt, § 312 Abs. 2 Nr. 8 BGB oder der Ausschluss nach § 312g Abs. 2 Nr. 2 BGB, und was soll die Muster-Widerrufsbelehrung dazu sagen?
|
||||
**Unsere Annahme.** Freitext genügt, wenn der Händler vollständig angibt; wir weisen ihn in der Leistungsbeschreibung (Nr. 5 Abs. 3) darauf hin.
|
||||
|
||||
### F23: Produktsicherheit (GPSR): Pflichtfelder am Artikel
|
||||
**Text.** Je Artikel erfasst der Händler den Hersteller (Name, Anschrift, E-Mail oder Website; Vorgabe „ich selbst“), bei Herstellern außerhalb der EU die verantwortliche Person in der EU sowie Warn- und Sicherheitshinweise. Das Widget zeigt diese Angaben am Artikel. Eine Produktkennung (Typ, Charge, Seriennummer) und ein Bild jedes Produkts als eigene Pflichtfelder gibt es nicht; Bilder sind optional (bis vier).
|
||||
**Frage.** Erfüllen diese Felder Art. 19 der Verordnung (EU) 2023/988 (GPSR) für Angebote im Fernabsatz? Fehlen Pflichtangaben (Produktkennung, Bild)? Trifft WEBklar als Anbieter des Widgets selbst Pflichten (siehe auch F4)?
|
||||
**Unsere Annahme.** Die Felder decken die Pflichtangaben für die üblichen Waren ab; die Verantwortung bleibt beim Händler.
|
||||
|
||||
### F24: Hinweis auf die gesetzliche Gewährleistung (Verordnung (EU) 2025/1960 Anhang I)
|
||||
**Text.** Seit dem 27.09.2026 ist der harmonisierte Hinweis hervorgehoben zu zeigen. Der Webshop zeigt die Grafik (`public/img/gewaehrleistung-eu.webp`) unmittelbar über dem Knopf „Zahlungspflichtig bestellen“, mit dem Wortlaut als Alternativtext, und führt denselben Text in der Bestellbestätigung. Der Wortlaut steht in Abschnitt 3.4. Der Abgleich der Grafikdatei mit der Originaldatei der Kommission steht noch aus.
|
||||
**Frage.** (a) Entspricht Wortlaut und Gestaltung Anhang I (Größe, Hervorhebung, Position in der Kasse und im Angebot)? (b) Genügt die Einblendung in der Kasse, oder muss der Hinweis schon am Artikel stehen? (c) Genügt der Text in der Bestellbestätigung? (d) Wie ist der Hinweis bei Waren zu halten, die nicht dem Gewährleistungsrecht für Waren unterliegen?
|
||||
**Unsere Annahme.** Kasse und Bestätigung genügen; die Grafik prüfen wir vor dem Livegang gegen die Datei der Kommission.
|
||||
|
||||
### F25: Muster-Rechtstexte des Händlers (AGB, Widerrufsbelehrung, Widerrufsformular, Versand und Zahlung)
|
||||
**Text.** Das Portal befüllt für den Händler Muster nach `docs/funktionen/shop-muster-rechtstexte.md` (Anlage C). Die Widerrufsbelehrung folgt Anlage 1 zu Art. 246a § 1 Abs. 2 S. 2 EGBGB (Fassung 2026 mit dem Baustein zur Widerrufsfunktion), enthält beide Fristbeginn-Alternativen (eine Ware, mehrere getrennt gelieferte Waren), Rücksendekosten zulasten des Käufers (umschaltbar), Abholung und Ausschlussgründe in allgemeiner Form (§ 312g Abs. 2 Nr. 1, 2, 3 BGB). Dazu kommen Muster-AGB für kleine Shops, das Muster-Widerrufsformular (Anlage 2) und Versand- und Zahlungsinformationen, die aus den Einstellungen erzeugt werden. Der Händler bestätigt die Texte mit dem Satz aus Abschnitt 3.2; die Prüfsumme der Texte steht an jeder Bestellung.
|
||||
**Frage.** (a) Erfüllen die Muster, richtig ausgefüllt, die Gesetzlichkeitsfiktion (Art. 246a § 1 Abs. 2 S. 3 EGBGB)? (b) Stimmen die Muster-AGB mit der Technik überein (Vertragsschluss, Eigentumsvorbehalt, Gewährleistung, Streitbeilegung nach § 36 VSBG ohne Link auf die eingestellte OS-Plattform)? (c) Ist es zulässig, Muster als Nebenleistung zu liefern (siehe F5)? (d) Teilsendungen und regelmäßige Lieferung sind nicht vorgesehen: Muss die Belehrung das ausdrücklich sagen?
|
||||
**Unsere Annahme.** Ja, wenn der Händler die Platzhalter richtig ausfüllt; Abweichungen im Einzelfall prüft der Händler (LB Nr. 6).
|
||||
**Alternative.** Nur die Belehrung als Muster, AGB nur als Gliederung.
|
||||
|
||||
### F26: Bestelltexte der Einrichtung: fünf Haken
|
||||
**Text.** Die Bestellung der Einrichtung für 299 € trägt fünf Haken und den Knopf „Zahlungspflichtig bestellen · 299 €“ (Abschnitt 3.1). Alle Besteller sehen die volle Widerrufsbelehrung mit Muster-Widerrufsformular; Haken 4 verlangt den vorzeitigen Beginn mit Erlöschen und Wertersatz (§ 356 Abs. 5 Nr. 2, § 357a Abs. 2 BGB); Haken 5 bündelt die Pflichtinformationen.
|
||||
**Frage.** Wie bei der Bestellung des Terminplaners (dort Q1 bis Q3, Q5, Q19, Q20): Genügen die fünf Haken, einschließlich Haken 3 („Die Zahlungen meiner Käufer laufen über mein eigenes Stripe-Konto …“) und Haken 5? Ist „der Webshop läuft, solange WEBklar die Website betreibt“ im Haken 5 als Information zur Laufzeit ausreichend, auch wenn das Ende des Webshops nicht an ein Abo, sondern an irgendeinen Vertrag über den Betrieb der Website gekoppelt ist (siehe F9)?
|
||||
**Unsere Annahme.** Ja; wir gleichen die Antworten zum Terminplaner an.
|
||||
|
||||
### F27: Bestellbestätigung an den Käufer (G09) und Absender
|
||||
**Text.** G09 enthält Anbieter, Bestellnummer, Positionen, Preise, Versand, Gesamtsumme, Lieferart und Lieferzeit, Adresse, Zahlungsart, Hinweis auf Widerrufsausschlüsse, die vollständigen AGB, die vollständige Widerrufsbelehrung und das Muster-Widerrufsformular im Mailtext, den Link „Vertrag widerrufen“ und den Gewährleistungshinweis. Absender ist die Versandadresse von WEBklar mit dem Namen des Shops, Antworten gehen an den Händler.
|
||||
**Frage.** (a) Erfüllt G09 § 312f Abs. 2 BGB (Bestätigung auf einem dauerhaften Datenträger mit den Vertragsinhalten und den Informationen nach Art. 246a EGBGB)? (b) Ist der Absender (Domain von WEBklar, Name des Shops, Reply-To Händler) transparent genug, oder muss der Händler als Absender erscheinen? (c) Müssen die Mails der Anlage zur Prüfung beigelegt werden? (Sie liegen als gerenderte Fassung nach der Mail-Überarbeitung vor.)
|
||||
**Unsere Annahme.** Ja.
|
||||
|
||||
### F28: Datenschutz: Abschnitt 6d, Stripe-Konto und Muster-Absatz
|
||||
**Text.** (a) Datenschutzerklärung von WEBklar Abschnitt 6d („Webshop auf Websites unserer Kunden“): Rolle Auftragsverarbeiter für Käuferdaten, Verantwortliche für Betriebsdaten und Bestellung der Funktion. (b) Eröffnung des Stripe-Kontos des Händlers: WEBklar übermittelt auf Klick des Händlers dessen E-Mail-Adresse, die Adresse seiner Website und den Namen seines Projekts an Stripe Payments Europe Ltd. (Dublin), speichert Konto-ID und Stand und stützt das auf Art. 6 Abs. 1 lit. b DSGVO. (c) Muster-Absatz für die Datenschutzerklärung der Website des Händlers (`docs/funktionen/shop-datenschutz-absatz.md`, Anlage D): Stripe als eigener Verantwortlicher (Betrugsvorbeugung), Drittlandübermittlung (EU-US Data Privacy Framework), Klarna und SEPA über Stripe, Warenkorb im Sitzungsspeicher, E-Mails, Speicherdauer.
|
||||
**Frage.** (a) Sind Rollen und Rechtsgrundlagen richtig (siehe auch F12, F13)? (b) Ist für die Übermittlung an Stripe bei der Kontoeröffnung eine Einwilligung oder Information des Händlers nötig, die über den Klick und Abschnitt 6d hinausgeht? (c) Reicht der Muster-Absatz für die Informationspflichten nach Art. 13 DSGVO (Verantwortlicher, Zwecke, Rechtsgrundlagen, Empfänger, Drittland, Dauer, Rechte), insbesondere für die Aussage zu Stripe und Klarna? (d) Passt die Speicherdauer (30 Tage unbezahlt, 3 Jahre bis zur Anonymisierung) zur Aufbewahrung des Händlers (siehe F14)?
|
||||
**Unsere Annahme.** Ja; der Muster-Absatz folgt dem AVV Webshop.
|
||||
|
||||
### F29: Ende des Webshops mit dem Vertrag über den Betrieb der Website, Abschalten von Hand
|
||||
**Text.** Der Webshop ist nicht mit Abo-Daten im Programm gekoppelt, die Bestellung hängt nicht an einem Abo, und das Programm schaltet nicht von selbst ab (Entscheidung W5). Das Team schaltet zum Vertragsende von Hand ab und schreibt dem Händler vorher (Anleitung `docs/funktionen/shop-einbau.md`, Abschnitt 8). LB Nr. 10 Abs. 1 verspricht dieses Vorgehen; ein verspätetes Abschalten bleibt ein Organisationsrisiko von WEBklar.
|
||||
**Frage.** Entstehen aus einem verspäteten Abschalten Ansprüche des Händlers oder der Käufer gegen WEBklar (etwa wenn der Shop nach dem Vertragsende weiter bestellbar bleibt)? Wie ist „zum Vertragsende“ in Abs. 1 zu fassen (Tag der Beendigung oder Tag danach)? Genügt die Haftungsregelung der LB Nr. 8 dafür?
|
||||
**Unsere Annahme.** Das Risiko ist gering und beschränkt sich auf die Zeit zwischen Vertragsende und Abschalten; die Haftung ist nach LB Nr. 8 begrenzt.
|
||||
|
||||
## 5. Fundstellen im Code
|
||||
|
||||
Pfade gelten im Repository des Kundenportals (`Webklar-Kundenbereich`); Bezeichner sind Funktionen oder Konstanten und stehen in der genannten Datei. Zeilennummern nennen wir nicht, weil sie sich ändern.
|
||||
|
||||
| Thema | Datei | Funktion/Konstante |
|
||||
|---|---|---|
|
||||
| Fassung, Preis, Betriebsart | `server/config.js` | `shop.fassung`, `SHOP_PREIS`, `SHOP_ENABLED` |
|
||||
| Bestell-Haken und Knopf des Webshops (Register) | `server/config.js` | `shopConsent` |
|
||||
| Betriebsart und Pilotliste | `server/services/shop/schalter.js` | `shopModus`, `shopFuerKunde` |
|
||||
| Anfrage und Freigabe | `server/services/shop/anfrage.js` | – |
|
||||
| Einstellungen, Zahlungsarten, Versand | `server/services/shop/einstellungen.js` | `STANDARD_EINSTELLUNGEN` |
|
||||
| Artikel, Produktsicherheit, Lebensmittel, Grundpreis | `server/services/shop/produkte.js`, `server/services/shop/geld.js` | `normalisiereProdukt`, `grundpreisText` |
|
||||
| Berechnung der Kasse (Preise nur aus der Datenbank) | `server/services/shop/preise.js` | `berechne` |
|
||||
| Muster-Rechtstexte, Bestätigung, Gewährleistungshinweis | `server/services/shop/rechtstexte.js` | `musterWiderrufsbelehrung`, `musterWiderrufsformular`, `musterAgb`, `versandUndZahlung`, `BESTAETIGUNG_TEXT`, `gewaehrleistungsHinweis` |
|
||||
| Stripe Connect: Konto, Onboarding, Checkout, Erstattung | `server/services/shop/connect.js` | `kontoAnlegen`, `onboardingLink`, `checkoutAnlegen`, `erstatten`, `connectEreignis` |
|
||||
| Bestellung anlegen (IP-Hash, Stripe-Kasse ohne Telefonnummer), abschließen, Eingangsmail G15, verwerfen, Status, Erstattung, CSV | `server/services/shop/bestellungen.js` | `legeBestellungAn`, `ipHashVon`, `schliesseAb`, `meldeEingang`, `verwirf`, `setzeStatus`, `erstatte`, `csv` |
|
||||
| Archiv der bestätigten Rechtstexte | `server/services/shop/archiv.js` | `archiviereRechtstexte`, `holeArchivierteRechtstexte` |
|
||||
| Zustimmung in Textform bei Freischaltung durch das Team | `server/routes/admin/funktionenAdmin.js`, `server/services/shop/schalter.js`, `server/services/featureConsents.js` | `textformFuerAktivierung`, `shopTextformNoetig`, `speichereZustimmung` |
|
||||
| Widerrufsfunktion der Käufer | `server/services/shop/widerrufe.js` | `normalisiereWiderruf` |
|
||||
| Mails an Käufer und Händler | `server/services/shop/mails.js`, `server/mail-templates/` | `kaeuferWerte`, `haendlerK25`, `haendlerK26`; G09 bis G15, K24 bis K26, T21 |
|
||||
| Öffentliche Schnittstelle des Widgets | `server/routes/publicShop.js` | Routen unter `/api/public/shop/v1/` |
|
||||
| Widget | `public/embed/shop.js` | – |
|
||||
| Löschfristen, Anonymisierung, Abschalten der Daten | `server/services/shop/jobs.js`, `server/services/shop/loeschen.js` | `taeglicherJob`, `anonymisierGrenze`, `loescheShopDaten` |
|
||||
| Kundenlöschung | `server/services/shop/kundenLoeschung.js` | `schliesseShopsVonKunde` |
|
||||
| Einrichtungs-Ticket mit Snippet und Schritten | `server/services/shop/ticketDetails.js` | `shopTicketDetails` |
|
||||
| Rechtsunterlagen je Fassung | `server/services/legalDocs.js` | Satz `shop` |
|
||||
| Widerruf der Bestellung | `server/routes/abo.js`, `public/widerrufen.html` | `ORDERS`, `FEATURE_OF_ORDER`, `orderLabel` |
|
||||
| HTML-Seiten und PDFs der Vertragsunterlagen | `scripts/build-legal-html.mjs`, `scripts/build-legal-pdfs.sh` | Satz und Modus `shop` |
|
||||
| Datenschutzerklärung von WEBklar | `public/datenschutz.html` | `id="webshop"` (Abschnitt 6d) |
|
||||
|
||||
## 6. Nicht Teil der Kanzleiprüfung
|
||||
|
||||
- **Hausrunde:** Datenschutzerklärung von WEBklar (Abschnitt 6d und die Ergänzungen in den Abschnitten 6, 7 und 12), die Hinweise im Kundenportal, die Mailtexte, die Einbauanleitung. Wir stellen nur die Fragen F27 und F28.
|
||||
- **Gemeinsame Widerrufsbelehrung der Funktionen:** wird im Paket des Terminplaners geprüft; hier nur F8.
|
||||
- **Die Rechtstexte der Händler im Einzelfall:** Jeder Händler bleibt für seine Texte verantwortlich (LB Nr. 5 Abs. 2 und Nr. 6). Wir bitten um Prüfung der Muster, nicht jedes Händlers.
|
||||
- **Später:** Plattformgebühr, PayPal, Verkauf an Verbraucher außerhalb von Deutschland und Österreich, digitale Inhalte, Gutscheine, Abonnements, Rechnungserstellung durch WEBklar. Dafür gibt es keine Texte.
|
||||
- **Steuerrecht der Händler** (Umsatzsteuer, Aufbewahrung): nur soweit F14 und F21 betroffen sind.
|
||||
|
||||
## 7. Checkliste für den Inhaber vor `SHOP_ENABLED=on`
|
||||
|
||||
Diese Punkte erledigt der Inhaber; kein Punkt ist Aufgabe der Kanzlei. Die Reihenfolge entspricht dem Ablauf bis zum ersten fremden Betrieb.
|
||||
|
||||
### 7.1 Vor der Beauftragung der Kanzlei
|
||||
|
||||
- [ ] Die PDFs der Anlagen A und B am Bildschirm ansehen (Kopfzeile, „Seite x von y“, Tabelle der Anlage des AVV); wir konnten sie nur über ihre HTML-Seiten prüfen.
|
||||
- [ ] Bestätigen, dass WEBklar Kleinunternehmer nach § 19 UStG bleibt (Preis 299 € ohne Umsatzsteuer).
|
||||
- [ ] Entscheiden, wie das Team den Vertragsende-Termin eines Händlers nicht verpasst (Erinnerung im Ticket, Kalender oder Abo-Liste), da der Webshop von Hand abgeschaltet wird (F29, `docs/funktionen/shop-einbau.md` Abschnitt 8).
|
||||
- [ ] Entscheiden, ob Mehraufwand wahlweise über das Stundenkontingent abgerechnet werden darf (F7).
|
||||
- [ ] Die Grafik `public/img/gewaehrleistung-eu.webp` mit der Originaldatei der Kommission abgleichen (F24).
|
||||
|
||||
### 7.2 Hetzner und Stripe (nur im Konto prüfbar)
|
||||
|
||||
- [ ] Auftragsverarbeitungsvertrag mit Hetzner für die Cloud **und** für das Webhosting (E-Mail-Versand) abgeschlossen; Anlage 1 nennt „Käufer“ und „Bestelldaten“ als Betroffene und Datenarten.
|
||||
- [ ] Datensicherung eingeschaltet (AVV Nr. 9 Abs. 7 verspricht sieben Tagessicherungen).
|
||||
- [ ] Stripe-Dashboard von WEBklar: Connect aktivieren (Plattformprofil: Verkäufer verkaufen eigene Waren, Direct Charges, keine Gebühr), Branding für das Onboarding setzen.
|
||||
- [ ] Prüfen, dass der Vertrag mit Stripe die Nutzung von Connect für diesen Zweck erlaubt (F3).
|
||||
|
||||
### 7.3 Technik
|
||||
|
||||
- [ ] `node scripts/shop-setup.mjs --check`, dann `node scripts/shop-setup.mjs` (fünf Collections, darunter das Archiv der bestätigten Rechtstexte) und `node scripts/stripe-products.mjs --anlegen` (Produkt `shopSetup`), vor dem Code-Deploy.
|
||||
- [ ] Connect-Webhook in Stripe anlegen (Ereignisse: `checkout.session.completed`, `checkout.session.async_payment_succeeded`, `checkout.session.async_payment_failed`, `checkout.session.expired`, `account.updated`, `charge.refunded`), Geheimnis als `STRIPE_CONNECT_WEBHOOK_SECRET` in `/home/webklar/apps/.env`, Zeilen `SHOP_ENABLED` und `SHOP_PILOT_EMAILS` in der Compose-Datei.
|
||||
- [ ] Code nach `main` (nach dem Terminplaner), WOMS-Zweig nach `test`, Deploy zu einer ruhigen Zeit.
|
||||
- [ ] PDF der Datenschutzerklärung mit `scripts/build-legal-pdfs.sh datenschutz` (nicht `all`), PDFs des Webshops mit `scripts/build-legal-pdfs.sh shop`; Prüfsummen mit den Tests abgleichen.
|
||||
|
||||
### 7.4 Uploads in WOMS „Rechtliches“ und Mailtexte
|
||||
|
||||
- [ ] In WOMS „Rechtliches“ Karten „Leistungsbeschreibung Webshop“ und „AVV Webshop“ bereitstellen (Setup-Skript von WOMS) und die Fassung 2026-10-05 hochladen; die neue Datenschutzerklärung (Stand 05.10.2026, `docs/WEBklar-Datenschutzerklaerung.pdf`) als neue Version hochladen.
|
||||
- [ ] Mailtexte G09 bis G15, K24 bis K26, T21 in der Mail-Überarbeitung auf den endgültigen Wortlaut bringen, danach gerendert als Anlage E nachreichen (F27).
|
||||
|
||||
### 7.5 Pilot, Beauftragung und Freigabe
|
||||
|
||||
- [ ] Pilot mit dem Testkonto (`SHOP_ENABLED=pilot`): Anfrage, Freigabe, Bestellung, Einbau auf der Test-Website, Stripe-Konto, Testkauf für 1 € mit Erstattung, Widerruf über „Vertrag widerrufen“, Abschalten und Wiedereinschalten.
|
||||
- [ ] Die Kanzlei mit der Prüfung des Kernpakets und den Fragen F1 bis F29 beauftragen (Anlagen A bis E beilegen; Umfang und Frist vereinbaren).
|
||||
- [ ] Den Befund einarbeiten: Texte ändern, neues Fassungsdatum setzen (`config.shop.fassung`), HTML und PDFs neu erzeugen (`node scripts/build-legal-html.mjs shop`, `scripts/build-legal-pdfs.sh shop`), Prüfsummen und Tests nachziehen; dieses Paket nachziehen.
|
||||
- [ ] Die Fassung ausdrücklich freigeben; erst danach `SHOP_ENABLED=on` setzen.
|
||||
- [ ] Vor dem ersten fremden Betrieb: der Händler bestätigt seine Rechtstexte (Abschnitt 3.2), wir fügen den Datenschutz-Absatz (Anlage D) ein, Testkauf mit dem Händler.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Widerrufsbelehrung und Muster-Widerrufsformular (Einrichtung von Funktionen)
|
||||
|
||||
**Fassung 2026-10-05**
|
||||
|
||||
Das Widerrufsrecht gilt nur für Verbraucher. Wir zeigen die Belehrung und das Muster-Widerrufsformular trotzdem allen, die eine Funktion bestellen. Die Belehrung gilt für die Bestellung der Einrichtung einer Funktion im Kundenportal, zum Beispiel des Terminplaners. Hinweise dazu stehen nach dem Muster-Widerrufsformular.
|
||||
|
||||
## Widerrufsbelehrung
|
||||
|
||||
### Widerrufsrecht
|
||||
|
||||
Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
|
||||
|
||||
Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag des Vertragsabschlusses.
|
||||
|
||||
Um Ihr Widerrufsrecht auszuüben, müssen Sie uns (WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, Telefon: 0176 23726355, E-Mail: support@webklar.com) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
|
||||
|
||||
Sie können Ihr Widerrufsrecht auch online unter https://project.webklar.com/widerrufen.html ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.
|
||||
|
||||
Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
|
||||
|
||||
### Folgen des Widerrufs
|
||||
|
||||
Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
|
||||
|
||||
Haben Sie verlangt, dass die Dienstleistungen während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht.
|
||||
|
||||
## Muster-Widerrufsformular
|
||||
|
||||
(Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
|
||||
|
||||
– An WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, E-Mail: support@webklar.com:
|
||||
|
||||
– Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)
|
||||
|
||||
– Bestellt am (*)/erhalten am (*)
|
||||
|
||||
– Name des/der Verbraucher(s)
|
||||
|
||||
– Anschrift des/der Verbraucher(s)
|
||||
|
||||
– Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
|
||||
|
||||
– Datum
|
||||
|
||||
(*) Unzutreffendes streichen.
|
||||
|
||||
## Hinweise zum Widerruf
|
||||
|
||||
Diese Hinweise gehören nicht zur gesetzlichen Widerrufsbelehrung. Sie erklären, wie sich das Widerrufsrecht auf die Bestellung einer Funktion auswirkt.
|
||||
|
||||
- Das Widerrufsrecht gilt für jede Bestellung einer Funktion gesondert, also für jedes Website-Projekt, für das der Kunde eine Funktion bestellt. Die Frist von vierzehn Tagen beginnt jeweils mit dem Vertragsschluss.
|
||||
- Widerruft der Kunde die Einrichtung einer Funktion, endet diese Funktion. Das Wartungs-Abo bleibt davon unberührt.
|
||||
- Hat WEBklar auf ausdrücklichen Wunsch des Kunden vor Ablauf der Widerrufsfrist mit der Einrichtung begonnen, schuldet der Kunde bei einem Widerruf einen angemessenen Betrag für die bis dahin erbrachte Leistung. Der Betrag bemisst sich als Anteil am Gesamtpreis; Maß ist der im Arbeitsblatt erfasste Aufwand für die Einrichtung.
|
||||
- Die Rückzahlung erfolgt über das ursprüngliche Zahlungsmittel; die Zahlung lief über Stripe.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Datenschutzerklärung von WEBklar: Abschnitt 6e „Öffnungszeiten live auf Websites unserer Kunden“ (Entwurf)
|
||||
|
||||
Entwurf für die Rechtsrunde (Spec „Öffnungszeiten live“, Abschnitt 9 Nr. 3), Stand 05.10.2026. Keine Rechtsberatung. **`public/datenschutz.html` ist nicht geändert**: Der Abschnitt wird erst nach der Freigabe durch den Inhaber (und nach der Kanzlei, soweit sie die Fragen aus `zeiten-kanzlei-fragen.md` bearbeitet) in die Erklärung übernommen. Reihenfolge der Rechtsrunde: Leistungsbeschreibung, dann dieser Abschnitt samt Website-Absatz (`docs/funktionen/website-datenschutz-absatz.md`), dann Mailtexte, dann Schalter `ZEITEN_ENABLED=on`.
|
||||
|
||||
Aufbau wie Abschnitt 6c (`termine-datenschutz-6c.md`): Rolle, gespeicherte Angaben, E-Mails, Aufruf der Kundenwebsite, Löschfristen. Die Leistungsbeschreibung verweist auf diesen Abschnitt (`/datenschutz.html#zeiten`, Nr. 12 Abs. 2): **Sie darf erst live gehen, wenn der Abschnitt mit dem Anker `id="zeiten"` in der Erklärung steht.**
|
||||
|
||||
Einbau in `public/datenschutz.html`: zwischen 6c und „7. Abos und Zahlung über Stripe“. Beim Übernehmen ändert sich der Stand der Erklärung (Abschnitt 15); die Tests `test/legalTexts.test.js` (Abschnitt 6c und Kundenansicht) und `test/zeitenRecht.test.js` (dieser Entwurf) werden dann angepasst.
|
||||
|
||||
**Nachtrag 06.10.2026 (nach dem Zusammenführen mit main):** Die Nummer 6d ist seit dem Webshop-Livegang vergeben („6d. Webshop auf Websites unserer Kunden“, Anker `id="webshop"`). Dieser Abschnitt heißt deshalb **6e** und steht hinter dem Webshop-Abschnitt und vor „7. Abos und Zahlung über Stripe“; der Anker `id="zeiten"` bleibt.
|
||||
|
||||
## Abschnitt 6e (HTML)
|
||||
|
||||
```html
|
||||
<h2 id="zeiten">6e. Öffnungszeiten live auf Websites unserer Kunden</h2>
|
||||
<p>Kunden können auf ihrer eigenen Website ihre Öffnungszeiten live anzeigen lassen („Öffnungszeiten live“): als Tabelle und als Status („Jetzt geöffnet“), als Hinweis-Leiste am Seitenanfang, auf dem Handy als Kontakt-Leiste mit Knöpfen zum Anrufen, für die Route und für WhatsApp sowie als Angaben für Suchmaschinen im Seitencode. Für die Besucher dieser Websites verarbeiten wir Daten ausschließlich als Auftragsverarbeiter nach den Weisungen des jeweiligen Kunden, der dafür Verantwortlicher ist. Wir verwenden diese Daten nicht für eigene Zwecke. Verantwortliche sind wir für die Angaben, die der Kunde im Kundenportal für die Funktion einträgt, für die E-Mails an den Kunden und für seine Bestellung. Diese Datenschutzerklärung gilt für unsere eigenen Seiten und das Kundenportal. Wer eine Kundenwebsite besucht, findet die Informationen zum Aufruf (Zwecke, Rechtsgrundlagen, Speicherdauer, Rechte) in der Datenschutzerklärung dieser Website.</p>
|
||||
<p>Im Kundenportal speichern wir, was der Kunde für die Funktion einträgt: seine Öffnungszeiten (benannte Zeitpläne, Zeiten je Wochentag, Abweichungen an Feiertagen, Betriebsurlaub und Sondertage sowie das gewählte Bundesland), Hinweise für die Hinweis-Leiste (Text, Art, Zeitraum und gegebenenfalls ein Knopf mit Link oder Telefonnummer), Kontaktangaben für die Kontakt-Leiste (Telefonnummer, Anschrift für die Route und auf Wunsch WhatsApp-Nummer und Starttext der Nachricht, dazu der Zeitpunkt, zu dem der Kunde bestätigt hat, den WhatsApp-Hinweis in seine Datenschutzerklärung aufgenommen zu haben), die Anzeige-Einstellungen, die unser Team für die Website des Kunden setzt, sowie Zeitpunkt und Urheber (Kunde oder Mitarbeiter) der letzten Änderung. Das sind überwiegend Betriebsdaten; personenbezogene Daten enthalten sie nur, soweit der Kunde sie einträgt, zum Beispiel die Telefonnummer eines Inhabers. Was auf der Website erscheinen soll (Zeiten, Hinweise, Telefonnummer, Anschrift, WhatsApp-Nummer, Name des Betriebs), ist dort öffentlich sichtbar. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO, wie bei den übrigen Portal-Daten (Abschnitt 6). Unsere Mitarbeiter können die Zeiten im Auftrag des Kunden ansehen und ändern (Abschnitt 6); den Haken für den WhatsApp-Hinweis setzen sie nicht. Die Bestellung und ihren Nachweis beschreibt Abschnitt 7.</p>
|
||||
<p>Hat der Kunde die Funktion eingeschaltet, erinnern wir ihn per E-Mail an die E-Mail-Adresse seines Kundenkontos vor Feiertagen: Die E-Mail nennt die Tage der nächsten sieben Tage, an denen ein Feiertag seines Bundeslands liegt oder an denen wir um Prüfung bitten, und fragt, ob seine Zeiten für diese Tage stimmen. Für jeden Tag senden wir höchstens eine Erinnerung; dafür speichern wir den Tag als Merker (ohne weitere Angaben) und löschen den Merker nach 30 Tagen. Außerdem senden wir dem Kunden E-Mails zum Stand der Funktion (eingeschaltet, Ende des Wartungs-Abos mit der Möglichkeit, die Funktion zu behalten, abgeschaltet). Der Versand läuft über den E-Mail-Dienst der Hetzner Online GmbH (Abschnitt 8). Die E-Mails enthalten keine Werbung. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO, weil die Erinnerung zur Funktion gehört.</p>
|
||||
<p>Beim Aufruf einer Kundenwebsite mit Öffnungszeiten live lädt der Browser ein kleines Programm von unserem Server (project.webklar.com) und die Öffnungszeiten des Kunden. Das Programm ruft die Daten einmal je Seitenaufruf ab und danach höchstens alle zehn Minuten erneut. Dabei gelten die Protokolle in Abschnitt 3. Das Programm setzt keine Cookies, speichert nichts im Browser des Besuchers und lädt nichts von anderen Anbietern. Die Knöpfe „Route“ und „WhatsApp“ sind Links: Erst der Klick des Besuchers öffnet Google Maps oder WhatsApp; vorher übertragen wir nichts an diese Dienste, und danach gelten deren Datenschutzhinweise. Zum Schutz vor Missbrauch begrenzen wir die Abrufe je IP-Adresse (höchstens 120 je Minute); dafür merken wir uns die Zeitpunkte der Abrufe je IP-Adresse nur im Arbeitsspeicher des Servers, nicht dauerhaft. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere und störungsfreie Betrieb.</p>
|
||||
<p>Löschfristen: Haben Sie die Funktion im Wartungs-Abo ohne Aufpreis eingeschaltet und endet das Wartungs-Abo, läuft sie noch 30 Tage weiter; in dieser Zeit können Sie sie für einmalig 30 € behalten. Andernfalls schalten wir sie danach ab, bewahren Ihre Eingaben noch weitere 30 Tage auf und löschen sie dann; beginnt in dieser Zeit wieder ein Wartungs-Abo, schalten wir die Funktion mit den vorhandenen Eingaben wieder ein. Haben Sie die Funktion für 30 € bestellt, bleiben Ihre Eingaben, solange die Funktion läuft. Schalten wir sie auf Ihren Wunsch ab, löschen wir Ihre Eingaben auf Ihr Verlangen, spätestens mit der Löschung Ihres Kundenkontos. In unseren Datensicherungen bleiben gelöschte Daten noch bis zu sieben Tage erhalten und werden dann überschrieben. Die Daten liegen auf unserem Server bei Hetzner im Rechenzentrum Falkenstein (Deutschland), wie die übrigen Portal-Daten (Abschnitt 3).</p>
|
||||
```
|
||||
|
||||
## Ergänzung Abschnitt 6 (Mitarbeiter im Kundenauftrag)
|
||||
|
||||
Heute steht in Abschnitt 6 (wörtlich, `public/datenschutz.html`; der Satz ist durch `legal_datenschutz_kundenansicht` in `test/legalTexts.test.js` gepinnt):
|
||||
|
||||
> Um Sie zu unterstützen, können unsere Mitarbeiter Ihr Portal so öffnen, wie Sie es sehen, und dabei für Sie News-Beiträge anlegen, ändern oder löschen und, wenn Sie den Terminplaner nutzen, dessen Einstellungen und Termine ansehen und ändern.
|
||||
|
||||
Vorgeschlagen:
|
||||
|
||||
> Um Sie zu unterstützen, können unsere Mitarbeiter Ihr Portal so öffnen, wie Sie es sehen, und dabei für Sie News-Beiträge anlegen, ändern oder löschen, wenn Sie den Terminplaner nutzen, dessen Einstellungen und Termine ansehen und ändern sowie, wenn Sie Öffnungszeiten live nutzen, Ihre Öffnungszeiten, Hinweise und Kontaktangaben ansehen und ändern.
|
||||
|
||||
Der folgende Satz („Bestellungen, Zahlungen, Änderungen an Ihrem Passwort sowie Nachrichten oder Anträge in Ihrem Namen sind dabei ausgeschlossen.“) und das Protokoll (Mitarbeiter und Zeitpunkt, 12 Monate) bleiben unverändert und decken auch das Speichern der Zeiten durch Mitarbeiter ab (Protokollaktion `zeiten_gespeichert`). Beim Übernehmen die beiden Pins in `test/legalTexts.test.js` (`legal_datenschutz_kundenansicht`) auf den neuen Satz umstellen.
|
||||
|
||||
## Folgeänderung Abschnitt 7 (Nachweis beim Einschalten)
|
||||
|
||||
Abschnitt 7 nennt heute die Funktionen und den Klick auf „Zahlungspflichtig bestellen“. Beim Einschalten im Wartungs-Abo gibt es keine Zahlung und keine Stripe-Sitzung, gespeichert wird der Nachweis der Zustimmung trotzdem. Heute (wörtlich):
|
||||
|
||||
> Bestellen Sie eine Funktion (News-Pinnwand, Terminplaner), speichern wir als Nachweis die Texte und Fassungen, denen Sie zugestimmt haben, den Zeitpunkt Ihres Klicks auf „Zahlungspflichtig bestellen“, die Stripe-Sitzung, IP-Adresse und Browserkennung sowie Ihr Konto und die angemeldete Person. Rechtsgrundlagen sind Art. 6 Abs. 1 lit. b, c und f DSGVO; unser berechtigtes Interesse ist der Nachweis des Vertragsschlusses und der Zustimmungen, insbesondere für den Widerruf. Diese Angaben löschen wir drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet. Bestellen Sie ohne Abo (zum Beispiel nur die News-Pinnwand), löschen wir diese Angaben spätestens drei Jahre nach Ablauf des Jahres der Bestellung. Den Entwurf Ihres Fragebogens speichern wir bis zu 90 Tage nach Ihrer letzten Änderung und löschen ihn danach, wenn Sie nichts bestellt haben.
|
||||
|
||||
Vorgeschlagen (Ergänzung der Klammer und des Klicks; der Rest bleibt):
|
||||
|
||||
> Bestellen Sie eine Funktion (News-Pinnwand, Terminplaner, Öffnungszeiten live) oder schalten Sie Öffnungszeiten live im Wartungs-Abo ein, speichern wir als Nachweis die Texte und Fassungen, denen Sie zugestimmt haben, den Zeitpunkt Ihres Klicks auf „Zahlungspflichtig bestellen“ oder „Einschalten“, bei einer Bestellung die Stripe-Sitzung, jeweils die IP-Adresse und die Browserkennung sowie Ihr Konto und die angemeldete Person.
|
||||
|
||||
Diese Folgeänderung steht nicht in Spec 9 Nr. 3; sie ergibt sich daraus, dass der Gratisweg einen Nachweis speichert. Gegen den Code geprüft (`speichereNachweisAusAnfrage` in `server/services/featureConsents.js`, Zeilen 196 und 197): Der Gratisweg speichert IP-Adresse und Browserkennung genauso wie die Bestellung; nur die Stripe-Sitzung entfällt (ohne `sessionId` trägt der Nachweis keine). Der Satz nennt deshalb die Stripe-Sitzung nur bei der Bestellung, IP-Adresse und Browserkennung bei jeder Zustimmung.
|
||||
|
||||
## Offene Punkte für die Rechtsrunde
|
||||
|
||||
1. **Rolle bei Websites ohne Abo oder bei Fremdhosting.** Der Entwurf sagt wie 6c: Wir sind Auftragsverarbeiter der Kunden für die Besucherdaten (Server-Protokolle). Mit Abo deckt das Anlage 1 der Abo-Bedingungen, ohne Abo oder bei Fremdhosting besteht kein Auftragsverarbeitungsvertrag (gleiche Lücke wie bei der News-Pinnwand). Frage `ZQ1` in `zeiten-kanzlei-fragen.md`.
|
||||
2. **Löschung bei Abschaltung durch das Team** (Spec Abschnitt 5: keine automatische Löschung bei Grund `team`). Der Entwurf sagt „auf Verlangen, spätestens mit der Löschung des Kundenkontos“. Soll stattdessen eine feste Frist gelten (zum Beispiel 30 Tage wie beim Ende des Abos)? Entscheidung des Inhabers; dann auch Leistungsbeschreibung Nr. 9 Abs. 5 und das Programm (`abschalten.js`) anpassen.
|
||||
3. **Erinnerungs-E-Mail.** Rechtsgrundlage Art. 6 Abs. 1 lit. b DSGVO (Teil der Funktion). Es ist keine Werbung; ob sie wegen des Hinweises „bei Google auch eintragen“ als Werbung gilt, ist nicht anzunehmen, steht aber in der Kanzlei-Frage `ZQ6`.
|
||||
4. **Nachweis beim Einschalten** (Folgeänderung Abschnitt 7, siehe oben): beantwortet. Beim Einschalten im Wartungs-Abo werden IP-Adresse und Browserkennung ebenfalls gespeichert, nur die Stripe-Sitzung entfällt (der Satz ist entsprechend gefasst). Offen bleibt allein die rechtliche Prüfung des Satzes.
|
||||
5. **Kontaktdaten öffentlich.** Die Telefonnummer und Anschrift stehen im Seitencode der Kundenwebsite (Abschnitt 6e, zweiter Absatz). Das ist die Entscheidung des Kunden; die Leistungsbeschreibung (Nr. 6 Abs. 3) verlangt, nur veröffentlichungsfähige Angaben einzutragen.
|
||||
@@ -0,0 +1,43 @@
|
||||
# K02 und K03: Block `funktionen` mit Öffnungszeiten live (Entwurf)
|
||||
|
||||
Entwurf für die Rechtsrunde (Spec „Öffnungszeiten live“, Abschnitte 7 und 9; Task 33), Stand 05.10.2026. **Keine Codeänderung:** Der Block `funktionen` in `server/mail-templates/K02 Kuendigungsbestaetigung.html` und `K03 Eingang Kuendigung.html` ist Rechtstext mit Pins; dieser Entwurf schlägt den neuen Satz nur vor. Der Inhaber entscheidet, die Kanzlei sieht ihn mit der Mailrunde (Spec 9: Oberflächen- und Mailtexte). Erst danach werden die Vorlagen und die Pins geändert.
|
||||
|
||||
Beide Vorlagen sind Kündigungsbestätigungen an den Kunden und stehen in der Sie-Form. Der Block erscheint nur beim Wartungs-Abo (`server/routes/abo.js`: K02 mit `funktionen: isWartung(sub)`, K03 mit `funktionen: !storage && isWartung(sub)`), nicht bei Hosting-Abo und nicht bei der Kündigung von Zusatzspeicher.
|
||||
|
||||
## Heute (gepinnt)
|
||||
|
||||
Der Block besteht aus diesem Satzpaar, in beiden Vorlagen wörtlich gleich:
|
||||
|
||||
> Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren.
|
||||
|
||||
Pins, die der Satz heute hat:
|
||||
|
||||
- `test/mailTemplates.test.js`, Test „K02, K03: Block funktionen nennt Terminplaner und Löschfrist, nur mit Block (in K03 nur innerhalb von abo)“: prüft das ganze Satzpaar mit einem regulären Ausdruck (beginnend mit „Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm.“ bis „… ansehen und exportieren.“), dass der Block ohne `funktionen` fehlt und dass „Terminplaner“ bei Hosting-Abo und bei Speicher-Kündigung nirgends steht.
|
||||
- `test/publicForms.test.js`, Konstante `FUNKTIONEN_SATZ` (nur der erste Satz, „… und enden mit ihm.“).
|
||||
- Die Vorlagen-Kommentare (Zeile 12 beider Vorlagen) beschreiben den Block als „Hinweis, dass Website-Analyse und Terminplaner mit dem Wartungs-Abo enden“.
|
||||
|
||||
## Vorschlag
|
||||
|
||||
Der heutige Satz bleibt unverändert der Anfang (damit beide Pins ohne Änderung grün bleiben); angehängt wird ein Satz für Öffnungszeiten live, der die 30 Tage zum Behalten und die 30 Tage Datenfrist nach Z11 und Leistungsbeschreibung Nr. 9 nennt:
|
||||
|
||||
> Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren. Haben Sie Öffnungszeiten live im Wartungs-Abo eingeschaltet, läuft die Funktion nach dem Ende des Abos noch 30 Tage weiter; in dieser Zeit können Sie sie im Kundenportal für einmalig 30 € behalten. Behalten Sie sie nicht, schalten wir sie danach ab, auf Ihrer Website stehen dann wieder feste Öffnungszeiten, und Ihre Eingaben löschen wir 30 Tage später.
|
||||
|
||||
Warum so:
|
||||
|
||||
- Der Satz ist bedingt („Haben Sie … eingeschaltet“), weil der Block bei jeder Kündigung eines Wartungs-Abos erscheint, auch bei Kunden ohne die Funktion.
|
||||
- Wer die Funktion für 30 € bestellt hat, behält sie ohne Abo (Leistungsbeschreibung Nr. 9 Abs. 2 letzter Satz); für ihn gilt der Satz nicht, was die Bedingung „im Wartungs-Abo eingeschaltet“ ausdrückt. Ein eigener Satz dazu wäre in einer Kündigungsbestätigung verwirrend.
|
||||
- Die Einzelheiten (Frist mit Datum, Knopf „Behalten“) sagt K29 („Wartungs-Abo beendet“), sobald das Ende des Abos erkannt ist; K02 und K03 sagen nur, dass es das gibt.
|
||||
- „Abschalten“ bedeutet: Hinweis-Leiste, Kontakt-Leiste, Status und Google-Angaben verschwinden, die feste Ersatz-Tabelle der Website bleibt stehen (Leistungsbeschreibung Nr. 9 Abs. 4).
|
||||
|
||||
Im HTML ändert sich nur der Inhalt der Zelle zwischen `<!-- WENN funktionen -->` und `<!-- ENDE funktionen -->`; Aufbau und Blockkommentare bleiben. Die Kopfkommentare beider Vorlagen sagen dann „Website-Analyse, Terminplaner und Öffnungszeiten live“.
|
||||
|
||||
## Alternative
|
||||
|
||||
Ein eigener Block `zeiten` (nur für Kunden, bei denen die Funktion im Abo läuft): kein Text für Kunden ohne Funktion und die Zeile steht ohne „Haben Sie …“. Dafür braucht das Programm in `server/routes/abo.js` an beiden Stellen eine zusätzliche Abfrage der Funktion, beide Vorlagen einen neuen Block, und die Pins im Mail-Pin-Test (K01 bis K30) ändern sich. Nur sinnvoll, wenn der Inhaber den bedingten Satz nicht möchte.
|
||||
|
||||
## Beim Übernehmen
|
||||
|
||||
1. Vorlagen K02 und K03 ändern (Zelle des Blocks, Kopfkommentar).
|
||||
2. `test/mailTemplates.test.js` (K02/K03-Test): zusätzlicher Wortlaut-Pin für den neuen Satz; die bestehenden Prüfungen bleiben.
|
||||
3. Den Mail-Pin-Test (K01 bis K30, ebenfalls `test/mailTemplates.test.js`) und, falls sich der Block ändert, die Prüfung in `test/publicForms.test.js` nachziehen.
|
||||
4. Kanzlei-Frage `ZQ5` (Ende mit dem Abo, 30 Tage zum Behalten) in `zeiten-kanzlei-fragen.md` abwarten, weil sie den Inhalt des Satzes betrifft.
|
||||
@@ -0,0 +1,134 @@
|
||||
# Fragen an die Kanzlei: Öffnungszeiten live (⚖)
|
||||
|
||||
Stand 05.10.2026, Entwurf (Spec „Öffnungszeiten live“, Abschnitt 9, und Task 33). Dies sind die Fragen zu den Texten dieser Funktion; sie ergänzen das Kanzlei-Paket des Terminplaners (`docs/rechtliches/termine-kanzlei-paket.md`, Fragen Q1 bis Q26).
|
||||
|
||||
**Warum eine eigene Datei.** Das Kanzlei-Paket des Terminplaners liegt auf dem Zweig `feature/terminplaner-tp2` und ist in diesen Zweig (`feature/oeffnungszeiten`) noch nicht eingeflossen; ändern wir es hier, entsteht beim Zusammenführen ein Konflikt. Die Fragen tragen deshalb die Kennung `ZQ1` bis `ZQ12`. Beim Zusammenführen: als `Q27` bis `Q38` in Abschnitt 4 des Kanzlei-Pakets übernehmen, die Fundstellen in Abschnitt 5 ergänzen, die Fassung der Leistungsbeschreibung in Abschnitt 3.4 daneben stellen. In veröffentlichten Dateien stehen keine ⚖-Marker.
|
||||
|
||||
Jede Frage hat vier Teile wie im Paket des Terminplaners: **Frage** (was wir wissen möchten), **Fundstelle im Text** (wo der Wortlaut steht), **Unsere Annahme** (der Stand, mit dem wir arbeiten; bitte bestätigen oder korrigieren), **Alternative** (was wir stattdessen täten). Einordnungen wie „Annahme“ sind keine Rechtsurteile. Die Texte, um die es geht: `public/vertragsunterlagen/zeiten-leistungsbeschreibung.html` (Leistungsbeschreibung, im Folgenden „LB“), `docs/rechtliches/zeiten-datenschutz-6e-entwurf.md` (Abschnitt 6e), `docs/rechtliches/zeiten-k02-k03-entwurf.md` (K02/K03), `docs/funktionen/website-datenschutz-absatz.md` (Website-Absatz) und die Haken im Register (`server/config.js`, `features.zeiten`).
|
||||
|
||||
Reihenfolge der Rechtsrunde (gemeinsam mit TP2): gemeinsame Bausteine (TP2 Block F) → Leistungsbeschreibung → Datenschutz § 6e/§ 6 und Website-Absatz → Oberflächen- und Mailtexte → diese Fragen ans Kanzlei-Paket → Inhaber lädt die Fassungen in WOMS „Rechtliches“ hoch → Schalter `ZEITEN_ENABLED=on`.
|
||||
|
||||
### ZQ1: ⚖ AVV Funktionen per Haken?
|
||||
|
||||
**Frage.** Für Öffnungszeiten live verarbeitet WEBklar beim Aufruf einer Kundenwebsite Besucherdaten (IP-Adresse, Zeitpunkt, Browserangaben in Server-Protokollen, 14 Tage). Mit einem Wartungs-Abo deckt Anlage 1 der Abo-Bedingungen (Auftragsverarbeitung, Server-Protokolle der Website) das ab. Beim 30-€-Weg ohne Abo oder bei Websites, die woanders liegen, gibt es keinen Auftragsverarbeitungsvertrag; das ist dieselbe Lücke wie bei der News-Pinnwand. Soll sie mit einem zusätzlichen Haken „AVV Funktionen“ beim Bestellen und Einschalten geschlossen werden (wie der Haken für den AVV Terminplaner und den AVV Website-Analyse), und genügt ein gemeinsamer AVV „Funktionen“ für News-Pinnwand und Öffnungszeiten live? Oder ist WEBklar für die Protokolle des eigenen Servers ohnehin selbst Verantwortlicher, sodass kein AVV nötig ist?
|
||||
|
||||
**Fundstelle im Text.** Spec Abschnitt 9 Nr. 5; Register `features.zeiten.avv: null`; Abschnitt 6e Absatz 1 (Rolle als Auftragsverarbeiter) und Offener Punkt 1; Website-Absatz Variante B (`[Prüfen: AVV mit WEBklar erforderlich]`); LB Nr. 12 Abs. 2.
|
||||
|
||||
**Unsere Annahme.** Mit Abo genügt Anlage 1. Ohne Abo und bei Fremdhosting braucht es einen AVV; wir würden einen kurzen AVV „Funktionen“ per Haken einführen (Aufbau und Nummern wie AVV Terminplaner Nr. 4 bis 9, ohne Termindaten, Unterauftragsverarbeiter nur Hetzner).
|
||||
|
||||
**Alternative.** Nichts ändern, weil die IP-Adresse in Server-Protokollen beim Abruf eines Skripts von `project.webklar.com` in der Verantwortung des Betreibers dieses Servers liegt; oder Öffnungszeiten live nur mit Wartungs-Abo oder bei WEBklar-Hosting verkaufen; oder der Rahmen-AVV mit Anlage je Funktion (TP2 Spec 7.4 „Später“).
|
||||
|
||||
### ZQ2: Behalten-Zustimmung nutzt den Haken „Einbau beginnt“
|
||||
|
||||
**Frage.** Endet das Wartungs-Abo, kann der Kunde Öffnungszeiten live im Kundenportal für einmalig 30 € behalten (LB Nr. 9 Abs. 2). Der Weg „Behalten“ verlangt dieselbe Zustimmung wie die Neubestellung, also auch den zweiten Haken (`beginn`): „Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit vollständiger Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).“ Beim Behalten gibt es keinen Einbau; die Funktion läuft bereits, es entsteht kein neues Ticket. Passt der Haken dann? Ist das Behalten ein eigener Vertrag mit eigenem Widerrufsrecht für Verbraucher (Frist ab dem Klick), und was ist die „Leistung“, deren vollständige Erbringung das Widerrufsrecht erlöschen lässt (der Weiterbetrieb ohne Abo hat kein Ende)?
|
||||
|
||||
**Fundstelle im Text.** Register `features.zeiten.consent.items` (Haken `leistung` und `beginn`); Programm: `POST /api/funktionen/zeiten/behalten` (`pruefeZustimmung(feature, body)`: „die der Bestellung, beide Haken“), `server/services/zeiten/behalten.js`; Spec Abschnitt 3.3; LB Nr. 9 Abs. 2 und Nr. 11.
|
||||
|
||||
**Unsere Annahme.** Der Haken ist für das Behalten unschädlich, weil er nur das Verlangen des Beginns und die Kenntnis vom Erlöschen bestätigt. Der Wortlaut „mit dem Einbau“ ist aber ungenau; wir würden für das Behalten „mit der Leistung“ oder „mit dem Weiterbetrieb“ sagen. Der Widerruf eines Behalten-Vertrags führt zur Abschaltung (LB Nr. 11 Abs. 3).
|
||||
|
||||
**Alternative.** Ein eigener Haken für das Behalten („Ich verlange, dass die Funktion nach dem Ende meines Wartungs-Abos ohne Unterbrechung weiterläuft …“); oder kein Beginn-Haken, wenn für das Behalten kein Widerrufsrecht besteht (Kunde ist Unternehmer, Leistung sofort).
|
||||
|
||||
### ZQ3: Beginn-Haken der bezahlten Bestellung: Wertersatz-Satz und Fassung
|
||||
|
||||
**Frage.** Der Haken `beginn` der Bestellung für 30 € trug zunächst nur das Zitat zum Erlöschen des Widerrufsrechts. Nach der Abschlussprüfung ist der Satz zum Wertersatz ergänzt, wörtlich wie in den Haken der News-Pinnwand und des Terminplaners (TP2 Q3 und Q5). Er lautet jetzt: „Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit vollständiger Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).“ Die Fassung der Zustimmung ist dafür auf `2026-10-05` gesetzt (die übrigen Funktionen-Unterlagen tragen ebenfalls `2026-10-05`; die Gratis-Zustimmung `consentFrei` und die LB bleiben bei `2026-10-04`, weil ihr Text unverändert ist). Bitte bestätigen: (a) Genügt der Satz in dieser Form, auch mit „der Leistung“ statt „des Einbaus“ im ersten Satzteil (bei der News-Pinnwand steht „des Einbaus“)? (b) Dürfen die Fassungen je Funktion unterschiedlich nummeriert sein, oder soll der Nachweis eine gemeinsame Fassung der Funktionen-Unterlagen tragen? Die LB nennt den Wertersatz weiterhin in Nr. 11 Abs. 2.
|
||||
|
||||
**Fundstelle im Text.** Register `features.zeiten.consent` (Fassung `2026-10-05`, Haken `leistung` und `beginn`) und `consentFrei` (Fassung `2026-10-04`); LB Nr. 11 Abs. 2; Widerrufsbelehrung der Funktionen (Hinweise zum Widerruf, Wertersatz als Anteil am Gesamtpreis); Test `test/zeitenRegister.test.js` pinnt den Wortlaut und prüft, dass der Satz mit dem der News-Pinnwand und des Terminplaners übereinstimmt.
|
||||
|
||||
**Unsere Annahme.** Der Wertersatz-Satz gehört wie beim Terminplaner in den Haken (Frage Q3 des Terminplaners gilt entsprechend) und ist jetzt enthalten; der Wortlaut bleibt Entwurf bis zur Freigabe, danach wird die Fassung auf den Tag der Freigabe gesetzt (Pins in `test/zeitenRegister.test.js` mitziehen).
|
||||
|
||||
**Alternative.** Den Satz wieder aus dem Haken nehmen und nur in der Belehrung und der LB lassen; eine gemeinsame Fassung für alle Funktionen.
|
||||
|
||||
### ZQ4: Haftung, ausgeschrieben für Kunden ohne Abo (LB Nr. 8)
|
||||
|
||||
**Frage.** Die LB schreibt die Haftung aus, statt auf § 13 der Abo-Bedingungen zu verweisen, weil Kunden ohne Wartungs-Abo die Abo-Bedingungen nicht haben. Nr. 8 Abs. 4 und 5 geben § 13 Abs. 1 und 2 wörtlich wieder, Abs. 6 den Satz zugunsten der Mitarbeiter und Beauftragten (§ 13 Abs. 5) und den Hinweis, dass der Abschnitt auch für Kunden ohne Abo gilt. Abs. 1 bis 3 sagen: Die Live-Anzeige ist ein Hilfsmittel, die Richtigkeit der Angaben liegt beim Kunden, Fehler in Feiertagsdaten und Softwarefehler beseitigt WEBklar nach Meldung, für entgangenen Gewinn und Folgeschäden (etwa weil ein Besucher wegen einer Fehlanzeige vor verschlossener Tür steht) gelten Abs. 4 und 5. Fragen: (a) Ist die Klausel gegenüber Verbrauchern mit § 309 Nr. 7 BGB vereinbar und hinreichend transparent? (b) Braucht es gegenüber Unternehmern einen Höchstbetrag? (c) Fehlen § 13 Abs. 3 (Datenverlust, Wiederherstellungsaufwand) und Abs. 4 (kein Schadensersatz nach § 536a Abs. 1 BGB für anfängliche Mängel) für Kunden ohne Abo? (d) Ist die Zusage „Fehler beseitigt WEBklar in angemessener Frist“ (Nr. 7 Abs. 1, Nr. 8 Abs. 2) für Kunden ohne Abo zu weit?
|
||||
|
||||
**Fundstelle im Text.** LB Nr. 7 und Nr. 8; Abo-Bedingungen § 13 (`public/abo-bedingungen.html`); der Test `test/zeitenRecht.test.js` hält den Wortlaut beider Klauseln gleich; TP2 Q8 (Haftung für verpasste Termine).
|
||||
|
||||
**Unsere Annahme.** Die Spiegelung von § 13 Abs. 1 und 2 genügt; ein Höchstbetrag ist nicht nötig, weil Abs. 2 den Schaden auf den vertragstypischen, vorhersehbaren begrenzt. Die Abs. 3 und 4 sind für diese Funktion nicht nötig (die Funktion speichert keine Daten Dritter; ein Schadensersatz für anfängliche Mängel ist bei einem Einmalpreis von 30 € kein praktisches Thema).
|
||||
|
||||
**Alternative.** Abs. 3 und 4 des § 13 ebenfalls ausschreiben; einen Höchstbetrag (zum Beispiel den Preis der Funktion oder den Jahresbetrag des Wartungs-Abos) gegenüber Unternehmern festlegen.
|
||||
|
||||
### ZQ5: Ende mit dem Abo: 30 Tage zum Behalten, Abschalten, Rückkehr (LB Nr. 9)
|
||||
|
||||
**Frage.** Wer Öffnungszeiten live im Wartungs-Abo ohne Aufpreis eingeschaltet hat, behält die Funktion nach dem Ende des Abos noch 30 Tage; in dieser Zeit kann er sie für einmalig 30 € behalten, sonst schaltet WEBklar sie ab (auf der Website bleibt die feste Ersatz-Tabelle), die Eingaben bleiben noch weitere 30 Tage und werden dann gelöscht; beginnt in dieser Zeit wieder ein Wartungs-Abo, läuft die Funktion mit den Eingaben weiter. Wer sie für 30 € bestellt hat, behält sie auch ohne Abo. Fragen: (a) Ist die Kopplung der Gratis-Funktion an das Abo mit dieser Frist zulässig, obwohl sie ein „freiwilliges Extra“ ist und die Abo-Bedingungen unverändert bleiben (Z19)? (b) Darf die Rückkehr bei neuem Abo ohne neue Zustimmung automatisch erfolgen? (c) Frist und Anker der Löschung (30 Tage nach dem Abschalten) und der Sicherungsüberhang von sieben Tagen. (d) Bei Abschaltung durch WEBklar auf Wunsch des Kunden oder aus anderem Grund als dem Ende des Abos gibt es im Programm keine automatische Löschung; die LB sagt „auf Verlangen, spätestens mit dem Löschen des Kundenkontos“ (Nr. 9 Abs. 5). Genügt das, oder braucht es eine feste Frist? (e) Gehört das einmalige Aktualisieren der Ersatz-Tabelle beim Abschalten (Nr. 9 Abs. 4) zu einer Leistung, die WEBklar auch nach dem Ende des Abos schuldet?
|
||||
|
||||
**Fundstelle im Text.** LB Nr. 9 Abs. 1 bis 5, Nr. 4 Abs. 3; Abschnitt 6e (Löschfristen) und Offener Punkt 2; K02/K03 (`zeiten-k02-k03-entwurf.md`); Programm: `server/services/zeiten/aboAbgleich.js` (30 Tage Frist zum Behalten, 30 Tage Datenfrist), `abschalten.js`; Spec Z11 und Abschnitt 5; Parallele: TP2 Q7 und Q9 (Terminplaner endet mit dem Abo).
|
||||
|
||||
**Unsere Annahme.** Ja zu (a) bis (c): Der Kunde hat die Funktion nur durch das Abo ohne Aufpreis; die Frist zum Behalten und die Datenfrist sind angemessen, die Rückkehr stellt den früheren Stand wieder her. Zu (d) genügt „auf Verlangen“, weil die Eingaben Betriebsdaten sind und der Kunde die Löschung jederzeit verlangen kann. Zu (e) ja, weil es zum Einbau gehört, den der Preis enthält (Z20).
|
||||
|
||||
**Alternative.** Löschung auch bei Abschaltung durch das Team nach 30 Tagen (das Programm müsste dafür angepasst werden); keine Frist zum Behalten (Abschaltung mit dem Ende des Abos); Rückkehr nur auf Antrag.
|
||||
|
||||
### ZQ6: Mails zur Funktion: Erinnerung vor Feiertagen und Zustandsmails
|
||||
|
||||
**Frage.** WEBklar sendet dem Kunden E-Mails: K27 (eingeschaltet), K28 (Erinnerung 1 bis 7 Tage vor einem Feiertag oder einem Prüftag: „Stimmen Ihre Zeiten an diesen Tagen?“, mit einem Knopf zum Editor und dem Hinweis, die Zeiten auch bei Google einzutragen), K29 (Wartungs-Abo beendet, Frist, „für einmalig 30 € behalten“), K30 (abgeschaltet). Sind das Vertragsmails (Art. 6 Abs. 1 lit. b DSGVO) ohne Einwilligung, oder wirkt K28 wegen des Hinweises „bei Google auch eintragen“ oder des Verweises auf die Funktion als Werbung (§ 7 UWG, Art. 6 Abs. 1 lit. f DSGVO mit Widerspruchsmöglichkeit)? Muss K28 einen Hinweis zum Abbestellen tragen? Die Erinnerung lässt sich im Programm nicht abschalten; der Kunde kann nur die Funktion abschalten lassen.
|
||||
|
||||
**Fundstelle im Text.** Abschnitt 6e Absatz 3; LB Nr. 2 Abs. 2; Mailvorlagen K27 bis K30 (`server/mail-templates/`), Spec Abschnitt 7 (K28: eine Mail für alle neuen Tage, Merker, 08:00 bis 20:00 Uhr Berliner Zeit, kein BCC ans Team).
|
||||
|
||||
**Unsere Annahme.** Die Erinnerung ist Teil der vertraglichen Leistung (LB Nr. 2 Abs. 2) und deshalb eine Vertragsmail ohne Einwilligung; der Hinweis auf Google dient der Durchführung (die Zeiten dort aktuell zu halten), nicht der Werbung.
|
||||
|
||||
**Alternative.** Hinweis auf Google aus K28 streichen; einen Abbestell-Link („Erinnerungen ausschalten“) mit Einstellung im Editor vorsehen.
|
||||
|
||||
### ZQ7: Einstellung ohne Grund bei Unternehmern im Gratisweg (LB Nr. 10 Abs. 4)
|
||||
|
||||
**Frage.** Die LB folgt in Nr. 10 der Einstellungsregel der Website-Analyse (Nr. 8 dort): Beenden oder Einstellen mit vier Wochen oder drei Monaten nur bei Rechtslage oder nicht behebbarer Sicherheitslücke (Abs. 2), zusätzlich nach Abs. 4 **ohne** besonderen Grund mit drei Monaten gegenüber Unternehmern, die die Funktion im Wartungs-Abo ohne Aufpreis haben; der Kunde kann das Abo dann innerhalb von 30 Tagen zum Einstellungszeitpunkt kündigen, auch vor Ablauf der Mindestlaufzeit. Wer die Funktion für 30 € bestellt hat, kann sie nur aus den Gründen von Abs. 2 verlieren (Abs. 5). Fragen: (a) Ist die Einstellung ohne Grund für ein kostenloses Zusatzangebot zulässig, auch wenn die Funktion im Abo „enthalten“ beworben wird (Z4)? (b) Ändert das Sonderkündigungsrecht in Abs. 4 die Abo-Bedingungen so, dass das Verfahren nach § 15 nötig wäre (Z19: freiwilliges Extra, keine Änderung der Abo-Bedingungen)? (c) Braucht es bei Einstellung einer gekauften Funktion durch WEBklar eine (anteilige) Erstattung des Preises (vgl. TP2 Q8)?
|
||||
|
||||
**Fundstelle im Text.** LB Nr. 10 Abs. 1 bis 5; Abo-Bedingungen §§ 3 Abs. 7, 8, 15; LB der Website-Analyse Nr. 8 (`docs/rechtliches/leistungsbeschreibung-website-analyse.md`); Spec Abschnitt 9 Nr. 1 („Einstellung/Änderung nach Vorbild Analyse Nr. 8“).
|
||||
|
||||
**Unsere Annahme.** Ja zu (a) und (b) wie bei der Analyse: Das Sonderkündigungsrecht stellt den Kunden besser; die Abo-Bedingungen bleiben unverändert. Zu (c): Bei Einstellung aus den Gründen von Abs. 2 erstattet WEBklar nichts, weil die Funktion bis dahin lief; die Rechte aus § 327r BGB bleiben.
|
||||
|
||||
**Alternative.** Abs. 4 streichen (Einstellung nur aus Gründen); bei gekauften Funktionen anteilig erstatten.
|
||||
|
||||
### ZQ8: Website-Absatz: WhatsApp und Google Maps erst nach Klick, Haken als Nachweis
|
||||
|
||||
**Frage.** (a) Der Absatz „Öffnungszeiten auf dieser Website“ (Variante A und B) sagt, dass die Knöpfe „Route“ und „WhatsApp“ einfache Links sind, dass Google Maps und WhatsApp erst nach dem Klick geöffnet werden und dass vorher nichts an Google oder Meta übertragen wird; das Skript setzt keine Cookies und speichert nichts im Browser. Genügt das, oder braucht es für diese Links eine Einwilligung (§ 25 TDDDG, Art. 6 Abs. 1 lit. a DSGVO)? (b) Der Kunde bestätigt im Portal mit einem Haken, dass er den „WhatsApp-Hinweis“ (Baustein WhatsApp) in seine Datenschutzerklärung aufgenommen hat; der Zeitpunkt wird gespeichert, Mitarbeiter setzen den Haken in der Kundenansicht nicht. Reicht der Haken als Nachweis? (c) Wenn WEBklar den Absatz beim Einbau in die Datenschutzerklärung einfügt (LB Nr. 6 Abs. 4), ist das Einfügen eines Mustertextes eine Rechtsdienstleistung (§ 2 RDG)? Das ist dieselbe Regel wie TP2 Q18 und soll für alle Funktionen gleich bleiben. (d) Die Angaben im Seitencode (strukturierte Daten zu den Öffnungszeiten) enthalten keine Daten von Besuchern; ist ein Satz dazu nötig?
|
||||
|
||||
**Fundstelle im Text.** `docs/funktionen/website-datenschutz-absatz.md` (Variante A und B, Bausteine Route und WhatsApp); LB Nr. 2 Abs. 4, Nr. 6 Abs. 3 und 4; Abschnitt 6e Absatz 4; Programm: `public/embed/zeiten.js` (keine Cookies, kein Storage, keine Anfragen an Dritte; Links erst beim Klick), Spec Z23 (Route öffnet Google Maps erst beim Klick).
|
||||
|
||||
**Unsere Annahme.** (a) Ein Link, den der Besucher anklickt, ist keine Übertragung durch WEBklar oder den Kunden vor dem Klick; eine Einwilligung ist nicht nötig. (b) Der Haken genügt als Nachweis. (c) Wie TP2 Q18: Technik und Mustertext, keine Rechtsdienstleistung im Einzelfall. (d) Ein Satz genügt, wie im Entwurf.
|
||||
|
||||
**Alternative.** Vor dem ersten Klick einen Hinweis einblenden („Sie verlassen diese Website“); die Schaltfläche „WhatsApp“ nur auf ausdrücklichen Wunsch einschalten.
|
||||
|
||||
### ZQ9: Vertragsrahmen ohne Abo (LB Nr. 1 Abs. 3)
|
||||
|
||||
**Frage.** Für Kunden ohne Wartungs-Abo gilt die LB „allein“. Sie regelt Leistung, Preis, Haftung, Ende, Einstellung und Widerruf, verweist für den Widerruf auf die Belehrung der Funktionen und für Datenschutz auf die Datenschutzerklärung. Fehlen Regeln, die sonst die Abo-Bedingungen liefern (Zahlung und Zahlungsverzug, Mängelrechte für digitale Dienstleistungen §§ 327 ff. BGB, Verjährung, Gerichtsstand, Textform, Änderungen der Bedingungen)? Sollen die Abo-Bedingungen auch für Kunden ohne Abo teilweise vereinbart werden (Verweis auf bestimmte Paragrafen), oder reichen die gesetzlichen Regeln?
|
||||
|
||||
**Fundstelle im Text.** LB Nr. 1 Abs. 3, Nr. 4 (Zahlung über Stripe), Nr. 7 Abs. 1 (Mängel in angemessener Frist), Nr. 8 (Haftung ausgeschrieben); Spec Abschnitt 9 Nr. 1 und Z19.
|
||||
|
||||
**Unsere Annahme.** Die gesetzlichen Regeln genügen für eine Einmalleistung von 30 €; die Haftung ist wegen der AGB-Kontrolle ausgeschrieben.
|
||||
|
||||
**Alternative.** Ausgewählte Paragrafen der Abo-Bedingungen (Zahlung, Mängel, Textform, Änderungen) in die LB aufnehmen oder auf sie verweisen.
|
||||
|
||||
### ZQ10: Hinweis zu Preisangaben in der Hinweis-Leiste
|
||||
|
||||
**Frage.** Der Editor zeigt beim Anlegen eines Hinweises, dessen Text „%“ oder „statt“ enthält, einen Hinweis zu Preisangaben (Endpreise, bei Waren der niedrigste Preis der letzten 30 Tage). LB Nr. 6 Abs. 2 verpflichtet den Kunden, Preise und Rabatte in Hinweisen rechtmäßig anzugeben, zum Beispiel als Endpreise und „bei Preisermäßigungen für Waren, mit dem niedrigsten Preis der letzten 30 Tage“; Nr. 12 Abs. 1 schließt eine Rechtsberatung aus. Sind diese Aussagen zutreffend und ausreichend, insbesondere die Beschränkung der 30-Tage-Regel auf Waren (Preisangabenverordnung) und die Abgrenzung, dass die Angaben des Kunden nicht von WEBklar geprüft werden (Nr. 8 Abs. 1)? Braucht es einen Hinweis zur Preisangabe auch für Leistungen, die Dienstleistungen sind?
|
||||
|
||||
**Fundstelle im Text.** LB Nr. 6 Abs. 2, Nr. 8 Abs. 1, Nr. 12 Abs. 1; Spec Abschnitt 3.4 Nr. 4 (Hinweis-Leiste); die genaue Zeile im Portal-Editor steht in `public/zeiten-leisten.js`.
|
||||
|
||||
**Unsere Annahme.** Der Hinweis im Editor und die Pflicht in der LB reichen als Warnung; die Prüfung bleibt beim Kunden. Die Zitate zur Preisangabenverordnung werden mit der Freigabe geprüft.
|
||||
|
||||
**Alternative.** Keinen Hinweis im Editor, nur die Pflicht in der LB; oder den Hinweis auf Preisermäßigungen insgesamt ausweiten.
|
||||
|
||||
### ZQ11: Aktivierung durch das Team ohne Zustimmung des Kunden
|
||||
|
||||
**Frage.** Das Team kann Öffnungszeiten live im WOMS für einen Kunden einschalten (Aktion „Aktivieren“, auch aus dem Zustand „abgeschaltet“). Das Programm speichert dabei keinen Zustimmungsnachweis (`einschaltenZeiten` mit `by: 'team'` ohne `nachweis`; Spec Abschnitt 5 und Ruling R32 lassen die Zustimmung für diesen Weg entfallen). Die LB setzt aber voraus, dass der Kunde den Haken setzt und auf „Einschalten“ oder „Zahlungspflichtig bestellen“ klickt (Nr. 1 Abs. 1). Beim Terminplaner verlangt TP2 für die Aktivierung durch das Team die Zustimmung des Kunden in Textform (F13a, `textformFuerAktivierung` in `server/routes/admin/funktionenAdmin.js` auf dem Zweig `feature/terminplaner-tp2`, Zeile 99: `if (key !== 'termine') return null`; die Prüfung gilt also nur für `termine`, und dort nur im Modus „on“, nicht für Pilotadressen). Soll diese Regel auf `zeiten` ausgeweitet werden (Kunde bestätigt in Textform, das Team vermerkt es), oder genügt es, dass die Funktion im Abo ohne Aufpreis ein freiwilliges Zusatzangebot ist und der Kunde die Aktivierung durch das Team ausdrücklich wünscht (Chat, Telefon)? Gilt das auch, wenn das Team ein Dokument wieder einschaltet, das der Kunde einmal gekauft hat?
|
||||
|
||||
**Fundstelle im Text.** LB Nr. 1 Abs. 1 und 2; Spec Abschnitt 5 (Team-Aktivierung), Ruling R32; Programm: `server/services/zeiten/einschalten.js` (`by: 'team'`), `server/routes/admin/funktionenAdmin.js` (`zeitenAktivieren`); TP2 F13a (Textform bei Aktivierung durch das Team, nur Terminplaner).
|
||||
|
||||
**Unsere Annahme.** Für die Gratis-Funktion genügt der ausdrückliche Wunsch des Kunden, den das Team im Feld „Notiz“ vermerkt; ein Zustimmungsnachweis fehlt dann bewusst. Bei einem Kunden ohne Wartungs-Abo wäre die Textform sauberer.
|
||||
|
||||
**Alternative.** Die Textform-Prüfung von TP2 auch für `zeiten` verlangen (Aktivieren nur mit Hinweis „Kunde hat in Textform zugestimmt“ und Notiz); die Aktivierung durch das Team nur für Kunden mit laufendem Wartungs-Abo zulassen.
|
||||
|
||||
### ZQ12: Pflichtinformationen als Haken und in K15
|
||||
|
||||
**Frage.** Beim Terminplaner gibt es neben `beginn` den Haken `information` („Ich habe die Pflichtinformationen zu dieser Bestellung gelesen: Leistung …, Gesamtpreis …, voraussichtliche Dauer …“; `server/config.js`, `features.termine.consent`, TP2 Q20), und K15 (Bestätigung der Bestellung) trägt die Pflichtinformationen als Anhang. Für Öffnungszeiten live gibt es beides nicht: Das Register hat nur die Haken `leistung` und `beginn`, und K15 trägt für diese Funktion keine Vertragsunterlagen (der Block `anlagen` mit Fassung, Links und PDF-Anhang entfällt, weil das Register weder `avv` noch `widerruf` hat und `unterlagenAdressen` in `server/services/siteFeatures.js` dann `null` liefert; die Mail nennt nur Funktion, Preis und Dauer im Text). Genügt das für einen Verbrauchervertrag im elektronischen Geschäftsverkehr (§ 312d BGB, Art. 246a EGBGB, § 312j BGB: Information vor Vertragsschluss, Bestätigung auf einem dauerhaften Datenträger), weil die LB und die Bestellseite die Pflichtinformationen enthalten, oder braucht es wie beim Terminplaner einen eigenen Haken `information` und die Pflichtinformationen als Anhang oder Abschnitt der K15?
|
||||
|
||||
**Fundstelle im Text.** Register `features.zeiten.consent` (Haken `leistung`, `beginn`) gegen `features.termine.consent` (Haken `information`); Bestellseite `public/zeiten-bestellen.js` (Preis, Belehrung, Leistungsbeschreibung vor dem Knopf); K15 (`server/mail-templates/K15 Bestellung Funktion.html`, `bestellMail` und `unterlagenAdressen` in `server/services/siteFeatures.js`); LB Nr. 2 bis 4 und 11; TP2 Q20.
|
||||
|
||||
**Unsere Annahme.** Die Bestellseite zeigt Leistung, Preis, Umsatzsteuerhinweis, Widerrufsbelehrung und LB-Link vor dem Knopf; das genügt für die vorvertragliche Information. Ob die Bestätigungsmail (K15) die Pflichtinformationen und die Belehrung zusätzlich tragen muss (Textform auf einem dauerhaften Datenträger), ist offen; für den Terminplaner hat TP2 es mit dem Block `anlagen` gelöst.
|
||||
|
||||
**Alternative.** Wie beim Terminplaner: Haken `information` im Register (Fassung hochzählen) und die Pflichtinformationen als Abschnitt in K15.
|
||||
|
||||
## Nicht Teil der Kanzleiprüfung
|
||||
|
||||
- Der Hinweis in der Hinweis-Leiste („Aktion“ verlangt ein Enddatum, Texte höchstens 140 Zeichen) ist Oberfläche, keine Rechtsaussage.
|
||||
- Technische Beobachtung beim Schreiben der Texte (kein Kanzlei-Thema, inzwischen behoben): Die öffentliche Widerrufsseite `widerrufen.html` kannte die Auswahl „Terminplaner (Einrichtung)“, aber keine „Öffnungszeiten live“; ein Verbraucher, der sie für 30 € bestellt hat, hätte „Funktion (z. B. News-Pinnwand)“ wählen müssen, und die Abwicklung hätte das der News-Pinnwand zugeordnet. Die Auswahl „Öffnungszeiten live“ gibt es jetzt (`ORDERS`, `FEATURE_OF_ORDER` und `orderLabel` in `server/routes/abo.js`, Radioknopf mit `data-text` in `public/widerrufen.html`).
|
||||
Loaded 100 of 1260 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user