Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0be345fe2b | ||
|
|
6e947b1d73 | ||
|
|
16da455633 | ||
|
|
a460ec3ee6 | ||
|
|
39faad9624 | ||
|
|
41a5958184 | ||
|
|
4f03a16d25 | ||
|
|
bb7393653e | ||
|
|
14b51dcd67 | ||
|
|
d71f75b0f2 | ||
|
|
f81c5c6734 | ||
|
|
965105913f | ||
|
|
574e638ed0 | ||
|
|
39407585f2 | ||
|
|
3575a35877 |
No files matched your search
@@ -1,8 +0,0 @@
|
||||
.git
|
||||
.gitea
|
||||
.DS_Store
|
||||
node_modules
|
||||
*.log
|
||||
leads
|
||||
gamejam
|
||||
public/gamejam-games
|
||||
@@ -1,62 +0,0 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: toolctrl-ci
|
||||
steps:
|
||||
- name: Setup hosts override and Docker CLI
|
||||
run: |
|
||||
echo "195.201.133.26 git.hg-int.dev" >> /etc/hosts
|
||||
apt-get update -qq
|
||||
apt-get install -qq -y --no-install-recommends ca-certificates curl git
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | tee /etc/apt/keyrings/docker.asc >/dev/null
|
||||
chmod a+r /etc/apt/keyrings/docker.asc
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list >/dev/null
|
||||
apt-get update -qq
|
||||
apt-get install -qq -y --no-install-recommends docker-ce-cli docker-buildx-plugin
|
||||
docker --version
|
||||
docker buildx version
|
||||
|
||||
- name: Checkout
|
||||
run: |
|
||||
mkdir -p /workspace/repo
|
||||
cd /workspace/repo
|
||||
git clone "https://${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_PAT }}@git.hg-int.dev/${{ github.repository }}.git" .
|
||||
git checkout "${{ github.sha }}"
|
||||
|
||||
- name: Static checks
|
||||
run: |
|
||||
cd /workspace/repo
|
||||
node --check server.js
|
||||
git diff --check
|
||||
|
||||
- name: Login to Gitea Registry
|
||||
run: echo "${{ secrets.REGISTRY_PAT }}" | docker login git.hg-int.dev --username "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
|
||||
- name: Build and push image
|
||||
env:
|
||||
IMAGE: git.hg-int.dev/${{ github.repository }}
|
||||
SHA: ${{ github.sha }}
|
||||
run: |
|
||||
cd /workspace/repo
|
||||
SHORT_SHA=${SHA:0:7}
|
||||
IMAGE_LC=$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]')
|
||||
docker build \
|
||||
--build-arg APP_REVISION="$SHA" \
|
||||
-t "$IMAGE_LC:$SHORT_SHA" \
|
||||
-t "$IMAGE_LC:main" \
|
||||
.
|
||||
docker push "$IMAGE_LC:$SHORT_SHA"
|
||||
docker push "$IMAGE_LC:main"
|
||||
|
||||
- name: Trigger Coolify deploy
|
||||
run: |
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}" \
|
||||
"https://saas-ctrl.com/api/v1/applications/ocwtt7rd2zijtlszgua005k9/start"
|
||||
@@ -1,35 +0,0 @@
|
||||
FROM node:22-bookworm-slim
|
||||
|
||||
LABEL org.opencontainers.image.description="ToolCTRL Website"
|
||||
|
||||
ARG APP_REVISION=local
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
PORT=3000 \
|
||||
HOST=0.0.0.0 \
|
||||
DATA_DIR=/app/data \
|
||||
CLAMAV_ENABLED=false \
|
||||
APP_REVISION=${APP_REVISION}
|
||||
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates curl \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& groupadd --gid 1001 nodejs \
|
||||
&& useradd --uid 1001 --gid nodejs --home-dir /app --shell /usr/sbin/nologin nodejs
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --chown=nodejs:nodejs public ./public
|
||||
COPY --chown=nodejs:nodejs server.js ./server.js
|
||||
|
||||
RUN mkdir -p /app/data \
|
||||
&& chown -R nodejs:nodejs /app/data
|
||||
|
||||
USER nodejs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
CMD curl -fsS http://127.0.0.1:3000/healthz >/dev/null || exit 1
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -83,22 +83,6 @@ Host/Port und An/Aus stehen in `server.js` im Objekt `CLAMAV`. Mit
|
||||
`enabled: false` läuft der Upload ohne Virenscan (nur für lokale Tests
|
||||
ohne ClamAV gedacht).
|
||||
|
||||
Per Environment kann der Runtime-Betrieb konfiguriert werden:
|
||||
|
||||
```bash
|
||||
PORT=3000
|
||||
HOST=0.0.0.0
|
||||
DATA_DIR=/app/data
|
||||
CLAMAV_HOST=127.0.0.1
|
||||
CLAMAV_PORT=3310
|
||||
CLAMAV_ENABLED=true
|
||||
APP_REVISION=<git-sha>
|
||||
```
|
||||
|
||||
`/healthz` ist die leichte Liveness-Probe für Container und Coolify.
|
||||
`/readyz` prüft zusätzlich Schreibzugriff auf die Datenverzeichnisse und,
|
||||
falls aktiv, die Erreichbarkeit von ClamAV.
|
||||
|
||||
### Funktion testen
|
||||
|
||||
Eine Datei mit der EICAR-Testsignatur hochladen — `clamd` meldet dann
|
||||
@@ -109,17 +93,3 @@ Eine Datei mit der EICAR-Testsignatur hochladen — `clamd` meldet dann
|
||||
Für den QR-Code am Stand muss der Rechner im selben WLAN erreichbar sein.
|
||||
Soll die Seite öffentlich laufen, hinter einen Reverse-Proxy mit HTTPS setzen
|
||||
oder bei einem Hoster bereitstellen — `tool-ctrl.de`.
|
||||
|
||||
## Deployment
|
||||
|
||||
Deployment folgt dem Gitea-Infra-Standard aus `toolctrl-labs/infra`:
|
||||
|
||||
1. Push auf `main` triggert `.gitea/workflows/deploy.yml`.
|
||||
2. Gitea Actions baut das Docker-Image und pusht es nach
|
||||
`git.hg-int.dev/toolctrl-labs/toolctrl-website`.
|
||||
3. Tags: kurzer Commit-SHA und `main`.
|
||||
4. Coolify-App `toolctrl-website` (`ocwtt7rd2zijtlszgua005k9`) zieht
|
||||
`:main` auf der Marketing-VM und startet neu.
|
||||
|
||||
Die Coolify-App läuft im Projekt `Marketing`, Environment `production`, Server
|
||||
`marketing-vm`, Port `3000`, mit persistentem Storage auf `/app/data`.
|
||||
|
Before Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 292 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="2300" height="2000" version="1.0">
|
||||
<path style="fill:#666" d="M1157.3928 199.37139c-.1561-.351-3.243-2.137-6.86-3.969-9.658-4.894-20.13-13.0491-32.228-25.098-10.972-10.9291-12.004-12.329-31.6471-42.912-3.1689-4.934-6.796-9.645-8.059-10.468-2.087-1.36-4.58-1.469-27.4779-1.206-27.382.315-37.043-.412-49.9371-3.755-18.12509-4.699-31.09801-12.288001-48.08206-28.123001-12.75503-11.893101-19.10594-18.966001-21.23697-23.651002-2.06203-4.532-1.98498-5.791.53398-8.781 2.6-3.086 6.57501-4.736 15.86602-6.585101 13.339-2.655 25.82102-10.881 42.01594-27.690001 27.24019-28.273002 49.92309-66.238004 62.40219-104.442006 22.544-69.020899 40.4279-213.453909 37.524-303.043919-2.466-76.05-9.554-108.479-24.358-111.436-9.0331-1.804-18.294 13.5529-25.24 41.853-4.2181 17.189-5.106 25.236-4.5591 41.309.538 15.8 2.3111 27.0349 7.9171 50.1769 2.126 8.7731 3.91 17.91601 3.964 20.31811.0869 3.815-.208 4.63-2.344 6.465-2.194 1.885-2.952 2.046-7.4181 1.58-15.831-1.652-31.538-10.826-59.80409-34.93001-15.40394-13.135-19.29891-15.761-23.38693-15.761-2.95903 0-3.14901.199-6.29802 6.588-8.40105 17.0499-30.92794 55.05191-47.98398 80.94701-13.42402 20.38-22.71602 33.032-32.47906 44.2229-3.87397 4.4421-7.98798 9.9231-9.14197 12.1802-6.07798 11.8959-1.32096 20.9719 10.99301 20.9719 6.33098 0 8.096-.788 21.70402-9.696 22.85602-14.963 39.85203-22.2741 53.95693-23.209 8.87509-.588 12.904.298 23.27309 5.118l7.66091 3.561 19.15103.04c17.53206.037 19.57116.1981 24.12316 1.894 6.138 2.287 14.5129 8.167 19.3109 13.5561 8.6371 9.7009 12.969 25.2479 11.197 40.18-1.724 14.5259-6.863 30.14591-13.6879 41.60681-3.3271 5.586-8.8301 11.9549-10.3311 11.9549-.4389 0-1.3849-1.4029-2.1029-3.1169-4.116-9.839-18.2071-26.49491-25.32818-29.93691-6.63397-3.208-9.25091-2.355-13.80897 4.50291-4.31795 6.4961-11.66698 13.4991-17.31103 16.494-10.31098 5.471-20.75796 6.758-43.13297 5.3169-18.58193-1.1979-30.32704-2.6529-49.36797-6.116-16.37707-2.9788-27.53798-3.3508-35.65998-1.1878-18.03399 4.8009-36.86601 22.6599-44.82301 42.5049-2.58505 6.447-2.691 7.1669-2.30905 15.711999.33105 7.418.85698 10.122 3.06498 15.755001 4.01603 10.251 8.26199 16.805001 17.01502 26.272001 22.32096 24.140101 33.88898 45.9380026 32.06408 60.4190034-.82907 6.5740006-3.50912 15.9820006-7.87506 27.6411016-4.90099 13.088001-5.04206 16.205001-1.07203 23.644001 4.27402 8.008001 10.37102 12.179001 22.72202 15.544001 3.66306.998 7.727 2.361 9.03202 3.029l2.37399 1.213-1.94803 3.472c-3.04497 5.426901-13.34996 14.474001-21.03296 18.466001-16.38598 8.514001-36.79802 9.508001-50.62102 2.464901-5.55703-2.830901-12.35897-10.466901-21.73599-24.397902-4.84702-7.202-10.50901-14.717001-12.581-16.700001-2.072-1.9821-8.01201-5.844-13.20101-8.5821-15.26999-8.056001-20.24599-13.750001-25.33299-28.990002-4.44-13.3020004-3.735-22.24700093 3.432-43.580902l3.551-10.571101-.361-11.07c-.299-9.210001-.692-11.825001-2.334-15.553001-2.315-5.259-5.55-8.2601-9.749-9.047001-9.155-1.715-14.093 9.350001-20.453 45.831003-5.554 31.858002-10.86401 47.075002-19.73301 56.545003-5.493 5.864-10.652 7.8711-21.374 8.315-7.90001.327-8.901.557-10.83201 2.485001-2.55899 2.5561-3.392 6.394-2.8 12.9101.544 5.993901 3.427 11.837901 9.095 18.435001 4.89802 5.701 8.06201 12.948001 7.25501 16.616001-2.156 9.807001-25.854 10.220001-45.97801.802-12.186-5.703-21.496-13.202001-27.711-22.322001-6.911-10.140001-8.373-17.960001-5.508-29.443102 1.931-7.735 1.489-13.448-1.373-17.782001-10.632-16.0949005-7.949-38.661002 6.365-53.549002 4.343-4.515901 6.944-6.245001 16.70701-11.105001 14.61-7.274 20.08099-10.911001 25.97599-17.270001 7.886-8.507 14.34-24.869101 12.879-32.649002-.793-4.219-5.715-9.252-10.288-10.5201-5.23-1.449999-13.107-1.133999-17.121.686-2.113.9591-7.73099 5.8041-14.044 12.1121-11.247 11.239001-15.697 14.228001-24.549 16.493001-4.987 1.276-5.901 1.263-13.955-.198-4.74-.861-11.361-1.564-14.713-1.564-5.524 0-6.399.244-9.362 2.61-3.855 3.078-8.229 10.837001-10.054 17.832901-.794 3.0461-1.302 8.714101-1.302 14.548101 0 10.604101-.879 13.648001-4.181 14.476001-4.816 1.207-20.202-4.755-29.09501-11.274001-5.61099-4.114-15.18499-14.536001-18.52999-20.172001-3.869-6.519-7.51701-17.451001-8.50501-25.482001-2.018-16.408001 5.87201-31.347 31.75101-60.1181 8.25799-9.1809 10.45099-12.8749 12.993-21.8908 3.563-12.63401 7.642-18.76101 16.86801-25.33611 6.08499-4.3359 11.93499-6.6639 20.05399-7.98 11.144-1.8059 22.516-.5989 55.681 5.9061 26.121 5.1231 38.328 6.448 47.582 5.163 8.37101-1.163 11.32702-2.588 23.51201-11.331 5.398-3.873 20.82001-13.675 34.27001-21.7809 27.81699-16.7641 29.583-18.2981 38.826-33.7191 2.99999-5.007 14.814-27.512 26.25297-50.0111 29.19398-57.42191 39.50902-76.45991 55.78305-102.96191 8.03497-13.084 9.79202-16.917 10.49498-22.8941.39199-3.3289.15103-5.2619-.943-7.564-2.68702-5.6559-8.75595-7.942-27.34401-10.2989-5.66996-.7191-13.71299-2.2851-17.87399-3.4791-9.99703-2.8689-22.15399-8.8389-25.86504-12.702-2.69697-2.806-2.83697-3.1799-1.47496-3.9459 12.07896-6.7901 43.73999-36.978 71.21204-67.899 50.11601-56.40801 88.41093-118.72001 116.41995-189.43401 13.96399-35.25401 17Line truncated
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 120" fill="none">
|
||||
<path d="M48 92h68c17 0 30-12 30-29 0-16-12-29-28-29h-4C107 18 93 8 75 8 50 8 31 27 31 52v3C18 58 10 68 10 81c0 7 3 11 8 11h12" stroke="#666" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24 108 136 12" stroke="#666" stroke-width="13" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 377 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 57 89">
|
||||
<path fill="#666" d="m18.4003 8.5c-6.2 4.2-7.029 12.8268-1.7007 18.3999l2.2002 2.3013-3.7002 3.5991c-3.7898 3.6863-4.9641 6.3366-4.6991 10.8505.265 4.5138 4.0514 9.3178 11.7773 14.9176 11.8224 8.5692 12.5212 9.3018 12.6006 12.2007.0667 2.4333-1.8696 5.0283-3.7002 6.0996-4.0361 2.362-9.6607.1086-8.6001-4.8999.7395-3.4919-1.0001-6-4.1001-6-4.2 0-6.0102 4.6048-3.6006 9.0996 2.7904 5.2052 15.9001 6.8001 22.1001 2.6001s7.0308-12.8285 1.7007-18.3999l-2.2002-2.2998 3.7002-3.6006c3.4302-3.3378 4.6235-5.9098 4.8299-10.2809.2064-4.3713-3.6116-9.4751-11.9081-15.4872-11.8234-8.5678-12.5448-9.2998-12.6006-12.1992-.0471-2.4515 1.5375-4.8693 3.7002-6.0996 3.9056-2.2218 9.6607-.1086 8.6001 4.8999-.7395 3.4919 1.0001 6 4.1001 6 4.2 0 6.0066-4.6051 3.5991-9.1011-1.7425-3.254-7.523-5.0933-12.0011-5.0136-4.4781.0797-7.7725.8385-10.0975 2.4135zM21.1 31l6.3999 4.8003c3.4879 2.6162 7.2523 5.6546 8.2002 6.7998 2.5504 3.0815 2.7671 7.4482.3779 10.3682l-1.8003 2.2002-6.3999-4.8003c-3.4879-2.6162-7.2493-5.6571-8.2002-6.7998-2.9396-3.5328-2.5681-7.6916-.3779-10.3682z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 565 KiB |
|
Before Width: | Height: | Size: 432 KiB |
|
Before Width: | Height: | Size: 613 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 573.11 136.81">
|
||||
<g transform="translate(0 -45.51)">
|
||||
<path d="M40.05,168.37c-4.3,2.14-10,3.92-14.3,3.92-12.86,0-15.73-8.03-15.73-19.64v-51.8h29.48v-9.1H10.02v-26.42l-9.51,2.09c-.29.07-.51.34-.51.63v9.22h.07v49.8h-.07v25.6c0,18.4,6.61,28.95,25.19,28.95,6.08,0,12.33-1.8,18.22-4.65l-3.4-8.57.03-.02Z"/>
|
||||
<path d="M94.52,89.96c-26.99,0-45.57,19.47-45.57,45.91s18.58,45.89,45.57,45.89,45.57-19.47,45.57-45.89-18.77-45.91-45.57-45.91ZM94.52,172.12c-20.91,0-35.38-15.72-35.38-36.25s14.47-36.27,35.38-36.27,35.38,15.72,35.38,36.27-14.66,36.25-35.38,36.25Z"/>
|
||||
<path d="M197.39,89.96c-26.97,0-45.57,19.47-45.57,45.91s18.58,45.89,45.57,45.89,45.57-19.47,45.57-45.89-18.75-45.91-45.57-45.91ZM197.39,172.12c-20.91,0-35.38-15.72-35.38-36.25s14.47-36.27,35.38-36.27,35.4,15.72,35.4,36.27-14.66,36.25-35.4,36.25Z"/>
|
||||
<path d="M259.33,57.7v122.28h10.19V54.95l-9.68,2.12c-.29.07-.49.34-.49.63h-.02Z"/>
|
||||
<path d="M439.9,162.7c-8.85,0-13.3-5.4-13.3-15.03v-44.14h27.38v-18.3h-27.38l-.03-27.17-20.82,4.57c-.32.07-.54.36-.54.68v84.37c0,22.73,11.57,34.11,31.22,34.11,7.9,0,14.06-1.36,21.4-4.64l-5.98-17.35c-3.65,1.73-8.1,2.9-11.96,2.9h.02Z"/>
|
||||
<path d="M516.47,82.49c-9.83,0-17.48,1.27-26.17,6.35-13.35,7.79-20.33,22.31-20.33,37.51v54.08h21.59v-46.44c0-18.69,9.83-30.07,25.25-30.07,5.79,0,10.6,1.55,16,5.21l9.63-17.54c-1.41-1.27-3.02-2.43-4.79-3.43-5.45-3.11-14.21-5.67-21.2-5.67h.02Z"/>
|
||||
<path d="M552.06,50.13c-.32.07-.54.36-.54.68v129.6h21.59V45.51l-21.06,4.62h.02Z"/>
|
||||
<path d="M303.47,132.12c0-29.44,20.4-49.65,50.63-49.65,13.86,0,25.8,4.8,35.99,14.62l-13.09,14.04c-5.78-5.77-14.05-9.05-22.52-9.05-17.32,0-29.45,12.31-29.45,30.02s11.94,30.02,29.45,30.02c9.24,0,17.71-3.46,23.49-9.81l13.28,14.43c-10.58,10.2-22.33,15.01-36.96,15.01-30.79,0-50.82-19.63-50.82-49.65v.02Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 573.11 136.81">
|
||||
<g transform="translate(0 -45.51)" fill="#fff">
|
||||
<path d="M40.05,168.37c-4.3,2.14-10,3.92-14.3,3.92-12.86,0-15.73-8.03-15.73-19.64v-51.8h29.48v-9.1H10.02v-26.42l-9.51,2.09c-.29.07-.51.34-.51.63v9.22h.07v49.8h-.07v25.6c0,18.4,6.61,28.95,25.19,28.95,6.08,0,12.33-1.8,18.22-4.65l-3.4-8.57.03-.02Z"/>
|
||||
<path d="M94.52,89.96c-26.99,0-45.57,19.47-45.57,45.91s18.58,45.89,45.57,45.89,45.57-19.47,45.57-45.89-18.77-45.91-45.57-45.91ZM94.52,172.12c-20.91,0-35.38-15.72-35.38-36.25s14.47-36.27,35.38-36.27,35.38,15.72,35.38,36.27-14.66,36.25-35.38,36.25Z"/>
|
||||
<path d="M197.39,89.96c-26.97,0-45.57,19.47-45.57,45.91s18.58,45.89,45.57,45.89,45.57-19.47,45.57-45.89-18.75-45.91-45.57-45.91ZM197.39,172.12c-20.91,0-35.38-15.72-35.38-36.25s14.47-36.27,35.38-36.27,35.4,15.72,35.4,36.27-14.66,36.25-35.4,36.25Z"/>
|
||||
<path d="M259.33,57.7v122.28h10.19V54.95l-9.68,2.12c-.29.07-.49.34-.49.63h-.02Z"/>
|
||||
<path d="M439.9,162.7c-8.85,0-13.3-5.4-13.3-15.03v-44.14h27.38v-18.3h-27.38l-.03-27.17-20.82,4.57c-.32.07-.54.36-.54.68v84.37c0,22.73,11.57,34.11,31.22,34.11,7.9,0,14.06-1.36,21.4-4.64l-5.98-17.35c-3.65,1.73-8.1,2.9-11.96,2.9h.02Z"/>
|
||||
<path d="M516.47,82.49c-9.83,0-17.48,1.27-26.17,6.35-13.35,7.79-20.33,22.31-20.33,37.51v54.08h21.59v-46.44c0-18.69,9.83-30.07,25.25-30.07,5.79,0,10.6,1.55,16,5.21l9.63-17.54c-1.41-1.27-3.02-2.43-4.79-3.43-5.45-3.11-14.21-5.67-21.2-5.67h.02Z"/>
|
||||
<path d="M552.06,50.13c-.32.07-.54.36-.54.68v129.6h21.59V45.51l-21.06,4.62h.02Z"/>
|
||||
<path d="M303.47,132.12c0-29.44,20.4-49.65,50.63-49.65,13.86,0,25.8,4.8,35.99,14.62l-13.09,14.04c-5.78-5.77-14.05-9.05-22.52-9.05-17.32,0-29.45,12.31-29.45,30.02s11.94,30.02,29.45,30.02c9.24,0,17.71-3.46,23.49-9.81l13.28,14.43c-10.58,10.2-22.33,15.01-36.96,15.01-30.79,0-50.82-19.63-50.82-49.65v.02Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -3,21 +3,42 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Datenschutzerklärung: ToolCTRL</title>
|
||||
<meta name="description" content="Datenschutzerklärung von ToolCTRL: Informationen zur Verarbeitung personenbezogener Daten auf dieser Website." />
|
||||
<title>Datenschutzerklärung — ToolCTRL</title>
|
||||
<meta name="description" content="Datenschutzerklärung von ToolCTRL — Informationen zur Verarbeitung personenbezogener Daten auf dieser Website." />
|
||||
<meta name="robots" content="noindex" />
|
||||
<link rel="canonical" href="https://tool-ctrl.de/datenschutz.html" />
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png?v=20260610" />
|
||||
<link rel="stylesheet" href="/styles.css?v=20260610" />
|
||||
<link rel="canonical" href="https://tool-ctrl.de/datenschutz" />
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png" />
|
||||
<link rel="preload" href="/assets/fonts/space-grotesk-latin.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="stylesheet" href="/styles.css?v=20260564" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="site-header">
|
||||
<div class="wrap header-inner">
|
||||
<a class="brand" href="/" aria-label="ToolCTRL Startseite">
|
||||
<img src="/assets/logo-black.svg" alt="ToolCTRL" class="brand-logo" />
|
||||
<img src="/assets/wordmark-black.svg" alt="ToolCTRL" class="brand-logo" />
|
||||
</a>
|
||||
<a class="legal-back" href="/">Zurück zur Startseite</a>
|
||||
<nav class="nav" aria-label="Hauptnavigation">
|
||||
<a href="/#leistungen">Leistungen</a>
|
||||
<a href="/#branchen">Branchen</a>
|
||||
<a href="/#vorgehen">Vorgehen</a>
|
||||
<a href="/#arbeit">Organisation</a>
|
||||
<a href="/techday">TechDay</a>
|
||||
<a href="/gamejam">GameJam</a>
|
||||
<a class="btn btn-solid btn-sm" href="/#kontakt">Ablauf prüfen</a>
|
||||
</nav>
|
||||
<button class="nav-toggle" aria-label="Menü" aria-expanded="false">
|
||||
<span></span><span></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mobile-nav" hidden>
|
||||
<a href="/#leistungen">Leistungen</a>
|
||||
<a href="/#branchen">Branchen</a>
|
||||
<a href="/#vorgehen">Vorgehen</a>
|
||||
<a href="/#arbeit">Organisation</a>
|
||||
<a href="/techday">TechDay</a>
|
||||
<a href="/gamejam">GameJam</a>
|
||||
<a class="btn btn-solid" href="/#kontakt">Ablauf prüfen</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -36,19 +57,18 @@
|
||||
</p>
|
||||
<p>
|
||||
R&D² UG (haftungsbeschränkt)<br />
|
||||
Orbinstraße 1<br />
|
||||
Orbinstr. 1<br />
|
||||
76646 Bruchsal<br />
|
||||
E-Mail: <a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a>
|
||||
</p>
|
||||
<p>Die vollständigen Anbieterangaben finden Sie im <a href="/impressum.html">Impressum</a>.</p>
|
||||
<p>Die vollständigen Anbieterangaben finden Sie im <a href="/impressum">Impressum</a>.</p>
|
||||
|
||||
<h2>2. Hosting und Server-Logfiles</h2>
|
||||
<p>
|
||||
Diese Website wird bei einem externen Dienstleister gehostet:
|
||||
Hetzner Online GmbH, Industriestr. 25, 91710 Gunzenhausen, Deutschland.
|
||||
Beim Aufruf der Website verarbeitet der Hosting-Anbieter automatisch
|
||||
technische Informationen, die Ihr Browser übermittelt (sogenannte
|
||||
Server-Logfiles):
|
||||
Beim Aufruf der Website verarbeitet der Hosting-Anbieter automatisch technische
|
||||
Informationen, die Ihr Browser übermittelt (sogenannte Server-Logfiles):
|
||||
</p>
|
||||
<ul>
|
||||
<li>IP-Adresse des anfragenden Geräts</li>
|
||||
@@ -155,16 +175,24 @@
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<footer class="legal-footer">
|
||||
<div class="wrap legal-footer-inner">
|
||||
<span>© 2026 ToolCTRL</span>
|
||||
<span>
|
||||
<a href="/">Startseite</a> ·
|
||||
<a href="/impressum.html">Impressum</a> ·
|
||||
<a href="/datenschutz.html">Datenschutz</a>
|
||||
</span>
|
||||
<footer class="site-footer">
|
||||
<div class="wrap footer-bar">
|
||||
<div class="footer-brand">
|
||||
<img src="/assets/wordmark-white.svg" alt="ToolCTRL" class="footer-logo" />
|
||||
<p>© <span id="year">2026</span> ToolCTRL · Bruchsal</p>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a>
|
||||
<i></i>
|
||||
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener">LinkedIn</a>
|
||||
<i></i>
|
||||
<a href="/impressum">Impressum</a>
|
||||
·
|
||||
<a href="/datenschutz">Datenschutz</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/script.js?v=20260554"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,25 +3,43 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI GameJam: Human in the Loop, TechDay Bruchsal 2026</title>
|
||||
<meta name="description" content="AI GameJam von ToolCTRL beim TechDay Bruchsal 2026: In wenigen Stunden mit KI ein spielbares Game bauen. Human in the Loop, im HubWerk01 Bruchsal.">
|
||||
<link rel="canonical" href="https://tool-ctrl.de/gamejam.html">
|
||||
<title>AI GameJam — Human in the Loop · TechDay Bruchsal 2026</title>
|
||||
<meta name="description" content="AI GameJam beim TechDay Bruchsal 2026: In zwei Stunden mit KI ein spielbares Game bauen. Human in the Loop." />
|
||||
<link rel="canonical" href="https://tool-ctrl.de/gamejam">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="ToolCTRL">
|
||||
<meta property="og:locale" content="de_DE">
|
||||
<meta property="og:url" content="https://tool-ctrl.de/gamejam.html">
|
||||
<meta property="og:title" content="AI GameJam: Human in the Loop, TechDay Bruchsal 2026">
|
||||
<meta property="og:description" content="AI GameJam von ToolCTRL beim TechDay Bruchsal 2026: In wenigen Stunden mit KI ein spielbares Game bauen. Human in the Loop, im HubWerk01 Bruchsal.">
|
||||
<meta property="og:image" content="https://tool-ctrl.de/assets/og-image.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta property="og:image:alt" content="ToolCTRL Logo mit Tagline: Die Software, die eure Branche längst verdient hat.">
|
||||
<meta property="og:site_name" content="ToolCTRL">
|
||||
<meta property="og:title" content="AI GameJam — Human in the Loop">
|
||||
<meta property="og:description" content="Zwei Stunden, KI, ein spielbares Ergebnis: die AI GameJam beim TechDay Bruchsal 2026.">
|
||||
<meta property="og:url" content="https://tool-ctrl.de/gamejam">
|
||||
<meta property="og:image" content="https://tool-ctrl.de/assets/logo-a.webp">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png?v=20260610">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
|
||||
<meta name="theme-color" content="#0c0c0e">
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png">
|
||||
<link rel="preload" href="/assets/fonts/space-grotesk-latin.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Space Grotesk";
|
||||
font-style: normal;
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/space-grotesk-latin.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
|
||||
U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
|
||||
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Space Grotesk";
|
||||
font-style: normal;
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/space-grotesk-latin-ext.woff2) format("woff2");
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
|
||||
U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
|
||||
U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
|
||||
U+A720-A7FF;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg2: #f5f5f6;
|
||||
@@ -109,15 +127,18 @@
|
||||
.hero .kicker {
|
||||
font-family: 'Space Mono', monospace; font-size: 13px; letter-spacing: 4px;
|
||||
text-transform: uppercase; color: var(--ac); margin-bottom: 26px;
|
||||
animation: heroRise .24s cubic-bezier(.2, 0, 0, 1) both;
|
||||
}
|
||||
.hero h1 {
|
||||
font-size: clamp(52px, 11vw, 142px); font-weight: 700; line-height: 0.93;
|
||||
letter-spacing: -0.04em; text-transform: uppercase; margin-bottom: 24px;
|
||||
animation: heroRise .24s cubic-bezier(.2, 0, 0, 1) .04s both;
|
||||
}
|
||||
.hero h1 .ln2 { color: var(--ac); }
|
||||
.hero .hsub {
|
||||
font-size: clamp(15px, 2vw, 19px); color: var(--muted);
|
||||
max-width: 540px; margin-bottom: 56px;
|
||||
animation: heroRise .24s cubic-bezier(.2, 0, 0, 1) .08s both;
|
||||
}
|
||||
.hero .hsub b { color: var(--text); font-weight: 600; }
|
||||
|
||||
@@ -126,6 +147,7 @@
|
||||
position: relative; background: var(--panel);
|
||||
border: 1px solid var(--line2); padding: 36px 56px;
|
||||
min-width: min(560px, 92vw);
|
||||
animation: heroRise .26s cubic-bezier(.2, 0, 0, 1) .12s both;
|
||||
}
|
||||
.hud .cn {
|
||||
position: absolute; width: 14px; height: 14px;
|
||||
@@ -153,6 +175,10 @@
|
||||
font-family: 'Space Mono', monospace; font-weight: 700;
|
||||
font-size: clamp(54px, 11vw, 108px); line-height: 1;
|
||||
letter-spacing: -0.02em; color: var(--text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.hud .timer.tick {
|
||||
animation: timerTick .16s cubic-bezier(.2, 0, 0, 1);
|
||||
}
|
||||
.hud .tl {
|
||||
font-family: 'Space Mono', monospace; font-size: 11px; letter-spacing: 2px;
|
||||
@@ -160,6 +186,7 @@
|
||||
}
|
||||
.hmeta {
|
||||
margin-top: 44px; display: flex; gap: 14px; flex-wrap: wrap; justify-content: center;
|
||||
animation: heroRise .24s cubic-bezier(.2, 0, 0, 1) .16s both;
|
||||
}
|
||||
.hmeta span {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
@@ -193,6 +220,7 @@
|
||||
.tl-row.done { opacity: 0.38; }
|
||||
.tl-row.active {
|
||||
background: var(--panel); border-left-color: var(--ac);
|
||||
box-shadow: inset 10px 0 18px -18px rgba(0,0,0,.45);
|
||||
}
|
||||
.tl-row.active .tt { color: var(--ac); }
|
||||
.tl-row.active .tn { color: var(--text); }
|
||||
@@ -273,41 +301,159 @@
|
||||
.field input {
|
||||
width: 100%; background: var(--bg2); border: 1px solid var(--line2);
|
||||
padding: 13px 15px; color: var(--text); font-size: 15px; font-family: inherit;
|
||||
transition: border-color .15s ease, background .15s ease;
|
||||
}
|
||||
.field input:focus { outline: none; border-color: var(--ac); }
|
||||
.field input:focus { outline: none; border-color: var(--ac); background: #fff; }
|
||||
.dropzone {
|
||||
border: 1.5px dashed var(--line2); padding: 40px 24px; text-align: center;
|
||||
cursor: pointer; transition: all .15s; background: var(--bg2);
|
||||
cursor: pointer;
|
||||
transition: border-color .15s ease, background .15s ease, transform .15s ease;
|
||||
background: var(--bg2);
|
||||
}
|
||||
.dropzone:hover, .dropzone.drag {
|
||||
border-color: var(--ac);
|
||||
background: var(--panel);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.dropzone:hover, .dropzone.drag { border-color: var(--ac); background: var(--panel); }
|
||||
.dropzone .dzi { color: var(--ac); margin-bottom: 12px; }
|
||||
.dropzone .dzi svg.ic { width: 28px; height: 28px; }
|
||||
.dropzone .big { font-size: 15px; font-weight: 600; margin-bottom: 4px; }
|
||||
.dropzone .sm { color: var(--muted); font-size: 13px; font-family: 'Space Mono', monospace; }
|
||||
.btn {
|
||||
background: var(--ac); color: #ffffff; border: none;
|
||||
position: relative; isolation: isolate; overflow: visible;
|
||||
background: linear-gradient(180deg, #3c3c3f 0%, #161617 60%, #070708 100%);
|
||||
color: #ffffff; border: 2px solid #0c0c0e; border-radius: 7px;
|
||||
padding: 13px 30px; font-size: 14px; font-weight: 700; cursor: pointer;
|
||||
font-family: inherit; text-transform: uppercase; letter-spacing: 1px;
|
||||
transition: opacity .15s;
|
||||
box-shadow: 0 7px 0 #000000, 0 11px 0 #343437, 0 17px 18px -10px rgba(0,0,0,.48),
|
||||
inset 0 2px 0 rgba(255,255,255,.22);
|
||||
transition: box-shadow .12s ease, transform .12s ease, background .15s ease;
|
||||
}
|
||||
.btn:hover { opacity: 0.82; }
|
||||
.btn:disabled { background: var(--line2); color: var(--muted); cursor: not-allowed; }
|
||||
.btn::after {
|
||||
content: ""; position: absolute; left: 5px; right: -6px; bottom: -8px;
|
||||
height: 8px; z-index: -1; border: 2px solid #0c0c0e; border-top: 0;
|
||||
border-radius: 0 0 7px 7px; background: #0c0c0e;
|
||||
transition: height .12s ease, bottom .12s ease;
|
||||
}
|
||||
.btn:hover { transform: translateY(-2px); box-shadow: 0 9px 0 #000000, 0 13px 0 #343437, 0 19px 20px -10px rgba(0,0,0,.5),
|
||||
inset 0 2px 0 rgba(255,255,255,.22); }
|
||||
.btn:active { transform: translateY(5px) scale(.985); box-shadow: 0 2px 0 #000000, 0 4px 0 #343437, 0 9px 14px -9px rgba(0,0,0,.45); }
|
||||
.btn:active::after { bottom: -3px; height: 3px; }
|
||||
.btn:disabled { background: var(--line2); color: var(--muted); cursor: not-allowed; box-shadow: none; }
|
||||
.btn:disabled::after { display: none; }
|
||||
.status {
|
||||
font-family: 'Space Mono', monospace; font-size: 13px; margin-top: 16px; min-height: 20px;
|
||||
}
|
||||
.status.ok { color: var(--ac); }
|
||||
.status.err { color: #c0392b; }
|
||||
.status.err { color: #0c0c0e; font-weight: 700; }
|
||||
|
||||
/* ===== GALLERY ===== */
|
||||
.btn-ghost {
|
||||
background: var(--panel); border: 1px solid var(--line2); color: var(--text);
|
||||
position: relative; isolation: isolate; overflow: visible;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f1f1f2 62%, #dfdfe1 100%);
|
||||
border: 2px solid var(--text); border-radius: 6px; color: var(--text);
|
||||
padding: 10px 16px; font-size: 12px; font-weight: 600; cursor: pointer;
|
||||
font-family: 'Space Mono', monospace; text-transform: uppercase; letter-spacing: 1px;
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
box-shadow: 0 5px 0 #000000, 0 8px 0 #bbbbbf, 0 13px 16px -10px rgba(0,0,0,.36);
|
||||
transition: box-shadow .12s ease, transform .12s ease;
|
||||
}
|
||||
.btn-ghost:hover { border-color: var(--ac); }
|
||||
.btn-ghost::after {
|
||||
content: ""; position: absolute; left: 4px; right: -5px; bottom: -7px;
|
||||
height: 7px; z-index: -1; border: 2px solid var(--text); border-top: 0;
|
||||
border-radius: 0 0 6px 6px; background: #d9d9dc;
|
||||
transition: height .12s ease, bottom .12s ease;
|
||||
}
|
||||
.btn-ghost:hover { transform: translateY(-2px); box-shadow: 0 7px 0 #000000, 0 10px 0 #bbbbbf, 0 15px 18px -10px rgba(0,0,0,.38); }
|
||||
.btn-ghost:active { transform: translateY(4px) scale(.985); box-shadow: 0 1px 0 #000000, 0 3px 0 #bbbbbf, 0 7px 12px -9px rgba(0,0,0,.34); }
|
||||
.btn-ghost:active::after { bottom: -3px; height: 3px; }
|
||||
.btn-ghost svg.ic { width: 13px; height: 13px; }
|
||||
|
||||
.btn,
|
||||
.btn-ghost {
|
||||
max-width: 100%;
|
||||
min-height: 40px;
|
||||
line-height: 1.1;
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.btn::after,
|
||||
.btn-ghost::after { display: none; }
|
||||
.btn {
|
||||
display: inline-flex; align-items: center;
|
||||
box-shadow: 0 5px 0 #000000, 0 8px 12px -8px rgba(0,0,0,.48),
|
||||
inset 0 2px 0 rgba(255,255,255,.22);
|
||||
}
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 0 #000000, 0 10px 14px -9px rgba(0,0,0,.5),
|
||||
inset 0 2px 0 rgba(255,255,255,.22);
|
||||
}
|
||||
.btn:active {
|
||||
transform: translateY(3px) scale(.96);
|
||||
box-shadow: 0 2px 0 #000000, 0 7px 10px -9px rgba(0,0,0,.45);
|
||||
}
|
||||
.btn-ghost {
|
||||
box-shadow: 0 5px 0 #000000, 0 8px 12px -8px rgba(0,0,0,.32);
|
||||
}
|
||||
.btn-ghost:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 0 #000000, 0 10px 14px -9px rgba(0,0,0,.34);
|
||||
}
|
||||
.btn-ghost:active {
|
||||
transform: translateY(3px) scale(.96);
|
||||
box-shadow: 0 2px 0 #000000, 0 7px 10px -9px rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
.btn,
|
||||
.btn-ghost {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
max-width: 100%;
|
||||
min-height: 40px;
|
||||
padding: 11px 20px;
|
||||
border: 2px solid var(--text);
|
||||
border-radius: 6px;
|
||||
line-height: 1.1;
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 4px 0 #000000, 0 10px 16px -12px rgba(0,0,0,.42);
|
||||
}
|
||||
.btn::before,
|
||||
.btn::after,
|
||||
.btn-ghost::before,
|
||||
.btn-ghost::after {
|
||||
display: none;
|
||||
}
|
||||
.btn:hover,
|
||||
.btn-ghost:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 5px 0 #000000, 0 12px 18px -12px rgba(0,0,0,.44);
|
||||
}
|
||||
.btn:active,
|
||||
.btn-ghost:active {
|
||||
transform: translateY(2px) scale(.98);
|
||||
box-shadow: 0 1px 0 #000000, 0 6px 10px -10px rgba(0,0,0,.4);
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-color: #ffffff;
|
||||
box-shadow: 0 4px 0 #9f9f9f, 0 10px 16px -12px rgba(255,255,255,.38),
|
||||
inset 0 2px 0 rgba(255,255,255,.24);
|
||||
}
|
||||
.btn:hover {
|
||||
box-shadow: 0 5px 0 #bcbcbc, 0 12px 18px -12px rgba(255,255,255,.42),
|
||||
inset 0 2px 0 rgba(255,255,255,.24);
|
||||
}
|
||||
.btn:active {
|
||||
box-shadow: 0 1px 0 #bcbcbc, 0 6px 10px -10px rgba(255,255,255,.34);
|
||||
}
|
||||
|
||||
.games { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-top: 6px; }
|
||||
.game { background: var(--panel); border: 1px solid var(--line); }
|
||||
.game .frame { width: 100%; aspect-ratio: 16/10; border: none; background: var(--panel2); display: block; }
|
||||
@@ -339,6 +485,22 @@
|
||||
font-family: 'Space Mono', monospace; color: var(--muted); font-size: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
footer .links {
|
||||
display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 14px;
|
||||
font-family: 'Space Mono', monospace; font-size: 12px; letter-spacing: .5px;
|
||||
color: var(--muted);
|
||||
}
|
||||
footer .links a { color: var(--muted); text-decoration: none; }
|
||||
footer .links a:hover { color: var(--text); }
|
||||
|
||||
@keyframes heroRise {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes timerTick {
|
||||
from { opacity: .78; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.steps { grid-template-columns: 1fr; }
|
||||
@@ -352,6 +514,17 @@
|
||||
section { padding: 70px 0; }
|
||||
.topbar .ev { display: none; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: .001ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: .001ms !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -360,12 +533,12 @@
|
||||
<div class="topbar">
|
||||
<div class="inner">
|
||||
<div class="left">
|
||||
<a href="/" aria-label="Zur ToolCTRL-Startseite"><img class="tc" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNzEzLjMyIDY3OC42MSI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC5jbHMtMSB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtNSk7CiAgICAgIH0KCiAgICAgIC5jbHMtMiB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtNik7CiAgICAgIH0KCiAgICAgIC5jbHMtMyB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtNCk7CiAgICAgIH0KCiAgICAgIC5jbHMtNCB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtMyk7CiAgICAgIH0KCiAgICAgIC5jbHMtNSB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtMik7CiAgICAgIH0KCiAgICAgIC5jbHMtNiB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQpOwogICAgICB9CiAgICA8L3N0eWxlPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQiIHgxPSI2NTguOTYiIHkxPSIzMS45NiIgeDI9IjI5Ny43IiB5Mj0iNDU4LjYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMDAwIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzMzMyIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTIiIHgxPSIxNjg0LjMiIHkxPSIxMTQyLjc1IiB4Mj0iMjIwNi4wOSIgeTI9IjE1NTMuNTEiIGdyYWRpZW50VHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE4MzMuNzQgLTExMDguOTYpIHJvdGF0ZSg1LjA2KSBza2V3WCgxNC4yNSkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjYjJiMmIyIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iLjc4IiBzdG9wLWNvbG9yPSIjZmZmIi8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtMyIgeDE9IjE3MTkuMTMiIHkxPSIxMDk4LjUxIiB4Mj0iMjI0MC45MSIgeTI9IjE1MDkuMjgiIHhsaW5rOmhyZWY9IiNsaW5lYXItZ3JhZGllbnQtMiIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtNCIgeDE9IjE3NjQuODIiIHkxPSIxMDQwLjQ3IiB4Mj0iMjI4Ni42IiB5Mj0iMTQ1MS4yNCIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudC0yIi8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudC01IiB4MT0iMTgwNi4yNyIgeTE9Ijk4Ny44MiIgeDI9IjIzMjguMDUiIHkyPSIxMzk4LjU5IiB4bGluazpocmVmPSIjbGluZWFyLWdyYWRpZW50LTIiLz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTYiIHgxPSIzNDUuMDMiIHkxPSItMTAuMTUiIHgyPSIxMDE0LjU1IiB5Mj0iNTE2LjkxIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsMCwwLDEsMCwwKSIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudC0yIi8+CiAgPC9kZWZzPgogIDxnPgogICAgPHBhdGggZD0iTTY4OC43LDU3Mi4wOWwtMTYwLDk4Ljk1Yy05LjE0LDUuNzMtMTkuOTksOC4zNS0zMC43Nyw3LjM3bC0zODAuNjgtMzQuMzZjLTIyLjQ4LTIuMDEtNDAuNzYtMTkuMDEtNDQuMzUtNDEuMzFMLjY0LDE1OS4zMWMtMy4xNy0xOS41Niw1LjYtMzkuMTIsMjIuMy00OS43MkwxODkuNDYsOC4wOGMtMTMuOTUsOS43NSwyMy43NywzMS41NSwyNi44OCw0OS45NWw5MS4zLDM2MS4xM2M2LjIxLDM2LjY4LDM2LjM3LDY0LjU4LDczLjQyLDY3LjkzbDI3OC42NCw0Mi45YzguNjUuNzksMjEuODcsNDUuNjgsMjguOTQsNDIuMDNsLjA2LjA2WiIvPgogICAgPHBhdGggY2xhc3M9ImNscy02IiBkPSJNNjg4LjY0LDU3Mi4wM2MtNy4wNywzLjY2LTE1LjIzLDUuMzYtMjMuODgsNC41N2wtMzQ5LTMxLjVjLTM3LjA0LTMuMzUtNjcuMjEtMzEuMjYtNzMuNDItNjcuOTNMMTcwLjM5LDUzLjM1Yy0zLjExLTE4LjQsNS4xMi0zNS41MiwxOS4wNy00NS4yN2wxLjk1LTEuMTZjNy45OC01LjA2LDE3LjYxLTcuNjIsMjcuOTEtNi43NmwzNTguNTYsMzAuMzRjMjkuNDksMi41LDUzLjU2LDI0LjYyLDU4LjYxLDUzLjhsNzYuMTYsNDQwLjM5YzMuNTMsMjAuMjktNy4xOSwzOC45OS0yNC4wMSw0Ny4zNFoiLz4KICA8L2c+CiAgPGc+CiAgICA8Zz4KICAgICAgPHBhdGggY2xhc3M9ImNscy01IiBkPSJNMzAwLjIyLDM0My45M2wtMTUuMDEsNC4yNiwxNS43Myw5Ni4wNWM0LjIzLDI1LjgzLDIxLjg4LDQyLjU5LDQ3LjIxLDQ0LjgzbDIwLjQyLDEuODEtMi40OC0xNS4xNC0yMC40Mi0xLjgxYy0xNS44MS0xLjQtMjYuMDQtMTEuNDQtMjguNzktMjguMjhsLTguMjQtNTAuMzQsNDQuOTMsMy45Ny0yLjQ1LTE0Ljk0LTQ0LjkzLTMuOTctNS45Ny0zNi40M1oiLz4KICAgICAgPHBhdGggY2xhc3M9ImNscy00IiBkPSJNNDIxLjk2LDM4Ny45N2MtMzEuODgtMi44Mi01MS4yNiwxOC4xLTQ2LjA4LDQ5LjczLDUuMTgsMzEuNjQsMzIuMDgsNTYuNjUsNjMuOTcsNTkuNDcsMzEuODgsMi44Miw1MS4yNi0xOC4xLDQ2LjA4LTQ5LjczLTUuMTgtMzEuNjQtMzIuMDgtNTYuNjUtNjMuOTctNTkuNDdaTTQzNy4zNiw0ODIuMDNjLTIyLjktMi4wMy00MS41NC0xOS42Ny00NS4zNS00Mi45LTMuOC0yMy4yMyw5LjUzLTM4LjA0LDMyLjQzLTM2LjAyLDIyLjksMi4wMyw0MS41NCwxOS42Niw0NS4zNSw0Mi45LDMuOCwyMy4yMy05LjUzLDM4LjA0LTMyLjQyLDM2LjAyWiIvPgogICAgICA8cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik01NDEuMzcsMzk4LjUzYy0zMS44OC0yLjgyLTUxLjI2LDE4LjEtNDYuMDgsNDkuNzMsNS4xOCwzMS42NCwzMi4wOCw1Ni42NSw2My45Nyw1OS40NywzMS44OCwyLjgyLDUxLjI2LTE4LjEsNDYuMDgtNDkuNzMtNS4xOC0zMS42NC0zMi4wOC01Ni42NS02My45Ny01OS40N1pNNTU2Ljc3LDQ5Mi41OWMtMjIuOS0yLjAzLTQxLjU0LTE5LjY3LTQ1LjM1LTQyLjktMy44LTIzLjIzLDkuNTMtMzguMDQsMzIuNDMtMzYuMDIsMjIuOSwyLjAzLDQxLjU0LDE5LjY2LDQ1LjM1LDQyLjksMy44LDIzLjIzLTkuNTMsMzguMDQtMzIuNDIsMzYuMDJaIi8+CiAgICAgIDxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSI2MDcuNjcgMzY4LjQxIDYzMS4xIDUxMS40NiA2NDcuMDQgNTEyLjg3IDYyMi42OCAzNjQuMTUgNjA3LjY3IDM2OC40MSIvPgogICAgPC9nPgogICAgPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNTgyLjM5LDE2NC4wN2wxOC4zLDEuNjItNTYuMjktNzUuMTJjLTMuMTUtNC4yLTcuOTMtNi44Ni0xMy4xNi03LjMzaDBjLTcuNjEtLjY3LTE0Ljg2LDMuNDEtMTguMjIsMTAuMjdsLTMwLjMsNjEuNzUsMTkuMiwxLjcsMjcuNTctNTYuMmMuOTktMi4wMSwzLjc1LTIuMjcsNS4xLS40OGw0Ny44MSw2My43OVoiLz4KICA8L2c+Cjwvc3ZnPg==" alt="ToolCTRL"></a>
|
||||
<a href="/" aria-label="Zur ToolCTRL-Startseite"><img class="tc" src="/assets/wordmark-black.svg" alt="ToolCTRL"></a>
|
||||
<a class="back" href="/"><svg class="ic" viewBox="0 0 24 24"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> Startseite</a>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span class="ev">TechDay Bruchsal · 27.05.2026</span>
|
||||
<a href="https://www.linkedin.com/company/it-meetup-bruchsal/" target="_blank" rel="noopener" aria-label="mITup Bruchsal auf LinkedIn"><img class="mit" src="data:image/png;base64,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 truncated
|
||||
<img class="mit" src="/assets/mitup-logo.webp" alt="mITup Bruchsal">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -445,18 +618,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===================== MITBRINGEN ===================== -->
|
||||
<section id="mitbringen">
|
||||
<div class="wrap">
|
||||
<div class="seclabel">Was du brauchst</div>
|
||||
<h2 class="big">Mitbringen</h2>
|
||||
<p class="lead">
|
||||
Dein eigener Mac- oder Linux-Laptop. Sonst nichts. Nur Bock,
|
||||
mit KI ein Spiel zu bauen.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===================== BEWERTUNG ===================== -->
|
||||
<section id="bewertung">
|
||||
<div class="wrap">
|
||||
@@ -472,6 +633,30 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===================== PREISE ===================== -->
|
||||
<section id="preise">
|
||||
<div class="wrap">
|
||||
<div class="seclabel">Preise</div>
|
||||
<div class="prizes">
|
||||
<div class="prizeitem first">
|
||||
<div class="pr">Erster Platz</div>
|
||||
<h3>Claude Max 5x</h3>
|
||||
<div class="dur">Ein Monat</div>
|
||||
</div>
|
||||
<div class="prizeitem">
|
||||
<div class="pr">Zweiter Platz</div>
|
||||
<h3>Claude Pro</h3>
|
||||
<div class="dur">Ein Monat</div>
|
||||
</div>
|
||||
<div class="prizeitem">
|
||||
<div class="pr">Dritter Platz</div>
|
||||
<h3>Claude Pro</h3>
|
||||
<div class="dur">Ein Monat</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===================== GAMES ===================== -->
|
||||
<section id="games">
|
||||
<div class="wrap">
|
||||
@@ -510,40 +695,23 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===================== GEWINNER ===================== -->
|
||||
<section id="gewinner">
|
||||
<div class="wrap">
|
||||
<div class="seclabel">Ergebnisse</div>
|
||||
<h2 class="big">Die Gewinner</h2>
|
||||
<p class="lead" style="margin-bottom:44px;">Die AI GameJam ist gelaufen. Das sind die Top 3.</p>
|
||||
<div class="prizes">
|
||||
<div class="prizeitem first">
|
||||
<div class="pr"><svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;margin-right:6px;"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6M18 9h1.5a2.5 2.5 0 0 0 0-5H18M4 22h16M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>Platz 1</div>
|
||||
<h3>Banana Bandit</h3>
|
||||
<div class="dur">50 € Amazon-Gutschein</div>
|
||||
</div>
|
||||
<div class="prizeitem">
|
||||
<div class="pr">Platz 2</div>
|
||||
<h3>Ship IT</h3>
|
||||
<div class="dur">25 € Amazon-Gutschein</div>
|
||||
</div>
|
||||
<div class="prizeitem">
|
||||
<div class="pr">Platz 3</div>
|
||||
<h3>Firestarter</h3>
|
||||
<div class="dur">25 € Amazon-Gutschein</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===================== FOOTER ===================== -->
|
||||
<footer>
|
||||
<div class="inner">
|
||||
<div class="logos">
|
||||
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener" aria-label="ToolCTRL auf LinkedIn"><img class="tc" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNzEzLjMyIDY3OC42MSI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC5jbHMtMSB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtNSk7CiAgICAgIH0KCiAgICAgIC5jbHMtMiB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtNik7CiAgICAgIH0KCiAgICAgIC5jbHMtMyB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtNCk7CiAgICAgIH0KCiAgICAgIC5jbHMtNCB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtMyk7CiAgICAgIH0KCiAgICAgIC5jbHMtNSB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtMik7CiAgICAgIH0KCiAgICAgIC5jbHMtNiB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQpOwogICAgICB9CiAgICA8L3N0eWxlPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQiIHgxPSI2NTguOTYiIHkxPSIzMS45NiIgeDI9IjI5Ny43IiB5Mj0iNDU4LjYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMDAwIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzMzMyIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTIiIHgxPSIxNjg0LjMiIHkxPSIxMTQyLjc1IiB4Mj0iMjIwNi4wOSIgeTI9IjE1NTMuNTEiIGdyYWRpZW50VHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE4MzMuNzQgLTExMDguOTYpIHJvdGF0ZSg1LjA2KSBza2V3WCgxNC4yNSkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjYjJiMmIyIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iLjc4IiBzdG9wLWNvbG9yPSIjZmZmIi8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtMyIgeDE9IjE3MTkuMTMiIHkxPSIxMDk4LjUxIiB4Mj0iMjI0MC45MSIgeTI9IjE1MDkuMjgiIHhsaW5rOmhyZWY9IiNsaW5lYXItZ3JhZGllbnQtMiIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtNCIgeDE9IjE3NjQuODIiIHkxPSIxMDQwLjQ3IiB4Mj0iMjI4Ni42IiB5Mj0iMTQ1MS4yNCIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudC0yIi8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudC01IiB4MT0iMTgwNi4yNyIgeTE9Ijk4Ny44MiIgeDI9IjIzMjguMDUiIHkyPSIxMzk4LjU5IiB4bGluazpocmVmPSIjbGluZWFyLWdyYWRpZW50LTIiLz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTYiIHgxPSIzNDUuMDMiIHkxPSItMTAuMTUiIHgyPSIxMDE0LjU1IiB5Mj0iNTE2LjkxIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsMCwwLDEsMCwwKSIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudC0yIi8+CiAgPC9kZWZzPgogIDxnPgogICAgPHBhdGggZD0iTTY4OC43LDU3Mi4wOWwtMTYwLDk4Ljk1Yy05LjE0LDUuNzMtMTkuOTksOC4zNS0zMC43Nyw3LjM3bC0zODAuNjgtMzQuMzZjLTIyLjQ4LTIuMDEtNDAuNzYtMTkuMDEtNDQuMzUtNDEuMzFMLjY0LDE1OS4zMWMtMy4xNy0xOS41Niw1LjYtMzkuMTIsMjIuMy00OS43MkwxODkuNDYsOC4wOGMtMTMuOTUsOS43NSwyMy43NywzMS41NSwyNi44OCw0OS45NWw5MS4zLDM2MS4xM2M2LjIxLDM2LjY4LDM2LjM3LDY0LjU4LDczLjQyLDY3LjkzbDI3OC42NCw0Mi45YzguNjUuNzksMjEuODcsNDUuNjgsMjguOTQsNDIuMDNsLjA2LjA2WiIvPgogICAgPHBhdGggY2xhc3M9ImNscy02IiBkPSJNNjg4LjY0LDU3Mi4wM2MtNy4wNywzLjY2LTE1LjIzLDUuMzYtMjMuODgsNC41N2wtMzQ5LTMxLjVjLTM3LjA0LTMuMzUtNjcuMjEtMzEuMjYtNzMuNDItNjcuOTNMMTcwLjM5LDUzLjM1Yy0zLjExLTE4LjQsNS4xMi0zNS41MiwxOS4wNy00NS4yN2wxLjk1LTEuMTZjNy45OC01LjA2LDE3LjYxLTcuNjIsMjcuOTEtNi43NmwzNTguNTYsMzAuMzRjMjkuNDksMi41LDUzLjU2LDI0LjYyLDU4LjYxLDUzLjhsNzYuMTYsNDQwLjM5YzMuNTMsMjAuMjktNy4xOSwzOC45OS0yNC4wMSw0Ny4zNFoiLz4KICA8L2c+CiAgPGc+CiAgICA8Zz4KICAgICAgPHBhdGggY2xhc3M9ImNscy01IiBkPSJNMzAwLjIyLDM0My45M2wtMTUuMDEsNC4yNiwxNS43Myw5Ni4wNWM0LjIzLDI1LjgzLDIxLjg4LDQyLjU5LDQ3LjIxLDQ0LjgzbDIwLjQyLDEuODEtMi40OC0xNS4xNC0yMC40Mi0xLjgxYy0xNS44MS0xLjQtMjYuMDQtMTEuNDQtMjguNzktMjguMjhsLTguMjQtNTAuMzQsNDQuOTMsMy45Ny0yLjQ1LTE0Ljk0LTQ0LjkzLTMuOTctNS45Ny0zNi40M1oiLz4KICAgICAgPHBhdGggY2xhc3M9ImNscy00IiBkPSJNNDIxLjk2LDM4Ny45N2MtMzEuODgtMi44Mi01MS4yNiwxOC4xLTQ2LjA4LDQ5LjczLDUuMTgsMzEuNjQsMzIuMDgsNTYuNjUsNjMuOTcsNTkuNDcsMzEuODgsMi44Miw1MS4yNi0xOC4xLDQ2LjA4LTQ5LjczLTUuMTgtMzEuNjQtMzIuMDgtNTYuNjUtNjMuOTctNTkuNDdaTTQzNy4zNiw0ODIuMDNjLTIyLjktMi4wMy00MS41NC0xOS42Ny00NS4zNS00Mi45LTMuOC0yMy4yMyw5LjUzLTM4LjA0LDMyLjQzLTM2LjAyLDIyLjksMi4wMyw0MS41NCwxOS42Niw0NS4zNSw0Mi45LDMuOCwyMy4yMy05LjUzLDM4LjA0LTMyLjQyLDM2LjAyWiIvPgogICAgICA8cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik01NDEuMzcsMzk4LjUzYy0zMS44OC0yLjgyLTUxLjI2LDE4LjEtNDYuMDgsNDkuNzMsNS4xOCwzMS42NCwzMi4wOCw1Ni42NSw2My45Nyw1OS40NywzMS44OCwyLjgyLDUxLjI2LTE4LjEsNDYuMDgtNDkuNzMtNS4xOC0zMS42NC0zMi4wOC01Ni42NS02My45Ny01OS40N1pNNTU2Ljc3LDQ5Mi41OWMtMjIuOS0yLjAzLTQxLjU0LTE5LjY3LTQ1LjM1LTQyLjktMy44LTIzLjIzLDkuNTMtMzguMDQsMzIuNDMtMzYuMDIsMjIuOSwyLjAzLDQxLjU0LDE5LjY2LDQ1LjM1LDQyLjksMy44LDIzLjIzLTkuNTMsMzguMDQtMzIuNDIsMzYuMDJaIi8+CiAgICAgIDxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSI2MDcuNjcgMzY4LjQxIDYzMS4xIDUxMS40NiA2NDcuMDQgNTEyLjg3IDYyMi42OCAzNjQuMTUgNjA3LjY3IDM2OC40MSIvPgogICAgPC9nPgogICAgPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNTgyLjM5LDE2NC4wN2wxOC4zLDEuNjItNTYuMjktNzUuMTJjLTMuMTUtNC4yLTcuOTMtNi44Ni0xMy4xNi03LjMzaDBjLTcuNjEtLjY3LTE0Ljg2LDMuNDEtMTguMjIsMTAuMjdsLTMwLjMsNjEuNzUsMTkuMiwxLjcsMjcuNTctNTYuMmMuOTktMi4wMSwzLjc1LTIuMjcsNS4xLS40OGw0Ny44MSw2My43OVoiLz4KICA8L2c+Cjwvc3ZnPg==" alt="ToolCTRL"></a>
|
||||
<a href="https://www.linkedin.com/company/it-meetup-bruchsal/" target="_blank" rel="noopener" aria-label="mITup Bruchsal auf LinkedIn"><img src="data:image/png;base64,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 truncated
|
||||
<img class="tc" src="/assets/wordmark-black.svg" alt="ToolCTRL">
|
||||
<img src="/assets/mitup-logo.webp" alt="mITup Bruchsal">
|
||||
</div>
|
||||
<div class="ft">AI GameJam · Human in the Loop · veranstaltet von ToolCTRL beim TechDay Bruchsal 2026</div>
|
||||
<div class="links">
|
||||
<a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a>
|
||||
<span>·</span>
|
||||
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener">LinkedIn</a>
|
||||
<span>·</span>
|
||||
<a href="/impressum">Impressum</a>
|
||||
<span>·</span>
|
||||
<a href="/datenschutz">Datenschutz</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -593,13 +761,13 @@ function renderHud() {
|
||||
nowLabelEl.innerHTML = DOT + "Startet bald";
|
||||
nameEl.textContent = "Gleich geht's los";
|
||||
subEl.textContent = "Die AI GameJam beginnt um " + PHASES[0].t + " Uhr";
|
||||
timerEl.textContent = fmt(start - now);
|
||||
setTimerText(timerEl, fmt(start - now));
|
||||
timerLabelEl.textContent = "bis Start";
|
||||
} else if (now >= end) {
|
||||
nowLabelEl.innerHTML = DOT + "Vorbei";
|
||||
nameEl.textContent = "Event beendet";
|
||||
subEl.textContent = "Danke an alle — die Games gibt's unten zum Spielen";
|
||||
timerEl.textContent = "//";
|
||||
setTimerText(timerEl, "//");
|
||||
timerLabelEl.textContent = "bis bald";
|
||||
} else {
|
||||
let idx = 0;
|
||||
@@ -611,14 +779,26 @@ function renderHud() {
|
||||
nameEl.textContent = cur.name;
|
||||
subEl.textContent = cur.sub;
|
||||
if (next) {
|
||||
timerEl.textContent = fmt(phaseDate(next.t) - now);
|
||||
setTimerText(timerEl, fmt(phaseDate(next.t) - now));
|
||||
timerLabelEl.textContent = "bis: " + next.name;
|
||||
} else { timerEl.textContent = ""; timerLabelEl.textContent = ""; }
|
||||
} else { setTimerText(timerEl, ""); timerLabelEl.textContent = ""; }
|
||||
}
|
||||
renderSchedule(now);
|
||||
}
|
||||
|
||||
const CHECK = '<svg class="ic" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>';
|
||||
let lastTimerText = "";
|
||||
|
||||
function setTimerText(el, text) {
|
||||
if (el.textContent === text) return;
|
||||
el.textContent = text;
|
||||
if (lastTimerText) {
|
||||
el.classList.remove("tick");
|
||||
void el.offsetWidth;
|
||||
el.classList.add("tick");
|
||||
}
|
||||
lastTimerText = text;
|
||||
}
|
||||
|
||||
function renderSchedule(now) {
|
||||
const box = document.getElementById("schedule");
|
||||
@@ -647,10 +827,6 @@ setInterval(renderHud, 1000);
|
||||
POST {API_BASE}/upload -> FormData { teilnehmer, game, file } */
|
||||
const API_BASE = "/api/gamejam";
|
||||
|
||||
/* Hart-Deadline für Einreichungen — danach ist die UI gesperrt und der Server lehnt ab. */
|
||||
const UPLOAD_DEADLINE = new Date("2026-05-27T22:00:00+02:00");
|
||||
function uploadsOpen() { return Date.now() < UPLOAD_DEADLINE.getTime(); }
|
||||
|
||||
const teamEl = document.getElementById("teamName");
|
||||
const gameEl = document.getElementById("gameName");
|
||||
const dz = document.getElementById("dropzone");
|
||||
@@ -686,23 +862,11 @@ function pickFile(f) {
|
||||
refreshUploadBtn();
|
||||
}
|
||||
function refreshUploadBtn() {
|
||||
uploadBtn.disabled = !(uploadsOpen() && API_BASE && chosenFile && teamEl.value.trim() && gameEl.value.trim());
|
||||
uploadBtn.disabled = !(API_BASE && chosenFile && teamEl.value.trim() && gameEl.value.trim());
|
||||
}
|
||||
teamEl.addEventListener("input", refreshUploadBtn);
|
||||
gameEl.addEventListener("input", refreshUploadBtn);
|
||||
|
||||
/* Wenn die Deadline überschritten ist: Formular sperren und Hinweis anzeigen. */
|
||||
if (!uploadsOpen()) {
|
||||
teamEl.disabled = true;
|
||||
gameEl.disabled = true;
|
||||
fileInput.disabled = true;
|
||||
dz.style.pointerEvents = "none";
|
||||
dz.style.opacity = "0.5";
|
||||
uploadBtn.textContent = "Einreichungen geschlossen";
|
||||
uploadBtn.disabled = true;
|
||||
setStatus("Die Einreichungsfrist ist am 27.05.2026 um 22:00 Uhr abgelaufen.", "err");
|
||||
}
|
||||
|
||||
uploadBtn.addEventListener("click", async () => {
|
||||
if (!API_BASE || !chosenFile) return;
|
||||
uploadBtn.disabled = true;
|
||||
@@ -754,7 +918,7 @@ async function loadGallery() {
|
||||
gallery.insertAdjacentHTML("beforeend", `
|
||||
<div class="game">
|
||||
<iframe class="frame" src="${url}" sandbox="allow-scripts allow-pointer-lock"
|
||||
allow="autoplay 'none'" loading="lazy" title="${name}"></iframe>
|
||||
loading="lazy" title="${name}"></iframe>
|
||||
<div class="gm">
|
||||
<div><div class="gt">${name}</div><div class="gte">${who}</div></div>
|
||||
<a href="${url}" target="_blank" rel="noopener">Spielen
|
||||
|
||||
@@ -3,21 +3,42 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Impressum: ToolCTRL</title>
|
||||
<title>Impressum — ToolCTRL</title>
|
||||
<meta name="description" content="Impressum und Anbieterkennzeichnung von ToolCTRL." />
|
||||
<meta name="robots" content="noindex" />
|
||||
<link rel="canonical" href="https://tool-ctrl.de/impressum.html" />
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png?v=20260610" />
|
||||
<link rel="stylesheet" href="/styles.css?v=20260610" />
|
||||
<link rel="canonical" href="https://tool-ctrl.de/impressum" />
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png" />
|
||||
<link rel="preload" href="/assets/fonts/space-grotesk-latin.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="stylesheet" href="/styles.css?v=20260564" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="site-header">
|
||||
<div class="wrap header-inner">
|
||||
<a class="brand" href="/" aria-label="ToolCTRL Startseite">
|
||||
<img src="/assets/logo-black.svg" alt="ToolCTRL" class="brand-logo" />
|
||||
<img src="/assets/wordmark-black.svg" alt="ToolCTRL" class="brand-logo" />
|
||||
</a>
|
||||
<a class="legal-back" href="/">Zurück zur Startseite</a>
|
||||
<nav class="nav" aria-label="Hauptnavigation">
|
||||
<a href="/#leistungen">Leistungen</a>
|
||||
<a href="/#branchen">Branchen</a>
|
||||
<a href="/#vorgehen">Vorgehen</a>
|
||||
<a href="/#arbeit">Organisation</a>
|
||||
<a href="/techday">TechDay</a>
|
||||
<a href="/gamejam">GameJam</a>
|
||||
<a class="btn btn-solid btn-sm" href="/#kontakt">Ablauf prüfen</a>
|
||||
</nav>
|
||||
<button class="nav-toggle" aria-label="Menü" aria-expanded="false">
|
||||
<span></span><span></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mobile-nav" hidden>
|
||||
<a href="/#leistungen">Leistungen</a>
|
||||
<a href="/#branchen">Branchen</a>
|
||||
<a href="/#vorgehen">Vorgehen</a>
|
||||
<a href="/#arbeit">Organisation</a>
|
||||
<a href="/techday">TechDay</a>
|
||||
<a href="/gamejam">GameJam</a>
|
||||
<a class="btn btn-solid" href="/#kontakt">Ablauf prüfen</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -28,7 +49,7 @@
|
||||
<h2>Anbieter</h2>
|
||||
<p>
|
||||
R&D² UG (haftungsbeschränkt)<br />
|
||||
Orbinstraße 1<br />
|
||||
Orbinstr. 1<br />
|
||||
76646 Bruchsal<br />
|
||||
Deutschland
|
||||
</p>
|
||||
@@ -40,7 +61,6 @@
|
||||
|
||||
<h2>Kontakt</h2>
|
||||
<p>
|
||||
Telefon: <a href="tel:+4915237750939">+49 1523 7750939</a><br />
|
||||
E-Mail: <a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a><br />
|
||||
Web: <a href="https://tool-ctrl.de">tool-ctrl.de</a>
|
||||
</p>
|
||||
@@ -60,7 +80,7 @@
|
||||
<h2>Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV</h2>
|
||||
<p>
|
||||
Raffael Sheikh<br />
|
||||
Orbinstraße 1<br />
|
||||
Orbinstr. 1<br />
|
||||
76646 Bruchsal
|
||||
</p>
|
||||
|
||||
@@ -90,16 +110,24 @@
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<footer class="legal-footer">
|
||||
<div class="wrap legal-footer-inner">
|
||||
<span>© 2026 ToolCTRL</span>
|
||||
<span>
|
||||
<a href="/">Startseite</a> ·
|
||||
<a href="/impressum.html">Impressum</a> ·
|
||||
<a href="/datenschutz.html">Datenschutz</a>
|
||||
</span>
|
||||
<footer class="site-footer">
|
||||
<div class="wrap footer-bar">
|
||||
<div class="footer-brand">
|
||||
<img src="/assets/wordmark-white.svg" alt="ToolCTRL" class="footer-logo" />
|
||||
<p>© <span id="year">2026</span> ToolCTRL · Bruchsal</p>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a>
|
||||
<i></i>
|
||||
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener">LinkedIn</a>
|
||||
<i></i>
|
||||
<a href="/impressum">Impressum</a>
|
||||
·
|
||||
<a href="/datenschutz">Datenschutz</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/script.js?v=20260554"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,44 +3,39 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>ToolCTRL: Moderne All-in-One-Software für eure Branche</title>
|
||||
<meta name="description" content="Software mit echtem Fokus auf Bedienung und Gestaltung, die gut funktioniert, klar aussieht und sich schnell bedienen lässt." />
|
||||
<title>ToolCTRL — EU-first Software für souveräne Betriebe</title>
|
||||
<meta name="description" content="ToolCTRL baut schnelle, bezahlbare SaaS-Systeme für Mittelständler: EU-first gedacht, DSGVO-konform und ohne unnötige US-Cloud-Pflicht." />
|
||||
<link rel="canonical" href="https://tool-ctrl.de/" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="ToolCTRL" />
|
||||
<meta property="og:locale" content="de_DE" />
|
||||
<meta property="og:site_name" content="ToolCTRL" />
|
||||
<meta property="og:title" content="ToolCTRL — EU-first Software für souveräne Betriebe" />
|
||||
<meta property="og:description" content="Schnelle SaaS-Systeme für Mittelständler, die keine US-Cloud-Pflicht und keine aufgeblasenen Agenturprojekte wollen." />
|
||||
<meta property="og:url" content="https://tool-ctrl.de/" />
|
||||
<meta property="og:title" content="ToolCTRL: Moderne All-in-One-Software für eure Branche" />
|
||||
<meta property="og:description" content="Software mit echtem Fokus auf Bedienung und Gestaltung, die gut funktioniert, klar aussieht und sich schnell bedienen lässt." />
|
||||
<meta property="og:image" content="https://tool-ctrl.de/assets/og-image.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content="ToolCTRL Logo mit Tagline: Die Software, die eure Branche längst verdient hat." />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="og:image" content="https://tool-ctrl.de/assets/logo-a.webp" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="ToolCTRL: Moderne All-in-One-Software für eure Branche" />
|
||||
<meta name="twitter:description" content="Software mit echtem Fokus auf Bedienung und Gestaltung, die gut funktioniert, klar aussieht und sich schnell bedienen lässt." />
|
||||
<meta name="twitter:image" content="https://tool-ctrl.de/assets/og-image.png" />
|
||||
|
||||
<!-- Structured Data -->
|
||||
<meta name="theme-color" content="#0A0A0A" />
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png" />
|
||||
<link rel="preload" href="/assets/fonts/space-grotesk-latin.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="stylesheet" href="/styles.css?v=20260564" />
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "ToolCTRL",
|
||||
"url": "https://tool-ctrl.de",
|
||||
"logo": "https://tool-ctrl.de/assets/logo-a.png",
|
||||
"description": "Software mit echtem Fokus auf Bedienung und Gestaltung, die gut funktioniert, klar aussieht und sich schnell bedienen lässt.",
|
||||
"legalName": "R&D² UG (haftungsbeschränkt)",
|
||||
"url": "https://tool-ctrl.de/",
|
||||
"email": "info@tool-ctrl.de",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "Orbinstr. 1",
|
||||
"postalCode": "76646",
|
||||
"addressLocality": "Bruchsal",
|
||||
"addressCountry": "DE"
|
||||
},
|
||||
"sameAs": ["https://www.linkedin.com/company/toolctrl/"]
|
||||
}
|
||||
</script>
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png?v=20260610" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="stylesheet" href="/styles.css?v=20260610" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -48,23 +43,29 @@
|
||||
<header class="site-header" id="top">
|
||||
<div class="wrap header-inner">
|
||||
<a class="brand" href="#top" aria-label="ToolCTRL Startseite">
|
||||
<img src="/assets/logo-black.svg" alt="ToolCTRL" class="brand-logo" />
|
||||
<img src="/assets/wordmark-black.svg" alt="ToolCTRL" class="brand-logo" />
|
||||
</a>
|
||||
<nav class="nav" aria-label="Hauptnavigation">
|
||||
<a href="/">Start</a>
|
||||
<a href="/techday.html">TechDay</a>
|
||||
<a href="/gamejam.html">GameJam</a>
|
||||
<a class="btn btn-solid btn-sm" href="#kontakt">Kontakt aufnehmen</a>
|
||||
<a href="#leistungen">Leistungen</a>
|
||||
<a href="#branchen">Branchen</a>
|
||||
<a href="#vorgehen">Vorgehen</a>
|
||||
<a href="#arbeit">Organisation</a>
|
||||
<a href="/techday">TechDay</a>
|
||||
<a href="/gamejam">GameJam</a>
|
||||
<a class="btn btn-solid btn-sm" href="#kontakt">Ablauf prüfen</a>
|
||||
</nav>
|
||||
<button class="nav-toggle" aria-label="Menü" aria-expanded="false">
|
||||
<span></span><span></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mobile-nav" hidden>
|
||||
<a href="/">Start</a>
|
||||
<a href="/techday.html">TechDay</a>
|
||||
<a href="/gamejam.html">GameJam</a>
|
||||
<a class="btn btn-solid" href="#kontakt">Kontakt aufnehmen</a>
|
||||
<a href="#leistungen">Leistungen</a>
|
||||
<a href="#branchen">Branchen</a>
|
||||
<a href="#vorgehen">Vorgehen</a>
|
||||
<a href="#arbeit">Organisation</a>
|
||||
<a href="/techday">TechDay</a>
|
||||
<a href="/gamejam">GameJam</a>
|
||||
<a class="btn btn-solid" href="#kontakt">Ablauf prüfen</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -75,197 +76,344 @@
|
||||
<div class="wrap hero-grid">
|
||||
<div class="hero-copy">
|
||||
<h1 class="hero-title">
|
||||
Moderne, durchdachte All-in-One-Software für Branchen, die zu
|
||||
lange übersehen wurden.
|
||||
<span class="no-break">EU-first</span> Software für souveräne Betriebe.
|
||||
</h1>
|
||||
<p class="hero-lead">
|
||||
Ganze Branchen arbeiten noch mit veralteten, zusammengestückelten
|
||||
Insellösungen. Wir bei ToolCTRL bauen die Alternative: Software mit
|
||||
echtem Fokus auf Bedienung und Gestaltung, die gut funktioniert,
|
||||
klar aussieht und sich schnell bedienen lässt.
|
||||
Keine US-Cloud-Pflicht. Kein Agentur-Bullshit. ToolCTRL baut
|
||||
individuelle SaaS-Systeme für Mittelständler, bei denen Anfragen,
|
||||
Termine, Daten und Freigaben nicht länger in Tabellen, Postfächern
|
||||
und halbfertigen Tools hängen bleiben.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a class="btn btn-solid" href="#kontakt">Kontakt aufnehmen</a>
|
||||
<a class="btn btn-solid" href="#kontakt">Ablauf prüfen lassen</a>
|
||||
<a class="btn btn-ghost" href="#leistungen">Leistungen ansehen</a>
|
||||
</div>
|
||||
<div class="trust-strip" aria-label="ToolCTRL Vertrauenssignale">
|
||||
<span>Made in Germany</span>
|
||||
<span>DSGVO-konform</span>
|
||||
<span>Keine US-Cloud</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Automatisierung trifft Kontrolle — generisch, kein reales Produkt -->
|
||||
<div class="hero-visual" aria-hidden="true">
|
||||
<div class="hero-visual" aria-label="Interaktive Workflow-Control-Grafik">
|
||||
<div class="visual-frame">
|
||||
<div class="visual-label">
|
||||
<span class="dot"></span> Workflow · ctrl
|
||||
<div class="visual-label" aria-hidden="true">
|
||||
<span class="window-dots"><i></i><i></i><i></i></span>
|
||||
</div>
|
||||
<div class="control-board" data-control-board>
|
||||
<svg class="control-lines" viewBox="0 0 540 400" preserveAspectRatio="none" aria-hidden="true">
|
||||
<path data-line="inbox"></path>
|
||||
<path data-line="docs"></path>
|
||||
<path data-line="chat"></path>
|
||||
<path data-line="crm"></path>
|
||||
<path data-line="team"></path>
|
||||
<path data-line="report"></path>
|
||||
</svg>
|
||||
<div class="logo-node" data-logo-node>
|
||||
<img src="/assets/emblem-black.svg" alt="" draggable="false" />
|
||||
</div>
|
||||
<button class="flow-node is-input" type="button" data-node="inbox" style="--x: 6%; --y: 18%; --r: -2deg;">Anfrage</button>
|
||||
<button class="flow-node is-input" type="button" data-node="docs" style="--x: 7%; --y: 56%; --r: 1deg;">Dokumente</button>
|
||||
<button class="flow-node is-input" type="button" data-node="chat" style="--x: 18%; --y: 78%; --r: -1deg;">Chat</button>
|
||||
<button class="flow-node is-output" type="button" data-node="crm" style="--x: 72%; --y: 16%; --r: 2deg;">CRM</button>
|
||||
<button class="flow-node is-output" type="button" data-node="team" style="--x: 76%; --y: 56%; --r: -1deg;">Team</button>
|
||||
<button class="flow-node is-output" type="button" data-node="report" style="--x: 63%; --y: 78%; --r: 1.5deg;">Report</button>
|
||||
</div>
|
||||
<svg class="node-graph" viewBox="0 0 540 400" role="presentation">
|
||||
<defs>
|
||||
<pattern id="grid" width="26" height="26" patternUnits="userSpaceOnUse">
|
||||
<path d="M26 0H0V26" fill="none" stroke="#ECECEC" stroke-width="1" />
|
||||
</pattern>
|
||||
<linearGradient id="capface" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#2A2A2A" />
|
||||
<stop offset="1" stop-color="#101010" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="540" height="400" fill="url(#grid)" />
|
||||
|
||||
<!-- Leitungen -->
|
||||
<g fill="none" stroke="#1A1A1A" stroke-width="1.6">
|
||||
<path d="M116 78 C 165 78, 152 182, 200 182" />
|
||||
<path d="M116 202 C 158 202, 168 202, 200 202" />
|
||||
<path d="M116 326 C 165 326, 152 224, 200 224" />
|
||||
<path d="M340 192 C 388 192, 378 118, 424 118" />
|
||||
<path d="M340 214 C 388 214, 378 292, 424 292" />
|
||||
</g>
|
||||
<g fill="#1A1A1A">
|
||||
<circle cx="116" cy="78" r="3" /><circle cx="116" cy="202" r="3" />
|
||||
<circle cx="116" cy="326" r="3" /><circle cx="424" cy="118" r="3" />
|
||||
<circle cx="424" cy="292" r="3" />
|
||||
</g>
|
||||
|
||||
<!-- Eingangs-Knoten -->
|
||||
<g class="gnode">
|
||||
<rect x="24" y="56" width="92" height="44" />
|
||||
<text x="70" y="83" text-anchor="middle">Daten</text>
|
||||
</g>
|
||||
<g class="gnode">
|
||||
<rect x="24" y="180" width="92" height="44" />
|
||||
<text x="70" y="207" text-anchor="middle">Auslöser</text>
|
||||
</g>
|
||||
<g class="gnode">
|
||||
<rect x="24" y="304" width="92" height="44" />
|
||||
<text x="70" y="331" text-anchor="middle">System</text>
|
||||
</g>
|
||||
|
||||
<!-- Ausgangs-Knoten -->
|
||||
<g class="gnode">
|
||||
<rect x="424" y="96" width="92" height="44" />
|
||||
<text x="470" y="123" text-anchor="middle">Aktion</text>
|
||||
</g>
|
||||
<g class="gnode">
|
||||
<rect x="424" y="270" width="92" height="44" />
|
||||
<text x="470" y="297" text-anchor="middle">Report</text>
|
||||
</g>
|
||||
|
||||
<!-- TEST: echtes Logo (Emblem) in der Mitte -->
|
||||
<image href="/assets/emblem-black.svg" x="190" y="128"
|
||||
width="160" height="152" />
|
||||
<g fill="#000000">
|
||||
<circle cx="200" cy="182" r="3" /><circle cx="200" cy="202" r="3" />
|
||||
<circle cx="200" cy="224" r="3" /><circle cx="340" cy="192" r="3" />
|
||||
<circle cx="340" cy="214" r="3" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="visual-meta">
|
||||
<span>Automatisiert</span>
|
||||
<span>Kontrolliert</span>
|
||||
<span>Wartbar</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ KAMPF / WERTE ============ -->
|
||||
<section class="section stance-section" id="kampf">
|
||||
<div class="wrap">
|
||||
<div class="section-head stance-head">
|
||||
<span class="eyebrow eyebrow-light">Wofür wir kämpfen</span>
|
||||
<h2 class="section-title">Keine Konzernleine. Keine Folienfabrik.</h2>
|
||||
<p class="section-intro">
|
||||
Wir bauen für Mittelständler, die Kontrolle über Daten, Kosten und
|
||||
Abläufe zurückholen wollen. EU-first, pragmatisch und allergisch gegen
|
||||
digitale Showprojekte.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="stance-grid">
|
||||
<article class="stance-card">
|
||||
<span>01</span>
|
||||
<h3>Digitale Souveränität</h3>
|
||||
<p>
|
||||
Keine automatische US-Cloud, kein blinder Plattformgehorsam. Wenn ein
|
||||
System sinnvoll europäisch laufen kann, wird es so geplant.
|
||||
</p>
|
||||
</article>
|
||||
<article class="stance-card stance-card-dark">
|
||||
<span>02</span>
|
||||
<h3>Agenturapparat aus</h3>
|
||||
<p>
|
||||
Keine Workshop-Show, keine Tagessatzmaschine, keine sechsstelligen
|
||||
Projekte, die nach Monaten immer noch vor allem gut aussehen.
|
||||
</p>
|
||||
</article>
|
||||
<article class="stance-card">
|
||||
<span>03</span>
|
||||
<h3>Ablauf vor Ego</h3>
|
||||
<p>
|
||||
Wir bauen keine Tools für Pitchdecks. Wir bauen Systeme, die im
|
||||
Alltag führen, Zuständigkeiten klären und Arbeit sichtbar machen.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ PRODUKTWIRKUNG ============ -->
|
||||
<section class="section product-section" id="produkt">
|
||||
<div class="wrap product-grid">
|
||||
<div class="product-copy">
|
||||
<span class="eyebrow">Produktwirkung</span>
|
||||
<h2 class="section-title">Nicht noch ein Login. Ein Arbeitsfluss, der führt.</h2>
|
||||
<p class="section-intro">
|
||||
Wir übersetzen gewachsene Prozesse in eine Oberfläche, die Teams
|
||||
sofort verstehen: klare Zustände, weniger Rückfragen, automatische
|
||||
Übergaben und ein Ort, an dem Arbeit sichtbar wird.
|
||||
</p>
|
||||
<div class="proof-points" aria-label="Typische Ergebnisse">
|
||||
<div>
|
||||
<strong>1 Oberfläche</strong>
|
||||
<span>statt verstreuter Tabellen, Chats und Ordner</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>0 Medienbrüche</strong>
|
||||
<span>zwischen Anfrage, Bearbeitung und Auswertung</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Live-Status</strong>
|
||||
<span>für Teams, Leitung und Kundenkommunikation</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-ui" aria-label="Beispielhafte ToolCTRL Oberfläche">
|
||||
<div class="ui-top">
|
||||
<span class="ui-mark">toolctrl</span>
|
||||
<span class="ui-status"><i></i> Auftrag #1842 · Live</span>
|
||||
</div>
|
||||
<div class="ui-layout">
|
||||
<aside class="ui-sidebar">
|
||||
<button class="ui-nav active" type="button" data-product-tab="eingang">Eingang</button>
|
||||
<button class="ui-nav" type="button" data-product-tab="angebote">Angebote</button>
|
||||
<button class="ui-nav" type="button" data-product-tab="planung">Planung</button>
|
||||
<button class="ui-nav" type="button" data-product-tab="abschluss">Abschluss</button>
|
||||
</aside>
|
||||
<div class="ui-main">
|
||||
<div class="ui-main-head">
|
||||
<div>
|
||||
<span class="ui-kicker" data-product-kicker>Heute · 14 aktive Vorgänge</span>
|
||||
<strong data-product-title>Workflow Control</strong>
|
||||
</div>
|
||||
<span class="ui-pill" data-product-pill>3 Blocker</span>
|
||||
</div>
|
||||
<div class="ui-flow" data-product-flow>
|
||||
<span class="done">Anfrage</span>
|
||||
<span class="active">Prüfung</span>
|
||||
<span>Angebot</span>
|
||||
<span>Termin</span>
|
||||
<span>Rechnung</span>
|
||||
</div>
|
||||
<div class="ui-metrics" data-product-metrics>
|
||||
<div><span>Antwortzeit</span><strong>18 min</strong></div>
|
||||
<div><span>Auto-Schritte</span><strong>74%</strong></div>
|
||||
<div><span>Offene Übergaben</span><strong>03</strong></div>
|
||||
</div>
|
||||
<div class="ui-table" data-product-table>
|
||||
<div class="ui-row is-hot"><span>Kunde wartet auf Rückmeldung</span><b>Jetzt</b></div>
|
||||
<div class="ui-row"><span>Dokumente aus Postfach übernommen</span><b>Auto</b></div>
|
||||
<div class="ui-row"><span>Team-Zuständigkeit gesetzt</span><b>11:15</b></div>
|
||||
<div class="ui-row is-done"><span>Status an Leitung gemeldet</span><b>Erledigt</b></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ VORSPRUNG ============ -->
|
||||
<section class="section velocity-section" id="vorsprung">
|
||||
<div class="wrap velocity-grid">
|
||||
<div class="velocity-copy">
|
||||
<span class="eyebrow">Unser Vorsprung</span>
|
||||
<h2 class="section-title">Schneller SaaS bauen. Ohne Baukasten-Vibe.</h2>
|
||||
<p class="section-intro">
|
||||
Wir starten nicht mit leerem Repository und endlosen Abstimmungen. Unsere
|
||||
eigene technische Grundlage deckt Rollen, Datenmodelle, Workflows,
|
||||
Dashboards, Schnittstellen und Betrieb ab. Dadurch wird Individualsoftware
|
||||
schneller, ohne nach Standardtool zu riechen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="velocity-stack" aria-label="Warum ToolCTRL schneller liefert">
|
||||
<article class="velocity-item">
|
||||
<span>01</span>
|
||||
<h3>Eigene SaaS-Basis</h3>
|
||||
<p>Bewährte Infrastruktur statt leerem Repository. Das spart Zeit,
|
||||
Kosten und Projektchaos.</p>
|
||||
</article>
|
||||
<article class="velocity-item">
|
||||
<span>02</span>
|
||||
<h3>Früh in den Betrieb</h3>
|
||||
<p>Wir bauen schnell eine echte Version, testen sie am Ablauf und
|
||||
verbessern dort, wo Arbeit wirklich passiert.</p>
|
||||
</article>
|
||||
<article class="velocity-item">
|
||||
<span>03</span>
|
||||
<h3>Budget ins Produkt</h3>
|
||||
<p>Weniger Reibung, weniger Meeting-Schleifen, weniger Neuentwicklung.
|
||||
Das Budget landet im System.</p>
|
||||
</article>
|
||||
<article class="velocity-item">
|
||||
<span>04</span>
|
||||
<h3>Individuell im Ergebnis</h3>
|
||||
<p>Die Oberfläche und Logik werden auf Ihren Betrieb zugeschnitten.
|
||||
Die Geschwindigkeit kommt aus unserer Technik.</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ LEISTUNGEN ============ -->
|
||||
<section class="section" id="leistungen">
|
||||
<section class="section services-section" id="leistungen">
|
||||
<div class="wrap">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">Leistungen</span>
|
||||
<h2 class="section-title">Die digitale Infrastruktur Ihres Betriebs.</h2>
|
||||
<p class="section-intro">
|
||||
Wir arbeiten dort, wo manuelle Schritte an alleinstehenden Tabellen
|
||||
und veralteten Insellösungen zu viel Zeit und Nerven kosten. Daraus
|
||||
bauen wir moderne und tragfähige Infrastrukturen aus Prozessen,
|
||||
Automatisierung und Schnittstellen, zugeschnitten auf Ihren
|
||||
Betrieb und Ihren Ablauf, nicht angepasst.
|
||||
Wir arbeiten dort, wo Tabellen, manuelle Schritte und getrennte
|
||||
Systeme zu viel Zeit kosten. Daraus bauen wir eine tragfähige
|
||||
Infrastruktur: Software, Automatisierung und Schnittstellen,
|
||||
zugeschnitten auf Ihren Ablauf, nicht umgekehrt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<article class="card">
|
||||
<span class="icon-key"><img src="/assets/icons/graph.svg" alt="" class="card-icon" /></span>
|
||||
<h3>Prozessautomatisierung</h3>
|
||||
<p>ToolCTRL wandelt repetitive Prozesse in eine komplett
|
||||
automatische Hands-Off-Erfahrung, während Sie sich um die
|
||||
wichtigeren Aufgaben im Betrieb kümmern.</p>
|
||||
<h3>Automation</h3>
|
||||
<p>Wiederkehrende Abläufe werden zu verlässlichen Workflows, die
|
||||
ohne manuelles Zutun ausgelöst, verarbeitet und dokumentiert
|
||||
werden.</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="icon-key"><img src="/assets/icons/settings.svg" alt="" class="card-icon" /></span>
|
||||
<h3>Individuelle Software</h3>
|
||||
<p>Wir bauen Software, die Ihrem Betrieb so gut sitzt wie ein
|
||||
maßgeschneiderter Anzug.</p>
|
||||
<p>Anwendungen, die exakt zu Ihrem Geschäft passen. Schlank
|
||||
gebaut, klar bedienbar und auf Dauer wartbar.</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="icon-key"><img src="/assets/icons/chart.svg" alt="" class="card-icon" /></span>
|
||||
<h3>Dashboards & Datenflüsse</h3>
|
||||
<p>Alle Datenpunkte integriert und aufbereitet. Ihre ganz eigene
|
||||
Kommandozentrale.</p>
|
||||
<p>Verstreute Daten an einem Ort: aktuell, übersichtlich und
|
||||
als Grundlage für schnelle Entscheidungen.</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="icon-key"><img src="/assets/icons/share.svg" alt="" class="card-icon" /></span>
|
||||
<h3>Schnittstellen & Integrationen</h3>
|
||||
<p>Bestehende Tools arbeiten zusammen, statt aneinander vorbei.
|
||||
Was in einem System passiert, ist im nächsten sofort da.</p>
|
||||
<p>Bestehende Tools, APIs und Systeme greifen ineinander, statt
|
||||
nebeneinander zu laufen.</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="icon-key"><img src="/assets/icons/columns.svg" alt="" class="card-icon" /></span>
|
||||
<h3>Interne Tools</h3>
|
||||
<p>Tools, die ganze Teams durch ihre Arbeitstage tragen. Keine
|
||||
zerstückelten Teillösungen und Tabellenblätter mehr.</p>
|
||||
<p>Werkzeuge für das eigene Team, die verstreute Tabellenlösungen
|
||||
ablösen und mit Ihren Abläufen mitwachsen.</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="icon-key"><img src="/assets/icons/time.svg" alt="" class="card-icon" /></span>
|
||||
<h3>Betrieb & Weiterentwicklung</h3>
|
||||
<p>Nach dem Launch ist nicht Schluss: Wartung, Anpassung,
|
||||
Erweiterungen und Updates aus einer Hand. Unsere Software
|
||||
wächst mit Ihrem Betrieb.</p>
|
||||
<h3>Betrieb & Ausbau</h3>
|
||||
<p>Nach dem Launch ist nicht Schluss: Wartung, Anpassung und
|
||||
Ausbau aus einer Hand.</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ VORGEHEN ============ -->
|
||||
<section class="section section-alt" id="vorgehen">
|
||||
<!-- ============ BRANCHEN ============ -->
|
||||
<section class="section industry-section" id="branchen">
|
||||
<div class="wrap">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">Konzept</span>
|
||||
<h2 class="section-title">Das ToolCTRL Konzept.</h2>
|
||||
<span class="eyebrow">Branchen</span>
|
||||
<h2 class="section-title">Nicht die Branche ist speziell. Der Ablauf ist es.</h2>
|
||||
<p class="section-intro">
|
||||
Ihr Anliegen wird in vier klare Schritte unterteilt: von der
|
||||
Situationsanalyse über die ersten Grundkonzepte in die konkrete
|
||||
Umsetzung und den Betrieb. Unser Team ist an jeder Stelle für
|
||||
Sie da, um bestmögliche Ergebnisse zu liefern und
|
||||
sicherzustellen.
|
||||
ToolCTRL ist interessant, wenn Ihr Ablauf speziell genug ist, dass
|
||||
Baukastenlösungen ständig Workarounds erzwingen und niemand mehr
|
||||
genau weiß, wo der aktuelle Stand liegt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="industry-map" aria-label="Typische Abläufe">
|
||||
<div class="industry-row">
|
||||
<span>01</span>
|
||||
<strong>Handwerk & Service</strong>
|
||||
<p>Anfrage, Angebot, Termin, mobile Rückmeldung und Abschluss in einem kontrollierten Ablauf.</p>
|
||||
<small>Anfrage → Angebot → Termin → Abschluss</small>
|
||||
</div>
|
||||
<div class="industry-row">
|
||||
<span>02</span>
|
||||
<strong>Events</strong>
|
||||
<p>Anmeldung, Check-in und Kommunikation laufen sichtbar zusammen.</p>
|
||||
<small>Ticket → Check-in → Info → Auswertung</small>
|
||||
</div>
|
||||
<div class="industry-row">
|
||||
<span>03</span>
|
||||
<strong>Vereine & Verbände</strong>
|
||||
<p>Mitglieder, Rollen, Beiträge und Projekte werden verwaltbar, auch wenn vieles nebenbei passiert.</p>
|
||||
<small>Mitglied → Rolle → Beitrag → Projekt</small>
|
||||
</div>
|
||||
<div class="industry-row">
|
||||
<span>04</span>
|
||||
<strong>Lokale Services</strong>
|
||||
<p>Leads, Termine und Angebote bleiben im Blick.</p>
|
||||
<small>Lead → Beratung → Auftrag → Bindung</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ VORGEHEN ============ -->
|
||||
<section class="section" id="vorgehen">
|
||||
<div class="wrap">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">Vorgehen</span>
|
||||
<h2 class="section-title">In vier klaren Schritten.</h2>
|
||||
<p class="section-intro">
|
||||
Kein Wasserfall, kein Blindflug. Ein Ablauf, der Ergebnisse früh
|
||||
sichtbar macht und Sie an jeder Stelle einbezieht.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ol class="steps">
|
||||
<li class="step">
|
||||
<span class="step-num">01</span>
|
||||
<h3>Situationsanalyse</h3>
|
||||
<p>Unser Team geht mit Ihnen alle Prozesse und Abläufe in Ihrem
|
||||
Betrieb durch, um klarzustellen, wo Zeit, Effizienz und damit
|
||||
auch Geld verloren gehen.</p>
|
||||
<h3>Analyse</h3>
|
||||
<p>Wir sehen uns Ihren Ablauf an und legen offen, wo Zeit, Daten
|
||||
und Klarheit verloren gehen.</p>
|
||||
</li>
|
||||
<li class="step">
|
||||
<span class="step-num">02</span>
|
||||
<h3>Grundkonzepte</h3>
|
||||
<p>Aus der Situationsanalyse erstellen wir zusammen ein
|
||||
Lastenheft. Daraus bauen wir Ihnen erste Lösungskonzepte, die
|
||||
wir iterativ auf Basis Ihres Feedbacks anpassen, bis wir zu
|
||||
Ihrem gewünschten Endergebnis kommen.</p>
|
||||
<h3>Konzept</h3>
|
||||
<p>Ein konkreter Lösungsvorschlag mit Aufwand, Umfang und
|
||||
erwartetem Nutzen, transparent kalkuliert.</p>
|
||||
</li>
|
||||
<li class="step">
|
||||
<span class="step-num">03</span>
|
||||
<h3>Umsetzung</h3>
|
||||
<p>Nach Vereinbarung des Lösungswegs bekommen Sie früh Ihre
|
||||
erste funktionierende Version mit allen Grundfunktionen.
|
||||
Iterativ wird Ihre Software verbessert und zum gewünschten
|
||||
Endergebnis geformt.</p>
|
||||
<p>Wir bauen in kurzen Zyklen. Sie sehen früh ein funktionierendes
|
||||
Ergebnis statt nur Folien.</p>
|
||||
</li>
|
||||
<li class="step">
|
||||
<span class="step-num">04</span>
|
||||
<h3>Betrieb & Wartung</h3>
|
||||
<p>Übergabe, Einweisungen, Updates und Wartung, damit Ihre
|
||||
Software langfristig tragfähig bleibt.</p>
|
||||
<h3>Betrieb</h3>
|
||||
<p>Übergabe, Einweisung und laufende Pflege, damit die Lösung
|
||||
dauerhaft trägt.</p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
@@ -318,47 +466,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ STANDORT / EU-HOSTING ============ -->
|
||||
<section class="section section-alt" id="standort">
|
||||
<div class="wrap">
|
||||
<div class="section-head standort-head">
|
||||
<span class="eyebrow">Standort</span>
|
||||
<h2 class="section-title">Ihre Daten gehören nicht nach Virginia.</h2>
|
||||
<p class="section-intro">
|
||||
Die meisten Software-Anbieter, mit denen deutsche Unternehmen täglich
|
||||
arbeiten, sind US-Konzerne. Heißt: Ihre Daten unterliegen US-Recht,
|
||||
unabhängig davon, was im Vertrag steht oder wo der Server tatsächlich
|
||||
läuft.
|
||||
</p>
|
||||
<p class="standort-claim">
|
||||
Wir entwickeln in Deutschland, hosten in Deutschland und haften in
|
||||
Deutschland.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ol class="steps steps-3">
|
||||
<li class="step">
|
||||
<span class="step-num">01</span>
|
||||
<h3>In Deutschland gehostet</h3>
|
||||
<p>Unsere Server stehen in deutschen Rechenzentren. Keine versteckte
|
||||
Subunternehmer-Kette, die im US-Cloud-Stack endet.</p>
|
||||
</li>
|
||||
<li class="step">
|
||||
<span class="step-num">02</span>
|
||||
<h3>Dem deutschen Recht unterstellt</h3>
|
||||
<p>Verträge nach BGB, Datenschutz nach DSGVO, ohne Hintertür. Der
|
||||
CLOUD Act hat hier nichts verloren.</p>
|
||||
</li>
|
||||
<li class="step">
|
||||
<span class="step-num">03</span>
|
||||
<h3>Von Menschen erreichbar</h3>
|
||||
<p>Kein Support-Ticket im Niemandsland. Wenn etwas brennt, sprechen
|
||||
Sie mit den Leuten, die Ihre Software gebaut haben.</p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ EVENTS / COMMUNITY ============ -->
|
||||
<section class="section event-section" id="events">
|
||||
<div class="wrap">
|
||||
@@ -370,16 +477,17 @@
|
||||
anfassen: lokal, direkt und mit Ergebnissen, über die man reden kann.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="event-grid">
|
||||
<article class="event-card event-card-media">
|
||||
<div class="event-poster">
|
||||
<img src="/assets/techday-event.png" alt="TechDay Bruchsal 2026 Motiv" width="683" height="384" loading="lazy" decoding="async" />
|
||||
<img src="/assets/techday-event.webp" alt="TechDay Bruchsal 2026 Motiv" width="683" height="384" loading="lazy" decoding="async" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="event-label">27. Mai 2026</span>
|
||||
<h3>TechDay Bruchsal</h3>
|
||||
<p>Talks, Workshops und Austausch rund um Technologie in Bruchsal.</p>
|
||||
<a href="/techday.html">TechDay ansehen</a>
|
||||
<a href="/techday">TechDay ansehen</a>
|
||||
</div>
|
||||
</article>
|
||||
<article class="event-card event-card-code">
|
||||
@@ -392,7 +500,7 @@
|
||||
<span class="event-label">TechDay Format</span>
|
||||
<h3>AI GameJam</h3>
|
||||
<p>Ein kompaktes Format, bei dem aus Idee, KI-Unterstützung und menschlicher Steuerung ein spielbares Ergebnis wird.</p>
|
||||
<a href="/gamejam.html">GameJam ansehen</a>
|
||||
<a href="/gamejam">GameJam ansehen</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
@@ -404,11 +512,16 @@
|
||||
<div class="wrap kontakt-grid">
|
||||
<div class="kontakt-copy">
|
||||
<span class="eyebrow eyebrow-light">Kontakt</span>
|
||||
<h2 class="section-title">Sprechen wir über Ihr Vorhaben.</h2>
|
||||
<h2 class="section-title">Lassen Sie uns Ihren Ablauf prüfen.</h2>
|
||||
<p class="section-intro">
|
||||
Interesse? Fragen? Lassen Sie uns darüber reden. Füllen Sie das
|
||||
Formular aus, wir melden uns in Kürze bei Ihnen.
|
||||
Schicken Sie uns kurz, wo Arbeit aktuell hängen bleibt. Wir melden uns
|
||||
mit einer ehrlichen Einschätzung, ob ein eigenes System sinnvoll ist.
|
||||
</p>
|
||||
<ul class="contact-promises">
|
||||
<li>Antwort in der Regel innerhalb eines Werktags</li>
|
||||
<li>Erstgespräch ohne Kosten und ohne Pitch-Deck</li>
|
||||
<li>Klare Einschätzung, auch wenn wir nicht die Richtigen sind</li>
|
||||
</ul>
|
||||
<dl class="contact-list">
|
||||
<div>
|
||||
<dt>E-Mail</dt>
|
||||
@@ -422,25 +535,13 @@
|
||||
</div>
|
||||
|
||||
<form class="lead-form" id="lead-form" novalidate>
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<label for="f-vorname">Vorname <span>*</span></label>
|
||||
<input id="f-vorname" name="vorname" type="text" autocomplete="given-name" required />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="f-nachname">Nachname <span>*</span></label>
|
||||
<input id="f-nachname" name="nachname" type="text" autocomplete="family-name" required />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="f-name">Name <span>*</span></label>
|
||||
<input id="f-name" name="name" type="text" autocomplete="name" required />
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<label for="f-firmenname">Firmenname</label>
|
||||
<input id="f-firmenname" name="firmenname" type="text" autocomplete="organization" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="f-branche">Branche</label>
|
||||
<input id="f-branche" name="branche" type="text" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="f-firma">Firma</label>
|
||||
<input id="f-firma" name="firma" type="text" autocomplete="organization" />
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
@@ -448,14 +549,14 @@
|
||||
<input id="f-email" name="email" type="email" autocomplete="email" required />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="f-telefon">Telefonnummer</label>
|
||||
<label for="f-telefon">Telefon</label>
|
||||
<input id="f-telefon" name="telefon" type="tel" autocomplete="tel" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="f-nachricht">Worum geht es Ihnen? <span>*</span></label>
|
||||
<label for="f-nachricht">Worum geht es? <span>*</span></label>
|
||||
<textarea id="f-nachricht" name="nachricht" rows="4" required
|
||||
placeholder="Kurz Ihr Anliegen, wir kümmern uns um die Details."></textarea>
|
||||
placeholder="Kurz Ihr Anliegen — wir kümmern uns um die Details."></textarea>
|
||||
</div>
|
||||
<!-- Honeypot gegen Spam -->
|
||||
<div class="hp" aria-hidden="true">
|
||||
@@ -466,7 +567,7 @@
|
||||
<button type="submit" class="btn btn-solid">Anfrage senden</button>
|
||||
<p class="form-note">Mit dem Absenden stimmen Sie der Bearbeitung
|
||||
Ihrer Angaben gemäß der
|
||||
<a href="/datenschutz.html">Datenschutzerklärung</a> zu.</p>
|
||||
<a href="/datenschutz">Datenschutzerklärung</a> zu.</p>
|
||||
</div>
|
||||
<p class="form-status" id="form-status" role="status" hidden></p>
|
||||
</form>
|
||||
@@ -477,36 +578,23 @@
|
||||
|
||||
<!-- ============ FOOTER ============ -->
|
||||
<footer class="site-footer">
|
||||
<div class="wrap footer-inner">
|
||||
<div class="wrap footer-bar">
|
||||
<div class="footer-brand">
|
||||
<img src="/assets/logo-white.svg" alt="ToolCTRL" class="footer-logo" />
|
||||
<p>Die Software, die eure Branche längst verdient hat.</p>
|
||||
<img src="/assets/wordmark-white.svg" alt="ToolCTRL" class="footer-logo" />
|
||||
<p>© <span id="year">2026</span> ToolCTRL · Bruchsal</p>
|
||||
</div>
|
||||
<div class="footer-cols">
|
||||
<div class="footer-col">
|
||||
<span class="footer-h">Navigation</span>
|
||||
<a href="#leistungen">Leistungen</a>
|
||||
<a href="#vorgehen">Vorgehen</a>
|
||||
<a href="#arbeit">Organisation</a>
|
||||
<a href="/techday.html">TechDay</a>
|
||||
<a href="/gamejam.html">GameJam</a>
|
||||
<a href="#kontakt">Kontakt</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<span class="footer-h">Kontakt</span>
|
||||
<a href="tel:+4915237750939">+49 1523 7750939</a>
|
||||
<a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a>
|
||||
<a href="https://tool-ctrl.de">tool-ctrl.de</a>
|
||||
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener">LinkedIn</a>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a>
|
||||
<i></i>
|
||||
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener">LinkedIn</a>
|
||||
<i></i>
|
||||
<a href="/impressum">Impressum</a>
|
||||
·
|
||||
<a href="/datenschutz">Datenschutz</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrap footer-base">
|
||||
<span>© <span id="year">2026</span> ToolCTRL</span>
|
||||
<span><a href="/impressum.html">Impressum</a> · <a href="/datenschutz.html">Datenschutz</a></span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/script.js?v=20260610"></script>
|
||||
<script src="/script.js?v=20260554"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /gamejam-games/
|
||||
|
||||
Sitemap: https://tool-ctrl.de/sitemap.xml
|
||||
@@ -9,6 +9,14 @@
|
||||
/* ---- Mobile-Navigation ---- */
|
||||
var toggle = document.querySelector(".nav-toggle");
|
||||
var mobileNav = document.querySelector(".mobile-nav");
|
||||
var header = document.querySelector(".site-header");
|
||||
function updateHeaderState() {
|
||||
if (!header) return;
|
||||
header.classList.toggle("is-scrolled", window.scrollY > 12);
|
||||
}
|
||||
updateHeaderState();
|
||||
window.addEventListener("scroll", updateHeaderState, { passive: true });
|
||||
|
||||
if (toggle && mobileNav) {
|
||||
toggle.addEventListener("click", function () {
|
||||
var open = mobileNav.hasAttribute("hidden");
|
||||
@@ -28,6 +36,303 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Console-Easter-Eggs fuer neugierige Inspektoren ---- */
|
||||
var inspectAscii = [
|
||||
" _______ _______ _______ ___ _______ _______ _______ ",
|
||||
"| || || || | | || || |",
|
||||
"|_ _|| _ || _ || | | ||_ _|| ___|",
|
||||
" | | | | | || | | || | | | | | | |___ ",
|
||||
" | | | |_| || |_| || |___ | _| | | | ___|",
|
||||
" | | | || || || |_ | | | |___ ",
|
||||
" |___| |_______||_______||_______||_______| |___| |_______|"
|
||||
].join("\n");
|
||||
var inspectHints = [
|
||||
"CTRL-KEY EVENT: Inspect gedrueckt. Bitte keine Produktivsysteme anfassen.",
|
||||
"Hidden combo: window.toolctrl.press()",
|
||||
"Status: schwarz/weiss. Farbe wurde zurueck in den Werkzeugkoffer gelegt."
|
||||
];
|
||||
var inspectLogged = false;
|
||||
|
||||
function logControlConsole(reason) {
|
||||
if (reason === "devtools" && inspectLogged) return;
|
||||
if (reason === "devtools") inspectLogged = true;
|
||||
console.log(
|
||||
"%c" + inspectAscii,
|
||||
"font-family: monospace; font-weight: 900; color: #000; background: #fff; padding: 10px; border: 3px solid #000;"
|
||||
);
|
||||
console.log("%c" + inspectHints[Math.floor(Math.random() * inspectHints.length)],
|
||||
"font-family: monospace; color: #fff; background: #000; padding: 6px 8px;");
|
||||
}
|
||||
|
||||
window.toolctrl = window.toolctrl || {};
|
||||
window.toolctrl.press = function () {
|
||||
console.log("%c[ CTRL ] *klack* Ablauf uebernommen.", "font-family: monospace; font-weight: 900;");
|
||||
return "pressed";
|
||||
};
|
||||
|
||||
window.addEventListener("load", function () {
|
||||
setTimeout(function () { logControlConsole("load"); }, 450);
|
||||
});
|
||||
setInterval(function () {
|
||||
var open = Math.abs(window.outerWidth - window.innerWidth) > 160 ||
|
||||
Math.abs(window.outerHeight - window.innerHeight) > 160;
|
||||
document.documentElement.toggleAttribute("data-inspecting", open);
|
||||
if (open) logControlConsole("devtools");
|
||||
}, 1200);
|
||||
|
||||
/* ---- Dezente Scroll-Reveals ---- */
|
||||
var reduceMotion = window.matchMedia &&
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
var revealEls = Array.prototype.slice.call(document.querySelectorAll([
|
||||
".hero-copy > *",
|
||||
".hero-visual",
|
||||
".trust-strip span",
|
||||
".stance-head > *",
|
||||
".stance-card",
|
||||
".product-copy > *",
|
||||
".product-ui",
|
||||
".velocity-copy > *",
|
||||
".velocity-item",
|
||||
".section-head",
|
||||
".card",
|
||||
".industry-card",
|
||||
".step",
|
||||
".arbeit-copy > *",
|
||||
".kanban-wrap",
|
||||
".event-poster",
|
||||
".mini-terminal",
|
||||
".event-card",
|
||||
".kontakt-copy > *",
|
||||
".contact-promises li",
|
||||
".lead-form",
|
||||
".td-hero .wrap > *",
|
||||
".td-feature > *",
|
||||
".td-talk",
|
||||
".speaker-card",
|
||||
".shot",
|
||||
".gj-teaser"
|
||||
].join(",")));
|
||||
|
||||
revealEls.forEach(function (el, index) {
|
||||
el.classList.add("reveal", "reveal-delay-" + (index % 5));
|
||||
});
|
||||
|
||||
if (reduceMotion || !("IntersectionObserver" in window)) {
|
||||
revealEls.forEach(function (el) { el.classList.add("is-visible"); });
|
||||
} else {
|
||||
var observer = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (!entry.isIntersecting) return;
|
||||
entry.target.classList.add("is-visible");
|
||||
observer.unobserve(entry.target);
|
||||
});
|
||||
}, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" });
|
||||
revealEls.forEach(function (el) { observer.observe(el); });
|
||||
}
|
||||
|
||||
/* ---- Hero Workflow Nodes: ziehbare Kontrollpunkte ---- */
|
||||
var board = document.querySelector("[data-control-board]");
|
||||
if (board) {
|
||||
var lineSvg = board.querySelector(".control-lines");
|
||||
var logoNode = board.querySelector("[data-logo-node]");
|
||||
var nodes = Array.prototype.slice.call(board.querySelectorAll("[data-node]"));
|
||||
var draggableItems = nodes.concat(logoNode ? [logoNode] : []);
|
||||
var activeDrag = null;
|
||||
var lineFrame = 0;
|
||||
|
||||
if (logoNode) {
|
||||
logoNode.addEventListener("dragstart", function (e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
function boardRect() {
|
||||
return board.getBoundingClientRect();
|
||||
}
|
||||
|
||||
function centerOf(el, b) {
|
||||
var r = el.getBoundingClientRect();
|
||||
return {
|
||||
x: r.left - b.left + r.width / 2,
|
||||
y: r.top - b.top + r.height / 2
|
||||
};
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function updateLines() {
|
||||
if (!lineSvg || !logoNode) return;
|
||||
var b = boardRect();
|
||||
var target = centerOf(logoNode, b);
|
||||
lineSvg.setAttribute("viewBox", "0 0 " + b.width + " " + b.height);
|
||||
nodes.forEach(function (node) {
|
||||
var path = lineSvg.querySelector('[data-line="' + node.dataset.node + '"]');
|
||||
if (!path) return;
|
||||
var nodeCenter = centerOf(node, b);
|
||||
var isOutput = node.classList.contains("is-output");
|
||||
var start = isOutput ? target : nodeCenter;
|
||||
var end = isOutput ? nodeCenter : target;
|
||||
var bend = Math.max(44, Math.abs(end.x - start.x) * 0.38);
|
||||
var c1x = start.x + (end.x > start.x ? bend : -bend);
|
||||
var c2x = end.x + (end.x > start.x ? -bend : bend);
|
||||
var d = "M " + start.x + " " + start.y +
|
||||
" C " + c1x + " " + start.y +
|
||||
", " + c2x + " " + end.y +
|
||||
", " + end.x + " " + end.y;
|
||||
path.classList.toggle("is-output", isOutput);
|
||||
path.setAttribute("d", d);
|
||||
});
|
||||
lineFrame = 0;
|
||||
}
|
||||
|
||||
function requestLineUpdate() {
|
||||
if (lineFrame) return;
|
||||
lineFrame = window.requestAnimationFrame(updateLines);
|
||||
}
|
||||
|
||||
draggableItems.forEach(function (item) {
|
||||
item.addEventListener("pointerdown", function (e) {
|
||||
var r = item.getBoundingClientRect();
|
||||
if (item === logoNode) item.classList.add("is-positioned");
|
||||
activeDrag = {
|
||||
node: item,
|
||||
isLogo: item === logoNode,
|
||||
offsetX: e.clientX - r.left,
|
||||
offsetY: e.clientY - r.top
|
||||
};
|
||||
item.classList.add("is-dragging");
|
||||
item.setPointerCapture(e.pointerId);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
item.addEventListener("pointermove", function (e) {
|
||||
if (!activeDrag || activeDrag.node !== item) return;
|
||||
var b = boardRect();
|
||||
var x = clamp(e.clientX - b.left - activeDrag.offsetX, 8, b.width - item.offsetWidth - 8);
|
||||
var y = clamp(e.clientY - b.top - activeDrag.offsetY, 8, b.height - item.offsetHeight - 8);
|
||||
item.style.left = x + "px";
|
||||
item.style.top = y + "px";
|
||||
if (activeDrag.isLogo) {
|
||||
item.classList.add("is-positioned");
|
||||
} else {
|
||||
item.style.removeProperty("--x");
|
||||
item.style.removeProperty("--y");
|
||||
}
|
||||
requestLineUpdate();
|
||||
});
|
||||
|
||||
item.addEventListener("pointerup", function (e) {
|
||||
if (!activeDrag || activeDrag.node !== item) return;
|
||||
item.classList.remove("is-dragging");
|
||||
item.releasePointerCapture(e.pointerId);
|
||||
activeDrag = null;
|
||||
requestLineUpdate();
|
||||
});
|
||||
|
||||
item.addEventListener("pointercancel", function () {
|
||||
item.classList.remove("is-dragging");
|
||||
activeDrag = null;
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("resize", requestLineUpdate, { passive: true });
|
||||
updateLines();
|
||||
setTimeout(requestLineUpdate, 150);
|
||||
}
|
||||
|
||||
/* ---- Produktwirkung: klickbare Workflow-Zustaende ---- */
|
||||
var productUi = document.querySelector(".product-ui");
|
||||
if (productUi) {
|
||||
var productStates = {
|
||||
eingang: {
|
||||
kicker: "Heute · 14 aktive Vorgänge",
|
||||
title: "Workflow Control",
|
||||
pill: "3 Blocker",
|
||||
flow: ["Anfrage", "Prüfung", "Angebot", "Termin", "Rechnung"],
|
||||
active: 1,
|
||||
done: 0,
|
||||
metrics: [["Antwortzeit", "18 min"], ["Auto-Schritte", "74%"], ["Offene Übergaben", "03"]],
|
||||
rows: [["Kunde wartet auf Rückmeldung", "Jetzt", "is-hot"], ["Dokumente aus Postfach übernommen", "Auto", ""], ["Team-Zuständigkeit gesetzt", "11:15", ""], ["Status an Leitung gemeldet", "Erledigt", "is-done"]]
|
||||
},
|
||||
angebote: {
|
||||
kicker: "Vertrieb · 8 Angebote offen",
|
||||
title: "Angebote steuern",
|
||||
pill: "2 fällig",
|
||||
flow: ["Lead", "Kalkulation", "Freigabe", "Versand", "Follow-up"],
|
||||
active: 2,
|
||||
done: 1,
|
||||
metrics: [["Durchlauf", "2,4 h"], ["Vorlagen", "91%"], ["Rückfragen", "05"]],
|
||||
rows: [["Angebot braucht interne Freigabe", "Heute", "is-hot"], ["PDF aus Kundendaten erstellt", "Auto", ""], ["Nachfass-Termin gesetzt", "Morgen", ""], ["Alte Version archiviert", "Erledigt", "is-done"]]
|
||||
},
|
||||
planung: {
|
||||
kicker: "Team · 21 Einsätze geplant",
|
||||
title: "Planung sichtbar",
|
||||
pill: "1 Konflikt",
|
||||
flow: ["Auftrag", "Ressource", "Slot", "Route", "Update"],
|
||||
active: 2,
|
||||
done: 1,
|
||||
metrics: [["Auslastung", "82%"], ["Auto-Routen", "68%"], ["Konflikte", "01"]],
|
||||
rows: [["Termin überschneidet sich", "Prüfen", "is-hot"], ["Route für Außendienst gebaut", "Auto", ""], ["Materialliste angehängt", "09:40", ""], ["Kunde informiert", "Erledigt", "is-done"]]
|
||||
},
|
||||
abschluss: {
|
||||
kicker: "Abschluss · 36 Vorgänge fertig",
|
||||
title: "Betrieb im Blick",
|
||||
pill: "0 kritisch",
|
||||
flow: ["Abnahme", "Doku", "Rechnung", "Feedback", "Archiv"],
|
||||
active: 3,
|
||||
done: 2,
|
||||
metrics: [["Abnahmen", "96%"], ["Auto-Doku", "88%"], ["Offen", "00"]],
|
||||
rows: [["Feedback für nächste Version notiert", "Neu", ""], ["Rechnung vorbereitet", "Auto", ""], ["Abnahme protokolliert", "13:20", "is-done"], ["Projekt sauber archiviert", "Erledigt", "is-done"]]
|
||||
}
|
||||
};
|
||||
var productTabs = Array.prototype.slice.call(productUi.querySelectorAll("[data-product-tab]"));
|
||||
var productKicker = productUi.querySelector("[data-product-kicker]");
|
||||
var productTitle = productUi.querySelector("[data-product-title]");
|
||||
var productPill = productUi.querySelector("[data-product-pill]");
|
||||
var productFlow = productUi.querySelector("[data-product-flow]");
|
||||
var productMetrics = productUi.querySelector("[data-product-metrics]");
|
||||
var productTable = productUi.querySelector("[data-product-table]");
|
||||
|
||||
function setProductState(key) {
|
||||
var state = productStates[key];
|
||||
if (!state) return;
|
||||
productUi.classList.add("is-switching");
|
||||
productTabs.forEach(function (tab) {
|
||||
tab.classList.toggle("active", tab.dataset.productTab === key);
|
||||
});
|
||||
window.setTimeout(function () {
|
||||
if (productKicker) productKicker.textContent = state.kicker;
|
||||
if (productTitle) productTitle.textContent = state.title;
|
||||
if (productPill) productPill.textContent = state.pill;
|
||||
if (productFlow) {
|
||||
productFlow.innerHTML = state.flow.map(function (label, index) {
|
||||
var cls = index === state.active ? "active" : (index <= state.done ? "done" : "");
|
||||
return '<span class="' + cls + '">' + label + '</span>';
|
||||
}).join("");
|
||||
}
|
||||
if (productMetrics) {
|
||||
productMetrics.innerHTML = state.metrics.map(function (metric) {
|
||||
return "<div><span>" + metric[0] + "</span><strong>" + metric[1] + "</strong></div>";
|
||||
}).join("");
|
||||
}
|
||||
if (productTable) {
|
||||
productTable.innerHTML = state.rows.map(function (row) {
|
||||
return '<div class="ui-row ' + row[2] + '"><span>' + row[0] + '</span><b>' + row[1] + '</b></div>';
|
||||
}).join("");
|
||||
}
|
||||
productUi.classList.remove("is-switching");
|
||||
}, 90);
|
||||
}
|
||||
|
||||
productTabs.forEach(function (tab) {
|
||||
tab.addEventListener("click", function () {
|
||||
setProductState(tab.dataset.productTab);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Lead-Formular ---- */
|
||||
var form = document.getElementById("lead-form");
|
||||
var status = document.getElementById("form-status");
|
||||
@@ -46,9 +351,8 @@
|
||||
|
||||
function validate() {
|
||||
var ok = true;
|
||||
["f-vorname", "f-nachname", "f-email", "f-nachricht"].forEach(function (id) {
|
||||
["f-name", "f-email", "f-nachricht"].forEach(function (id) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
var row = fieldRow(el);
|
||||
var valid = el.value.trim() !== "";
|
||||
if (id === "f-email" && valid) {
|
||||
@@ -69,22 +373,14 @@
|
||||
}
|
||||
|
||||
var btn = form.querySelector("button[type=submit]");
|
||||
var vorname = form.vorname.value.trim();
|
||||
var nachname = form.nachname.value.trim();
|
||||
var firmenname = form.firmenname.value.trim();
|
||||
var branche = form.branche.value.trim();
|
||||
var payload = {
|
||||
name: (vorname + " " + nachname).trim(),
|
||||
vorname: vorname,
|
||||
nachname: nachname,
|
||||
firma: firmenname + (branche ? " (" + branche + ")" : ""),
|
||||
firmenname: firmenname,
|
||||
branche: branche,
|
||||
name: form.name.value.trim(),
|
||||
firma: form.firma.value.trim(),
|
||||
email: form.email.value.trim(),
|
||||
telefon: form.telefon.value.trim(),
|
||||
nachricht: form.nachricht.value.trim(),
|
||||
website: form.website.value.trim(), // Honeypot
|
||||
quelle: "Landing Page"
|
||||
quelle: "Messe — Landing Page"
|
||||
};
|
||||
|
||||
if (btn) { btn.disabled = true; btn.textContent = "Wird gesendet …"; }
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://tool-ctrl.de/</loc>
|
||||
<lastmod>2026-06-10</lastmod>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://tool-ctrl.de/techday.html</loc>
|
||||
<lastmod>2026-06-10</lastmod>
|
||||
<priority>0.8</priority>
|
||||
<loc>https://tool-ctrl.de/techday</loc>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://tool-ctrl.de/gamejam.html</loc>
|
||||
<lastmod>2026-06-10</lastmod>
|
||||
<priority>0.6</priority>
|
||||
<loc>https://tool-ctrl.de/gamejam</loc>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://tool-ctrl.de/impressum</loc>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://tool-ctrl.de/datenschutz</loc>
|
||||
</url>
|
||||
</urlset>
|
||||
@@ -3,52 +3,21 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>TechDay Bruchsal 2026: ToolCTRL</title>
|
||||
<title>TechDay Bruchsal 2026 — ToolCTRL</title>
|
||||
<meta name="description" content="ToolCTRL beim Bruchsaler TechDay 2026: Vortrag, AI GameJam und die mITapp. Am 27. Mai 2026 im HubWerk01 Bruchsal." />
|
||||
<link rel="canonical" href="https://tool-ctrl.de/techday.html" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<link rel="canonical" href="https://tool-ctrl.de/techday" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="ToolCTRL" />
|
||||
<meta property="og:locale" content="de_DE" />
|
||||
<meta property="og:url" content="https://tool-ctrl.de/techday.html" />
|
||||
<meta property="og:title" content="TechDay Bruchsal 2026: ToolCTRL" />
|
||||
<meta property="og:description" content="ToolCTRL beim Bruchsaler TechDay 2026: Vortrag, AI GameJam und die mITapp. Am 27. Mai 2026 im HubWerk01 Bruchsal." />
|
||||
<meta property="og:image" content="https://tool-ctrl.de/assets/techday-event.png" />
|
||||
<meta property="og:image:width" content="683" />
|
||||
<meta property="og:image:height" content="384" />
|
||||
<meta property="og:image:alt" content="TechDay Bruchsal 2026 Eventmotiv" />
|
||||
<meta property="og:site_name" content="ToolCTRL" />
|
||||
<meta property="og:title" content="ToolCTRL beim TechDay Bruchsal 2026" />
|
||||
<meta property="og:description" content="Vortrag, AI GameJam und mITapp beim ersten Bruchsaler TechDay am 27. Mai 2026." />
|
||||
<meta property="og:url" content="https://tool-ctrl.de/techday" />
|
||||
<meta property="og:image" content="https://tool-ctrl.de/assets/techday-event.webp" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
|
||||
<!-- Structured Data -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Event",
|
||||
"name": "TechDay Bruchsal 2026",
|
||||
"startDate": "2026-05-27",
|
||||
"eventStatus": "https://schema.org/EventScheduled",
|
||||
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
|
||||
"location": {
|
||||
"@type": "Place",
|
||||
"name": "HubWerk01",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressLocality": "Bruchsal",
|
||||
"addressCountry": "DE"
|
||||
}
|
||||
},
|
||||
"image": "https://tool-ctrl.de/assets/techday-event.png",
|
||||
"description": "ToolCTRL beim Bruchsaler TechDay 2026: Vortrag, AI GameJam und die mITapp. Am 27. Mai 2026 im HubWerk01 Bruchsal.",
|
||||
"organizer": {
|
||||
"@type": "Organization",
|
||||
"name": "ToolCTRL",
|
||||
"url": "https://tool-ctrl.de"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png?v=20260610" />
|
||||
<link rel="stylesheet" href="/styles.css?v=20260610" />
|
||||
<meta name="theme-color" content="#0A0A0A" />
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png" />
|
||||
<link rel="preload" href="/assets/fonts/space-grotesk-latin.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="stylesheet" href="/styles.css?v=20260564" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -56,23 +25,29 @@
|
||||
<header class="site-header" id="top">
|
||||
<div class="wrap header-inner">
|
||||
<a class="brand" href="/" aria-label="ToolCTRL Startseite">
|
||||
<img src="/assets/logo-black.svg" alt="ToolCTRL" class="brand-logo" />
|
||||
<img src="/assets/wordmark-black.svg" alt="ToolCTRL" class="brand-logo" />
|
||||
</a>
|
||||
<nav class="nav" aria-label="Hauptnavigation">
|
||||
<a href="/">Start</a>
|
||||
<a href="/techday.html">TechDay</a>
|
||||
<a href="/gamejam.html">GameJam</a>
|
||||
<a class="btn btn-solid btn-sm" href="/#kontakt">Kontakt aufnehmen</a>
|
||||
<a href="/#leistungen">Leistungen</a>
|
||||
<a href="/#branchen">Branchen</a>
|
||||
<a href="/#vorgehen">Vorgehen</a>
|
||||
<a href="/#arbeit">Organisation</a>
|
||||
<a href="/techday">TechDay</a>
|
||||
<a href="/gamejam">GameJam</a>
|
||||
<a class="btn btn-solid btn-sm" href="/#kontakt">Ablauf prüfen</a>
|
||||
</nav>
|
||||
<button class="nav-toggle" aria-label="Menü" aria-expanded="false">
|
||||
<span></span><span></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mobile-nav" hidden>
|
||||
<a href="/">Start</a>
|
||||
<a href="/techday.html">TechDay</a>
|
||||
<a href="/gamejam.html">GameJam</a>
|
||||
<a class="btn btn-solid" href="/#kontakt">Kontakt aufnehmen</a>
|
||||
<a href="/#leistungen">Leistungen</a>
|
||||
<a href="/#branchen">Branchen</a>
|
||||
<a href="/#vorgehen">Vorgehen</a>
|
||||
<a href="/#arbeit">Organisation</a>
|
||||
<a href="/techday">TechDay</a>
|
||||
<a href="/gamejam">GameJam</a>
|
||||
<a class="btn btn-solid" href="/#kontakt">Ablauf prüfen</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -115,7 +90,7 @@
|
||||
mit der mITapp als digitalem Zuhause des Events.
|
||||
</p>
|
||||
</div>
|
||||
<img class="td-feature-img" src="/assets/techday-event.png"
|
||||
<img class="td-feature-img" src="/assets/techday-event.webp" width="683" height="384" decoding="async" fetchpriority="high"
|
||||
alt="TechDay Bruchsal 2026: Human in the Loop" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,7 +137,7 @@
|
||||
<aside class="speaker-card">
|
||||
<div class="sp-label">Speaker</div>
|
||||
<div class="sp-head">
|
||||
<img class="sp-photo" src="/assets/speaker-david.jpg" alt="David Bertram" width="320" height="320" loading="lazy" decoding="async" />
|
||||
<img class="sp-photo" src="/assets/speaker-david.webp" alt="David Bertram" width="320" height="320" loading="lazy" decoding="async" />
|
||||
<div>
|
||||
<div class="sp-name">David Bertram</div>
|
||||
<div class="sp-role">Co-Founder & Head of Product, ToolCTRL</div>
|
||||
@@ -230,7 +205,7 @@
|
||||
Upload für die fertigen Spiele. Die GameJam hat ihre eigene Seite.
|
||||
</p>
|
||||
<div class="hero-actions" style="margin-top:28px;">
|
||||
<a class="btn btn-solid" href="/gamejam.html">Zur GameJam-Seite</a>
|
||||
<a class="btn btn-solid" href="/gamejam">Zur GameJam-Seite</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gj-teaser" aria-hidden="true">
|
||||
@@ -279,36 +254,23 @@
|
||||
|
||||
<!-- ============ FOOTER ============ -->
|
||||
<footer class="site-footer">
|
||||
<div class="wrap footer-inner">
|
||||
<div class="wrap footer-bar">
|
||||
<div class="footer-brand">
|
||||
<img src="/assets/logo-white.svg" alt="ToolCTRL" class="footer-logo" />
|
||||
<p>Die Software, die eure Branche längst verdient hat.</p>
|
||||
<img src="/assets/wordmark-white.svg" alt="ToolCTRL" class="footer-logo" />
|
||||
<p>© <span id="year">2026</span> ToolCTRL · Bruchsal</p>
|
||||
</div>
|
||||
<div class="footer-cols">
|
||||
<div class="footer-col">
|
||||
<span class="footer-h">Navigation</span>
|
||||
<a href="/#leistungen">Leistungen</a>
|
||||
<a href="/#vorgehen">Vorgehen</a>
|
||||
<a href="/#arbeit">Organisation</a>
|
||||
<a href="/techday.html">TechDay</a>
|
||||
<a href="/gamejam.html">GameJam</a>
|
||||
<a href="/#kontakt">Kontakt</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<span class="footer-h">Kontakt</span>
|
||||
<a href="tel:+4915237750939">+49 1523 7750939</a>
|
||||
<a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a>
|
||||
<a href="https://tool-ctrl.de">tool-ctrl.de</a>
|
||||
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener">LinkedIn</a>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a>
|
||||
<i></i>
|
||||
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener">LinkedIn</a>
|
||||
<i></i>
|
||||
<a href="/impressum">Impressum</a>
|
||||
·
|
||||
<a href="/datenschutz">Datenschutz</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrap footer-base">
|
||||
<span>© <span id="year">2026</span> ToolCTRL</span>
|
||||
<span><a href="/impressum.html">Impressum</a> · <a href="/datenschutz.html">Datenschutz</a></span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/script.js?v=20260610"></script>
|
||||
<script src="/script.js?v=20260554"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,82 +16,36 @@ const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const os = require("os");
|
||||
const net = require("net");
|
||||
const zlib = require("zlib");
|
||||
|
||||
const ROOT = __dirname;
|
||||
const PUBLIC_DIR = path.join(ROOT, "public");
|
||||
const DATA_DIR = process.env.DATA_DIR
|
||||
? path.resolve(process.env.DATA_DIR)
|
||||
: ROOT;
|
||||
const LEADS_DIR = path.join(DATA_DIR, "leads");
|
||||
const LEADS_DIR = path.join(ROOT, "leads");
|
||||
const LEADS_JSON = path.join(LEADS_DIR, "leads.ndjson");
|
||||
const LEADS_CSV = path.join(LEADS_DIR, "leads.csv");
|
||||
|
||||
/* GameJam-Speicher */
|
||||
const GAMEJAM_DIR = path.join(DATA_DIR, "gamejam");
|
||||
const GAMEJAM_DIR = path.join(ROOT, "gamejam");
|
||||
const SUBMISSIONS_JSON = path.join(GAMEJAM_DIR, "submissions.ndjson");
|
||||
const GAMES_DIR = process.env.DATA_DIR
|
||||
? path.join(DATA_DIR, "gamejam-games")
|
||||
: path.join(PUBLIC_DIR, "gamejam-games");
|
||||
const GAMES_DIR = path.join(PUBLIC_DIR, "gamejam-games");
|
||||
const MAX_UPLOAD = 5 * 1024 * 1024; /* 5 MB pro eingereichtem Spiel */
|
||||
|
||||
/* Hart-Deadline für GameJam-Uploads: 27.05.2026, 22:00 Europe/Berlin (CEST = UTC+2). */
|
||||
const UPLOAD_DEADLINE = new Date("2026-05-27T22:00:00+02:00");
|
||||
const APP_REVISION = process.env.APP_REVISION || "local";
|
||||
|
||||
/* Central lead-inbox (infra ADR 012). Sites forward leads here; local files
|
||||
stay as a fallback. Unset env => old local-only behavior. */
|
||||
const LEAD_INBOX_URL = process.env.LEAD_INBOX_URL || "";
|
||||
const LEAD_INBOX_KEY = process.env.LEAD_INBOX_KEY || "";
|
||||
|
||||
async function forwardLeadToInbox(lead) {
|
||||
if (!LEAD_INBOX_URL || !LEAD_INBOX_KEY) return false;
|
||||
try {
|
||||
const res = await fetch(LEAD_INBOX_URL.replace(/\/+$/, "") + "/v1/leads", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + LEAD_INBOX_KEY
|
||||
},
|
||||
/* inbox accepts DE aliases: firma, telefon, nachricht, quelle */
|
||||
body: JSON.stringify(lead),
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
return res.ok;
|
||||
} catch (e) {
|
||||
console.error("[lead] inbox forward failed:", e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function envFlag(name, defaultValue) {
|
||||
const raw = process.env[name];
|
||||
if (raw == null || raw === "") return defaultValue;
|
||||
return !/^(0|false|no|off)$/i.test(raw);
|
||||
}
|
||||
|
||||
function envInt(name, defaultValue) {
|
||||
const raw = Number.parseInt(process.env[name] || "", 10);
|
||||
return Number.isFinite(raw) ? raw : defaultValue;
|
||||
}
|
||||
|
||||
/* Virenscan über ClamAV. Der Server spricht das clamd-Socket-Protokoll
|
||||
direkt — keine zusätzliche Abhängigkeit. Voraussetzung: ClamAV ist
|
||||
installiert und der Daemon "clamd" läuft (Einrichtung siehe README).
|
||||
enabled:false schaltet den Scan ab (z. B. lokal ohne ClamAV). */
|
||||
const CLAMAV = {
|
||||
enabled: envFlag("CLAMAV_ENABLED", true),
|
||||
host: process.env.CLAMAV_HOST || "127.0.0.1",
|
||||
port: envInt("CLAMAV_PORT", 3310), /* clamd TCPSocket-Standardport */
|
||||
timeoutMs: envInt("CLAMAV_TIMEOUT_MS", 15000)
|
||||
enabled: true,
|
||||
host: "127.0.0.1",
|
||||
port: 3310, /* clamd TCPSocket-Standardport */
|
||||
timeoutMs: 15000
|
||||
};
|
||||
|
||||
/* Bevorzugte Ports — der erste freie wird genutzt. */
|
||||
const PORT_CANDIDATES = process.env.PORT
|
||||
? [envInt("PORT", 3000)]
|
||||
: [4317, 4318, 4417, 5317, 8317, 0];
|
||||
const PORT_CANDIDATES = [4317, 4318, 4417, 5317, 8317, 0];
|
||||
/* 0.0.0.0 = auf allen Schnittstellen erreichbar, auch für andere
|
||||
Geräte im selben WLAN (z. B. Handys, die den QR-Code scannen). */
|
||||
const HOST = process.env.HOST || process.env.HOSTNAME || "0.0.0.0";
|
||||
const HOST = "0.0.0.0";
|
||||
|
||||
/* Erste IPv4-LAN-Adresse dieses Rechners ermitteln. */
|
||||
function getLanIP() {
|
||||
@@ -115,11 +69,45 @@ const MIME = {
|
||||
".webp": "image/webp",
|
||||
".ico": "image/x-icon",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".woff2": "font/woff2",
|
||||
".txt": "text/plain; charset=utf-8",
|
||||
".xml": "application/xml; charset=utf-8"
|
||||
".woff2": "font/woff2"
|
||||
};
|
||||
|
||||
const CLEAN_ROUTES = {
|
||||
"/techday": "/techday.html",
|
||||
"/gamejam": "/gamejam.html",
|
||||
"/impressum": "/impressum.html",
|
||||
"/datenschutz": "/datenschutz.html"
|
||||
};
|
||||
|
||||
const IMMUTABLE_EXTS = new Set([".webp", ".svg", ".woff2", ".png", ".jpg", ".jpeg", ".ico"]);
|
||||
const COMPRESSIBLE_EXTS = new Set([".html", ".css", ".js", ".svg", ".json", ".txt"]);
|
||||
|
||||
function redirect(res, location) {
|
||||
res.writeHead(308, { Location: location });
|
||||
res.end();
|
||||
}
|
||||
|
||||
function cacheControlFor(urlPath, ext, cacheKey) {
|
||||
if (urlPath.startsWith("/gamejam-games/")) return "public, max-age=60";
|
||||
if (ext === ".html") return "no-cache";
|
||||
if (ext === ".css" || ext === ".js") {
|
||||
return cacheKey ? "public, max-age=31536000, immutable" : "no-cache";
|
||||
}
|
||||
if (IMMUTABLE_EXTS.has(ext)) return "public, max-age=31536000, immutable";
|
||||
return "no-cache";
|
||||
}
|
||||
|
||||
function etagFor(stat) {
|
||||
return 'W/"' + stat.size.toString(16) + "-" + Math.floor(stat.mtimeMs).toString(16) + '"';
|
||||
}
|
||||
|
||||
function preferredEncoding(req) {
|
||||
const accepted = req.headers["accept-encoding"] || "";
|
||||
if (/\bbr\b/.test(accepted)) return "br";
|
||||
if (/\bgzip\b/.test(accepted)) return "gzip";
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ---------- Lead-Speicher vorbereiten ---------- */
|
||||
function ensureLeadStore() {
|
||||
if (!fs.existsSync(LEADS_DIR)) fs.mkdirSync(LEADS_DIR, { recursive: true });
|
||||
@@ -138,18 +126,6 @@ function ensureGamejamStore() {
|
||||
if (!fs.existsSync(GAMES_DIR)) fs.mkdirSync(GAMES_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function assertInside(baseDir, candidate) {
|
||||
const relative = path.relative(baseDir, candidate);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
function writeProbe(dir) {
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
const file = path.join(dir, ".write-probe-" + process.pid + "-" + Date.now());
|
||||
fs.writeFileSync(file, "ok", "utf8");
|
||||
fs.unlinkSync(file);
|
||||
}
|
||||
|
||||
function csvCell(value) {
|
||||
const s = String(value == null ? "" : value);
|
||||
return '"' + s.replace(/"/g, '""').replace(/\r?\n/g, " ") + '"';
|
||||
@@ -174,31 +150,34 @@ function saveLead(lead) {
|
||||
|
||||
/* ---------- Statische Dateien ---------- */
|
||||
function serveStatic(req, res) {
|
||||
let urlPath = decodeURIComponent(req.url.split("?")[0]);
|
||||
|
||||
/* SEO: kanonischer Host (www -> non-www) und /index.html -> / */
|
||||
const host = (req.headers.host || "").toLowerCase();
|
||||
if (host === "www.tool-ctrl.de") {
|
||||
res.writeHead(301, { Location: "https://tool-ctrl.de" + req.url });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
if (urlPath === "/index.html") {
|
||||
res.writeHead(301, { Location: "/" });
|
||||
res.end();
|
||||
let parsedUrl;
|
||||
try {
|
||||
parsedUrl = new URL(req.url, "http://local");
|
||||
} catch (e) {
|
||||
res.writeHead(400).end("Bad Request");
|
||||
return;
|
||||
}
|
||||
|
||||
let urlPath = decodeURIComponent(parsedUrl.pathname);
|
||||
if (urlPath === "/") urlPath = "/index.html";
|
||||
|
||||
/* Path-Traversal verhindern */
|
||||
let baseDir = PUBLIC_DIR;
|
||||
let filePath = path.normalize(path.join(PUBLIC_DIR, urlPath));
|
||||
if (urlPath.startsWith("/gamejam-games/")) {
|
||||
baseDir = GAMES_DIR;
|
||||
filePath = path.normalize(path.join(GAMES_DIR, urlPath.replace(/^\/gamejam-games\//, "")));
|
||||
if (urlPath.endsWith("/") && CLEAN_ROUTES[urlPath.slice(0, -1)]) {
|
||||
redirect(res, urlPath.slice(0, -1) + parsedUrl.search);
|
||||
return;
|
||||
}
|
||||
if (!assertInside(baseDir, filePath)) {
|
||||
|
||||
for (const cleanPath of Object.keys(CLEAN_ROUTES)) {
|
||||
if (urlPath === CLEAN_ROUTES[cleanPath]) {
|
||||
redirect(res, cleanPath + parsedUrl.search);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (CLEAN_ROUTES[urlPath]) urlPath = CLEAN_ROUTES[urlPath];
|
||||
|
||||
/* Path-Traversal verhindern */
|
||||
const filePath = path.normalize(path.join(PUBLIC_DIR, urlPath));
|
||||
if (!filePath.startsWith(PUBLIC_DIR)) {
|
||||
res.writeHead(403).end("Forbidden");
|
||||
return;
|
||||
}
|
||||
@@ -210,20 +189,35 @@ function serveStatic(req, res) {
|
||||
return;
|
||||
}
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
/* HTML immer frisch, statische Assets lange cachen (Versionierung via ?v=) */
|
||||
const cacheControl = ext === ".html"
|
||||
? "no-cache"
|
||||
: "public, max-age=31536000, immutable";
|
||||
const etag = etagFor(stat);
|
||||
const headers = {
|
||||
"Content-Type": MIME[ext] || "application/octet-stream",
|
||||
"Cache-Control": cacheControl
|
||||
"Cache-Control": cacheControlFor(urlPath, ext, parsedUrl.search),
|
||||
"ETag": etag,
|
||||
"Last-Modified": stat.mtime.toUTCString(),
|
||||
"X-Content-Type-Options": "nosniff"
|
||||
};
|
||||
/* Hochgeladene GameJam-Spiele nicht indexieren (Fremdcontent) */
|
||||
if (urlPath.startsWith("/gamejam-games/")) {
|
||||
headers["X-Robots-Tag"] = "noindex";
|
||||
|
||||
if (COMPRESSIBLE_EXTS.has(ext)) headers["Vary"] = "Accept-Encoding";
|
||||
|
||||
if ((req.headers["if-none-match"] || "").includes(etag)) {
|
||||
res.writeHead(304, headers);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
let stream = fs.createReadStream(filePath);
|
||||
const encoding = COMPRESSIBLE_EXTS.has(ext) ? preferredEncoding(req) : null;
|
||||
if (encoding === "br") {
|
||||
headers["Content-Encoding"] = "br";
|
||||
stream = stream.pipe(zlib.createBrotliCompress());
|
||||
} else if (encoding === "gzip") {
|
||||
headers["Content-Encoding"] = "gzip";
|
||||
stream = stream.pipe(zlib.createGzip({ level: 6 }));
|
||||
}
|
||||
|
||||
res.writeHead(200, headers);
|
||||
fs.createReadStream(filePath).pipe(res);
|
||||
stream.pipe(res);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -278,27 +272,20 @@ function handleLead(req, res) {
|
||||
quelle: String(data.quelle || "Landing Page").slice(0, 120)
|
||||
};
|
||||
|
||||
/* Central inbox is source of truth; local files are the fallback so a
|
||||
lead is never lost if the inbox is unreachable (infra ADR 012). */
|
||||
forwardLeadToInbox(lead).then((forwarded) => {
|
||||
if (!forwarded) {
|
||||
try {
|
||||
saveLead(lead);
|
||||
} catch (e) {
|
||||
console.error("[lead] Speichern fehlgeschlagen:", e.message);
|
||||
return sendJSON(res, 500, {
|
||||
error: "Speichern fehlgeschlagen. Bitte später erneut versuchen."
|
||||
});
|
||||
}
|
||||
}
|
||||
try {
|
||||
saveLead(lead);
|
||||
} catch (e) {
|
||||
console.error("[lead] Speichern fehlgeschlagen:", e.message);
|
||||
return sendJSON(res, 500, {
|
||||
error: "Speichern fehlgeschlagen. Bitte später erneut versuchen."
|
||||
});
|
||||
}
|
||||
|
||||
console.log(
|
||||
"[lead] " + (forwarded ? "→inbox " : "→local ") +
|
||||
lead.zeitpunkt + " — " + lead.name +
|
||||
(lead.firma ? " (" + lead.firma + ")" : "") + " <" + lead.email + ">"
|
||||
);
|
||||
sendJSON(res, 200, { ok: true });
|
||||
});
|
||||
console.log(
|
||||
"[lead] " + lead.zeitpunkt + " — " + lead.name +
|
||||
(lead.firma ? " (" + lead.firma + ")" : "") + " <" + lead.email + ">"
|
||||
);
|
||||
sendJSON(res, 200, { ok: true });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -406,13 +393,6 @@ function clamavPing() {
|
||||
|
||||
/* ---------- GameJam: Spiel einreichen ---------- */
|
||||
function handleGamejamUpload(req, res) {
|
||||
/* Hart-Deadline durchsetzen — danach werden keine Einreichungen mehr angenommen. */
|
||||
if (Date.now() > UPLOAD_DEADLINE.getTime()) {
|
||||
return sendJSON(res, 410, {
|
||||
error: "Einreichungen sind seit dem 27.05.2026, 22:00 Uhr geschlossen."
|
||||
});
|
||||
}
|
||||
|
||||
const ctype = req.headers["content-type"] || "";
|
||||
const bM = ctype.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
|
||||
if (!/multipart\/form-data/i.test(ctype) || !bM) {
|
||||
@@ -543,57 +523,10 @@ function sendJSON(res, code, obj) {
|
||||
res.end(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
function handleHealthz(req, res) {
|
||||
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||
return sendJSON(res, 405, { error: "Methode nicht erlaubt." });
|
||||
}
|
||||
sendJSON(res, 200, {
|
||||
ok: true,
|
||||
service: "toolctrl-website",
|
||||
revision: APP_REVISION
|
||||
});
|
||||
}
|
||||
|
||||
async function handleReadyz(req, res) {
|
||||
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||
return sendJSON(res, 405, { error: "Methode nicht erlaubt." });
|
||||
}
|
||||
|
||||
const checks = {
|
||||
leadsWritable: false,
|
||||
gamejamWritable: false,
|
||||
gamesWritable: false,
|
||||
clamav: !CLAMAV.enabled
|
||||
};
|
||||
|
||||
try {
|
||||
writeProbe(LEADS_DIR);
|
||||
checks.leadsWritable = true;
|
||||
writeProbe(GAMEJAM_DIR);
|
||||
checks.gamejamWritable = true;
|
||||
writeProbe(GAMES_DIR);
|
||||
checks.gamesWritable = true;
|
||||
if (CLAMAV.enabled) checks.clamav = await clamavPing();
|
||||
} catch (e) {
|
||||
console.error("[readyz] Prüfung fehlgeschlagen:", e.message);
|
||||
}
|
||||
|
||||
const ok = Object.keys(checks).every((key) => checks[key] === true);
|
||||
sendJSON(res, ok ? 200 : 503, {
|
||||
ok: ok,
|
||||
service: "toolctrl-website",
|
||||
revision: APP_REVISION,
|
||||
checks: checks
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Router ---------- */
|
||||
const server = http.createServer((req, res) => {
|
||||
const route = req.url.split("?")[0];
|
||||
|
||||
if (route === "/healthz") return handleHealthz(req, res);
|
||||
if (route === "/readyz") return handleReadyz(req, res);
|
||||
|
||||
if (route === "/api/lead") {
|
||||
if (req.method !== "POST") {
|
||||
return sendJSON(res, 405, { error: "Methode nicht erlaubt." });
|
||||
@@ -653,8 +586,6 @@ function listen(candidates) {
|
||||
}
|
||||
console.log(" Leads: " + LEADS_CSV);
|
||||
console.log(" GameJam-Einreichungen: " + SUBMISSIONS_JSON);
|
||||
console.log(" Daten: " + DATA_DIR);
|
||||
console.log(" Revision: " + APP_REVISION);
|
||||
if (CLAMAV.enabled) {
|
||||
clamavPing().then((ok) => {
|
||||
console.log(" Virenscan: " + (ok
|
||||
|
||||