diff --git a/.editorconfig b/.editorconfig
index 6e5b9f9..4e8e071 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -9,6 +9,11 @@ charset = utf-8
end_of_line = lf
insert_final_newline = true
-[*.{js,yaml}]
+[*.py]
+indent_style = space
+indent_size = 4
+
+[*.yaml]
indent_style = space
indent_size = 2
+
diff --git a/.forgejo/workflows/deploy.yaml b/.forgejo/workflows/deploy.yaml
index 04e8003..414eea3 100644
--- a/.forgejo/workflows/deploy.yaml
+++ b/.forgejo/workflows/deploy.yaml
@@ -1,9 +1,6 @@
name: deploy blog
-
on:
workflow_dispatch:
- schedule:
- - cron: '0 10 * * *' # daily at 10:00
push:
branches:
- staging
@@ -11,44 +8,46 @@ on:
jobs:
deploy:
- runs-on: alpine-latest
- steps:
+ runs-on: ubuntu-latest
+ steps:
- name: Install dependencies
- run: apk --no-cache add hugo git openssh-client rsync
+ run: apk --no-cache add hugo python3 py3-pip git openssh-client rsync
- name: Check versions
run: |
cat /etc/os-release
git version
hugo version
- rsync --version
- ssh -V
-
- - name: Set envionment vars
- run: echo "GIT_BRANCH=${{ forgejo.event_name == 'schedule' && 'production' || forgejo.ref_name }}" >> "$FORGEJO_ENV"
-
+ python --version
+
- name: Checkout repository
run: |
- git clone -b $GIT_BRANCH --recursive https://git.berlin.ccc.de/cccb-website-team/www.git .
+ git clone -b ${{ forgejo.ref_name }} --recursive https://git.berlin.ccc.de/cccb-website-team/www.git .
git status
+ - name: Install Python depenndencies
+ run: python -m pip install -r requirements.txt --break-system-packages
+
- name: Render site
run: ./build.sh
- name: Setup SSH
+ env:
+ SSH_PRIVATE_KEY: ${{ forgejo.ref_name == 'production' && secrets.SSH_PRIVATE_KEY_PRODUCTION || secrets.SSH_PRIVATE_KEY_STAGING }}
run: |
mkdir -p ~/.ssh
printf "%s" "${{ secrets.KNOWN_HOSTS }}" | base64 -d > ~/.ssh/known_hosts
- printf "%s" "${{ env.GIT_BRANCH == 'production' && secrets.SSH_PRIVATE_KEY_PRODUCTION || secrets.SSH_PRIVATE_KEY_STAGING }}" | base64 -d > ~/.ssh/id_ed25519
+ printf "%s" "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keygen -f ~/.ssh/id_ed25519 -y > ~/.ssh/id_ed25519.pub
cat ~/.ssh/id_ed25519.pub
- name: Rsync rendered site
+ env:
+ DEPLOY_DIR: ${{ forgejo.ref_name == 'production' && '/srv/http/www/' || '/srv/http/www-staging/' }}
# TODO: add --delete
- run: rsync -var -e 'ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes' ./public/ deploy@www.berlin.ccc.de:${{ env.GIT_BRANCH == 'production' && '/srv/http/www/' || '/srv/http/www-staging/' }}
+ run: rsync -var -e 'ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes' ./public/ deploy@www.berlin.ccc.de:$DEPLOY_DIR
- name: Cleanup
if: ${{ always() }}
run: rm -rf ~/.ssh
-
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..ae018fa
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+*.ics text eol=crlf
diff --git a/.gitignore b/.gitignore
index 008e122..18286ce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,8 @@
-# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo,pycharm+all,vim,direnv
-# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,hugo,pycharm+all,vim,direnv
-
-### direnv ###
-.direnv
+static/all.ics
.envrc
+*.swp
+# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo
+# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,hugo
### Hugo ###
# Generated files by hugo
@@ -42,7 +41,8 @@ hugo.linux
.LSOverride
# Icon must end with two \r
-Icon
+Icon
+
# Thumbnails
._*
@@ -67,114 +67,6 @@ Temporary Items
# iCloud generated files
*.icloud
-### PyCharm+all ###
-# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
-# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
-
-# User-specific stuff
-.idea/**/workspace.xml
-.idea/**/tasks.xml
-.idea/**/usage.statistics.xml
-.idea/**/dictionaries
-.idea/**/shelf
-
-# AWS User-specific
-.idea/**/aws.xml
-
-# Generated files
-.idea/**/contentModel.xml
-
-# Sensitive or high-churn files
-.idea/**/dataSources/
-.idea/**/dataSources.ids
-.idea/**/dataSources.local.xml
-.idea/**/sqlDataSources.xml
-.idea/**/dynamic.xml
-.idea/**/uiDesigner.xml
-.idea/**/dbnavigator.xml
-
-# Gradle
-.idea/**/gradle.xml
-.idea/**/libraries
-
-# Gradle and Maven with auto-import
-# When using Gradle or Maven with auto-import, you should exclude module files,
-# since they will be recreated, and may cause churn. Uncomment if using
-# auto-import.
-# .idea/artifacts
-# .idea/compiler.xml
-# .idea/jarRepositories.xml
-# .idea/modules.xml
-# .idea/*.iml
-# .idea/modules
-# *.iml
-# *.ipr
-
-# CMake
-cmake-build-*/
-
-# Mongo Explorer plugin
-.idea/**/mongoSettings.xml
-
-# File-based project format
-*.iws
-
-# IntelliJ
-out/
-
-# mpeltonen/sbt-idea plugin
-.idea_modules/
-
-# JIRA plugin
-atlassian-ide-plugin.xml
-
-# Cursive Clojure plugin
-.idea/replstate.xml
-
-# SonarLint plugin
-.idea/sonarlint/
-
-# Crashlytics plugin (for Android Studio and IntelliJ)
-com_crashlytics_export_strings.xml
-crashlytics.properties
-crashlytics-build.properties
-fabric.properties
-
-# Editor-based Rest Client
-.idea/httpRequests
-
-# Android studio 3.1+ serialized cache file
-.idea/caches/build_file_checksums.ser
-
-### PyCharm+all Patch ###
-# Ignore everything but code style settings and run configurations
-# that are supposed to be shared within teams.
-
-.idea/*
-
-!.idea/codeStyles
-!.idea/runConfigurations
-
-### Vim ###
-# Swap
-[._]*.s[a-v][a-z]
-!*.svg # comment out if you don't need vector files
-[._]*.sw[a-p]
-[._]s[a-rt-v][a-z]
-[._]ss[a-gi-z]
-[._]sw[a-p]
-
-# Session
-Session.vim
-Sessionx.vim
-
-# Temporary
-.netrwhist
-# Auto-generated tag files
-tags
-# Persistent undo
-[._]*.un~
-
### Windows ###
# Windows thumbnail cache files
Thumbs.db
@@ -201,4 +93,17 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
-# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo,pycharm+all,vim,direnv
+# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo
+
+# NixOS:
+.envrc
+shell.nix
+.direnv
+
+# Python
+.venv
+
+# murmeldin
+overview.md
+
+/result
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..2c07333
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.11
diff --git a/README.md b/README.md
index 5f72bd0..86698bc 100644
--- a/README.md
+++ b/README.md
@@ -27,20 +27,10 @@ This is the website of the CCCB.
5. Point your browser to:
-Every change you make on the project will be reflected in your browser as long as `hugo serve` is running.
-
-The *"Nächste Veranstaltungen"* table on the home page is generated by post-processing in `./build.sh`, not by Hugo, so
-it is **not** visible under `hugo serve`. To preview the fully built site (including the home-page calendar), or to
-ready the site for upload, run:
-
-```shell
-./build.sh
-python3 -m http.server -d public 1313
-```
-
-`build.sh` replaces the `CALENDAR` placeholder in `index.html` with the upcoming-events table. It depends on Python
-with the `icalendar`, `python-dateutil`, and `pytz` packages, plus a `de_DE.UTF-8` locale (used to format weekday
-names). Inside `nix develop` these are provided automatically.
+To ready your site for upload, run `./build.sh`, which also generates `all.ics`
+and adds the calendar table to `index.html`.
+Every change you make on the project will be reflected in your browser
+as long as `hugo serve` is running.
To build with *nix*: `nix build '.?submodules=1#production-content'`
@@ -51,8 +41,7 @@ To build with *nix*: `nix build '.?submodules=1#production-content'`
3. Commit (and push) your change.
4. ~~GitHub Actions is running the release workflow.~~
- If successful, check [Staging Website](https://staging.berlin.ccc.de/) if change is correct.
-5. Create a merge request to merge changes from `staging` to `production` branch. Ask somebody to check merge request or
- if small change, merge yourself.
+5. Create merge request to merge changes from `staging` to `production` branch. Ask somebody to check merge request or if small change, merge yourself.
6. ~~GitHub Actions is running the release workflow.~~
- If successfull, check [Website](https://berlin.ccc.de/) if change is correct.
7. Profit!
@@ -61,12 +50,9 @@ To build with *nix*: `nix build '.?submodules=1#production-content'`
- After entering the shell with `nix develop`, hugo is available and `hugo serve` should work
- Python including required packages will be available, so the `build.sh` should work without a venv
-- You can build the staging and production builds with `nix build .#staging-content` and
- `nix build .#production-content`
-- Do not update the nixpkgs branch - 25.05 contains a newer hugo version that is incompatible with the theme (last
- checked June 2025)
+- You can build the staging and production builds with `nix build .#staging-content` and `nix build .#production-content`
+- Do not update the nixpkgs branch - 25.05 contains a newer hugo version that is incompatible with the theme (last checked June 2025)
---
Made with ❤️ and [Hugo](https://gohugo.io).
-
diff --git a/assets/js/calendar.js b/assets/js/calendar.js
index 18b4768..5aa9d6b 100644
--- a/assets/js/calendar.js
+++ b/assets/js/calendar.js
@@ -19,10 +19,10 @@ document.addEventListener('DOMContentLoaded', function() {
if (colonIndex > -1) {
let key = line.substring(0, colonIndex);
let value = line.substring(colonIndex + 1);
-
+
// Handle properties with parameters (like TZID)
const baseKey = key.split(";")[0];
-
+
if (baseKey === "DTSTART") {
event.start = value;
event.startParams = key.includes(";") ? key.substring(key.indexOf(";") + 1) : null;
@@ -46,14 +46,14 @@ document.addEventListener('DOMContentLoaded', function() {
function parseDateString(icsDateStr) {
// Handle different date formats
if (!icsDateStr) return null;
-
+
// For basic date format: YYYYMMDD
if (icsDateStr.length === 8) {
let year = icsDateStr.substring(0, 4);
let month = icsDateStr.substring(4, 6);
let day = icsDateStr.substring(6, 8);
return `${year}-${month}-${day}`;
- }
+ }
// For datetime formats: YYYYMMDDTHHmmssZ or YYYYMMDDTHHmmss
else if (icsDateStr.includes("T")) {
let year = icsDateStr.substring(0, 4);
@@ -67,34 +67,34 @@ document.addEventListener('DOMContentLoaded', function() {
// Extract date components from different date formats
function getDateComponents(icsDateStr) {
if (!icsDateStr) return null;
-
+
// Basic handling - extract YYYY, MM, DD regardless of format
const year = parseInt(icsDateStr.substring(0, 4));
const month = parseInt(icsDateStr.substring(4, 6)) - 1; // 0-based months
const day = parseInt(icsDateStr.substring(6, 8));
-
+
return { year, month, day };
}
function expandRecurringEvents(event, year, month) {
if (!event.rrule) return [event];
-
+
const rruleStr = event.rrule;
-
+
// Get start date components
const startComponents = getDateComponents(event.start);
if (!startComponents) return [event];
-
+
const startDate = new Date(
startComponents.year,
startComponents.month,
startComponents.day
);
-
+
const rangeStart = new Date(year, month, 1);
const rangeEnd = new Date(year, month + 1, 0);
const expandedEvents = [];
-
+
if (rruleStr.includes("FREQ=WEEKLY") && rruleStr.includes("BYDAY")) {
const bydayMatch = rruleStr.match(/BYDAY=([^;]+)/);
if (bydayMatch) {
@@ -103,7 +103,7 @@ document.addEventListener('DOMContentLoaded', function() {
'MO': 1, 'TU': 2, 'WE': 3, 'TH': 4, 'FR': 5, 'SA': 6, 'SU': 0
};
const targetDay = dayMap[dayCode];
-
+
if (targetDay !== undefined) {
// Create events for each matching day in the month
let day = 1;
@@ -112,13 +112,13 @@ document.addEventListener('DOMContentLoaded', function() {
if (testDate.getDay() === targetDay && testDate >= startDate) {
const newEvent = {...event};
const eventDate = formatDateForICS(testDate);
-
+
// Preserve time portion from original event
- const timePart = event.start.includes('T') ?
+ const timePart = event.start.includes('T') ?
event.start.substring(event.start.indexOf('T')) : '';
- const endTimePart = event.end.includes('T') ?
+ const endTimePart = event.end.includes('T') ?
event.end.substring(event.end.indexOf('T')) : '';
-
+
newEvent.start = eventDate + timePart;
newEvent.end = eventDate + endTimePart;
expandedEvents.push(newEvent);
@@ -131,25 +131,19 @@ document.addEventListener('DOMContentLoaded', function() {
else if (rruleStr.includes("FREQ=MONTHLY") && rruleStr.includes("BYDAY")) {
const bydayMatch = rruleStr.match(/BYDAY=([^;]+)/);
if (bydayMatch) {
- const intervalMatch = rruleStr.match(/INTERVAL=(\d+)/);
- const interval = intervalMatch ? parseInt(intervalMatch[1]) : 1;
- const monthsFromStart = (year - startDate.getFullYear()) * 12 + (month - startDate.getMonth());
- if (monthsFromStart < 0 || monthsFromStart % interval !== 0) {
- return [];
- }
const bydays = bydayMatch[1].split(',');
const dayMap = {
'MO': 1, 'TU': 2, 'WE': 3, 'TH': 4, 'FR': 5, 'SA': 6, 'SU': 0
};
-
+
bydays.forEach(byday => {
const occurrence = parseInt(byday) || 1;
const dayCode = byday.slice(-2);
const dayIndex = dayMap[dayCode];
-
+
let day = 1;
let count = 0;
-
+
while (day <= rangeEnd.getDate()) {
const testDate = new Date(year, month, day);
if (testDate.getDay() === dayIndex) {
@@ -157,13 +151,13 @@ document.addEventListener('DOMContentLoaded', function() {
if (count === occurrence || (occurrence < 0 && day > rangeEnd.getDate() + occurrence * 7)) {
const newEvent = {...event};
const eventDate = new Date(year, month, day);
-
+
// Preserve time portion from original event
- const timePart = event.start.includes('T') ?
+ const timePart = event.start.includes('T') ?
event.start.substring(event.start.indexOf('T')) : '';
- const endTimePart = event.end.includes('T') ?
+ const endTimePart = event.end.includes('T') ?
event.end.substring(event.end.indexOf('T')) : '';
-
+
newEvent.start = formatDateForICS(eventDate) + timePart;
newEvent.end = formatDateForICS(eventDate) + endTimePart;
expandedEvents.push(newEvent);
@@ -174,7 +168,7 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
}
-
+
return expandedEvents.length > 0 ? expandedEvents : [event];
}
@@ -206,7 +200,7 @@ document.addEventListener('DOMContentLoaded', function() {
function updateEventsForMonth(year, month) {
// Clear existing events for this month view
eventsByDate = {};
-
+
// Process each event, expanding recurring ones
events.forEach(ev => {
if (ev.rrule) {
@@ -228,7 +222,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Check if this event belongs to current month view
const eventYear = parseInt(dateKey.split('-')[0]);
const eventMonth = parseInt(dateKey.split('-')[1]) - 1;
-
+
if (eventYear === year && eventMonth === month) {
if (!eventsByDate[dateKey]) {
eventsByDate[dateKey] = [];
@@ -238,7 +232,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
});
-
+
renderCalendar(year, month);
}
@@ -267,16 +261,16 @@ document.addEventListener('DOMContentLoaded', function() {
}
let cell = document.createElement("td");
cell.innerHTML = "" + day + "";
-
+
let dayStr = day < 10 ? "0" + day : day;
let monthStr = (month + 1) < 10 ? "0" + (month + 1) : (month + 1);
let dateKey = year + "-" + monthStr + "-" + dayStr;
-
+
if (eventsByDate[dateKey]) {
let dotsContainer = document.createElement("div");
dotsContainer.className = "event-dots-container";
cell.classList.add("has-event");
-
+
// Gruppe Events nach Typ
const events = eventsByDate[dateKey];
const hasMembersOnly = events.some(e => e.summary.toLowerCase().includes("members only"));
@@ -285,12 +279,12 @@ document.addEventListener('DOMContentLoaded', function() {
const hasSpieleabend = events.some(e => e.summary.toLowerCase().includes("spieleabend"));
const hasRegular = events.some(e => {
const title = e.summary.toLowerCase();
- return !title.includes("members only") &&
- !title.includes("subbotnik") &&
- !title.includes("bastelabend") &&
+ return !title.includes("members only") &&
+ !title.includes("subbotnik") &&
+ !title.includes("bastelabend") &&
!title.includes("spieleabend");
});
-
+
// Füge Dots entsprechend der Event-Typen hinzu
if (hasMembersOnly) {
let dot = document.createElement("div");
@@ -307,7 +301,7 @@ document.addEventListener('DOMContentLoaded', function() {
dot.className = "event-dot event-dot-greenyellow";
dotsContainer.appendChild(dot);
}
-
+
cell.appendChild(dotsContainer);
cell.dataset.dateKey = dateKey;
cell.addEventListener("click", function() {
@@ -336,8 +330,8 @@ document.addEventListener('DOMContentLoaded', function() {
if (match && match[1]) {
return `https://berlin.ccc.de/datengarten/${match[1]}/`;
}
- }
-
+ }
+
// For other titles, convert to lowercase and use as path
const slug = eventTitle.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '');
return `https://berlin.ccc.de/page/${slug}/`;
@@ -355,13 +349,13 @@ document.addEventListener('DOMContentLoaded', function() {
let eventTitle = document.createElement("div");
eventTitle.className = "event-title";
-
+
// Create a link for the event title
let titleLink = document.createElement("h");
titleLink.textContent = ev.summary;
titleLink.target = "_blank";
eventTitle.appendChild(titleLink);
-
+
eventItem.appendChild(eventTitle);
let eventTime = document.createElement("div");
@@ -372,7 +366,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (ev.description) {
let eventDescription = document.createElement("div");
eventDescription.className = "event-description";
-
+
// Check if the description is a URL and make it a clickable link
if (ev.description.trim().startsWith('http')) {
let linkElement = document.createElement("a");
@@ -384,7 +378,7 @@ document.addEventListener('DOMContentLoaded', function() {
} else {
eventDescription.textContent = ev.description;
}
-
+
eventItem.appendChild(eventDescription);
}
@@ -409,7 +403,7 @@ document.addEventListener('DOMContentLoaded', function() {
function formatTime(icsTimeStr) {
// Format time for display
if (!icsTimeStr) return "";
-
+
if (icsTimeStr.length === 8) {
// All-day event
return "Ganztägig";
@@ -431,18 +425,20 @@ document.addEventListener('DOMContentLoaded', function() {
}
// ICS-Datei abrufen und Events verarbeiten
- fetch('/calendars/all.ics')
+ fetch('/all.ics')
.then(response => response.text())
.then(data => {
events = parseICS(data);
-
+
// Initialize with current date
let today = new Date();
currentYear = today.getFullYear();
currentMonth = today.getMonth();
-
+
// Process events for current month
- updateEventsForMonth(currentYear, currentMonth);
+
+ updateEventsForMonth(currentYear, currentMonth);
+
})
.catch(err => console.error('Fehler beim Laden der ICS-Datei:', err));
})();
diff --git a/assets/js/clubstatus.js b/assets/js/clubstatus.js
index fffd93d..d87eb04 100644
--- a/assets/js/clubstatus.js
+++ b/assets/js/clubstatus.js
@@ -9,11 +9,11 @@ function timeDistanceDE(pastDate) {
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
- if (days === 1) return `since ${days}d`
+ if (days = 1) return `since ${days}d`
if (days > 1) return `since ${days}d`
- if (hours === 1) return `since ${hours}h`
+ if (hours = 1) return `since ${hours}h`
if (hours > 1) return `since ${hours}h`
- if (minutes === 1) return `since ${minutes}min`
+ if (minutes = 1) return `since ${minutes}min`
if (minutes > 1) return `since ${minutes}min`
return `gerade eben`
}
diff --git a/build.sh b/build.sh
index 625a262..6bc707a 100755
--- a/build.sh
+++ b/build.sh
@@ -1,7 +1,12 @@
#!/bin/sh
-set -ex
+set -e
+set -x
rm -rf public/ # delete old generated files
hugo $(cat .hugo-params)
+./tools/merge_cals.py
+upcoming="$(tools/gen_upcoming.py static/all.ics 20 5 | tr '\n' ' ')"
+cp static/all.ics public/all.ics
+sed -i "s#CALENDAR#$upcoming#g" public/index.html
diff --git a/config/_default/hugo.toml b/config/_default/hugo.toml
index 218ffbb..13192d6 100644
--- a/config/_default/hugo.toml
+++ b/config/_default/hugo.toml
@@ -28,7 +28,7 @@ filename = "sitemap.xml"
priority = 0.5
[permalinks]
-post = "/post/:year/:month/:day/:slug/"
+post = "/post/:year/:month/:day/:title/"
[outputs]
home = ["html", "rss", "json"]
diff --git a/content/_index.md b/content/_index.md
index 4d73f01..531fa85 100644
--- a/content/_index.md
+++ b/content/_index.md
@@ -25,9 +25,6 @@ description: "Startseite CCCB mit Kurzkalender"
### Nächste Veranstaltungen
-
+CALENDAR
Weitere Termine findest du im [Veranstaltungskalender](/verein/calendar/).
-
-
diff --git a/content/post/2026-07-04-kein-diday-im-cccb/index.md b/content/post/2026-07-04-kein-diday-im-cccb/index.md
deleted file mode 100644
index d64dd4b..0000000
--- a/content/post/2026-07-04-kein-diday-im-cccb/index.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-title: "Kein DI.Day am 5. Juli im CCCB"
-date: 2026-07-04T12:00:00+02:00
-showHero: false
-tags: ["di.day", "Ankündigung"]
----
-
-**Kurzfristige Änderung:** Der [Digital Independence Day](/veranstaltungen/didit/) findet am **Sonntag, dem 5. Juli 2026, nicht** im Chaos Computer Club Berlin (Marienstraße 11) statt.
-
-Wer trotzdem am DI.Day teilnehmen und den Weg in die digitale Unabhängigkeit weitergehen möchte, ist herzlich bei den anderen Berliner Veranstaltungen willkommen:
-
-- [„Let's DID it!" mit Linux & Sommerfest der BELUG](https://f.termine.di.day/events/ee48fdda-a134-4f27-932e-b87c7a9fa24a) in der c-base
-- [DI.Day im Stadtschloss Moabit](https://f.termine.di.day/events/41ab17fe-05ab-4308-9abb-d38d9485fd4f)
-
-Weitere Termine und Orte findet ihr wie immer auf [termine.di.day](https://termine.di.day/).
diff --git a/content/post/2026-08-23-aktionstag-ueberwachung/index.md b/content/post/2026-08-23-aktionstag-ueberwachung/index.md
deleted file mode 100644
index 34b1320..0000000
--- a/content/post/2026-08-23-aktionstag-ueberwachung/index.md
+++ /dev/null
@@ -1,57 +0,0 @@
----
-title: "Aktionstag gegen Überwachung"
-date: 2026-08-11T12:00:00+02:00
-showHero: false
-tags: ["Überwachung", "Ankündigung"]
----
-
-### Was?
-
-KI-gestützte Überwachungskameras sollen jetzt auch nach Berlin kommen.
-Das [im Dezember 2025 verschärfte Berliner Polizeigesetz](https://www.beck-aktuell.de/heute-im-recht/rechtspolitik-gesetzgebung/berlin-verschaerfung-polizeigesetz-2025-12-04) erlaubt der Polizei, solche Kameras an öffentlichen Plätzen aufzuhängen. Erste Versuche laufen schon.
-
-Darüber wollen wir reden.
-Deshalb laden wir am 23. August zum Aktionstag gegen Überwachung in den Club ein.
-
-### Worum geht es?
-
-Die KI-Videoüberwachung soll [ein Jahr lang an sechs sogenannten „kriminalitätsbelasteten Orten" getestet werden](https://taz.de/KI-Videoueberwachung-in-Berlin/!6160851/), darunter am Alexanderplatz, am Roten Rathaus und am Alten Stadthaus. Vor dem Abgeordnetenhaus kommt mobile Technik dazu.
-
-Diese Kameras zeichnen nicht nur auf. Sogenannte Verhaltensscanner sollen automatisch melden, wenn sich jemand „auffällig" verhält. Trainiert wird das Ganze [mit den Daten von Passant:innen, die einfach nur vorbeilaufen](https://netzpolitik.org/2026/ki-gestuetzte-videoueberwachung-in-berlin-so-wehrt-man-sich-gegen-verhaltensscanner/).
-
-In Mannheim läuft so ein System seit 2018, in Hamburg seit 2023. Dort hält die Software [eine Umarmung schon mal für einen Ringkampf](https://taz.de/Kuenstliche-Intelligenz-im-Einsatz/!6099559/). In Mannheim gab es laut Polizei [jeden Tag Fehlalarme im „niedrigen zweistelligen Bereich"](https://netzpolitik.org/2023/intelligente-videoueberwachung-polizei-hamburg-will-ab-juli-verhalten-automatisch-scannen/). Wer sich hinlegt oder taumelt, löst Alarm aus. Was überhaupt als „auffällig" gilt, entscheidet der Algorithmus. Warum das [Menschen diskriminieren kann, erklärt der Verfassungsblog](https://verfassungsblog.de/berlin-asog-novelle-kbos/).
-
-Die Kameras sind außerdem nur ein Teil der Gesetzesänderung. Das neue ASOG erlaubt auch [längere Datenspeicherung und weitere Überwachungsbefugnisse](https://www.telepolis.de/article/Berliner-Polizeigesetz-Koalition-weitet-Ueberwachung-aus-11083345.html). Die Berliner Datenschutzbeauftragte hat das kritisiert. Wir auch: Der CCC war dazu bereits im Innenausschuss des Abgeordnetenhauses.
-
-Ihr braucht kein Vorwissen. Kommt einfach vorbei.
-
-### Programm
-
-**13:00 Uhr: Open Space** \
-Wir fangen mit offenem Austausch an. Bringt eure Fragen mit, egal ob ihr euch schon lange mit dem Thema beschäftigt oder gerade zum ersten Mal davon hört.
-
-**15:00 Uhr: Vortrag** \
-Was in Berlin geplant ist, was die Technik wirklich kann und was das für uns alle heißt.
-
-**16:00 Uhr: Podiumsdiskussion** \
-Danach diskutieren wir auf dem Podium, wie wir uns gegen die KI-Videoüberwachung wehren können. Fragen aus dem Publikum sind ausdrücklich erwünscht.
-
-### Wo?
-
-📍 **Chaos Computer Club Berlin, Marienstraße 11, 10117 Berlin**
-
-🚈 Etwa 5 Minuten ab dem S-Bahnhof Friedrichstraße
-
-### Wann?
-
-**📆 Sonntag, 23. August 2026** \
-**ab 13:00 Uhr**
-
-### Zum Weiterlesen
-
-- taz: [KI-Videoüberwachung in Berlin: Verhaltensscanner bald auch vor dem Abgeordnetenhaus](https://taz.de/KI-Videoueberwachung-in-Berlin/!6160851/)
-- netzpolitik.org: [So wehrt man sich gegen Verhaltensscanner](https://netzpolitik.org/2026/ki-gestuetzte-videoueberwachung-in-berlin-so-wehrt-man-sich-gegen-verhaltensscanner/) (mit Tools und Material für die Zivilgesellschaft)
-- Verfassungsblog: [„Try harder hilft selten": die ASOG-Novelle verfassungsrechtlich eingeordnet](https://verfassungsblog.de/berlin-asog-novelle-kbos/)
-- taz: [Reform des Berliner Polizeigesetzes: Riskantes Manöver](https://taz.de/Reform-des-Berliner-Polizeigesetzes/!6096087/)
-- Telepolis: [Berliner Polizeigesetz: Koalition weitet Überwachung aus](https://www.telepolis.de/article/Berliner-Polizeigesetz-Koalition-weitet-Ueberwachung-aus-11083345.html)
-- beck-aktuell: [Kameras und KI-Einsatz: Berlin verschärft Polizeigesetz](https://www.beck-aktuell.de/heute-im-recht/rechtspolitik-gesetzgebung/berlin-verschaerfung-polizeigesetz-2025-12-04)
diff --git a/content/post/Neujahresempfang-2026/index.md b/content/post/Neujahresempfang-2026/index.md
index 17a6269..61844e1 100644
--- a/content/post/Neujahresempfang-2026/index.md
+++ b/content/post/Neujahresempfang-2026/index.md
@@ -1,6 +1,5 @@
---
title: "Der Neujahresempfang 2026 #NJE26"
-slug: "neujahresempfang-2026"
date: 2026-03-05T12:00:00+01:00
showHero: false
tags: ["NJE", "Ankündigung"]
diff --git a/content/veranstaltungen/aktionstag-ueberwachung.md b/content/veranstaltungen/aktionstag-ueberwachung.md
deleted file mode 100644
index 6976864..0000000
--- a/content/veranstaltungen/aktionstag-ueberwachung.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-title: "Aktionstag gegen Überwachung"
-subtitle: "KI-Überwachungskameras in Berlin: Vortrag, Podium und Austausch"
-date: 2026-08-11T12:00:00+02:00
-dtstart: 20260823T130000
-dtend: 20260823T180000
-menu:
- main:
- parent: "Veranstaltungen"
-tag: ["Veranstaltung"]
----
-
-**Am 23. August lädt der Chaos Computer Club Berlin zum Aktionstag gegen Überwachung.**
-
-KI-gestützte Überwachungskameras sollen jetzt auch nach Berlin kommen. Das neue Berliner Polizeigesetz erlaubt es, sie an öffentlichen Plätzen aufzuhängen. Erste Versuche laufen schon, Verhaltenstracking inklusive. Darüber wollen wir reden.
-
-Das Programm:
-
-- **13:00 Uhr:** Open Space, offener Austausch untereinander
-- **15:00 Uhr:** Vortrag zu den KI-Kameras und dem neuen Berliner Polizeigesetz
-- **16:00 Uhr:** Podiumsdiskussion
-
-📍 Chaos Computer Club Berlin, Marienstraße 11, 10117 Berlin
diff --git a/content/veranstaltungen/bastelabend.md b/content/veranstaltungen/bastelabend.md
index c787c17..5f3d995 100644
--- a/content/veranstaltungen/bastelabend.md
+++ b/content/veranstaltungen/bastelabend.md
@@ -26,7 +26,7 @@ draft: true
{{< alert "circle-info" >}}
-Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/veranstaltungen/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen.
+Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/page/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen.
-Generell sind aber alle, die schon mal im Club waren, herzlich eingeladen, an den Bastelabenden vorbeizukommen.
+Generell sind aber alle, die schonmal im Club waren, herzlich eingeladen, an den Bastelabenden vorbeizukommen.
{{< /alert >}}
diff --git a/content/veranstaltungen/didit.md b/content/veranstaltungen/didit.md
deleted file mode 100644
index de8ddc6..0000000
--- a/content/veranstaltungen/didit.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
-title: "Di.Day"
-subtitle: "Digital Independence Day im CCCB"
-date: 2026-01-04T12:23:00+01:00
-dtstart: 20260104T122300
-dtend: 20260104T174200
-rrule: "FREQ=MONTHLY;INTERVAL=2;BYDAY=1SU;WKST=MO;UNTIL=20260503T235959Z"
-menu:
- main:
- parent: "Veranstaltungen"
-tag: ["Veranstaltung"]
----
-
-In Berlin wird der [Digital Independence Day](https://diday.org) an verschiedenen Orten von einer breiten Zahl von Initiativen getragen.
-
-Der CCCB beteiligt sich an dieser Graswurzelbewegung und ruft dazu auf, zu demokratiefreundlichen digitalen Alternativen zu wechseln, um die Abhängigkeit von großen Technologiekonzernen zu verringern.
-
-Am ersten Sonntag im Monat gibt es praktische Hilfe beim Wechsel zu datenschutzfreundlichen Diensten, beim Einrichten von Linux oder beim Betrieb von Smartphones ohne große Tech-Konzerne. Egal, ob Anfänger:in oder Fortgeschrittene:r. Kommt vorbei und bringt eure Geräte und Fragen mit.
-
-Die Aktiven im CCCB unterstützen dabei auf verschiedenen Veranstaltungen in Berlin.
-
-Weitere Informationen zu konkreten Veranstaltungen gibt es im Terminkalender unter [https://events.diday.org/](https://events.diday.org/) bzw [https://events.diday.org/organisation/cccb](https://events.diday.org/organisation/cccb).
diff --git a/content/veranstaltungen/neujahresempfang.md b/content/veranstaltungen/neujahresempfang.md
index 98de614..6bcd354 100644
--- a/content/veranstaltungen/neujahresempfang.md
+++ b/content/veranstaltungen/neujahresempfang.md
@@ -2,12 +2,13 @@
title: "Neujahresempfang"
subtitle: "#NJE 26"
date: 2025-03-05T18:00:00+02:00
-dtstart: 20260523T180000
-dtend: 20260524T100000
+dtstart: 20250523T180000
+dtend: 20250524T100000
+rrule: "FREQ=MOTHLY;BYDAY=4SA;WKST=MO"
menu:
main:
parent: "Veranstaltungen"
tag: ["Veranstaltung"]
---
-Am 23. Mai lädt der Chaos Computer Club Berlin zum Neujahresempfang. [Hier geht es zur Infoseite!](https://berlin.ccc.de/post/2026/03/05/neujahresempfang-2026/)
\ No newline at end of file
+Am 23. Mai lädt der Chaos Computer Club Berlin zum Neujahresempfang. [Hier geht es zur Infoseite!](https://berlin.ccc.de/post/2026/03/05/der-neujahresempfang-2026-%23nje26/)
\ No newline at end of file
diff --git a/content/veranstaltungen/openwrt.md b/content/veranstaltungen/openwrt.md
index 282c130..c2e66e8 100644
--- a/content/veranstaltungen/openwrt.md
+++ b/content/veranstaltungen/openwrt.md
@@ -2,9 +2,9 @@
title: "OpenWrt"
subtitle: "OpenWrt Stammtisch"
date: 2025-01-08T20:00:00+02:00
-dtstart: 20251119T200000
-dtend: 20251119T230000
-rrule: "FREQ=MONTHLY;INTERVAL=3;BYDAY=3WE;WKST=MO"
+dtstart: 20250115T200000
+dtend: 20250115T230000
+rrule: "FREQ=MONTHLY;BYDAY=3WE;WKST=MO"
menu:
main:
parent: "Veranstaltungen"
@@ -13,7 +13,7 @@ tag: ["Veranstaltung"]

-**Jeden 3. Mittwoch im Februar, Mai, August und November ab 20 Uhr** treffen sich die OpenWrt begeisterten und die es werden wollen zum OpenWrt Stammtisch im CCCB.
+**Jeden 3. Mittwoch im Monat ab 20 Uhr** treffen sich die OpenWrt begeisterten und die es werden wollen zum OpenWrt Stammtisch im CCCB.
Das OpenWrt Meetup richtet sich an alle die OpenWrt benutzen oder benutzen wollen aber auch Leute die an OpenWrt entwickeln.
diff --git a/content/veranstaltungen/spieleabend.md b/content/veranstaltungen/spieleabend.md
index ab3d120..e1122f9 100644
--- a/content/veranstaltungen/spieleabend.md
+++ b/content/veranstaltungen/spieleabend.md
@@ -16,7 +16,7 @@ tag: ["Veranstaltung"]
**Jeden 2. und 4. Samstag im Monat ist ab 17 Uhr Spieleabend im Club.**
{{< alert "circle-info" >}}
-Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/veranstaltungen/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen.
+Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/page/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen.
-Generell sind aber alle, die schon mal im Club waren, herzlich eingeladen, an den Spieleabenden vorbeizukommen.
+Generell sind aber alle, die schonmal im Club waren, herzlich eingeladen, an den Spieleabenden vorbeizukommen.
{{< /alert >}}
diff --git a/content/veranstaltungen/subbotnik.md b/content/veranstaltungen/subbotnik.md
index a520035..5e86aa2 100644
--- a/content/veranstaltungen/subbotnik.md
+++ b/content/veranstaltungen/subbotnik.md
@@ -23,8 +23,8 @@ Der Begriff [Subbotnik](https://de.wikipedia.org/wiki/Subbotnik) ist eine in Sow
---
{{< alert "circle-info" >}}
-Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/veranstaltungen/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen.
+Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/page/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen.
-Generell sind aber alle, die schon mal im Club waren, herzlich eingeladen am Subbotnik vorbeizukommen und mitzuhelfen.
+Generell sind aber alle, die schonmal im Club waren, herzlich eingeladen am Subbotnik vorbeizukommen und mitzuhelfen.
{{< /alert >}}
diff --git a/content/verein/calendar/index.md b/content/verein/calendar/index.md
index 3fccf38..fdac4e4 100644
--- a/content/verein/calendar/index.md
+++ b/content/verein/calendar/index.md
@@ -12,15 +12,4 @@ herostyle: big
{{< calendar >}}
-Keinen Termin mehr verpeilen? Einfach den [Veranstaltungskalender abonnieren](/calendars/all.ics)!
-
-Oder willst du nur den Kalender von bestimmten Events?
-
-- [Club Discordia](/calendars/Club_Discordia.ics)
-- [Neujahresempfang](/calendars/Neujahresempfang.ics)
-- [Datengarten](/calendars/Datengarten.ics)
-- [Spieleabend](/calendars/Spieleabend.ics)
-- [OpenWRT](/calendars/OpenWRT.ics)
-- [Plenum](/calendars/Plenum.ics)
-- [Amateurfunk](/calendars/Amateurfunk.ics)
-- [Irreguläre Events](/calendars/Random.ics)
+Keinen Termin mehr verpeilen? Einfach den [Veranstaltungskalender abonnieren](/all.ics)!
diff --git a/flake.lock b/flake.lock
index 33ab9db..2869045 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,16 +2,16 @@
"nodes": {
"nixpkgs": {
"locked": {
- "lastModified": 1786313170,
- "narHash": "sha256-9BG7OgUWdu0ONDO5X2q6+K4bsuBITkX/3W4nNJu1Ito=",
+ "lastModified": 1748995628,
+ "narHash": "sha256-bFufQGSAEYQgjtc4wMrobS5HWN0hDP+ZX+zthYcml9U=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "fcb8fcd6bf2d0adecae5bd491afaaaf8311b758d",
+ "rev": "8eb3b6a2366a7095939cd22f0dc0e9991313294b",
"type": "github"
},
"original": {
"owner": "nixos",
- "ref": "nixos-26.05",
+ "ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}
diff --git a/flake.nix b/flake.nix
index c29b73a..39d9b6d 100644
--- a/flake.nix
+++ b/flake.nix
@@ -2,7 +2,7 @@
description = "A flake containing a development environment for the CCCB website.";
inputs = {
- nixpkgs.url = "github:nixos/nixpkgs/nixos-26.05";
+ nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
};
outputs =
@@ -34,7 +34,7 @@
devShells = forAllSystems (import ./devShells.nix);
- formatter = forAllSystems ({ pkgs, ... }: pkgs.nixfmt-tree);
+ formatter = forAllSystems ({ pkgs, ... }: pkgs.nixfmt-rfc-style);
lib.mkWwwContent =
{ domain, system }:
@@ -50,6 +50,10 @@
nativeBuildInputs = [
pkgs.hugo
pkgs.glibcLocales
+ (pkgs.python3.withPackages (python-pkgs: [
+ python-pkgs.icalendar
+ python-pkgs.pytz
+ ]))
];
# LOCALE_ARCHIVE = builtins.trace pkgs.glibcLocales "${pkgs.glibcLocales}/lib/locale/locale-archive";
@@ -58,6 +62,11 @@
buildPhase = ''
mkdir -p public
hugo --baseURL=https://${domain}/
+
+ python3 ./tools/merge_cals.py
+ upcoming="$(python3 tools/gen_upcoming.py static/all.ics 20 5 | tr '\n' ' ')"
+ cp static/all.ics public/all.ics
+ sed -i "s#CALENDAR#$upcoming#g" public/index.html
'';
# Install phase - copy the public directory to the output
diff --git a/layouts/_default/baseof.calendar.html b/layouts/_default/baseof.calendar.html
new file mode 100644
index 0000000..bddf04a
--- /dev/null
+++ b/layouts/_default/baseof.calendar.html
@@ -0,0 +1 @@
+{{ block "main" . }}{{ .Content }}{{ end }}
diff --git a/layouts/_default/list.calendar.html b/layouts/_default/list.calendar.html
new file mode 100644
index 0000000..3259690
--- /dev/null
+++ b/layouts/_default/list.calendar.html
@@ -0,0 +1,21 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CCCB//Calendar//DE
+{{ with .Title }}
+X-WR-CALNAME:{{ . }}
+{{ end }}
+CALSCALE:GREGORIAN
+METHOD:PUBLISH
+{{ range .Pages }}
+{{ if .Date }}
+BEGIN:VEVENT
+UID:{{ .File.UniqueID }}@berlin.ccc.de
+DTSTAMP:{{ .Date.Format "20060102T150405Z" }}
+DTSTART:{{ .Date.Format "20060102T150405Z" }}
+SUMMARY:{{ .Title }}
+DESCRIPTION:{{ .Summary | plainify }}
+URL:{{ .Permalink }}
+END:VEVENT
+{{ end }}
+{{ end }}
+END:VCALENDAR
diff --git a/layouts/_default/list.xml.xml b/layouts/_default/list.xml.html
similarity index 100%
rename from layouts/_default/list.xml.xml
rename to layouts/_default/list.xml.html
diff --git a/layouts/_default/single.calendar.html b/layouts/_default/single.calendar.html
new file mode 100644
index 0000000..5a22ec4
--- /dev/null
+++ b/layouts/_default/single.calendar.html
@@ -0,0 +1,21 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CCCB//Calendar//DE
+{{ with .Title }}
+X-WR-CALNAME:{{ . }}
+{{ end }}
+CALSCALE:GREGORIAN
+METHOD:PUBLISH
+{{ if .Date }}
+BEGIN:VEVENT
+UID:{{ .File.UniqueID }}@berlin.ccc.de
+DTSTAMP:{{ .Date.Format "20060102T150405Z" }}
+DTSTART:{{ .Date.Format "20060102T150405Z" }}
+{{ with .Params.event.end }}DTEND:{{ dateFormat "20060102T150405Z" . }}{{ end }}
+SUMMARY:{{ .Title }}
+DESCRIPTION:{{ .Summary | plainify }}
+URL:{{ .Permalink }}
+{{ with .Params.location }}LOCATION:{{ . }}{{ end }}
+END:VEVENT
+{{ end }}
+END:VCALENDAR
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..c61ba7f
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+icalendar==5.0.7
+pytz
\ No newline at end of file
diff --git a/static/js/upcoming.js b/static/js/upcoming.js
deleted file mode 100644
index 6b7a711..0000000
--- a/static/js/upcoming.js
+++ /dev/null
@@ -1,113 +0,0 @@
-import ICAL from "https://unpkg.com/ical.js/dist/ical.min.js";
-
-/**
- * Parse an ICS calendar and return upcoming event occurrences.
- *
- * @param {string} icsText The contents of the .ics file
- * @param {Date} now Events must start after this date
- * @param {number} maxEvents Maximum number of events to return
- * @param {number} maxDays Maximum number of days into the future
- * @returns {{start: Date, name: string, url: string}[]}
- */
-function getUpcomingEvents(icsText, now, maxEvents, maxDays) {
- const jcal = ICAL.parse(icsText);
- const calendar = new ICAL.Component(jcal);
-
- const end = new Date(now.getTime());
- end.setDate(end.getDate() + maxDays);
-
- const events = [];
-
- for (const component of calendar.getAllSubcomponents("vevent")) {
- const event = new ICAL.Event(component);
-
- if (!event.startDate) {
- continue;
- }
-
- if (event.isRecurring()) {
- const iterator = event.iterator();
-
- while (true) {
- const occurrence = iterator.next();
-
- if (!occurrence) {
- break;
- }
-
- const start = occurrence.toJSDate();
-
- // Recurrences are chronological, so we're done
- // once we pass the end of our search window.
- if (start > end) {
- break;
- }
-
- if (start > now) {
- events.push({
- start,
- name: event.summary ?? "",
- url: event.url ?? "",
- });
- }
- }
- } else {
- const start = event.startDate.toJSDate();
-
- if (start > now && start <= end) {
- events.push({
- start,
- name: event.summary ?? "",
- url: event.url ?? "",
- });
- }
- }
- }
-
- // We have occurrences from multiple events, so sort them
- // before applying the maximum event count.
- events.sort((a, b) => a.start - b.start);
-
- return events.slice(0, maxEvents);
-}
-
-document.addEventListener("DOMContentLoaded", () => {
- const ics = "/calendars/all.ics";
- const max_days = 20;
- const max_items = 5;
-
- const now = new Date();
-
- const table = document.getElementById("upcoming");
- fetch(ics)
- .then(response => response.text())
- .then(icsText => {
- getUpcomingEvents(icsText, now, max_items, max_days).forEach(event => {
- const row = document.createElement("tr");
-
- const colBegin = document.createElement("td");
-
- const formattedStart = event.start.toLocaleString("de-DE", {
- weekday: "long",
- day: "2-digit",
- month: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- }).replace(",", "");
-
- colBegin.innerText = `${formattedStart.replace(" ", ", ")} Uhr`;
- row.appendChild(colBegin);
-
- const colName = document.createElement("td");
-
- const a = document.createElement("a");
- a.href = event.url;
- a.text = event.name;
-
- colName.appendChild(a);
- row.appendChild(colName);
-
- table.appendChild(row);
- });
- });
-});
diff --git a/tools/gen_upcoming.py b/tools/gen_upcoming.py
new file mode 100755
index 0000000..0f69f6a
--- /dev/null
+++ b/tools/gen_upcoming.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+
+import sys
+import logging
+import locale
+from dateutil.parser import parse
+from datetime import datetime, timedelta
+from dateutil.rrule import rruleset, rrulestr
+
+import icalendar
+
+
+def vevent_to_event(event, rrstart=None):
+ if rrstart == None:
+ begin = parse(event["DTSTART"].to_ical())
+ else:
+ begin = rrstart
+
+ return {
+ "name": event["SUMMARY"].to_ical(),
+ "url": event["URL"].to_ical(),
+ "begin": begin
+ }
+
+
+def parse_single_event(event, start, end):
+ logging.info(f"Processing single event {event['SUMMARY'].to_ical().decode('utf-8')}")
+ dtstart = parse(event["DTSTART"].to_ical())
+ if dtstart >= start and dtstart < end:
+ return vevent_to_event(event)
+
+
+def parse_recurring_event(event, start, end):
+ logging.info(f"Processing recurring event {event['SUMMARY'].to_ical().decode('utf-8')}")
+ dtstart = parse(event["DTSTART"].to_ical())
+ rs = rruleset()
+ rs.rrule(rrulestr(event["RRULE"].to_ical().decode("utf-8"), dtstart=dtstart))
+ if "EXDATE" in event.keys():
+ for exdate in event["EXDATE"]:
+ rs.exdate(parse(exdate.to_ical()))
+
+ events = []
+ for date in list(rs):
+ if date >= start and date < end:
+ events.append(vevent_to_event(event, date))
+
+ return events
+
+
+def find_events(icsfilestr, start, end, num):
+ with open(icsfilestr, "r") as icsfile:
+ cal = icalendar.Calendar.from_ical(icsfile.read())
+
+ events = []
+ for event in cal.subcomponents:
+ if event.name == "VEVENT":
+ if "RRULE" in event.keys():
+ events.extend(parse_recurring_event(event, start, end))
+ else:
+ ev = parse_single_event(event, start, end)
+ if ev is not None:
+ events.append(ev)
+
+ events = sorted(events, key=lambda k: k["begin"])
+ events = events[0:num]
+
+ return events
+
+
+def format_events(events):
+ print("")
+
+
+if __name__ == "__main__":
+ if len(sys.argv) < 3:
+ print(f"Usage: {sys.argv[0]} calendar max_days max_items")
+ sys.exit(-1)
+
+ locale.setlocale(locale.LC_TIME, "de_DE.UTF-8")
+ calendar = sys.argv[1]
+ max_days = int(sys.argv[2])
+ max_items = int(sys.argv[3])
+
+ now = datetime.now()
+ events = find_events(calendar, now, now + timedelta(days=max_days), max_items)
+ format_events(events)
+
diff --git a/tools/merge_cals.py b/tools/merge_cals.py
new file mode 100755
index 0000000..136904e
--- /dev/null
+++ b/tools/merge_cals.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+
+from glob import glob
+
+import pytz
+import icalendar
+
+
+calendars = []
+merged = icalendar.Calendar()
+merged.add("prodid", "-//CCCB Calendar Generator//berlin.ccc.de//")
+merged.add("version", "2.0")
+
+for icsfilestr in glob("public/*/**/*.ics", recursive=True):
+ with open(icsfilestr, "r") as icsfile:
+ print(f"Importing {icsfilestr}")
+ calendars.append(icalendar.Calendar.from_ical(icsfile.read()))
+
+for calendar in calendars:
+ for event in calendar.subcomponents:
+ if event.name != "VEVENT":
+ continue
+ if "DTSTART" not in event:
+ continue
+ merged.add_component(event)
+
+outfile = "static/all.ics"
+with open(outfile, "wb") as f:
+ print(f"writing to {outfile}...")
+ f.write(merged.to_ical())
+