diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml
new file mode 100644
index 000000000..8e3bc0d25
--- /dev/null
+++ b/.github/workflows/check-links.yml
@@ -0,0 +1,38 @@
+name: Check Links
+
+on:
+ pull_request:
+ branches: [main]
+ push:
+ branches: [main]
+
+jobs:
+ check-links:
+ name: Check Internal Links
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: '22'
+ cache: 'pnpm'
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Build static site
+ run: pnpm build
+ env:
+ NODE_OPTIONS: --max-old-space-size=8192
+
+ - name: Check for broken links
+ run: pnpm check-links
diff --git a/app/posts/[slug]/page.tsx b/app/posts/[slug]/page.tsx
index a542b2363..4e9ded007 100644
--- a/app/posts/[slug]/page.tsx
+++ b/app/posts/[slug]/page.tsx
@@ -64,7 +64,6 @@ export async function generateMetadata({
],
publishedTime: post.publishedAt || post.date,
modifiedTime: post.updatedAt || post.date,
- authors: post.author ? [`https://devops-daily.com/authors/${post.author.slug}`] : undefined,
section: post.category?.name,
tags: post.tags,
},
diff --git a/components/post-header.tsx b/components/post-header.tsx
index 6d70d030a..88419d187 100644
--- a/components/post-header.tsx
+++ b/components/post-header.tsx
@@ -1,6 +1,6 @@
import Link from 'next/link';
import { Badge } from '@/components/ui/badge';
-import { Clock, Calendar, User, Info } from 'lucide-react';
+import { Clock, Calendar, Info } from 'lucide-react';
interface PostHeaderProps {
post: {
@@ -34,12 +34,6 @@ export function PostHeader({ post, hasAffiliateLinks = false }: PostHeaderProps)
{post.readingTime}
- {post.author && (
-
-
- {post.author.name}
-
- )}
{post.title}
{hasAffiliateLinks && (
diff --git a/content/exercises/terraform-digitalocean-droplet.json b/content/exercises/terraform-digitalocean-droplet.json
index cdd3b1caf..9dc79ba14 100644
--- a/content/exercises/terraform-digitalocean-droplet.json
+++ b/content/exercises/terraform-digitalocean-droplet.json
@@ -247,11 +247,6 @@
"url": "https://www.terraform.io/docs/cloud/guides/recommended-practices/index.html",
"type": "guide",
"external": true
- },
- {
- "title": "Infrastructure as Code Fundamentals",
- "url": "/posts/infrastructure-as-code-fundamentals",
- "type": "tutorial"
}
],
"troubleshooting": [
diff --git a/content/news/2026/week-4.md b/content/news/2026/week-4.md
index 870c250cc..506af7e14 100644
--- a/content/news/2026/week-4.md
+++ b/content/news/2026/week-4.md
@@ -184,7 +184,7 @@ On 2025-12-26, at 07:35 UTC, the SSL certificates for many *.bazel.build domains
**📅 Jan 16, 2026** • **📰 Bazel Blog**
-[**🔗 Read more**](/2026/01/16/ssl-cert-expiry.html)
+[**🔗 Read more**](https://bazel.build/release/postmortem-ssl-cert-expiry)
### 📄 Building an agentic memory system for GitHub Copilot
diff --git a/content/news/2026/week-5.md b/content/news/2026/week-5.md
index 73603d26d..4516d28e6 100644
--- a/content/news/2026/week-5.md
+++ b/content/news/2026/week-5.md
@@ -232,7 +232,7 @@ We're pleased to announce the release of Bazel 9.0! This LTS release is the culm
**📅 Jan 20, 2026** • **📰 Bazel Blog**
-[**🔗 Read more**](/2026/01/20/bazel-9.html)
+[**🔗 Read more**](https://blog.bazel.build/2026/01/20/bazel-9-lts.html)
---
diff --git a/content/news/2026/week-9.md b/content/news/2026/week-9.md
index 59020be49..61475bbcc 100644
--- a/content/news/2026/week-9.md
+++ b/content/news/2026/week-9.md
@@ -152,7 +152,7 @@ We're thrilled to announce Bazel support in Dependabot Version Updates! 🎉 Thr
**📅 Feb 20, 2026** • **📰 Bazel Blog**
-[**🔗 Read more**](/2026/02/20/dependabot.html)
+[**🔗 Read more**](https://blog.bazel.build/2026/02/20/dependabot-bazel-support.html)
### 📄 How AI is reshaping developer choice (and Octoverse data proves it)
diff --git a/content/posts/deployment-strategies-guide.md b/content/posts/deployment-strategies-guide.md
index 80d6de92f..0ca7e8fbe 100644
--- a/content/posts/deployment-strategies-guide.md
+++ b/content/posts/deployment-strategies-guide.md
@@ -15,7 +15,7 @@ tags:
- DevOps
- Deployment
- Kubernetes
- - CI/CD
+ - CICD
- Blue-Green Deployment
- Canary Deployment
- Rolling Deployment
diff --git a/content/posts/detect-network-connection-type-android.md b/content/posts/detect-network-connection-type-android.md
deleted file mode 100644
index ccdfe7b81..000000000
--- a/content/posts/detect-network-connection-type-android.md
+++ /dev/null
@@ -1,481 +0,0 @@
----
-title: 'How to Detect Network Connection Type on Android'
-excerpt: "Learn how to detect WiFi, cellular, and other network types in Android apps using ConnectivityManager, NetworkCapabilities, and handle network changes with callbacks."
-category:
- name: 'Android'
- slug: 'android'
-date: '2024-11-05'
-publishedAt: '2024-11-05T12:00:00Z'
-updatedAt: '2024-11-05T12:00:00Z'
-readingTime: '8 min read'
-author:
- name: 'DevOps Daily Team'
- slug: 'devops-daily-team'
-tags:
- - Android
- - Mobile
- - Networking
- - Java
- - Kotlin
- - Development
----
-
-**TLDR:** Use `ConnectivityManager` and `NetworkCapabilities` (Android 6.0+) to detect network type. Call `getActiveNetwork()` and `getNetworkCapabilities()` to check for WiFi, cellular, or Ethernet. For older devices, use the deprecated `getActiveNetworkInfo()`. Register a `NetworkCallback` to monitor connection changes in real-time. Always request `ACCESS_NETWORK_STATE` permission in your manifest.
-
-Android apps often need to know what type of network connection is available - WiFi, cellular, or none. This helps optimize data usage, adjust quality settings, or warn users about expensive mobile data.
-
-## Required Permission
-
-First, add the network state permission to your `AndroidManifest.xml`:
-
-```xml
-
-
-
-
-
-
- ...
-
-
-```
-
-This permission is normal-level (not dangerous), so you don't need to request it at runtime - just declare it in the manifest.
-
-## Modern Approach (Android 6.0+)
-
-For apps targeting Android M (API 23) and higher, use `NetworkCapabilities`:
-
-```kotlin
-import android.content.Context
-import android.net.ConnectivityManager
-import android.net.NetworkCapabilities
-
-fun getNetworkType(context: Context): String {
- val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
- as ConnectivityManager
-
- // Get the currently active network
- val network = connectivityManager.activeNetwork ?: return "No connection"
-
- // Get the capabilities of the active network
- val capabilities = connectivityManager.getNetworkCapabilities(network)
- ?: return "No connection"
-
- return when {
- capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "WiFi"
- capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> "Cellular"
- capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> "Ethernet"
- capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> "Bluetooth"
- capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> "VPN"
- else -> "Unknown"
- }
-}
-
-// Usage
-val networkType = getNetworkType(context)
-Log.d("Network", "Connected via: $networkType")
-```
-
-This API is cleaner and more reliable than the older methods. It correctly handles VPN connections and provides more detailed transport information.
-
-### Checking for Internet Connectivity
-
-Having a network connection doesn't guarantee internet access. Check for validated internet:
-
-```kotlin
-fun hasInternetConnection(context: Context): Boolean {
- val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
- as ConnectivityManager
-
- val network = connectivityManager.activeNetwork ?: return false
- val capabilities = connectivityManager.getNetworkCapabilities(network)
- ?: return false
-
- return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
- capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
-}
-```
-
-`NET_CAPABILITY_VALIDATED` means Android has verified the connection can reach the internet (by contacting a server).
-
-### Checking for Metered Connections
-
-Cellular connections and some WiFi networks (like hotspots) are metered - they have data limits:
-
-```kotlin
-fun isMeteredConnection(context: Context): Boolean {
- val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
- as ConnectivityManager
-
- val network = connectivityManager.activeNetwork ?: return false
- val capabilities = connectivityManager.getNetworkCapabilities(network)
- ?: return false
-
- return !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
-}
-
-// Use this to adjust behavior
-if (isMeteredConnection(context)) {
- // User is on cellular or metered WiFi
- // Reduce video quality, defer large downloads, etc.
- downloadQuality = Quality.LOW
-} else {
- // User is on unlimited WiFi
- downloadQuality = Quality.HIGH
-}
-```
-
-## Legacy Approach (Pre-Android 6.0)
-
-For compatibility with older devices, use the deprecated but still functional `NetworkInfo`:
-
-```kotlin
-@Suppress("DEPRECATION")
-fun getNetworkTypeLegacy(context: Context): String {
- val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
- as ConnectivityManager
-
- val networkInfo = connectivityManager.activeNetworkInfo
-
- return when {
- networkInfo == null -> "No connection"
- !networkInfo.isConnected -> "No connection"
- networkInfo.type == ConnectivityManager.TYPE_WIFI -> "WiFi"
- networkInfo.type == ConnectivityManager.TYPE_MOBILE -> "Cellular"
- networkInfo.type == ConnectivityManager.TYPE_ETHERNET -> "Ethernet"
- else -> "Unknown"
- }
-}
-```
-
-Note the `@Suppress("DEPRECATION")` annotation - this API is deprecated but still works. Use it only if you need to support very old Android versions.
-
-## Monitoring Network Changes
-
-To respond when the network changes (WiFi to cellular, or vice versa), register a `NetworkCallback`:
-
-```kotlin
-class NetworkMonitor(private val context: Context) {
-
- private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
- as ConnectivityManager
-
- private val networkCallback = object : ConnectivityManager.NetworkCallback() {
- override fun onAvailable(network: Network) {
- Log.d("Network", "Network available")
- // Network connection is available
- }
-
- override fun onLost(network: Network) {
- Log.d("Network", "Network lost")
- // Network connection lost
- }
-
- override fun onCapabilitiesChanged(
- network: Network,
- capabilities: NetworkCapabilities
- ) {
- Log.d("Network", "Network capabilities changed")
-
- when {
- capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> {
- Log.d("Network", "Connected to WiFi")
- // Switch to high quality
- }
- capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> {
- Log.d("Network", "Connected to cellular")
- // Switch to lower quality to save data
- }
- }
- }
-
- override fun onUnavailable() {
- Log.d("Network", "Network unavailable")
- // No network available
- }
- }
-
- fun startMonitoring() {
- connectivityManager.registerDefaultNetworkCallback(networkCallback)
- }
-
- fun stopMonitoring() {
- connectivityManager.unregisterNetworkCallback(networkCallback)
- }
-}
-
-// Usage in Activity or Service
-class MainActivity : AppCompatActivity() {
- private lateinit var networkMonitor: NetworkMonitor
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- networkMonitor = NetworkMonitor(this)
- networkMonitor.startMonitoring()
- }
-
- override fun onDestroy() {
- super.onDestroy()
- networkMonitor.stopMonitoring()
- }
-}
-```
-
-The callback runs whenever the network state changes, letting you adjust your app's behavior dynamically.
-
-## Real-World Example: Adaptive Video Quality
-
-Here's how you might use network detection for adaptive streaming:
-
-```kotlin
-class VideoPlayer(private val context: Context) {
-
- private var currentQuality = VideoQuality.AUTO
-
- enum class VideoQuality {
- LOW, // 360p
- MEDIUM, // 720p
- HIGH, // 1080p
- AUTO
- }
-
- fun determineOptimalQuality(): VideoQuality {
- val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
- as ConnectivityManager
-
- val network = connectivityManager.activeNetwork ?: return VideoQuality.LOW
- val capabilities = connectivityManager.getNetworkCapabilities(network)
- ?: return VideoQuality.LOW
-
- // No internet? Use lowest quality (if cached)
- if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
- return VideoQuality.LOW
- }
-
- // Get connection type and bandwidth
- return when {
- capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> {
- // WiFi - check if metered
- if (capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) {
- VideoQuality.HIGH // Unlimited WiFi - go HD
- } else {
- VideoQuality.MEDIUM // Metered WiFi - be careful
- }
- }
-
- capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> {
- // Cellular - check bandwidth estimate
- val downstreamBandwidth = capabilities.linkDownstreamBandwidthKbps
-
- when {
- downstreamBandwidth > 5000 -> VideoQuality.MEDIUM // 5+ Mbps - 4G/5G
- downstreamBandwidth > 1000 -> VideoQuality.LOW // 1-5 Mbps - 3G
- else -> VideoQuality.LOW // <1 Mbps - 2G
- }
- }
-
- capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> {
- VideoQuality.HIGH // Wired connection - full quality
- }
-
- else -> VideoQuality.LOW // Unknown connection type - be conservative
- }
- }
-
- fun setQuality(quality: VideoQuality) {
- currentQuality = quality
- Log.d("VideoPlayer", "Video quality set to: $quality")
-
- // Apply quality to video player
- when (quality) {
- VideoQuality.LOW -> loadVideoUrl(lowQualityUrl)
- VideoQuality.MEDIUM -> loadVideoUrl(mediumQualityUrl)
- VideoQuality.HIGH -> loadVideoUrl(highQualityUrl)
- VideoQuality.AUTO -> setQuality(determineOptimalQuality())
- }
- }
-
- private fun loadVideoUrl(url: String) {
- // Load video from URL
- Log.d("VideoPlayer", "Loading: $url")
- }
-}
-```
-
-## Java Version
-
-If you're using Java instead of Kotlin:
-
-```java
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-
-public class NetworkUtils {
-
- public static String getNetworkType(Context context) {
- ConnectivityManager connectivityManager =
- (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
-
- if (connectivityManager == null) {
- return "No connection";
- }
-
- Network network = connectivityManager.getActiveNetwork();
- if (network == null) {
- return "No connection";
- }
-
- NetworkCapabilities capabilities =
- connectivityManager.getNetworkCapabilities(network);
-
- if (capabilities == null) {
- return "No connection";
- }
-
- if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
- return "WiFi";
- } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
- return "Cellular";
- } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
- return "Ethernet";
- } else {
- return "Unknown";
- }
- }
-
- public static boolean isConnectedToWiFi(Context context) {
- return "WiFi".equals(getNetworkType(context));
- }
-
- public static boolean isConnectedToCellular(Context context) {
- return "Cellular".equals(getNetworkType(context));
- }
-
- public static boolean hasInternet(Context context) {
- ConnectivityManager connectivityManager =
- (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
-
- if (connectivityManager == null) return false;
-
- Network network = connectivityManager.getActiveNetwork();
- if (network == null) return false;
-
- NetworkCapabilities capabilities =
- connectivityManager.getNetworkCapabilities(network);
-
- return capabilities != null &&
- capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
- capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
- }
-}
-```
-
-## Common Use Cases
-
-### Download Large Files Only on WiFi
-
-```kotlin
-fun downloadLargeFile(context: Context, url: String) {
- val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
- as ConnectivityManager
-
- val network = connectivityManager.activeNetwork
- val capabilities = connectivityManager.getNetworkCapabilities(network)
-
- val isWiFi = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
-
- if (isWiFi) {
- // Start download
- startDownload(url)
- } else {
- // Show dialog asking user if they want to download on cellular
- showDataWarningDialog {
- startDownload(url)
- }
- }
-}
-```
-
-### Sync Data Based on Connection Type
-
-```kotlin
-fun syncData(context: Context) {
- val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
- as ConnectivityManager
-
- val network = connectivityManager.activeNetwork ?: return
- val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return
-
- when {
- capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> {
- // Full sync with images and videos
- syncAll()
- }
- capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> {
- // Sync only essential data, no media
- syncEssentialDataOnly()
- }
- else -> {
- // No suitable connection
- deferSync()
- }
- }
-}
-```
-
-### Show Network Status in UI
-
-```kotlin
-class NetworkStatusViewModel(application: Application) : AndroidViewModel(application) {
-
- private val _networkStatus = MutableLiveData()
- val networkStatus: LiveData = _networkStatus
-
- private val connectivityManager =
- application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
-
- private val networkCallback = object : ConnectivityManager.NetworkCallback() {
- override fun onCapabilitiesChanged(
- network: Network,
- capabilities: NetworkCapabilities
- ) {
- val status = when {
- capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ->
- "Connected via WiFi"
- capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ->
- "Connected via Cellular"
- else -> "Connected"
- }
- _networkStatus.postValue(status)
- }
-
- override fun onLost(network: Network) {
- _networkStatus.postValue("No connection")
- }
- }
-
- init {
- connectivityManager.registerDefaultNetworkCallback(networkCallback)
- }
-
- override fun onCleared() {
- super.onCleared()
- connectivityManager.unregisterNetworkCallback(networkCallback)
- }
-}
-```
-
-## Testing Network Detection
-
-During development, you can simulate different network conditions:
-
-1. **Android Emulator:** Use the Extended Controls (three dots) → Cellular → Network type
-2. **Physical Device:** Toggle WiFi and cellular in settings
-3. **Network Speed:** Use Android Studio's Network Profiler to throttle bandwidth
-
-The key to good network detection in Android is using the modern `NetworkCapabilities` API, monitoring changes with callbacks, and adapting your app's behavior to save users' data and provide a better experience on different connection types.
diff --git a/content/posts/find-ssh-client-ip-address.md b/content/posts/find-ssh-client-ip-address.md
deleted file mode 100644
index 38b462fc0..000000000
--- a/content/posts/find-ssh-client-ip-address.md
+++ /dev/null
@@ -1,335 +0,0 @@
----
-title: 'How to Find the IP Address of an SSH Client'
-excerpt: "Learn multiple ways to identify the IP address of clients connected to your SSH server, from environment variables to logs and active connection monitoring."
-category:
- name: 'SSH'
- slug: 'ssh'
-date: '2024-12-09'
-publishedAt: '2024-12-09T14:00:00Z'
-updatedAt: '2024-12-09T14:00:00Z'
-readingTime: '6 min read'
-author:
- name: 'DevOps Daily Team'
- slug: 'devops-daily-team'
-tags:
- - SSH
- - Networking
- - Security
- - Linux
- - Server Management
----
-
-**TLDR:** Use the `SSH_CLIENT` or `SSH_CONNECTION` environment variables to get the client IP address from within an SSH session. For monitoring all active SSH connections, use `who`, `w`, or check `/var/log/auth.log`. The client IP is also available in SSH logs and can be accessed programmatically through the connection info.
-
-When you're logged into a server via SSH, you might need to know the IP address of the client machine that connected. This is useful for security auditing, access control, debugging connection issues, or just understanding where connections are coming from.
-
-## From Within an Active SSH Session
-
-The simplest method when you're already connected is to check the SSH environment variables. SSH automatically sets several variables that contain connection information:
-
-```bash
-# Show the client IP address
-echo $SSH_CLIENT
-# Output: 192.168.1.100 54321 22
-# Format: client_ip client_port server_port
-```
-
-The `SSH_CLIENT` variable contains three space-separated values: the client's IP address, the client's source port, and the server's SSH port (usually 22).
-
-To extract just the IP address:
-
-```bash
-# Get only the client IP
-echo $SSH_CLIENT | awk '{print $1}'
-# Output: 192.168.1.100
-
-# Or using cut
-echo $SSH_CLIENT | cut -d' ' -f1
-```
-
-The `SSH_CONNECTION` variable provides similar information but includes the server's IP as well:
-
-```bash
-echo $SSH_CONNECTION
-# Output: 192.168.1.100 54321 10.0.0.50 22
-# Format: client_ip client_port server_ip server_port
-
-# Extract client IP
-echo $SSH_CONNECTION | awk '{print $1}'
-```
-
-You can use either variable - `SSH_CLIENT` is simpler if you only need the client IP, while `SSH_CONNECTION` is useful if you also need to know which server interface accepted the connection (helpful on multi-homed servers).
-
-## Checking All Active SSH Connections
-
-To see all currently logged-in users and their IP addresses, use the `who` or `w` commands:
-
-```bash
-# Show logged-in users with IP addresses
-who
-# Output:
-# john pts/0 2024-12-09 14:32 (192.168.1.100)
-# sarah pts/1 2024-12-09 14:45 (10.50.20.15)
-```
-
-The `w` command provides more detail, including what each user is doing:
-
-```bash
-w
-# Output:
-# USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
-# john pts/0 192.168.1.100 14:32 0.00s 0.12s 0.00s w
-# sarah pts/1 10.50.20.15 14:45 5:23 0.05s 0.05s vim config.yml
-```
-
-For a cleaner output with just IPs:
-
-```bash
-# List only IP addresses of connected clients
-who | awk '{print $5}' | tr -d '()'
-
-# Or with w
-w -h | awk '{print $3}'
-```
-
-## Monitoring SSH Connections in Real-Time
-
-To see active SSH connections at the network level, use `ss` or `netstat`:
-
-```bash
-# Show established SSH connections (port 22)
-ss -tn state established '( dport = :22 or sport = :22 )'
-
-# Output shows local and remote addresses
-# Recv-Q Send-Q Local Address:Port Peer Address:Port
-# 0 0 10.0.0.50:22 192.168.1.100:54321
-```
-
-To extract just the client IPs from established connections:
-
-```bash
-# Get unique client IPs connected to SSH
-ss -tn state established '( sport = :22 )' |
- awk 'NR>1 {print $4}' |
- cut -d: -f1 |
- sort -u
-
-# Or with netstat (older systems)
-netstat -tn | grep ':22.*ESTABLISHED' | awk '{print $5}' | cut -d: -f1 | sort -u
-```
-
-This shows all IP addresses with active SSH connections, even if they haven't fully logged in yet (useful for detecting connection attempts).
-
-## From SSH Authentication Logs
-
-SSH logs every connection attempt, successful or not. The location varies by distribution:
-
-```bash
-# Most Linux distributions (Debian, Ubuntu)
-tail -f /var/log/auth.log | grep sshd
-
-# Red Hat, CentOS, Fedora
-tail -f /var/log/secure | grep sshd
-
-# Or use journalctl on systemd systems
-journalctl -u ssh -f
-# or
-journalctl -u sshd -f
-```
-
-Successful login entries look like:
-
-```
-Dec 09 14:32:15 server sshd[12345]: Accepted publickey for john from 192.168.1.100 port 54321 ssh2: RSA SHA256:abc123...
-```
-
-To extract recent successful logins with IPs:
-
-```bash
-# Last 20 successful SSH logins
-grep "Accepted" /var/log/auth.log | tail -20
-
-# Extract just the IPs from successful logins
-grep "Accepted" /var/log/auth.log |
- awk '{print $(NF-3)}' |
- sort | uniq -c | sort -rn
-
-# Output shows connection count per IP:
-# 15 192.168.1.100
-# 8 10.50.20.15
-# 3 203.0.113.45
-```
-
-## Using the `last` Command
-
-The `last` command shows login history from the wtmp database:
-
-```bash
-# Show recent SSH logins
-last -a
-
-# Output:
-# john pts/0 Mon Dec 9 14:32 still logged in 192.168.1.100
-# sarah pts/1 Mon Dec 9 14:45 still logged in 10.50.20.15
-# john pts/0 Mon Dec 9 09:15 - 12:30 (03:15) 192.168.1.100
-```
-
-To see only SSH sessions (not console logins):
-
-```bash
-# Filter for pts (pseudo-terminal, used by SSH)
-last -a | grep pts
-
-# Show last 10 SSH sessions for a specific user
-last -a john | grep pts | head -10
-```
-
-## Creating a Script to Monitor Client Connections
-
-Here's a practical script to monitor and log client connections:
-
-```bash
-#!/bin/bash
-# ssh-client-monitor.sh - Track SSH client connections
-
-log_connection() {
- # This runs when a user logs in
- # Call it from /etc/profile or ~/.bashrc
-
- if [ -n "$SSH_CLIENT" ]; then
- CLIENT_IP=$(echo $SSH_CLIENT | awk '{print $1}')
- TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
- LOGFILE="/var/log/ssh-connections.log"
-
- # Log the connection
- echo "$TIMESTAMP - User $USER logged in from $CLIENT_IP" >> "$LOGFILE"
-
- # Optional: Send alert for non-local IPs
- if ! [[ $CLIENT_IP =~ ^192\.168\. ]] && ! [[ $CLIENT_IP =~ ^10\. ]]; then
- echo "Warning: External SSH login from $CLIENT_IP by $USER at $TIMESTAMP" |
- mail -s "SSH Alert" admin@example.com
- fi
- fi
-}
-
-# If sourced in profile, log the connection
-log_connection
-```
-
-Add this to `/etc/profile` or individual user profiles to track all SSH logins:
-
-```bash
-# Add to /etc/profile
-if [ -f /usr/local/bin/ssh-client-monitor.sh ]; then
- source /usr/local/bin/ssh-client-monitor.sh
-fi
-```
-
-## Programmatic Access in Scripts
-
-When writing scripts that need to know the client IP:
-
-```bash
-#!/bin/bash
-# Example: Block certain actions from specific networks
-
-CLIENT_IP=$(echo $SSH_CLIENT | awk '{print $1}')
-
-# Check if connection is from production network
-if [[ $CLIENT_IP =~ ^10\.50\. ]]; then
- echo "Connection from production network detected"
- echo "Destructive commands are disabled"
- exit 1
-fi
-
-# Check if connection is local
-if [[ $CLIENT_IP =~ ^127\.0\.0\.1$ ]] || [[ $CLIENT_IP =~ ^::1$ ]]; then
- echo "Local connection - full access granted"
-fi
-
-# Continue with script logic
-echo "Client IP: $CLIENT_IP"
-```
-
-Or in Python, if you're running a Python script over SSH:
-
-```python
-import os
-
-def get_ssh_client_ip():
- """Get the IP address of the SSH client."""
- ssh_client = os.environ.get('SSH_CLIENT', '')
- if ssh_client:
- return ssh_client.split()[0]
- return None
-
-client_ip = get_ssh_client_ip()
-if client_ip:
- print(f"Connected from: {client_ip}")
-
- # Example: Restrict access based on IP
- if client_ip.startswith('192.168.'):
- print("Local network access")
- else:
- print("External access - enabling audit logging")
-else:
- print("Not connected via SSH")
-```
-
-## Distinguishing Between Direct and Proxied Connections
-
-If users connect through a jump host or proxy, `SSH_CLIENT` shows the proxy's IP, not the original client:
-
-```
-Original Client (192.168.1.100)
- ↓
-Jump Host (10.50.1.10)
- ↓
-Destination Server (10.50.2.20)
-
-# On destination server:
-echo $SSH_CLIENT
-# Shows: 10.50.1.10 (jump host, not original client)
-```
-
-To track the original client through a jump host, you need to pass the information explicitly:
-
-```bash
-# On the jump host, pass client IP as an environment variable
-ssh -o SendEnv=ORIGINAL_CLIENT_IP user@destination.server
-
-# Set ORIGINAL_CLIENT_IP on jump host in ~/.ssh/rc or profile:
-export ORIGINAL_CLIENT_IP=$SSH_CLIENT
-
-# On destination server, check both:
-echo "Immediate connection from: $SSH_CLIENT"
-echo "Original client: $ORIGINAL_CLIENT_IP"
-```
-
-Note that the receiving server needs to accept the custom environment variable in `/etc/ssh/sshd_config`:
-
-```
-AcceptEnv ORIGINAL_CLIENT_IP
-```
-
-## Finding Historical Connection Data
-
-For long-term analysis of who's been connecting:
-
-```bash
-# Count connections per IP from auth.log
-grep "Accepted" /var/log/auth.log* |
- awk '{print $(NF-3)}' |
- sort | uniq -c | sort -rn | head -20
-
-# Show failed authentication attempts by IP
-grep "Failed password" /var/log/auth.log* |
- awk '{print $(NF-3)}' |
- sort | uniq -c | sort -rn | head -20
-
-# This helps identify brute-force attempts
-```
-
-For more sophisticated analysis, consider tools like `fail2ban` which automatically parse logs and can ban IPs with too many failed attempts.
-
-The combination of environment variables for active sessions and log analysis for historical data gives you complete visibility into SSH client connections. For automated monitoring, integrate these checks into your server provisioning or security scanning workflows.
diff --git a/content/posts/how-to-close-tcp-and-udp-ports-via-windows-command-line.md b/content/posts/how-to-close-tcp-and-udp-ports-via-windows-command-line.md
deleted file mode 100644
index 650437ad1..000000000
--- a/content/posts/how-to-close-tcp-and-udp-ports-via-windows-command-line.md
+++ /dev/null
@@ -1,437 +0,0 @@
----
-title: 'How to Close TCP and UDP Ports via Windows Command Line'
-excerpt: 'Learn how to close open ports on Windows using command-line tools. Find and terminate processes listening on ports, manage Windows Firewall rules, and stop services to free up ports.'
-category:
- name: 'Windows'
- slug: 'windows'
-date: '2025-03-28'
-publishedAt: '2025-03-28T13:00:00Z'
-updatedAt: '2025-03-28T13:00:00Z'
-readingTime: '7 min read'
-author:
- name: 'DevOps Daily Team'
- slug: 'devops-daily-team'
-tags:
- - Windows
- - Networking
- - Command Line
- - Firewall
- - Troubleshooting
----
-
-When a port is in use on Windows, you might need to close it to free it for another application, resolve conflicts, or improve security. Unlike Linux where you can directly kill processes bound to ports, Windows requires identifying the process first, then either stopping it or blocking the port via firewall rules.
-
-This guide shows you how to find what's using a port and close it using Windows command-line tools.
-
-## TLDR
-
-Find the process using a port with `netstat -ano | findstr :PORT`, then kill it with `taskkill /PID /F`. To block a port with Windows Firewall, use `netsh advfirewall firewall add rule` to create a blocking rule. For services, use `net stop` or `sc stop` to stop the service listening on the port.
-
-## Prerequisites
-
-You need administrative privileges (Run as Administrator) for most port-closing operations. Basic familiarity with Windows Command Prompt or PowerShell helps.
-
-## Finding What's Using a Port
-
-Before closing a port, identify which process is using it.
-
-### Using netstat
-
-```cmd
-netstat -ano | findstr :8080
-```
-
-Output:
-
-```
-TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 4532
-TCP [::]:8080 [::]:0 LISTENING 4532
-```
-
-The last column (`4532`) is the Process ID (PID).
-
-### Using PowerShell
-
-```powershell
-Get-NetTCPConnection -LocalPort 8080
-```
-
-Output shows more detail:
-
-```
-LocalAddress LocalPort RemoteAddress RemotePort State OwningProcess
------------- --------- ------------- ---------- ----- -------------
-0.0.0.0 8080 0.0.0.0 0 Listen 4532
-```
-
-### Identify the Process Name
-
-Once you have the PID, find which program it is:
-
-```cmd
-tasklist | findstr 4532
-```
-
-Output:
-
-```
-node.exe 4532 Console 1 45,234 K
-```
-
-Or get more details with PowerShell:
-
-```powershell
-Get-Process -Id 4532
-```
-
-## Killing the Process
-
-Once you know the PID, terminate the process to free the port.
-
-### Using taskkill
-
-```cmd
-taskkill /PID 4532 /F
-```
-
-The `/F` flag forces termination.
-
-Or kill by process name:
-
-```cmd
-taskkill /IM node.exe /F
-```
-
-This kills all instances of `node.exe`.
-
-### Using PowerShell
-
-```powershell
-Stop-Process -Id 4532 -Force
-```
-
-Or by name:
-
-```powershell
-Stop-Process -Name "node" -Force
-```
-
-### Verify Port is Closed
-
-```cmd
-netstat -ano | findstr :8080
-```
-
-No output means the port is now free.
-
-## Stopping Windows Services
-
-If a Windows Service is using the port, stop the service rather than killing the process.
-
-### Find the Service
-
-```cmd
-sc query | findstr /C:"SERVICE_NAME"
-```
-
-Or use PowerShell to find services by PID:
-
-```powershell
-Get-WmiObject Win32_Service | Where-Object {$_.ProcessId -eq 4532} | Select Name, DisplayName
-```
-
-### Stop the Service
-
-```cmd
-net stop "Service Name"
-```
-
-Or using sc:
-
-```cmd
-sc stop ServiceName
-```
-
-PowerShell alternative:
-
-```powershell
-Stop-Service -Name "ServiceName"
-```
-
-### Common Services and Ports
-
-```cmd
-# Stop IIS (uses port 80/443)
-iisreset /stop
-
-# Stop SQL Server (port 1433)
-net stop MSSQLSERVER
-
-# Stop Remote Desktop (port 3389)
-net stop TermService
-```
-
-## Blocking Ports with Windows Firewall
-
-Instead of killing processes, block ports using firewall rules.
-
-### Block Inbound Traffic on a Port
-
-```cmd
-netsh advfirewall firewall add rule name="Block Port 8080" dir=in action=block protocol=TCP localport=8080
-```
-
-This prevents any inbound connections to port 8080.
-
-### Block Outbound Traffic
-
-```cmd
-netsh advfirewall firewall add rule name="Block Outbound 8080" dir=out action=block protocol=TCP localport=8080
-```
-
-### Block UDP Port
-
-```cmd
-netsh advfirewall firewall add rule name="Block UDP 53" dir=in action=block protocol=UDP localport=53
-```
-
-### Remove Firewall Rule
-
-```cmd
-netsh advfirewall firewall delete rule name="Block Port 8080"
-```
-
-### List All Firewall Rules
-
-```cmd
-netsh advfirewall firewall show rule name=all
-```
-
-Or filter for specific port:
-
-```cmd
-netsh advfirewall firewall show rule name=all | findstr 8080
-```
-
-## PowerShell Firewall Management
-
-### Block a Port
-
-```powershell
-New-NetFirewallRule -DisplayName "Block Port 8080" -Direction Inbound -LocalPort 8080 -Protocol TCP -Action Block
-```
-
-### Remove Rule
-
-```powershell
-Remove-NetFirewallRule -DisplayName "Block Port 8080"
-```
-
-### List Rules
-
-```powershell
-Get-NetFirewallRule | Where-Object {$_.LocalPort -eq 8080}
-```
-
-## Closing Specific Application Ports
-
-### Stop Web Servers
-
-IIS (Internet Information Services):
-
-```cmd
-# Stop IIS
-iisreset /stop
-
-# Or stop specific site
-%windir%\system32\inetsrv\appcmd stop site "Default Web Site"
-```
-
-Apache:
-
-```cmd
-# Stop Apache service
-net stop Apache2.4
-
-# Or if running from command line
-httpd -k stop
-```
-
-### Stop Database Servers
-
-SQL Server:
-
-```cmd
-net stop MSSQLSERVER
-```
-
-MySQL:
-
-```cmd
-net stop MySQL80
-```
-
-PostgreSQL:
-
-```cmd
-net stop postgresql-x64-13
-```
-
-### Stop Development Servers
-
-Node.js applications:
-
-```cmd
-# Find all node processes
-tasklist | findstr node.exe
-
-# Kill them
-taskkill /IM node.exe /F
-```
-
-Python Flask/Django:
-
-```cmd
-tasklist | findstr python.exe
-taskkill /IM python.exe /F
-```
-
-## Handling "Access Denied" Errors
-
-If you get "Access Denied" when trying to kill a process:
-
-1. **Run as Administrator**: Right-click Command Prompt or PowerShell and select "Run as administrator"
-
-2. **Check if it's a system process**: Some processes are protected. Use Process Explorer to see if it's a critical system process.
-
-3. **Stop the parent service**: If the process is started by a service, stop the service instead.
-
-## Preventing Processes from Restarting
-
-Some processes automatically restart. To prevent this:
-
-### Disable the Service
-
-```cmd
-sc config ServiceName start= disabled
-net stop ServiceName
-```
-
-### Change Application Startup
-
-For applications that start automatically:
-
-1. Open Task Manager (Ctrl+Shift+Esc)
-2. Go to Startup tab
-3. Disable the application
-
-Or via command line:
-
-```powershell
-Get-CimInstance -ClassName Win32_StartupCommand | Select-Object Name, Location, Command
-```
-
-## Troubleshooting Common Issues
-
-### Port Still Shows as Listening
-
-After killing a process, the port might remain in TIME_WAIT:
-
-```cmd
-netstat -ano | findstr :8080
-```
-
-Output:
-
-```
-TCP 127.0.0.1:8080 127.0.0.1:54321 TIME_WAIT 0
-```
-
-TIME_WAIT connections clear automatically within 30-120 seconds. To force it:
-
-```powershell
-# Restart TCP/IP stack (requires admin)
-netsh int ip reset
-```
-
-Then restart your computer.
-
-### Multiple Processes on Same Port
-
-If multiple processes share a port:
-
-```cmd
-netstat -ano | findstr :80
-```
-
-Kill each PID:
-
-```cmd
-taskkill /PID 1234 /F
-taskkill /PID 5678 /F
-```
-
-### Cannot Find Process
-
-If netstat shows a port in use but you can't find the process:
-
-```cmd
-# Show all processes including system
-netstat -anob
-```
-
-The `-b` flag shows the executable name (requires admin).
-
-## Automating Port Cleanup
-
-### PowerShell Script to Kill Process on Port
-
-```powershell
-# kill-port.ps1
-param([int]$Port)
-
-$process = Get-NetTCPConnection -LocalPort $Port -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique
-
-if ($process) {
- Stop-Process -Id $process -Force
- Write-Host "Killed process $process using port $Port"
-} else {
- Write-Host "No process found using port $Port"
-}
-```
-
-Usage:
-
-```powershell
-.\kill-port.ps1 -Port 8080
-```
-
-### Batch Script
-
-```cmd
-@echo off
-REM kill-port.bat
-SET PORT=%1
-
-FOR /F "tokens=5" %%P IN ('netstat -ano ^| findstr :%PORT%') DO (
- taskkill /PID %%P /F
-)
-```
-
-Usage:
-
-```cmd
-kill-port.bat 8080
-```
-
-## Security Considerations
-
-**Don't kill critical system processes**: Processes like `svchost.exe`, `System`, or `csrss.exe` are critical. Killing them can crash Windows.
-
-**Check what you're stopping**: Before killing a process, verify it's safe to terminate.
-
-**Use firewall rules for security**: If you want to prevent access to a port, use firewall rules rather than constantly killing processes.
-
-**Monitor for malware**: If unknown processes are binding to ports, scan for malware.
-
-Closing ports on Windows involves finding the process using the port and either terminating it, stopping its service, or blocking the port via firewall rules. Use `netstat` or PowerShell to identify the process, `taskkill` or `Stop-Process` to terminate it, and `netsh` or `New-NetFirewallRule` to block ports. Always verify you're not stopping critical system processes before proceeding.
diff --git a/content/posts/using-ls-list-directories-sizes.md b/content/posts/using-ls-list-directories-sizes.md
deleted file mode 100644
index 74e7dc163..000000000
--- a/content/posts/using-ls-list-directories-sizes.md
+++ /dev/null
@@ -1,145 +0,0 @@
----
-title: 'Using ls to list directories and their total sizes'
-excerpt: 'How to show directory sizes from the shell - practical commands for macOS and Linux, and why `ls` alone is not enough.'
-category:
- name: 'Shell'
- slug: 'shell'
-date: '2025-04-22'
-publishedAt: '2025-04-22T09:00:00Z'
-updatedAt: '2025-04-22T09:00:00Z'
-readingTime: '5 min read'
-author:
- name: 'DevOps Daily Team'
- slug: 'devops-daily-team'
-tags:
- - Linux
- - macOS
- - CLI
- - du
- - ls
----
-
-## TLDR
-
-`ls` does not report the total size of a directory's contents - it reports metadata about the directory entry. To get directory sizes use `du` (disk usage) and combine it with `sort` or `ls`-style output if you need human-readable listings. This post shows practical commands for GNU/Linux and macOS, how to list directories only, and common pitfalls to watch for.
-
-Working at the shell you might instinctively try `ls -lh` to learn which directories are taking space. That leads to confusion because `ls` shows the directory entry size, not the sum of files inside. Use `du` to measure contents and then sort or format the output so it looks familiar.
-
-## Prerequisites
-
-- A POSIX-like shell (bash, zsh).
-- `du` and `sort` are available on macOS and Linux by default. GNU `du` (Linux) and BSD `du` (macOS) have slightly different flags - I show both variants below.
-
-## Quick: human-friendly directory totals (recommended)
-
-Before the code: this lists the size of each item in the current directory and sorts them smallest to largest. It works on both macOS and Linux.
-
-```bash
-# list sizes of items in current directory, human-readable, sorted by size
-# - du: estimate file space usage
-# - -sh: summarize each path and print sizes in human readable form
-# - *: expands to all non-hidden items
-# - sort -h: sort by human-readable numbers
-
-du -sh * 2>/dev/null | sort -h
-```
-
-- What this does: prints one line per entry like "4.0K ./bin" and sorts by size.
-- Why it matters: quick and reliable way to see which directories (and files) use space.
-
-## Show only directories
-
-Before the code: filter the results to directories only. This uses `find` to restrict to directories and `du -sh` to size them.
-
-```bash
-# GNU and BSD compatible: find directories at depth 1 and show their sizes
-find . -maxdepth 1 -type d -print0 | xargs -0 du -sh 2>/dev/null | sort -h
-```
-
-- What this does: finds only first-level directories (including `.`), sizes them, and sorts the output.
-- Why it matters: avoids listing files, which is useful if you only care about directory totals.
-
-## Recursive view with depth (GNU vs macOS)
-
-Before the code: get sizes of top-level directories with a single command. Flags differ between GNU and BSD variants.
-
-```bash
-# GNU (Linux): show human sizes, only depth 1
-du -h --max-depth=1 | sort -h
-
-# BSD (macOS): show human sizes, only depth 1
-du -h -d 1 | sort -h
-```
-
-- What this does: prints cumulative sizes for the current directory and its immediate children.
-- Why it matters: a concise tree-like view of where space is going.
-
-## Add a familiar ls-like column layout
-
-Before the code: if you prefer a two-column layout similar to `ls -lh`, transform `du` output to align columns.
-
-```bash
-# align columns: size and name
-du -sh * 2>/dev/null | sort -h | awk '{printf "%-8s %s\n", $1, $2}'
-```
-
-- What this does: sorts the sizes and prints a formatted column with size then name.
-- Why it matters: easier to scan when you want a neat table in scripts or notes.
-
-## Handling hidden files and permission errors
-
-Hidden files starting with a dot are not matched by `*`. To include them use a shell expansion or `find`:
-
-```bash
-# include hidden entries (bash/zsh): dotglob on bash or use this pattern in zsh
-# bash: shopt -s dotglob; du -sh * .[!.]* ..?*; shopt -u dotglob
-# portable: use find at depth 1
-find . -mindepth 1 -maxdepth 1 -print0 | xargs -0 du -sh 2>/dev/null | sort -h
-```
-
-Permissions can block `du` from reading subdirectories. You may see "Permission denied" messages - redirect stderr to /dev/null if you prefer a clean list, but investigate denied paths when you expect to be able to read them.
-
-## Why `ls` alone is misleading
-
-- `ls -ld some_dir` prints the size of the directory inode - this is not the sum of files inside.
-- `stat` also reports filesystem metadata, not recursive totals.
-
-Example that confuses people:
-
-```bash
-# shows directory entry size, not content size
-ls -ld mydir
-stat mydir
-```
-
-If you need the contents' total, use `du` as shown above.
-
-## Useful scripts and aliases
-
-Before the code: a small shell alias you can add to `~/.zshrc` or `~/.bashrc` for convenience.
-
-```bash
-# add to shell config (zsh or bash)
-# 'ds' for directory sizes in current folder
-alias ds='du -sh -- * 2>/dev/null | sort -h'
-
-# macOS variant (BSD du uses -d)
-alias ds_mac='du -h -d 1 | sort -h'
-```
-
-- What this does: gives a quick command `ds` to inspect sizes.
-- Why it matters: small ergonomics change that saves time in daily work.
-
-## ASCII workflow - quick mental model
-
-```
-list entries -> size them with du -> sort by human numbers -> display
- | | |
- shell du(estimate) sort -h
-```
-
-## Short practical conclusion
-
-Use `du` for directory totals and combine it with `sort -h` to get readable, ordered results. Use `find` and `xargs` when you want to restrict to directories or include hidden entries. Add a small alias if you run this often so you can check disk usage at a glance.
-
-Next steps you can explore: pipe the output into monitoring scripts, use `ncdu` for interactive exploration, or integrate these commands into periodic disk usage reports.
diff --git a/content/posts/which-postgresql-version-am-i-running.md b/content/posts/which-postgresql-version-am-i-running.md
deleted file mode 100644
index becf75da6..000000000
--- a/content/posts/which-postgresql-version-am-i-running.md
+++ /dev/null
@@ -1,419 +0,0 @@
----
-title: 'How to Check Which Version of PostgreSQL You Are Running'
-excerpt: "Learn multiple ways to check your PostgreSQL version, including psql commands, SQL queries, and system commands. Find version numbers from the server, client, and package manager."
-category:
- name: 'Database'
- slug: 'database'
-date: '2024-12-15'
-publishedAt: '2024-12-15T09:00:00Z'
-updatedAt: '2024-12-15T09:00:00Z'
-readingTime: '6 min read'
-author:
- name: 'DevOps Daily Team'
- slug: 'devops-daily-team'
-tags:
- - PostgreSQL
- - Database
- - Version
- - System Administration
- - SQL
----
-
-You need to know which version of PostgreSQL you're running - maybe for compatibility checks, bug reports, or upgrade planning. What's the quickest way to find out?
-
-## TL;DR
-
-Use `psql --version` to check the client version from the command line. To check the server version, connect with `psql` and run `SELECT version();` or use `SHOW server_version;`. From the shell without connecting, use `postgres --version` (if you have direct server access) or `psql -c "SELECT version();"`. The server version is what matters for compatibility and features.
-
-There's a difference between the client version (psql tool) and the server version (actual database). Both are important to know.
-
-## Checking Client Version
-
-The psql client version:
-
-```bash
-psql --version
-```
-
-Output:
-```
-psql (PostgreSQL) 14.8 (Ubuntu 14.8-1.pgdg22.04+1)
-```
-
-This tells you the version of the `psql` command-line tool installed on your machine, not necessarily the server version.
-
-## Checking Server Version from SQL
-
-Connect to PostgreSQL and run SQL commands:
-
-```bash
-psql -U postgres
-```
-
-Then in the PostgreSQL prompt:
-
-```sql
-SELECT version();
-```
-
-Output:
-```
- version
----------------------------------------------------------------------------------------------------------
- PostgreSQL 14.8 on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0, 64-bit
-(1 row)
-```
-
-This shows the actual server version along with platform and compiler information.
-
-For just the version number:
-
-```sql
-SHOW server_version;
-```
-
-Output:
-```
- server_version
-----------------
- 14.8
-(1 row)
-```
-
-## Checking Server Version from Command Line
-
-Without entering the PostgreSQL prompt:
-
-```bash
-psql -c "SELECT version();"
-```
-
-Or for just the version number:
-
-```bash
-psql -c "SHOW server_version;"
-```
-
-Add connection parameters if needed:
-
-```bash
-psql -h localhost -U postgres -d mydatabase -c "SHOW server_version;"
-```
-
-## Getting Version Number Only
-
-Extract just the numeric version:
-
-```bash
-psql -t -c "SHOW server_version;" | tr -d ' '
-```
-
-The `-t` flag outputs without headers, and `tr` removes spaces.
-
-Or with `awk`:
-
-```bash
-psql -t -c "SELECT version();" | awk '{print $2}'
-```
-
-## Checking from the postgres Binary
-
-If you have access to the PostgreSQL server binary:
-
-```bash
-postgres --version
-```
-
-Or the specific binary path:
-
-```bash
-/usr/lib/postgresql/14/bin/postgres --version
-```
-
-This shows the version of the PostgreSQL server software installed, regardless of whether it's running.
-
-## Checking Package Version
-
-On Debian/Ubuntu:
-
-```bash
-# Check installed package version
-dpkg -l | grep postgresql
-
-# More specific
-dpkg -l postgresql postgresql-14
-```
-
-On Red Hat/CentOS:
-
-```bash
-# Check installed package
-rpm -qa | grep postgresql
-
-# Or with yum
-yum list installed | grep postgresql
-```
-
-This shows the version of the PostgreSQL package installed via the system package manager.
-
-## Checking for Running Server
-
-Verify PostgreSQL is running and which version:
-
-```bash
-# Check systemd service status
-systemctl status postgresql
-
-# Check running process
-ps aux | grep postgres
-
-# Show listening port and version
-sudo netstat -tlnp | grep postgres
-```
-
-## Checking from Within Application Code
-
-**Python (psycopg2):**
-
-```python
-import psycopg2
-
-conn = psycopg2.connect(
- host="localhost",
- database="mydb",
- user="postgres",
- password="password"
-)
-
-cur = conn.cursor()
-cur.execute("SELECT version();")
-version = cur.fetchone()[0]
-print(version)
-
-conn.close()
-```
-
-**Node.js (pg):**
-
-```javascript
-const { Client } = require('pg');
-
-const client = new Client({
- host: 'localhost',
- database: 'mydb',
- user: 'postgres',
- password: 'password'
-});
-
-client.connect();
-
-client.query('SELECT version()', (err, res) => {
- if (err) throw err;
- console.log(res.rows[0].version);
- client.end();
-});
-```
-
-**Ruby (pg gem):**
-
-```ruby
-require 'pg'
-
-conn = PG.connect(
- host: 'localhost',
- dbname: 'mydb',
- user: 'postgres',
- password: 'password'
-)
-
-result = conn.exec('SELECT version()')
-puts result.getvalue(0, 0)
-
-conn.close
-```
-
-## Getting Major Version Only
-
-Extract just the major version number (e.g., "14" from "14.8"):
-
-```bash
-psql -t -c "SHOW server_version;" | cut -d. -f1 | tr -d ' '
-```
-
-Or with SQL:
-
-```sql
-SELECT split_part(version(), ' ', 2) AS version_number;
-```
-
-For programmatic use:
-
-```bash
-PGVERSION=$(psql -t -c "SHOW server_version;" | cut -d. -f1 | tr -d ' ')
-echo "Major version: $PGVERSION"
-```
-
-## Checking Multiple Clusters
-
-If you have multiple PostgreSQL clusters:
-
-```bash
-# List all clusters (on Debian/Ubuntu)
-pg_lsclusters
-
-# Connect to specific cluster
-psql --cluster 14/main -c "SHOW server_version;"
-```
-
-## Checking Remote Server
-
-Connect to a remote PostgreSQL server:
-
-```bash
-psql -h remote.example.com -U username -d database -c "SELECT version();"
-```
-
-Or set environment variables:
-
-```bash
-export PGHOST=remote.example.com
-export PGUSER=username
-export PGDATABASE=database
-psql -c "SELECT version();"
-```
-
-## Version Information in Scripts
-
-Create a version check script:
-
-```bash
-#!/bin/bash
-
-echo "=== PostgreSQL Version Information ==="
-
-echo "Client version:"
-psql --version
-
-echo ""
-echo "Server version (full):"
-psql -t -c "SELECT version();"
-
-echo ""
-echo "Server version (short):"
-psql -t -c "SHOW server_version;"
-
-echo ""
-echo "Major version:"
-psql -t -c "SHOW server_version;" | cut -d. -f1 | tr -d ' '
-```
-
-## Checking Version Compatibility
-
-Compare client and server versions:
-
-```bash
-#!/bin/bash
-
-CLIENT_VERSION=$(psql --version | awk '{print $3}' | cut -d. -f1)
-SERVER_VERSION=$(psql -t -c "SHOW server_version;" | cut -d. -f1 | tr -d ' ')
-
-echo "Client major version: $CLIENT_VERSION"
-echo "Server major version: $SERVER_VERSION"
-
-if [ "$CLIENT_VERSION" != "$SERVER_VERSION" ]; then
- echo "WARNING: Client and server versions don't match!"
-fi
-```
-
-## Common PostgreSQL Versions
-
-Understanding version numbers:
-
-- PostgreSQL 14.8: Major version 14, minor version 8
-- PostgreSQL 13.11: Major version 13, minor version 11
-- PostgreSQL 12.15: Major version 12, minor version 15
-
-Major versions introduce new features and break compatibility. Minor versions are bug fixes and security updates within the same major version.
-
-## Checking Available Versions
-
-See what PostgreSQL versions are available:
-
-```bash
-# Ubuntu/Debian
-apt-cache search postgresql | grep "^postgresql-[0-9]"
-
-# Red Hat/CentOS
-yum search postgresql | grep "postgresql[0-9]"
-```
-
-## Troubleshooting Version Checks
-
-If commands fail:
-
-**"psql: command not found"**
-
-PostgreSQL client isn't installed or not in PATH:
-
-```bash
-# Find psql location
-which psql
-sudo find / -name psql 2>/dev/null
-
-# Add to PATH
-export PATH=$PATH:/usr/lib/postgresql/14/bin
-```
-
-**"could not connect to server"**
-
-PostgreSQL server isn't running:
-
-```bash
-# Start PostgreSQL
-sudo systemctl start postgresql
-
-# Check status
-sudo systemctl status postgresql
-```
-
-**Connection refused:**
-
-Check if PostgreSQL is listening:
-
-```bash
-sudo netstat -tlnp | grep 5432
-sudo ss -tlnp | grep 5432
-```
-
-## Version-Specific Features
-
-Knowing your version helps you understand what features are available:
-
-- **PostgreSQL 14**: Multirange types, pg_stat_statements improvements
-- **PostgreSQL 13**: Parallel queries, B-tree improvements
-- **PostgreSQL 12**: Generated columns, partitioning improvements
-- **PostgreSQL 11**: Stored procedures, parallelization enhancements
-
-Check the official release notes for your version to see what features you have access to.
-
-## Upgrading PostgreSQL
-
-Once you know your version, you might want to upgrade:
-
-```bash
-# Check current version
-psql -c "SHOW server_version;"
-
-# See available versions
-apt-cache search postgresql | grep "^postgresql-[0-9]"
-
-# Install new version (doesn't auto-upgrade data)
-sudo apt install postgresql-15
-
-# Migrate data (requires careful planning)
-pg_upgrade ...
-```
-
-Note: Upgrading requires careful planning and data migration. Always backup first.
-
-To check your PostgreSQL version, use `psql --version` for the client or `psql -c "SELECT version();"` for the server. The server version is what determines compatibility and available features, so that's usually the one you need to know.
diff --git a/content/posts/why-c-preprocessor-interprets-linux-as-one.md b/content/posts/why-c-preprocessor-interprets-linux-as-one.md
deleted file mode 100644
index 4b4ff08f9..000000000
--- a/content/posts/why-c-preprocessor-interprets-linux-as-one.md
+++ /dev/null
@@ -1,294 +0,0 @@
----
-title: 'Why Does the C Preprocessor Interpret "linux" as "1"?'
-excerpt: "Discover why the word 'linux' is predefined as the constant 1 in the C preprocessor on Linux systems, and how this historical quirk can cause unexpected compilation errors."
-category:
- name: 'C'
- slug: 'c'
-date: '2025-05-22'
-publishedAt: '2025-05-22T09:00:00Z'
-updatedAt: '2025-05-22T09:00:00Z'
-readingTime: '6 min read'
-author:
- name: 'DevOps Daily Team'
- slug: 'devops-daily-team'
-tags:
- - C
- - Preprocessor
- - Linux
- - Compiler
- - Debugging
----
-
-You're writing C code and you create a variable named `linux`, then your code won't compile. Or worse, it compiles but behaves strangely. What's going on?
-
-## TL;DR
-
-The C preprocessor on Linux systems automatically defines the macro `linux` with the value `1`. This is a legacy feature from older compilers that predefined system names as macros. Modern code should use standard macros like `__linux__` instead. If you need to use `linux` as an identifier, you can undefine it with `#undef linux` or use compiler flags to prevent the predefinition.
-
-This is one of those unexpected behaviors that can waste hours of debugging time if you don't know about it. The issue comes from how compilers historically handled platform detection.
-
-Let's say you write this seemingly innocent code:
-
-```c
-#include
-
-int main() {
- int linux = 5;
- printf("linux = %d\n", linux);
- return 0;
-}
-```
-
-When you compile it on a Linux system with gcc:
-
-```bash
-gcc test.c -o test
-```
-
-The preprocessor replaces `linux` with `1` before compilation, so your code effectively becomes:
-
-```c
-#include
-
-int main() {
- int 1 = 5; // Syntax error!
- printf("1 = %d\n", 1);
- return 0;
-}
-```
-
-This produces a confusing error message like "expected identifier before numeric constant."
-
-## Why Does This Happen?
-
-In the early days of Unix and C, compilers predefined macros for the operating system to help with platform-specific code. The idea was that you could write:
-
-```c
-#ifdef unix
- // Unix-specific code
-#endif
-
-#ifdef linux
- // Linux-specific code
-#endif
-```
-
-The compiler would define these symbols automatically, so you could check which platform you were compiling for without needing to pass flags.
-
-This seemed convenient at the time, but it had a major problem: it polluted the global namespace with common words like `unix` and `linux`. Any variable, function, or struct member with these names would be replaced by the preprocessor.
-
-## The Modern Standard
-
-C standards organizations recognized this was a bad idea. The C standard now specifies that implementation-defined macros should start with an underscore followed by a capital letter (like `__linux__` or `__unix__`).
-
-Modern compilers define proper macros:
-
-```c
-#ifdef __linux__
- // Linux-specific code
-#endif
-
-#ifdef __unix__
- // Unix-specific code
-#endif
-
-#ifdef __APPLE__
- // macOS-specific code
-#endif
-```
-
-But for backward compatibility, gcc still defines the old `linux` and `unix` macros by default when compiling C code (not C++).
-
-## Seeing the Predefined Macros
-
-You can ask gcc to show you all its predefined macros:
-
-```bash
-# Show all predefined macros
-gcc -dM -E - < /dev/null
-
-# Filter for linux-related macros
-gcc -dM -E - < /dev/null | grep linux
-```
-
-You'll see output like:
-
-```
-#define __linux 1
-#define __linux__ 1
-#define linux 1
-```
-
-All three macros are defined with the value `1`.
-
-## How to Work Around It
-
-If you need to use `linux` as an identifier (variable name, function name, etc.), you have several options.
-
-The simplest is to undefine the macro at the top of your file:
-
-```c
-#undef linux
-
-#include
-
-int main() {
- int linux = 5; // Now this works
- printf("linux = %d\n", linux);
- return 0;
-}
-```
-
-Put `#undef linux` before any includes to avoid issues with headers that might use the macro.
-
-Alternatively, compile with the `-std=c99` or `-std=c11` flag, which disables these non-standard predefined macros:
-
-```bash
-gcc -std=c99 test.c -o test
-```
-
-Or use `-ansi` for strict ANSI C compliance:
-
-```bash
-gcc -ansi test.c -o test
-```
-
-Both approaches prevent gcc from defining `linux` as a macro.
-
-## Real-World Example: The Linux Kernel
-
-Interestingly, even the Linux kernel itself has to deal with this. If you look at kernel headers, you'll find code like:
-
-```c
-#undef unix
-#undef linux
-
-// ... kernel code
-```
-
-The kernel developers have to undefine these macros to avoid conflicts with their own code.
-
-## When This Causes Subtle Bugs
-
-The real danger isn't syntax errors (those are obvious). It's when the code compiles but behaves unexpectedly.
-
-Consider this code:
-
-```c
-#include
-
-struct system_info {
- char *name;
- int linux; // Meant to be a boolean flag
-};
-
-int main() {
- struct system_info info = { "Server01", 0 };
-
- if (info.linux) {
- printf("Running Linux\n");
- } else {
- printf("Not running Linux\n");
- }
-
- return 0;
-}
-```
-
-After preprocessor expansion, the struct becomes:
-
-```c
-struct system_info {
- char *name;
- int 1; // Syntax error
-};
-```
-
-But if you had a different name collision that didn't cause a syntax error, you might get runtime bugs that are hard to track down.
-
-## Checking for Platform at Compile Time
-
-If you're writing portable code and need to check the platform, use the modern macros:
-
-```c
-#include
-
-int main() {
- #ifdef __linux__
- printf("Compiled on Linux\n");
- #elif defined(__APPLE__)
- printf("Compiled on macOS\n");
- #elif defined(_WIN32)
- printf("Compiled on Windows\n");
- #else
- printf("Unknown platform\n");
- #endif
-
- return 0;
-}
-```
-
-These macros are standardized and won't interfere with your identifiers.
-
-## C++ Doesn't Have This Problem
-
-If you compile C code as C++ (using `g++` instead of `gcc`), the `linux` macro isn't defined:
-
-```bash
-# C compiler - defines linux
-gcc test.c -o test
-
-# C++ compiler - doesn't define linux
-g++ test.c -o test
-```
-
-This is because C++ has stricter namespace rules, and the standards committee decided not to carry over this legacy behavior.
-
-## Finding the Problem in Your Code
-
-If you're getting weird errors and suspect the `linux` macro might be involved, preprocess your code to see what the compiler actually sees:
-
-```bash
-# Preprocess only, output to stdout
-gcc -E test.c
-
-# Preprocess and save to a file
-gcc -E test.c -o test.i
-```
-
-Look at the output to see if `linux` has been replaced with `1`.
-
-You can also add a check at the top of your file during debugging:
-
-```c
-#ifdef linux
- #warning "linux macro is defined!"
-#endif
-```
-
-This will produce a warning during compilation if the macro is defined.
-
-## Other Problematic Predefined Macros
-
-It's not just `linux` - there are other common words that get predefined:
-
-```c
-// Other potentially problematic macros
-#define unix 1
-#define i386 1 // On x86 systems
-#define arm 1 // On ARM systems
-```
-
-If you're writing portable code or library code, be aware of these. Use the standard `__linux__`, `__unix__`, `__i386__`, `__arm__` variants instead.
-
-## Best Practices
-
-To avoid problems with predefined macros:
-
-- Use modern platform detection macros (`__linux__`, `__unix__`, etc.) instead of legacy ones
-- If you must use common words as identifiers, undefine problematic macros at the top of your file
-- Compile with strict standards flags (`-std=c99`, `-std=c11`) to disable non-standard extensions
-- Check preprocessor output (`gcc -E`) when debugging weird compilation errors
-- For library code, prefix your identifiers to avoid collisions (`mylib_linux` instead of `linux`)
-
-The `linux` macro is a historical artifact that modern C programmers need to be aware of. While it made sense in the 1970s, today it's mostly a source of confusion. Understanding why it exists and how to work around it will save you debugging time and help you write more portable code.
diff --git a/content/posts/why-your-ci-pipeline-is-slower.md b/content/posts/why-your-ci-pipeline-is-slower.md
index 633b7a7a9..7eddb6da7 100644
--- a/content/posts/why-your-ci-pipeline-is-slower.md
+++ b/content/posts/why-your-ci-pipeline-is-slower.md
@@ -1,12 +1,16 @@
---
title: 'Why Your CI/CD Pipeline Is Slower Than It Should Be (and How to Fix It)'
excerpt: 'Small pipeline changes give big wins. Parallelize jobs, cache dependencies, pin images, reuse build artifacts, and run only the tests you need.'
-category: 'ci'
+category:
+ name: 'CI/CD'
+ slug: 'ci-cd'
date: '2025-06-10T09:00:00.000Z'
publishedAt: '2025-06-10T09:00:00.000Z'
updatedAt: '2025-06-10T09:00:00.000Z'
readingTime: 4
-author: 'DevOps Daily'
+author:
+ name: 'DevOps Daily Team'
+ slug: 'devops-daily-team'
tags:
- ci
- cicd
diff --git a/package.json b/package.json
index 2337f4b84..e92c17675 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"lint:fix": "eslint --fix .",
"format": "prettier --write .",
"check-format": "prettier --check .",
+ "check-links": "tsx scripts/check-broken-links.ts",
"quiz:validate": "tsx scripts/quiz-dev.ts validate",
"quiz:create": "tsx scripts/quiz-dev.ts create",
"quiz:list": "tsx scripts/quiz-dev.ts list",
@@ -123,6 +124,7 @@
"@tailwindcss/postcss": "^4.1.18",
"@tailwindcss/typography": "0.5.19",
"@types/js-yaml": "^4.0.9",
+ "@types/jsdom": "^28.0.0",
"@types/node": "^25.2.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
@@ -134,6 +136,7 @@
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"fast-glob": "^3.3.3",
+ "jsdom": "^28.1.0",
"postcss": "^8.5.6",
"prettier": "^3.8.1",
"sharp": "^0.34.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 39cdf2ab9..429d685eb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -240,6 +240,9 @@ importers:
'@types/js-yaml':
specifier: ^4.0.9
version: 4.0.9
+ '@types/jsdom':
+ specifier: ^28.0.0
+ version: 28.0.0
'@types/node':
specifier: ^25.2.3
version: 25.2.3
@@ -273,6 +276,9 @@ importers:
fast-glob:
specifier: ^3.3.3
version: 3.3.3
+ jsdom:
+ specifier: ^28.1.0
+ version: 28.1.0(canvas@3.2.1)
postcss:
specifier: ^8.5.6
version: 8.5.6
@@ -293,10 +299,13 @@ importers:
version: 5.9.3
vitest:
specifier: ^4.0.18
- version: 4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)
+ version: 4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@28.1.0(canvas@3.2.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)
packages:
+ '@acemir/cssom@0.9.31':
+ resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==}
+
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
@@ -307,6 +316,16 @@ packages:
peerDependencies:
ajv: '>=8'
+ '@asamuzakjp/css-color@5.0.1':
+ resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ '@asamuzakjp/dom-selector@6.8.1':
+ resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==}
+
+ '@asamuzakjp/nwsapi@2.3.9':
+ resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
+
'@babel/code-frame@7.29.0':
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'}
@@ -802,6 +821,41 @@ packages:
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
+ '@bramus/specificity@2.4.2':
+ resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
+ hasBin: true
+
+ '@csstools/color-helpers@6.0.2':
+ resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==}
+ engines: {node: '>=20.19.0'}
+
+ '@csstools/css-calc@3.1.1':
+ resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^4.0.0
+ '@csstools/css-tokenizer': ^4.0.0
+
+ '@csstools/css-color-parser@4.0.2':
+ resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^4.0.0
+ '@csstools/css-tokenizer': ^4.0.0
+
+ '@csstools/css-parser-algorithms@4.0.0':
+ resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-tokenizer': ^4.0.0
+
+ '@csstools/css-syntax-patches-for-csstree@1.0.28':
+ resolution: {integrity: sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==}
+
+ '@csstools/css-tokenizer@4.0.0':
+ resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
+ engines: {node: '>=20.19.0'}
+
'@date-fns/tz@1.4.1':
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
@@ -1036,6 +1090,15 @@ packages:
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@exodus/bytes@1.14.1':
+ resolution: {integrity: sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+ peerDependencies:
+ '@noble/hashes': ^1.8.0 || ^2.0.0
+ peerDependenciesMeta:
+ '@noble/hashes':
+ optional: true
+
'@floating-ui/core@1.7.4':
resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==}
@@ -1108,89 +1171,105 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -1272,24 +1351,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@next/swc-linux-arm64-musl@16.1.6':
resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@next/swc-linux-x64-gnu@16.1.6':
resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@next/swc-linux-x64-musl@16.1.6':
resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@next/swc-win32-arm64-msvc@16.1.6':
resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==}
@@ -2047,24 +2130,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@resvg/resvg-js-linux-arm64-musl@2.6.2':
resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@resvg/resvg-js-linux-x64-gnu@2.6.2':
resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@resvg/resvg-js-linux-x64-musl@2.6.2':
resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@resvg/resvg-js-win32-arm64-msvc@2.6.2':
resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==}
@@ -2171,66 +2258,79 @@ packages:
resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.57.1':
resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==}
cpu: [arm]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.57.1':
resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.57.1':
resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.57.1':
resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==}
cpu: [loong64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.57.1':
resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==}
cpu: [loong64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.57.1':
resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.57.1':
resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==}
cpu: [ppc64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.57.1':
resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.57.1':
resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.57.1':
resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.57.1':
resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.57.1':
resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-openbsd-x64@4.57.1':
resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==}
@@ -2315,24 +2415,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
@@ -2424,6 +2528,9 @@ packages:
'@types/js-yaml@4.0.9':
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
+ '@types/jsdom@28.0.0':
+ resolution: {integrity: sha512-A8TBQQC/xAOojy9kM8E46cqT00sF0h7dWjV8t8BJhUi2rG6JRh7XXQo/oLoENuZIQEpXsxLccLCnknyQd7qssQ==}
+
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -2447,6 +2554,9 @@ packages:
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
+ '@types/tough-cookie@4.0.5':
+ resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
+
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@@ -2588,41 +2698,49 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@@ -2747,6 +2865,10 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
+ agent-base@7.1.4:
+ resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
+ engines: {node: '>= 14'}
+
ajv-formats@2.1.1:
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
peerDependencies:
@@ -2891,6 +3013,9 @@ packages:
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
hasBin: true
+ bidi-js@1.0.3:
+ resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
+
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
@@ -3034,6 +3159,10 @@ packages:
resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==}
engines: {node: '>=8.0.0'}
+ css-tree@3.1.0:
+ resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
css-what@6.2.2:
resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
engines: {node: '>= 6'}
@@ -3043,6 +3172,10 @@ packages:
engines: {node: '>=4'}
hasBin: true
+ cssstyle@6.1.0:
+ resolution: {integrity: sha512-Ml4fP2UT2K3CUBQnVlbdV/8aFDdlY69E+YnwJM+3VUWl08S3J8c8aRuJqCkD9Py8DHZ7zNNvsfKl8psocHZEFg==}
+ engines: {node: '>=20'}
+
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -3093,6 +3226,10 @@ packages:
damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
+ data-urls@7.0.0:
+ resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
data-view-buffer@1.0.2:
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
engines: {node: '>= 0.4'}
@@ -3131,6 +3268,9 @@ packages:
decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
@@ -3717,9 +3857,21 @@ packages:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
+ html-encoding-sniffer@6.0.0:
+ resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
htmlparser2@10.1.0:
resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
+ http-proxy-agent@7.0.2:
+ resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
+ engines: {node: '>= 14'}
+
+ https-proxy-agent@7.0.6:
+ resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
+ engines: {node: '>= 14'}
+
hyphenate-style-name@1.1.0:
resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==}
@@ -3860,6 +4012,9 @@ packages:
resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==}
engines: {node: '>=0.10.0'}
+ is-potential-custom-element-name@1.0.1:
+ resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+
is-regex@1.2.1:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
engines: {node: '>= 0.4'}
@@ -3941,6 +4096,15 @@ packages:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
+ jsdom@28.1.0:
+ resolution: {integrity: sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+ peerDependencies:
+ canvas: ^3.0.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
@@ -4041,24 +4205,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
@@ -4109,6 +4277,10 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
+ lru-cache@11.2.6:
+ resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==}
+ engines: {node: 20 || >=22}
+
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -4145,6 +4317,9 @@ packages:
mdn-data@2.0.14:
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
+ mdn-data@2.12.2:
+ resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
+
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -4349,6 +4524,9 @@ packages:
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+ parse5@8.0.0:
+ resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==}
+
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -4653,6 +4831,10 @@ packages:
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
engines: {node: '>=11.0.0'}
+ saxes@6.0.0:
+ resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
+ engines: {node: '>=v12.22.7'}
+
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
@@ -4888,6 +5070,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
+ symbol-tree@3.2.4:
+ resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+
synckit@0.11.12:
resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -4968,6 +5153,13 @@ packages:
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
engines: {node: '>=14.0.0'}
+ tldts-core@7.0.23:
+ resolution: {integrity: sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==}
+
+ tldts@7.0.23:
+ resolution: {integrity: sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==}
+ hasBin: true
+
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -4979,9 +5171,17 @@ packages:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
+ tough-cookie@6.0.0:
+ resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
+ engines: {node: '>=16'}
+
tr46@1.0.1:
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
+ tr46@6.0.0:
+ resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
+ engines: {node: '>=20'}
+
ts-api-utils@2.4.0:
resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
engines: {node: '>=18.12'}
@@ -5054,6 +5254,9 @@ packages:
undici-types@7.18.2:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
+ undici-types@7.22.0:
+ resolution: {integrity: sha512-RKZvifiL60xdsIuC80UY0dq8Z7DbJUV8/l2hOVbyZAxBzEeQU4Z58+4ZzJ6WN2Lidi9KzT5EbiGX+PI/UGYuRw==}
+
undici@7.21.0:
resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==}
engines: {node: '>=20.18.1'}
@@ -5209,6 +5412,10 @@ packages:
jsdom:
optional: true
+ w3c-xmlserializer@5.0.0:
+ resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
+ engines: {node: '>=18'}
+
watchpack@2.5.1:
resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==}
engines: {node: '>=10.13.0'}
@@ -5216,6 +5423,10 @@ packages:
webidl-conversions@4.0.2:
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
+ webidl-conversions@8.0.1:
+ resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
+ engines: {node: '>=20'}
+
webpack-sources@1.4.3:
resolution: {integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==}
@@ -5242,6 +5453,14 @@ packages:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
+ whatwg-mimetype@5.0.0:
+ resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
+ engines: {node: '>=20'}
+
+ whatwg-url@16.0.1:
+ resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
whatwg-url@7.1.0:
resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
@@ -5337,6 +5556,10 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ xml-name-validator@5.0.0:
+ resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+ engines: {node: '>=18'}
+
xml2js@0.5.0:
resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==}
engines: {node: '>=4.0.0'}
@@ -5345,6 +5568,9 @@ packages:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'}
+ xmlchars@2.2.0:
+ resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@@ -5363,6 +5589,8 @@ packages:
snapshots:
+ '@acemir/cssom@0.9.31': {}
+
'@alloc/quick-lru@5.2.0': {}
'@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)':
@@ -5372,6 +5600,24 @@ snapshots:
jsonpointer: 5.0.1
leven: 3.1.0
+ '@asamuzakjp/css-color@5.0.1':
+ dependencies:
+ '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+ lru-cache: 11.2.6
+
+ '@asamuzakjp/dom-selector@6.8.1':
+ dependencies:
+ '@asamuzakjp/nwsapi': 2.3.9
+ bidi-js: 1.0.3
+ css-tree: 3.1.0
+ is-potential-custom-element-name: 1.0.1
+ lru-cache: 11.2.6
+
+ '@asamuzakjp/nwsapi@2.3.9': {}
+
'@babel/code-frame@7.29.0':
dependencies:
'@babel/helper-validator-identifier': 7.28.5
@@ -6026,6 +6272,32 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
+ '@bramus/specificity@2.4.2':
+ dependencies:
+ css-tree: 3.1.0
+
+ '@csstools/color-helpers@6.0.2': {}
+
+ '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/color-helpers': 6.0.2
+ '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@csstools/css-syntax-patches-for-csstree@1.0.28': {}
+
+ '@csstools/css-tokenizer@4.0.0': {}
+
'@date-fns/tz@1.4.1': {}
'@dnd-kit/accessibility@3.1.1(react@19.2.4)':
@@ -6208,6 +6480,8 @@ snapshots:
'@eslint/core': 0.17.0
levn: 0.4.1
+ '@exodus/bytes@1.14.1': {}
+
'@floating-ui/core@1.7.4':
dependencies:
'@floating-ui/utils': 0.2.10
@@ -7477,6 +7751,13 @@ snapshots:
'@types/js-yaml@4.0.9': {}
+ '@types/jsdom@28.0.0':
+ dependencies:
+ '@types/node': 25.3.0
+ '@types/tough-cookie': 4.0.5
+ parse5: 7.3.0
+ undici-types: 7.22.0
+
'@types/json-schema@7.0.15': {}
'@types/json5@0.0.29': {}
@@ -7499,6 +7780,8 @@ snapshots:
'@types/resolve@1.20.2': {}
+ '@types/tough-cookie@4.0.5': {}
+
'@types/trusted-types@2.0.7': {}
'@types/use-sync-external-store@0.0.6': {}
@@ -7764,7 +8047,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
- vitest: 4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)
+ vitest: 4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@28.1.0(canvas@3.2.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)
'@vitest/utils@4.0.18':
dependencies:
@@ -7861,6 +8144,8 @@ snapshots:
acorn@8.16.0: {}
+ agent-base@7.1.4: {}
+
ajv-formats@2.1.1(ajv@8.18.0):
optionalDependencies:
ajv: 8.18.0
@@ -8043,6 +8328,10 @@ snapshots:
baseline-browser-mapping@2.9.19: {}
+ bidi-js@1.0.3:
+ dependencies:
+ require-from-string: 2.0.2
+
bl@4.1.0:
dependencies:
buffer: 5.7.1
@@ -8216,10 +8505,22 @@ snapshots:
mdn-data: 2.0.14
source-map: 0.6.1
+ css-tree@3.1.0:
+ dependencies:
+ mdn-data: 2.12.2
+ source-map-js: 1.2.1
+
css-what@6.2.2: {}
cssesc@3.0.0: {}
+ cssstyle@6.1.0:
+ dependencies:
+ '@asamuzakjp/css-color': 5.0.1
+ '@csstools/css-syntax-patches-for-csstree': 1.0.28
+ css-tree: 3.1.0
+ lru-cache: 11.2.6
+
csstype@3.2.3: {}
d3-array@3.2.4:
@@ -8262,6 +8563,13 @@ snapshots:
damerau-levenshtein@1.0.8: {}
+ data-urls@7.0.0:
+ dependencies:
+ whatwg-mimetype: 5.0.0
+ whatwg-url: 16.0.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
data-view-buffer@1.0.2:
dependencies:
call-bound: 1.0.4
@@ -8294,6 +8602,8 @@ snapshots:
decimal.js-light@2.5.1: {}
+ decimal.js@10.6.0: {}
+
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
@@ -9022,6 +9332,12 @@ snapshots:
highlight.js@11.11.1: {}
+ html-encoding-sniffer@6.0.0:
+ dependencies:
+ '@exodus/bytes': 1.14.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
htmlparser2@10.1.0:
dependencies:
domelementtype: 2.3.0
@@ -9029,6 +9345,20 @@ snapshots:
domutils: 3.2.2
entities: 7.0.1
+ http-proxy-agent@7.0.2:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ https-proxy-agent@7.0.6:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
hyphenate-style-name@1.1.0: {}
iconv-lite@0.6.3:
@@ -9159,6 +9489,8 @@ snapshots:
is-obj@1.0.1: {}
+ is-potential-custom-element-name@1.0.1: {}
+
is-regex@1.2.1:
dependencies:
call-bound: 1.0.4
@@ -9242,6 +9574,35 @@ snapshots:
dependencies:
argparse: 2.0.1
+ jsdom@28.1.0(canvas@3.2.1):
+ dependencies:
+ '@acemir/cssom': 0.9.31
+ '@asamuzakjp/dom-selector': 6.8.1
+ '@bramus/specificity': 2.4.2
+ '@exodus/bytes': 1.14.1
+ cssstyle: 6.1.0
+ data-urls: 7.0.0
+ decimal.js: 10.6.0
+ html-encoding-sniffer: 6.0.0
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.6
+ is-potential-custom-element-name: 1.0.1
+ parse5: 8.0.0
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 6.0.0
+ undici: 7.21.0
+ w3c-xmlserializer: 5.0.0
+ webidl-conversions: 8.0.1
+ whatwg-mimetype: 5.0.0
+ whatwg-url: 16.0.1
+ xml-name-validator: 5.0.0
+ optionalDependencies:
+ canvas: 3.2.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+ - supports-color
+
jsesc@3.1.0: {}
json-buffer@3.0.1: {}
@@ -9379,6 +9740,8 @@ snapshots:
dependencies:
js-tokens: 4.0.0
+ lru-cache@11.2.6: {}
+
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -9410,6 +9773,8 @@ snapshots:
mdn-data@2.0.14: {}
+ mdn-data@2.12.2: {}
+
merge-stream@2.0.0: {}
merge2@1.4.1: {}
@@ -9617,6 +9982,10 @@ snapshots:
dependencies:
entities: 6.0.1
+ parse5@8.0.0:
+ dependencies:
+ entities: 6.0.1
+
path-exists@4.0.0: {}
path-is-absolute@1.0.1: {}
@@ -9969,6 +10338,10 @@ snapshots:
sax@1.4.4: {}
+ saxes@6.0.0:
+ dependencies:
+ xmlchars: 2.2.0
+
scheduler@0.27.0: {}
schema-utils@4.3.3:
@@ -10253,6 +10626,8 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
+ symbol-tree@3.2.4: {}
+
synckit@0.11.12:
dependencies:
'@pkgr/core': 0.2.9
@@ -10324,6 +10699,12 @@ snapshots:
tinyrainbow@3.0.3: {}
+ tldts-core@7.0.23: {}
+
+ tldts@7.0.23:
+ dependencies:
+ tldts-core: 7.0.23
+
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -10332,10 +10713,18 @@ snapshots:
totalist@3.0.1: {}
+ tough-cookie@6.0.0:
+ dependencies:
+ tldts: 7.0.23
+
tr46@1.0.1:
dependencies:
punycode: 2.3.1
+ tr46@6.0.0:
+ dependencies:
+ punycode: 2.3.1
+
ts-api-utils@2.4.0(typescript@5.9.3):
dependencies:
typescript: 5.9.3
@@ -10427,6 +10816,8 @@ snapshots:
undici-types@7.18.2: {}
+ undici-types@7.22.0: {}
+
undici@7.21.0: {}
unicode-canonical-property-names-ecmascript@2.0.1: {}
@@ -10545,7 +10936,7 @@ snapshots:
terser: 5.46.0
tsx: 4.21.0
- vitest@4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0):
+ vitest@4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@28.1.0(canvas@3.2.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0):
dependencies:
'@vitest/expect': 4.0.18
'@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0))
@@ -10570,6 +10961,7 @@ snapshots:
optionalDependencies:
'@types/node': 25.2.3
'@vitest/ui': 4.0.18(vitest@4.0.18)
+ jsdom: 28.1.0(canvas@3.2.1)
transitivePeerDependencies:
- jiti
- less
@@ -10583,6 +10975,10 @@ snapshots:
- tsx
- yaml
+ w3c-xmlserializer@5.0.0:
+ dependencies:
+ xml-name-validator: 5.0.0
+
watchpack@2.5.1:
dependencies:
glob-to-regexp: 0.4.1
@@ -10590,6 +10986,8 @@ snapshots:
webidl-conversions@4.0.2: {}
+ webidl-conversions@8.0.1: {}
+
webpack-sources@1.4.3:
dependencies:
source-list-map: 2.0.1
@@ -10634,6 +11032,16 @@ snapshots:
whatwg-mimetype@4.0.0: {}
+ whatwg-mimetype@5.0.0: {}
+
+ whatwg-url@16.0.1:
+ dependencies:
+ '@exodus/bytes': 1.14.1
+ tr46: 6.0.0
+ webidl-conversions: 8.0.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
whatwg-url@7.1.0:
dependencies:
lodash.sortby: 4.7.0
@@ -10862,6 +11270,8 @@ snapshots:
wrappy@1.0.2: {}
+ xml-name-validator@5.0.0: {}
+
xml2js@0.5.0:
dependencies:
sax: 1.4.4
@@ -10869,6 +11279,8 @@ snapshots:
xmlbuilder@11.0.1: {}
+ xmlchars@2.2.0: {}
+
yallist@3.1.1: {}
yocto-queue@0.1.0: {}
diff --git a/scripts/check-broken-links.ts b/scripts/check-broken-links.ts
new file mode 100644
index 000000000..29d707a4e
--- /dev/null
+++ b/scripts/check-broken-links.ts
@@ -0,0 +1,154 @@
+import fs from 'fs/promises';
+import path from 'path';
+import { JSDOM } from 'jsdom';
+
+const OUT_DIR = path.join(process.cwd(), 'out');
+
+interface BrokenLink {
+ file: string;
+ link: string;
+ target: string;
+}
+
+function normalizePath(href: string): string {
+ if (href !== '/' && href.endsWith('/')) {
+ href = href.slice(0, -1);
+ }
+ return href.split('#')[0];
+}
+
+async function fileExists(filePath: string): Promise {
+ try {
+ await fs.access(filePath);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+async function getHtmlFiles(dir: string): Promise {
+ const files: string[] = [];
+
+ async function walk(currentDir: string) {
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
+
+ for (const entry of entries) {
+ const fullPath = path.join(currentDir, entry.name);
+
+ if (entry.isDirectory()) {
+ await walk(fullPath);
+ } else if (entry.name.endsWith('.html')) {
+ files.push(fullPath);
+ }
+ }
+ }
+
+ await walk(dir);
+ return files;
+}
+
+async function extractInternalLinks(filePath: string): Promise {
+ const content = await fs.readFile(filePath, 'utf-8');
+ const dom = new JSDOM(content);
+ const links: string[] = [];
+
+ const anchors = dom.window.document.querySelectorAll('a[href]');
+
+ anchors.forEach((anchor) => {
+ const href = anchor.getAttribute('href');
+ if (!href) return;
+
+ if (href.startsWith('/') && !href.startsWith('//')) {
+ links.push(normalizePath(href));
+ }
+ });
+
+ return [...new Set(links)];
+}
+
+function resolveLink(link: string): string[] {
+ const possiblePaths: string[] = [];
+
+ const cleanLink = link.startsWith('/') ? link.slice(1) : link;
+
+ if (cleanLink === '' || cleanLink === '/') {
+ possiblePaths.push(path.join(OUT_DIR, 'index.html'));
+ } else {
+ possiblePaths.push(path.join(OUT_DIR, cleanLink, 'index.html'));
+ possiblePaths.push(path.join(OUT_DIR, `${cleanLink}.html`));
+ possiblePaths.push(path.join(OUT_DIR, cleanLink));
+ }
+
+ return possiblePaths;
+}
+
+async function main() {
+ console.log('🔍 Checking for broken internal links in static build...');
+
+ if (!(await fileExists(OUT_DIR))) {
+ console.error('❌ Error: out/ directory not found. Run `pnpm build` first.');
+ process.exit(1);
+ }
+
+ const htmlFiles = await getHtmlFiles(OUT_DIR);
+ console.log(`📄 Found ${htmlFiles.length} HTML files\n`);
+
+ const brokenLinks: BrokenLink[] = [];
+ const checkedLinks = new Set();
+
+ for (const file of htmlFiles) {
+ const relativePath = path.relative(OUT_DIR, file);
+ const links = await extractInternalLinks(file);
+
+ for (const link of links) {
+ if (checkedLinks.has(link)) continue;
+ checkedLinks.add(link);
+
+ const possiblePaths = resolveLink(link);
+ let found = false;
+
+ for (const possiblePath of possiblePaths) {
+ if (await fileExists(possiblePath)) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ brokenLinks.push({
+ file: relativePath,
+ link,
+ target: possiblePaths[0],
+ });
+ }
+ }
+ }
+
+ if (brokenLinks.length > 0) {
+ console.error(`❌ Found ${brokenLinks.length} broken internal link(s):\n`);
+
+ const linkGroups = new Map();
+ brokenLinks.forEach((item) => {
+ if (!linkGroups.has(item.link)) {
+ linkGroups.set(item.link, []);
+ }
+ linkGroups.get(item.link)!.push(item);
+ });
+
+ linkGroups.forEach((items, link) => {
+ console.error(` 🔗 ${link}`);
+ console.error(` Referenced in ${items.length} file(s)`);
+ console.error(` Example: ${items[0].file}\n`);
+ });
+
+ process.exit(1);
+ }
+
+ console.log('✅ No broken internal links found!');
+ console.log(`✓ Checked ${checkedLinks.size} unique internal links`);
+}
+
+main().catch((error) => {
+ console.error('Error checking links:', error);
+ process.exit(1);
+});