Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,7 @@ jobs:
--gc \
--minify \
--destination public

- name: Validate iCalendar feed
run: node scripts/validate-calendar.mjs public/calendar/calendar.ics

1 change: 1 addition & 0 deletions assets/css/fontawesome-subset.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,6 @@
.fa-twitter:before{content:"\f099"}
.fa-user-circle:before{content:"\f2bd"}
.fa-users:before{content:"\f0c0"}
.fa-video:before{content:"\f03d"}
.fa-windows:before{content:"\f17a"}
.fa-youtube:before{content:"\f167"}
2 changes: 1 addition & 1 deletion assets/css/tailwind.css

Large diffs are not rendered by default.

Binary file modified assets/fonts/fa-solid-subset.woff2
Binary file not shown.
30 changes: 30 additions & 0 deletions content/calendar/swiss-psug-09-2026-2026.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
endDate: '2026-09-09'
externalUrl: https://www.meetup.com/swiss-powershell-user-group/events/315958850/
startDate: '2026-09-09'
title: Swiss PSUG 09/2026
virtual: true
where: Bern, Switzerland
---
**Place & Language:**
Place: Isolutions AG, Schanzenstrasse 4c, 3008 Bern (Hybrid with Teams)
Language: EN

**Session 1: More Than Scripting: Advanced PowerShell with C#**
Description:
Have you ever wished PowerShell had generics, better performance, or easier access to low-level .NET features? It already does—you just need a little C#. This session explores how experienced PowerShell users can leverage C# without becoming full-time C# developers. We’ll cover practical integration techniques, discuss common pitfalls, and demonstrate how projects like PowerShell itself, the Microsoft Graph SDK, and many community modules combine both languages to deliver powerful tooling.
Speaker: [MVP Fabien Tschanz](https://www.linkedin.com/in/fabientschanz/)

**Session 2: The Azure VM Extension Mystery**
Description:
Have you ever checked an Azure VM extension with PowerShell and thought you knew which version was installed? Think again.
In this session, we'll dive into the surprisingly tricky world of Azure VM Extensions and discover why the information returned by standard PowerShell cmdlets is not always enough. Using a real-world example, we'll investigate how Azure exposes extension versions, why build numbers can be difficult to obtain, and how browser developer tools can reveal the hidden REST API calls used by the Azure Portal.
If you enjoy PowerShell, reverse engineering portal behaviour, and solving cloud mysteries, this session is for you.
Speaker: [MVP Andres Bohren](https://www.linkedin.com/in/andres-bohren/)

**Agenda:**
17:00 Welcome
17:30 Session 1
18:15 Session 2
19:00 Networking and Beer
20:00 End of Event
2 changes: 1 addition & 1 deletion hugo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ params:
branch: "main"

ical:
timezone: UTC
timezone: Etc/UTC

module:
imports:
Expand Down
21 changes: 21 additions & 0 deletions layouts/list.calendar.ics
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{{- $calendar := "BEGIN:VCALENDAR\n" -}}
{{- $calendar = printf "%s%s" $calendar (partial "header.ics" .) -}}
{{- $timezones := slice -}}
{{- range .Pages -}}
{{- if .Params.startDate -}}
{{- $timezone := partial "ical/get_timezone.ics" . -}}
{{- if not (in $timezones $timezone) -}}
{{- $timezones = $timezones | append $timezone -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- range $timezones -}}
{{- $calendar = printf "%s%s" $calendar (partial "timezone.ics" .) -}}
{{- end -}}
{{- range .Pages -}}
{{- if .Params.startDate -}}
{{- $calendar = printf "%s%s" $calendar (partial "event.ics" .) -}}
{{- end -}}
{{- end -}}
{{- $calendar = printf "%sEND:VCALENDAR\n" $calendar -}}
{{- replace $calendar "\n" "\r\n" -}}
105 changes: 105 additions & 0 deletions scripts/validate-calendar.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { readFile } from 'node:fs/promises';

const input = process.argv[2];

if (!input) {
throw new Error('Usage: node scripts/validate-calendar.mjs <calendar.ics|URL>');
}

const calendar = new URL(input, 'file:').protocol === 'file:'
? await readFile(input, 'utf8')
: await (await fetch(input)).text();

const errors = [];
const fail = (message) => errors.push(message);

if (/(^|[^\r])\n/.test(calendar)) {
fail('Content lines must use CRLF line endings.');
}

const lines = calendar.replace(/\r?\n[ \t]/g, '').split(/\r?\n/).filter(Boolean);
const components = [];
const events = [];
const timezones = new Set();
const timezoneReferences = new Set();
let event;

for (const line of lines) {
if (line.startsWith('BEGIN:')) {
const component = line.slice('BEGIN:'.length);
components.push(component);
if (component === 'VEVENT') {
event = new Map();
events.push(event);
}
continue;
}

if (line.startsWith('END:')) {
const component = line.slice('END:'.length);
if (components.pop() !== component) {
fail(`Mismatched component terminator: ${line}`);
}
if (component === 'VEVENT') {
event = undefined;
}
continue;
}

const separator = line.indexOf(':');
if (separator < 1) {
fail(`Malformed content line: ${line}`);
continue;
}

const declaration = line.slice(0, separator);
const [name, ...parameters] = declaration.split(';');
const value = line.slice(separator + 1);

for (const parameter of parameters) {
if (parameter.startsWith('TZID=')) {
timezoneReferences.add(parameter.slice('TZID='.length));
}
}

if (components.at(-1) === 'VTIMEZONE' && name === 'TZID') {
timezones.add(value);
}

if (event) {
event.set(name, [...(event.get(name) ?? []), value]);
}
}

if (components.length) {
fail(`Unclosed component: ${components.at(-1)}`);
}

if (lines[0] !== 'BEGIN:VCALENDAR' || lines.at(-1) !== 'END:VCALENDAR') {
fail('Calendar must be wrapped in BEGIN:VCALENDAR and END:VCALENDAR.');
}

for (const [index, properties] of events.entries()) {
for (const property of ['UID', 'DTSTAMP', 'DTSTART']) {
if (properties.get(property)?.length !== 1) {
fail(`VEVENT ${index + 1} must contain exactly one ${property}.`);
}
}

const uid = properties.get('UID')?.[0];
if (uid && events.some((other) => other !== properties && other.get('UID')?.[0] === uid)) {
fail(`VEVENT ${index + 1} reuses UID ${uid}.`);
}
}

for (const timezone of timezoneReferences) {
if (!timezones.has(timezone)) {
fail(`TZID=${timezone} has no matching VTIMEZONE component.`);
}
}

if (errors.length) {
throw new Error(`Invalid RFC 5545 calendar:\n- ${errors.join('\n- ')}`);
}

console.log(`Validated ${events.length} VEVENT components.`);
2 changes: 1 addition & 1 deletion themes/powershell-community/layouts/_default/calendar.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ <h2 class="text-3xl font-bold text-gray-900">Upcoming Events</h2>
<!-- Event Details -->
<div class="p-6 flex-1">
<h3 class="text-xl font-bold text-gray-900 mb-2">{{ .Title }}</h3>
<p class="text-gray-600 mb-3">{{ .Summary }}</p>
<p class="text-gray-600 mb-3">{{ .Summary | plainify | truncate 240 }}</p>
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-500">
<div class="flex items-center">
<i class="fas fa-map-marker-alt text-purple-600 mr-2"></i>
Expand Down
Loading