]> git.dpolakovic.space - dpolakovic-space/commitdiff
feat: Added JS&PHP mechanism for generating schedule tables master 1.3.0
authorDavid Polakovic
Sat, 8 Aug 2026 10:58:12 +0000 (12:58 +0200)
committerDavid Polakovic
Sat, 8 Aug 2026 10:58:12 +0000 (12:58 +0200)
README.txt
Root-domain/optional-get-time.js [new file with mode: 0644]
Root-domain/php/config.php

index 36d2e899acea70f2feb80d0ff6a0579dbdaf936d..1ab99bd1e70d55e69f69befce6437dfbc0b10659 100644 (file)
-================================================================================
-  dpolakovic.space
-  website files for dpolakovic.space and all it's subdomains
-================================================================================
-
-AUTHOR      : David Polakovic
-EMAIL       : email@dpolakovic.space
-TXT VERSION : 1.0.0
-LAST UPDATED: 2026-02-24
-LICENSE     : GPLv3, with exception of /Git-server/Gtiweb folder
-
---------------------------------------------------------------------------------
-TABLE OF CONTENTS
---------------------------------------------------------------------------------
-
-  1. Overview
-  2. Project structure
-  3. Requirements
-  4. Installation
-  5. Usage
-  6. Reference
-  7. Configuration
-  8. Examples
-  9. Troubleshooting
- 10. License
-
---------------------------------------------------------------------------------
-1. OVERVIEW
---------------------------------------------------------------------------------
-
-  This file describes everything I host on my server.
-
---------------------------------------------------------------------------------
-2. PROJECT STRUCTURE
---------------------------------------------------------------------------------
-
-    dpolakovic-space                    
-    ├── Git-server                      ## files from git.dpolakovic.space
-    │   ├── Coffee                      ## RFC 2324 implementation
-    │   │   ├── coffee-counter.js           # counts served coffees from 48h ago
-    │   │   └── coffee-server.php           # custom http methods definitions
-    │   ├── Gitweb                      ## gitweb (front page) files
-    │   │   ├── static                      
-    │   │   │   ├── gitweb.css              
-    │   │   │   └── gitweb.js               
-    │   │   ├── banner.html                 # contains JS for clicky images 
-    │   │   ├── footer.html                 # contains JS which counts coffees
-    │   │   ├── gitweb.cgi                  # front page logic customization
-    │   │   └── indextext.html              # contains styles for front page 
-    │   └── Stuff                       ## other pages present on git server
-    │       ├── coffee-server.html          
-    │       ├── contributors-guide.html
-    │       ├── dot.png
-    │       ├── server-rules.html
-    │       ├── stuff.css
-    │       └── stuff.js 
-    │                   
-    ├── Root-domain                     ## files from www.dpolakovic.space
-    │   ├── blogs                       ## contains all blog pages
-    │   ├── error                           
-    │   │   └── error.php                   # custom http status code handler
-    │   ├── php                            
-    │   │   ├── config.php                  # logic file included in all pages
-    │   │   └── leaderboard.php             # logic file for leaderboard of CTF
-    │   ├── Pictures    
-    │   ├── Styles  
-    │   │   ├── FONT_LICENSE.txt
-    │   │   ├── NotoSerif-Bold.ttf
-    │   │   ├── NotoSerif-Italic.ttf
-    │   │   ├── NotoSerif-Regular.ttf
-    │   │   └── styles.css
-    │   ├── brew.php                        # implementation of RFC 2324
-    │   ├── clicky-images.js
-    │   └── example-page.php
-    │
-    ├── Tor-site                        ## files from Tor site
-    │   ├── Dead-drops                  ## encrypted messages from dead drop
-    │   ├── php
-    │   │   ├── captcha.php                 # captcha for dead drops
-    │   │   ├── config.php                  # config for all pages
-    │   │   └── dd2.php                     # dead drop logic
-    │   ├── Pictures
-    │   ├── Styles
-    │   │   ├── FONT_LICENSE.txt
-    │   │   ├── NotoSerif-Bold.ttf
-    │   │   ├── NotoSerif-Italic.ttf
-    │   │   ├── NotoSerif-Regular.ttf
-    │   │   └── styles.css
-    │   ├── dead-drop.php                   # dead drop front page
-    │   └── example-page.php
-    │
-    ├── LICENSE.txt
-    └── README.txt
-
-
---------------------------------------------------------------------------------
-3. REQUIREMENTS
---------------------------------------------------------------------------------
-
-  - web server (tested on Apache)
-  - PHP (any version)
-  - git (any version < 3.0.0)
-  - gitweb (any version)
-
---------------------------------------------------------------------------------
-4. INSTALLATION
---------------------------------------------------------------------------------
-
-  1. Clone or download this repository.
-
-  2. Copy any file you want into your server.
-
---------------------------------------------------------------------------------
-5. USAGE
---------------------------------------------------------------------------------
-
-  -
-
---------------------------------------------------------------------------------
-6. REFERENCE
---------------------------------------------------------------------------------
-
-  Short references are written in project structure.
-
---------------------------------------------------------------------------------
-7. CONFIGURATION
---------------------------------------------------------------------------------
-
-  -
-
---------------------------------------------------------------------------------
-8. EXAMPLES
---------------------------------------------------------------------------------
-
-  -
-
---------------------------------------------------------------------------------
-9. TROUBLESHOOTING
---------------------------------------------------------------------------------
-
-  Problem : Coffee counter shows error.
-  Solution: Restart the web server. This generates new json files and usually 
-            solves this
-
-  Problem : Dead drops don't show contents.
-  Solution: Check permissions and ownership on Dead-drops/ folder. It should 
-            be apache:apache
-
---------------------------------------------------------------------------------
-10. LICENSE
---------------------------------------------------------------------------------
-
-  This project is licensed under the GPLv3 License.
-  See the LICENSE.txt file for full details.
-
-  Copyright (c) 2026 David Polakovic
-
-================================================================================
+\r
+dpolakovic.space\r
+website files and configs for dpolakovic.space, git server and the onion site\r
+\r
+AUTHOR  : David Polakovic\r
+EMAIL   : email@dpolakovic.space\r
+VERSION        : 1.3.0\r
+LICENSE : various, see section LICENSE for more details\r
+\r
+OVERVIEW\r
+\r
+  This repository contains a collection of html, php, javascript files \r
+  which I host these domains:\r
+  - https://www.dpolakovic.space\r
+  - https://git.dpolakovic.space\r
+  - and .onion site\r
+  \r
+  I do not "git" content of sites, only example pages. Exceptions \r
+  are files in /Git-server/Stuff/* since changes on them are good to track \r
+  for existing and potential git contributors.\r
+\r
+\r
+PROJECT STRUCTURE (tree --dirsfirst)\r
+\r
+  ├── Git-server\r
+  │   ├── Coffee\r
+  │   │   ├── coffee-counter.js\r
+  │   │   └── coffee-server.php\r
+  │   ├── Gitweb\r
+  │   │   ├── static\r
+  │   │   │   ├── gitweb.css\r
+  │   │   │   └── gitweb.js\r
+  │   │   ├── banner.html\r
+  │   │   ├── footer.html\r
+  │   │   ├── gitweb.cgi\r
+  │   │   └── indextext.html\r
+  │   └── Stuff\r
+  │       ├── coffee-server.html\r
+  │       ├── contributors-guide.html\r
+  │       ├── dot.png\r
+  │       ├── server-rules.html\r
+  │       ├── stuff.css\r
+  │       └── stuff.js\r
+  │ \r
+  ├── Root-domain\r
+  │   ├── blogs\r
+  │   ├── error\r
+  │   │   └── error.php\r
+  │   ├── php\r
+  │   │   ├── config.php\r
+  │   │   └── leaderboard.php\r
+  │   ├── Pictures\r
+  │   ├── Styles\r
+  │   │   ├── FONT_LICENSE.txt\r
+  │   │   ├── NotoSerif-Bold.ttf\r
+  │   │   ├── NotoSerif-Italic.ttf\r
+  │   │   ├── NotoSerif-Regular.ttf\r
+  │   │   └── styles.css\r
+  │   ├── brew.php\r
+  │   ├── clicky-images.js\r
+  │   ├── optional-get-time.js\r
+  │   └── example-page.php\r
+  │ \r
+  ├── Scripts\r
+  │   └── Rsser\r
+  │       └── rsser.pl\r
+  │ \r
+  ├── Tor-site\r
+  │   ├── Dead-drops\r
+  │   ├── php\r
+  │   │   ├── captcha.php\r
+  │   │   ├── config.php\r
+  │   │   └── dd2.php\r
+  │   ├── Pictures\r
+  │   ├── Styles\r
+  │   │   ├── FONT_LICENSE.txt\r
+  │   │   ├── NotoSerif-Bold.ttf\r
+  │   │   ├── NotoSerif-Italic.ttf\r
+  │   │   ├── NotoSerif-Regular.ttf\r
+  │   │   └── styles.css\r
+  │   ├── dead-drop.php\r
+  │   └── example-page.php\r
+  │ \r
+  ├── DOC_LICENSE.txt\r
+  ├── LICENSE.txt\r
+  ├── README.txt\r
+  └── TO_DO.txt\r
+\r
+\r
+\r
+\r
+REQUIREMENTS\r
+\r
+  In order to roll out these files on your setup you would need\r
+  - RHEL 9 or RHEL 9 clone\r
+  - apache server  (any version)\r
+  - php           (any version > 2.0.0)\r
+  - git           (any version < 3.0.0)\r
+  - gitweb        (any version)\r
+  - highlight     (any version)\r
+\r
+  The httpd and RHEL in general are optional, but other distros and/or server\r
+  would require some config changes from you.\r
+\r
+\r
+\r
+COMPONENT REFERENCE\r
+\r
+  In this repo I also bundled the rsser.pl which is script I use to transle html \r
+  formated text from my blogs into rss formated text. Then I paste the output \r
+  to the rss.xml so users fetching my rss can read the blog in full length in \r
+  their favorite reader.\r
+\r
+  rsser.pl\r
+    Purpose : converts HTML formatted text into CSS 2.0 format.\r
+    Usage   : perl scripts/example_script.pl --input data.csv\r
+    Options : --help, --verbose, --output DIR\r
+\r
+\r
+\r
+CONFIGURATION\r
+\r
+\r
+  Don't forget to change the API token in Tor-site/php/dd2.php .\r
+\r
+\r
+\r
+KNOWN ISSUES\r
+\r
+ Problem : Coffee counter shows error.\r
+ Solution: Restart the web server. This generates new .json files and usually \r
+           solves this\r
\r
+ Problem : Dead drops don't show contents.\r
+ Solution: Check permissions and ownership on Dead-drops/ folder. It should \r
+           be apache:apache\r
+\r
+\r
+\r
+LICENSE\r
+\r
+\r
+  This project uses various licenses. Check which files uses which one.\r
+  - files located in /Git-server are licensed under GPLv2\r
+  - files located in /Root-domain and /Tor-site are licensed under GPLv3\r
+\r
+\r
+  See the LICENSE.txt in  file for full details.\r
+\r
+  Copyright (c) 2022-2026 David Polakovic
\ No newline at end of file
diff --git a/Root-domain/optional-get-time.js b/Root-domain/optional-get-time.js
new file mode 100644 (file)
index 0000000..bf1c335
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ *
+ * @licstart  The following is the entire license notice for the 
+ *  JavaScript code in this page.
+ *
+ * Copyright (C) 2026  David Polakovic
+ *
+ *
+ * The JavaScript code in this page is free software: you can
+ * redistribute it and/or modify it under the terms of the GNU
+ * General Public License (GNU GPL) as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option)
+ * any later version.  The code is distributed WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.
+ *
+ * As additional permission under GNU GPL version 3 section 7, you
+ * may distribute non-source (e.g., minimized or compacted) forms of
+ * that code without the copy of the GNU GPL normally required by
+ * section 4, provided you include this license notice and a URL
+ * through which recipients can access the Corresponding Source.
+ *
+ * @licend  The above is the entire license notice
+ * for the JavaScript code in this page.
+ *
+ */
+
+(function () {
+  "use strict";
+  var offset = -new Date().getTimezoneOffset();   // getTimezoneOffset counts the other way
+  var current = document.cookie.match(/(?:^|;\s*)tzo=(-?\d+)/);
+  if (current && parseInt(current[1], 10) === offset) return;   // already correct
+  document.cookie = "tzo=" + offset + ";path=/;max-age=31536000;SameSite=Lax";
+  // Read it back: if the cookie didn't stick, reloading would loop forever.
+  var check = document.cookie.match(/(?:^|;\s*)tzo=(-?\d+)/);
+  if (!check || parseInt(check[1], 10) !== offset) return;
+  location.replace(location.href);   // replace, so the back button stays sane
+})();
index 0a818ccb824922fd47c496486cc94028782b4e23..67eb524ef7f63487e5fc36e4d971a382e04b257a 100755 (executable)
@@ -1,9 +1,7 @@
 <?php
 <?php
