feat: major enhancement — OS gallery, server rename, traffic chart, backups, VNC toggle, password reset, Redis caching, UX improvements

- Remove client IP removal capability (keep backend methods removed too)
- Add copy-to-clipboard buttons for IP addresses with tooltip feedback
- Replace OS dropdown with tile gallery (grouped, searchable, brand colors, EOL badges) in rebuild panel and checkout page
- Add inline server rename with friendly name generator and RFC 1123 validation
- Add traffic statistics canvas chart with responsive resize in resources panel
- Add backup listing timeline in manage panel with show-all expansion
- Add VNC enable/disable toggle with connection details and password copy
- Add server root password reset with auto-clipboard copy (never displayed)
- Add skeleton loading placeholders, action cooldowns (power 3s, rebuild 30s), progress indicator with elapsed timer
- Sanitize all client-facing error messages (no raw API errors exposed)
- Convert all state-mutating AJAX calls from GET to POST
- Add explicit break after all output() calls in client.php
- Add Redis-backed API response caching (Cache.php): OS templates 10min, traffic/backups 2min, currencies 30min, packages 10min
- Add GitHub Actions workflow for weekly VirtFusion API change detection
- Move cache busting step after semantic-release in publish workflow
- Add endpoint doc generator script and OpenAPI baseline placeholder
- Improve hostname generation entropy (bin2hex random_bytes)
- Add .superpowers/ to .gitignore

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Prophet731
2026-03-19 05:40:32 -05:00
parent 538974e0fe
commit 90a97c4afb
13 changed files with 1647 additions and 249 deletions

View File

@@ -0,0 +1,131 @@
<?php
namespace WHMCS\Module\Server\VirtFusionDirect;
class Cache
{
const PREFIX = 'vfd:';
/** @var \Redis|null */
private static $redis = null;
/** @var bool */
private static $available = true;
/**
* Get a Redis connection, or null if unavailable.
*
* @return \Redis|null
*/
private static function redis()
{
if (!self::$available) {
return null;
}
if (self::$redis !== null) {
return self::$redis;
}
if (!class_exists('Redis')) {
self::$available = false;
return null;
}
try {
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379, 1.0);
self::$redis = $redis;
return $redis;
} catch (\Exception $e) {
self::$available = false;
return null;
}
}
/**
* Get a cached value.
*
* @param string $key
* @return mixed|null Returns null on miss
*/
public static function get($key)
{
$redis = self::redis();
if (!$redis) {
return null;
}
try {
$data = $redis->get(self::PREFIX . $key);
if ($data === false) {
return null;
}
return json_decode($data, true);
} catch (\Exception $e) {
return null;
}
}
/**
* Store a value in cache.
*
* @param string $key
* @param mixed $value
* @param int $ttl Time-to-live in seconds
*/
public static function set($key, $value, $ttl = 300)
{
$redis = self::redis();
if (!$redis) {
return;
}
try {
$redis->setex(self::PREFIX . $key, $ttl, json_encode($value));
} catch (\Exception $e) {
// Silently fail — caching is optional
}
}
/**
* Delete a cached value.
*
* @param string $key
*/
public static function forget($key)
{
$redis = self::redis();
if (!$redis) {
return;
}
try {
$redis->del(self::PREFIX . $key);
} catch (\Exception $e) {
// Silently fail
}
}
/**
* Delete all cache keys matching a pattern.
*
* @param string $pattern Glob pattern (e.g., "os:*")
*/
public static function forgetPattern($pattern)
{
$redis = self::redis();
if (!$redis) {
return;
}
try {
$keys = $redis->keys(self::PREFIX . $pattern);
if (!empty($keys)) {
$redis->del($keys);
}
} catch (\Exception $e) {
// Silently fail
}
}
}