-// Debuging stuff...
 // error_reporting(E_ALL);
 // ini_set('display_errors', 1);
 
 // error_reporting(E_ALL);
 // ini_set('display_errors', 1);
 
-// Helper function for serverStatus();
 function isWebsiteOnline($url) {
     $domain = parse_url($url, PHP_URL_HOST);
     $port = 80;
 function isWebsiteOnline($url) {
     $domain = parse_url($url, PHP_URL_HOST);
     $port = 80;
@@ -21,7 +19,6 @@ function isWebsiteOnline($url) {
     }
 }
 
     }
 }
 
-// Show if git server is down or running
 function serverStatus() {
     $websiteUrl = "http://git.dpolakovic.space";
     if (isWebsiteOnline($websiteUrl)) {
 function serverStatus() {
     $websiteUrl = "http://git.dpolakovic.space";
     if (isWebsiteOnline($websiteUrl)) {
@@ -31,7 +28,7 @@ function serverStatus() {
     }
 }
 
     }
 }
 
-// Pick random second sentence on index.php
+// Function for index.php to generate random second sentence
 function randomSentence() {
    $file = "./php/rnd.txt";
 
 function randomSentence() {
    $file = "./php/rnd.txt";
 
@@ -47,10 +44,10 @@ function randomSentence() {
       echo $lines[$randomIndex];
 
    }
       echo $lines[$randomIndex];
 
    }
+
    else {  echo "this is an error message."; }
 }
 
    else {  echo "this is an error message."; }
 }
 
-// Print time of last update
 function lastFileUpdate(...$files) {
     $latestTimestamp = 0;
     foreach ($files as $file) {
 function lastFileUpdate(...$files) {
     $latestTimestamp = 0;
     foreach ($files as $file) {
@@ -70,7 +67,6 @@ function lastFileUpdate(...$files) {
     }
 }
 
     }
 }
 
-// Counts lines...
 function countFileLines($filename) {
     if (!file_exists($filename)) {
         echo "File not found: $filename<br>";
 function countFileLines($filename) {
     if (!file_exists($filename)) {
         echo "File not found: $filename<br>";
@@ -81,7 +77,6 @@ function countFileLines($filename) {
     echo $lineCount;
 }
 
     echo $lineCount;
 }
 
-// Print footer on every page
 function printFooter($usesJS, $usesCookies, ...$files) {
     // Print JS status
     echo "Copyright 2023-" . date("Y") . " David Polakovic. All publications licensed under ";
 function printFooter($usesJS, $usesCookies, ...$files) {
     // Print JS status
     echo "Copyright 2023-" . date("Y") . " David Polakovic. All publications licensed under ";
@@ -109,21 +104,43 @@ function printFooter($usesJS, $usesCookies, ...$files) {
     }
     echo 'Server for this domain is ';
     echo '<a href="https://www.rfc-editor.org/rfc/rfc1464.html">RFC 1464</a> and ';
     }
     echo 'Server for this domain is ';
     echo '<a href="https://www.rfc-editor.org/rfc/rfc1464.html">RFC 1464</a> and ';
-    echo '<a href="https://www.rfc-editor.org/rfc/rfc2324">RFC 2324</a> compliant.';
+    echo '<a href="https://www.rfc-editor.org/rfc/rfc2324.html">RFC 2324</a> compliant.';
 }
 
 }
 
-// Just like footer...
+
 function printNavBar(){
     echo <<<HTML
 function printNavBar(){
     echo <<<HTML
-<nav class="nav-bar">
-<input type="checkbox" id="mobile-menu-toggle" class="mobile-menu-toggle">
-<label for="mobile-menu-toggle" class="mobile-menu-icon"></label>
-<ul>
-<li><a href="https://www.dpolakovic.space">Home</a></li>
-<li><a href="https://www.dpolakovic.space/noticeboard.php">Noticeboard</a></li>
-<li><a href="https://www.dpolakovic.space/blog.php">Blog</a></li>      
-<li><a href="https://www.dpolakovic.space/dir.php">Web directory</a></li>
-<!-- <li class="desktop-only"><a href="">example</a></li> -->
+    <nav class="nav-bar">
+    <input type="checkbox" id="mobile-menu-toggle" class="mobile-menu-toggle">
+    <label for="mobile-menu-toggle" class="mobile-menu-icon"></label>
+    <ul>
+    <li><a href="https://www.dpolakovic.space">Home</a></li>
+    <li><a href="https://www.dpolakovic.space/blog.php">Blog</a></li>      
+    <li><a href="https://www.dpolakovic.space/dir.php">Web directory</a></li>
+    <!-- <li><a href="https://www.dpolakovic.space/mars.php">Mars clock</a></li> -->
+    <li class="dropdown">
+    <a href="https://www.dpolakovic.space/quake/servers.php">Quake HvH
+        <svg class="dropdown-caret" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
+        <path fill="rgb(248, 248, 248)" d="M1 3.25h8L5 7.5z"/></svg>
+    </a>
+        <ul class="dropdown-content">
+            <li><a href="https://www.dpolakovic.space/quake/servers.php">Server list</a></li>
+            <li><a href="https://www.dpolakovic.space/quake/roll-your-server.php">Roll your server</a></li>
+            <!-- <li><a href="https://www.dpolakovic.space/quake/landscape-mapping.php">Landscape mapping</a></li>
+            <li><a href="https://www.dpolakovic.space/quake/ezquake.php">ezQuake</a></li>
+            <li><a href="https://www.dpolakovic.space/quake/fte.php">FTE client</a></li>
+            <li><a href="https://www.dpolakovic.space/quake/vanilla.php">Original client</a></li> -->
+        </ul>
+    </li>
+    <li style="display:flex; align-items:center;">
+    <a href=" http://apebkcjcj7da3f7hfpkbp4csylu53fqrbfcw6tb7n4os3efyjbsa4cyd.onion/ ">More</a>
+    <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" style="margin-left:-15px; margin-top:4px; flex-shrink:0;">
+        <path fill="rgb(248, 248, 248)" d="M5.002 1.01h7v7l-2-2-3 2v-1l3-2.25 1 1V2.01h-3.75l1 1-2.25 3h-1l2-3z"/>
+        <path fill="rgb(248, 248, 248)" d="M7.002 3.01h-5v8h8v-5h-1v4h-6v-6h4z"/>
+        <path fill="rgb(248, 248, 248)" d="M4.082 5.51c0-.621.621-.621.621-.621q2.796.932 3.728 3.728s0 .621-.62.621C6.566 7.374 5.945 6.753 4.081 5.51z"/>
+    </svg>
+    </li>
+    <!-- <li class="desktop-only"><a href="">example</a></li> -->
 <li>
 HTML;
 echo serverStatus();
 <li>
 HTML;
 echo serverStatus();
@@ -134,7 +151,7 @@ echo <<<HTML
 HTML;
 }
 
 HTML;
 }
 
-// Checks file size ( used in javascript blog)
+
 function getFileSize($filename) {
     if (file_exists($filename)) {
         $sizeInBytes = filesize($filename);
 function getFileSize($filename) {
     if (file_exists($filename)) {
         $sizeInBytes = filesize($filename);
@@ -145,7 +162,7 @@ function getFileSize($filename) {
     }
 }
 
     }
 }
 
-// Cconverts RSS file into blog page
+// Function to convert RSS file into blog page
 function printBlog2($rss_file) {
     // Get the selected category from URL parameter (default to 'all')
     $selected_category = isset($_GET['category']) ? $_GET['category'] : 'all';
 function printBlog2($rss_file) {
     // Get the selected category from URL parameter (default to 'all')
     $selected_category = isset($_GET['category']) ? $_GET['category'] : 'all';
@@ -193,7 +210,7 @@ function printBlog2($rss_file) {
     }    
 }
 
     }    
 }
 
-// Checks current TorBrowser version
+
 function getTorBrowserVersion() {
     $cacheFile = sys_get_temp_dir() . '/tor_browser_version_cache.txt';
     $cacheTime = 3600; // 1 hour in seconds
 function getTorBrowserVersion() {
     $cacheFile = sys_get_temp_dir() . '/tor_browser_version_cache.txt';
     $cacheTime = 3600; // 1 hour in seconds
@@ -252,4 +269,260 @@ function getTorBrowserVersion() {
     echo $output;
 }
 
     echo $output;
 }
 
+
+function qw_status_cached(string $host, int $port, int $maxAge = 30): ?array {
+    $cacheFile = sys_get_temp_dir() . "/qwstatus_{$host}_{$port}.json";
+
+    // If we have a fresh cached result, use it — don't hit the game server.
+    if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $maxAge) {
+        $cached = json_decode(file_get_contents($cacheFile), true);
+        if ($cached !== null) return $cached;
+    }
+
+    // Cache stale or missing — query the server once, then store it.
+    $status = qw_status($host, $port);
+    if ($status !== null) {
+        file_put_contents($cacheFile, json_encode($status));
+    }
+    return $status;
+}
+
+
+function qw_status(string $host, int $port = 27500, float $timeout = 2.0): ?array {
+    $sock = @stream_socket_client(
+        "udp://{$host}:{$port}",
+        $errno, $errstr,
+        $timeout
+    );
+    if (!$sock) {
+        return null;
+    }
+    stream_set_timeout($sock, (int)$timeout);
+
+    // status 23 = serverinfo + players + spectators + spec frag marker
+    fwrite($sock, "\xFF\xFF\xFF\xFFstatus 23\n");
+
+    $response = fread($sock, 8192);
+    fclose($sock);
+
+    if (!$response || strlen($response) < 5) {
+        return null;
+    }
+
+    // Strip 4-byte 0xFF header + 1-byte response type ('n')
+    $payload = substr($response, 5);
+    $lines = explode("\n", $payload);
+
+    // First line: serverinfo key/value string
+    $infoLine = ltrim(array_shift($lines), '\\');
+    $parts = explode('\\', $infoLine);
+    $info = [];
+    for ($i = 0; $i + 1 < count($parts); $i += 2) {
+        $info[$parts[$i]] = $parts[$i + 1];
+    }
+
+    // Remaining lines: players AND spectators
+    $players = [];
+    $spectators = [];
+    foreach ($lines as $line) {
+        $line = trim($line);
+        if ($line === '') continue;
+
+        // Standard format:
+        //   id frags time ping "name" "skin" topcolor bottomcolor
+        // Spectator format (mvdsv with flag 4+8):
+        //   id "S" time ping "\s\name" "skin" topcolor bottomcolor
+        if (preg_match(
+            '/^(\d+)\s+(-?\d+|"S")\s+(\d+)\s+(\d+)\s+"([^"]*)"\s+"([^"]*)"\s+(\d+)\s+(\d+)/',
+            $line, $m
+        )) {
+            $isSpec = ($m[2] === '"S"') || str_starts_with($m[5], '\\s\\');
+            $name   = $m[5];
+            if (str_starts_with($name, '\\s\\')) {
+                $name = substr($name, 3); // strip the \s\ prefix
+            }
+
+            $entry = [
+                'id'     => (int)$m[1],
+                'frags'  => $isSpec ? null : (int)$m[2],
+                'time'   => (int)$m[3],
+                'ping'   => (int)$m[4],
+                'name'   => $name,
+                'skin'   => $m[6],
+                'top'    => (int)$m[7],
+                'bottom' => (int)$m[8],
+            ];
+
+            if ($isSpec) {
+                $spectators[] = $entry;
+            } else {
+                $players[] = $entry;
+            }
+        }
+    }
+
+    return [
+        'info'        => $info,
+        'players'     => $players,
+        'spectators'  => $spectators,
+    ];
+}
+
+
+function draw_dynamic_table($room = 'mvdsv')
+{
+    /* ---------------------------------------------------------------------
+       1. YOUR DATA — edit only this block.
+          Two rooms. Keys are the UTC start time of the slot:
+          "00:00", "02:00", "04:00" ... "22:00". Leave a slot out if it's free.
+       ------------------------------------------------------------------- */
+    $reservations = array(
+        'mvdsv' => array(
+            'Monday'    => array(),
+            'Tuesday'   => array('16:00' => 'upkeep'),
+            'Wednesday' => array(),
+            'Thursday'  => array(),
+            'Friday'    => array('04:00' => 'deauth pckt'),
+            'Saturday'  => array(),
+            'Sunday'    => array(),
+        ),
+        'fte' => array(
+            'Monday'    => array(),
+            'Tuesday'   => array('16:00' => 'upkeep'),
+            'Wednesday' => array(),
+            'Thursday'  => array(),
+            'Friday'    => array(),
+            'Saturday'  => array(),
+            'Sunday'    => array(),
+        ),
+    );
+    /* ---------------------------------------------------------------------
+       2. Machinery — no need to touch anything below.
+       ------------------------------------------------------------------- */
+    if (!isset($reservations[$room])) {
+        echo '<!-- draw_dynamic_table: unknown room -->';
+        return;
+    }
+    $book = $reservations[$room];
+    $days         = array('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday');
+    $slot_minutes = 120;                    // 2 hours per row
+    $slot_count   = 1440 / $slot_minutes;   // 12 rows
+    /* The visitor's UTC offset in minutes, planted by optional-get-time.js.
+       No cookie (or a junk one) means no JavaScript: fall back to UTC+0. */
+    $offset = 0;
+    if (isset($_COOKIE['tzo']) && preg_match('/^-?\d{1,4}$/', $_COOKIE['tzo'])) {
+        $candidate = (int) $_COOKIE['tzo'];
+        if ($candidate >= -720 && $candidate <= 840) {   // UTC-12 .. UTC+14
+            $offset = $candidate;
+        }
+    }
+    $wrap_day = function ($minutes) {
+        return (($minutes % 1440) + 1440) % 1440;
+    };
+    $clock = function ($minutes) use ($wrap_day) {
+        $m = $wrap_day($minutes);
+        return sprintf('%02d:%02d', intdiv($m, 60), $m % 60);
+    };
+    $zone_label = function ($minutes) {
+        $sign = $minutes < 0 ? '-' : '+';
+        $abs  = abs($minutes);
+        $h    = intdiv($abs, 60);
+        $m    = $abs % 60;
+        return 'UTC' . $sign . $h . ($m ? sprintf(':%02d', $m) : '');
+    };
+    /* Each UTC row, translated into the visitor's day. day_shift says whether
+       that UTC slot lands on the previous, same, or next local day. Rows keep
+       their UTC alignment (so no booking gets split across two cells) and are
+       sorted by local start time. */
+    $rows = array();
+    for ($slot = 0; $slot < $slot_count; $slot++) {
+        $utc_start = $slot * $slot_minutes;
+        $shifted   = $utc_start + $offset;
+        $rows[] = array(
+            'utc_start'   => $utc_start,
+            'local_start' => $wrap_day($shifted),
+            'day_shift'   => (int) floor($shifted / 1440),
+        );
+    }
+    usort($rows, function ($a, $b) {
+        return $a['local_start'] - $b['local_start'];
+    });
+    /* "Now" in the visitor's zone, so today's column can be marked. */
+    $now_shifted = ((int) gmdate('G')) * 60 + ((int) gmdate('i')) + $offset;
+    $now_minutes = $wrap_day($now_shifted);
+    $now_day     = ((((int) gmdate('N')) - 1) + (int) floor($now_shifted / 1440) + 7) % 7;
+    /* ---- output ------------------------------------------------------- */
+    $time_column_width = '8.5em';
+    echo '<table class="reservation-table" style="table-layout:fixed;width:100%">';
+    /* Fixed layout: the time column gets a set width, the seven day columns
+       split the rest equally. */
+    echo '<colgroup><col style="width:' . $time_column_width . '">'
+       . str_repeat('<col>', count($days))
+       . '</colgroup>';
+    /* Header: timezone in the corner, then the seven days. */
+    echo '<thead><tr>';
+    echo '<th class="rt-zone" scope="col" style="text-align:center">'
+       . htmlspecialchars($zone_label($offset), ENT_QUOTES, 'UTF-8')
+       . '</th>';
+    foreach ($days as $index => $day) {
+        echo '<th class="rt-day' . ($index === $now_day ? ' rt-today' : '') . '" scope="col">'
+           . htmlspecialchars($day, ENT_QUOTES, 'UTF-8')
+           . '</th>';
+    }
+    echo '</tr></thead>';
+    /* Body: one row per two-hour slot. */
+    echo '<tbody>';
+    foreach ($rows as $row) {
+        echo '<tr>';
+        echo '<td class="rt-time" style="text-align:center;white-space:nowrap">'
+           . htmlspecialchars(
+                 $clock($row['local_start']) . ' to ' . $clock($row['local_start'] + $slot_minutes - 5),
+                 ENT_QUOTES, 'UTF-8')
+           . '</td>';
+        $is_now_row = ($now_minutes >= $row['local_start'])
+                   && ($now_minutes <  $row['local_start'] + $slot_minutes);
+        foreach ($days as $index => $day) {
+            $utc_day = $days[($index - $row['day_shift'] + 7) % 7];
+            $key     = $clock($row['utc_start']);
+            $name    = isset($book[$utc_day][$key]) ? $book[$utc_day][$key] : '';
+            $classes = array('rt-slot', $name !== '' ? 'rt-booked' : 'rt-open');
+            if ($index === $now_day) {
+                $classes[] = 'rt-today';
+                if ($is_now_row) {
+                    $classes[] = 'rt-now';
+                }
+            }
+            $safe = htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
+            echo '<td class="' . implode(' ', $classes) . '"'
+               . ' style="overflow:hidden;white-space:nowrap;text-overflow:ellipsis"'
+               . ($name !== '' ? ' title="' . $safe . '"' : '')
+               . '>' . $safe . '</td>';
+        }
+        echo '</tr>';
+    }
+    echo '</tbody></table>';
+}
+
+
 ?>
\ No newline at end of file
 ?>
\ No newline at end of file
Copyright 2022-2026 David Polakovic. Individual project licenses are located in project root in full length.

Site generated using Gitweb. Read the documentation for JavaScript and cookie information. Additional source code available here under GPLv3 license.

Server for this subdomain is RFC 2324 compliant