This commit is contained in:
Benjamin Toby 2026-03-10 05:41:27 +00:00
parent 58ef4eaaae
commit f26227c7a8
14 changed files with 374 additions and 29 deletions

View File

@ -15,8 +15,6 @@ type Props = {
export default function AdminHero({ title, ctas, description }: Props) {
const { pageProps } = useContext(AppContext);
console.log("pageProps.pageUrl", pageProps.pageUrl);
return (
<Row className="w-full grid-cell-content justify-between">
<Stack className="gap-2">

View File

@ -0,0 +1,41 @@
import Stack from "@/twui/components/layout/Stack";
import { useContext, useEffect, useRef } from "react";
import { AppContext } from "@/src/pages/_app";
import {
NormalizedServerObject,
ParsedDeploymentServiceConfig,
WebSocketDataType,
} from "@/src/types";
import useIntersectionObserver from "@/twui/components/hooks/useIntersectionObserver";
import Center from "@/twui/components/layout/Center";
import Loading from "@/twui/components/elements/Loading";
import useWebSocketEventHandler from "@/twui/components/hooks/useWebSocketEventHandler";
type Props = {
service: ParsedDeploymentServiceConfig;
server: NormalizedServerObject;
};
export default function ServiceClusterServerViews({ service, server }: Props) {
const { pageProps, ws } = useContext(AppContext);
const { data } = useWebSocketEventHandler<WebSocketDataType>();
useEffect(() => {
if (!ws?.socket) {
return;
}
ws.sendData({
event: "client:ping",
server,
service,
});
}, [ws]);
useEffect(() => {
console.log("data", data);
}, [data]);
return <Stack className="gap-0 w-full"></Stack>;
}

View File

@ -1,10 +1,14 @@
import Stack from "@/twui/components/layout/Stack";
import { useContext } from "react";
import { useContext, useRef } from "react";
import { AppContext } from "@/src/pages/_app";
import {
NormalizedServerObject,
ParsedDeploymentServiceConfig,
} from "@/src/types";
import useIntersectionObserver from "@/twui/components/hooks/useIntersectionObserver";
import Center from "@/twui/components/layout/Center";
import Loading from "@/twui/components/elements/Loading";
import ServiceClusterServerViews from "./cluster-server-views";
type Props = {
service: ParsedDeploymentServiceConfig;
@ -13,20 +17,23 @@ type Props = {
export default function ServiceClusterServer({ service, server }: Props) {
const { pageProps } = useContext(AppContext);
const { deployment, children_services } = pageProps;
const all_services = [service, ...(children_services || [])];
const deployment_name = deployment?.deployment_name;
const service_name = service?.service_name;
const cluster_servers = service.servers;
const elementRef = useRef<HTMLDivElement>(undefined);
const { isIntersecting } = useIntersectionObserver({ elementRef });
return (
<Stack className="w-full grid-cell col-span-1">
{cluster_servers?.map((server, index) => {
return null;
})}
<Stack className="grid-cell gap-0" componentRef={elementRef as any}>
<Stack className="grid-cell-content">
<code>{server.private_ip}</code>
</Stack>
<hr />
{isIntersecting ? (
<ServiceClusterServerViews {...{ server, service }} />
) : (
<Center>
<Loading />
</Center>
)}
</Stack>
);
}

View File

@ -4,6 +4,7 @@ import { AppContext } from "@/src/pages/_app";
import { ParsedDeploymentServiceConfig } from "@/src/types";
import Row from "@/twui/components/layout/Row";
import H2 from "@/twui/components/layout/H2";
import ServiceClusterServer from "./cluster-server";
type Props = {
service: ParsedDeploymentServiceConfig;
@ -12,25 +13,25 @@ type Props = {
export default function ServiceCluster({ service, cluster_index }: Props) {
const { pageProps } = useContext(AppContext);
const { deployment, children_services } = pageProps;
const all_services = [service, ...(children_services || [])];
const deployment_name = deployment?.deployment_name;
const service_name = service?.service_name;
const cluster_servers = service.servers;
return (
<Stack className="w-full grid-cell col-span-1">
<Stack className="w-full grid-cell col-span-1 gap-0">
<Stack className="grid-cell-content">
<H2>Cluster #{cluster_index}</H2>
<Row className="">
{cluster_servers?.map((server, index) => {
return <code>{server.private_ip}</code>;
})}
</Row>
</Stack>
<hr />
<Row className="nested-grid-frame xl:grid-cols-2">
{cluster_servers?.map((server, index) => {
return (
<ServiceClusterServer
server={server}
service={service}
key={index}
/>
);
})}
</Row>
</Stack>
);
}

View File

@ -10,7 +10,7 @@ export default function ServiceClusters() {
const all_services = [service, ...(children_services || [])];
return (
<Stack className="w-full nested-grid-frame grid-cols-1 xl:grid-cols-2">
<Stack className="w-full nested-grid-frame grid-cols-1">
{all_services.map((srv, index) => {
if (!srv) return null;
return (

View File

@ -20,4 +20,6 @@ export const AppData = {
AuthCookieName: `x-turboci-admin-auth-key`,
AuthCSRFCookieName: `x-turboci-admin-csrf`,
CookieExpirationTime: 1000 * 60 * 60 * 24 * 7, // One Week
DynamicPortStart: 4700,
} as const;

View File

@ -0,0 +1,38 @@
server {
listen 80;
server_name _;
client_max_body_size 20M;
# Restrict port range to 4700 and above
location ^/ttyd/(?<port>(?:470[0-9]|47[1-9]\d|4[89]\d{2}|[5-9]\d{3}|[1-9]\d{4,}))(/.*)? {
proxy_pass http://localhost:$port$2;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
location /ws {
proxy_pass http://localhost:3773;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 60s;
proxy_buffering off;
keepalive_timeout 75s;
tcp_nodelay on;
}
location / {
proxy_pass http://localhost:3772;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
}

View File

@ -0,0 +1,83 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
# server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Gzip Settings
##
gzip on;
# gzip_vary on;
# gzip_proxied any;
# gzip_comp_level 6;
# gzip_buffers 16 8k;
# gzip_http_version 1.1;
# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
#mail {
# # See sample authentication script at:
# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
#
# # auth_http localhost/auth.php;
# # pop3_capabilities "TOP" "USER";
# # imap_capabilities "IMAP4rev1" "UIDPLUS";
#
# server {
# listen localhost:110;
# protocol pop3;
# proxy on;
# }
#
# server {
# listen localhost:143;
# protocol imap;
# proxy on;
# }
#}

View File

@ -0,0 +1,91 @@
##
# You should look at the following URL's in order to grasp a solid understanding
# of Nginx configuration files in order to fully unleash the power of Nginx.
# https://www.nginx.com/resources/wiki/start/
# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/
# https://wiki.debian.org/Nginx/DirectoryStructure
#
# In most cases, administrators will remove this file from sites-enabled/ and
# leave it as reference inside of sites-available where it will continue to be
# updated by the nginx packaging team.
#
# This file will automatically load configuration files provided by other
# applications, such as Drupal or Wordpress. These applications will be made
# available underneath a path with that package name, such as /drupal8.
#
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
##
# Default server configuration
#
server {
listen 80 default_server;
listen [::]:80 default_server;
# SSL configuration
#
# listen 443 ssl default_server;
# listen [::]:443 ssl default_server;
#
# Note: You should disable gzip for SSL traffic.
# See: https://bugs.debian.org/773332
#
# Read up on ssl_ciphers to ensure a secure configuration.
# See: https://bugs.debian.org/765782
#
# Self signed certs generated by the ssl-cert package
# Don't use them in a production server!
#
# include snippets/snakeoil.conf;
root /var/www/html;
# Add index.php to the list if you are using PHP
index index.html index.htm index.nginx-debian.html;
server_name _;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
# pass PHP scripts to FastCGI server
#
#location ~ \.php$ {
# include snippets/fastcgi-php.conf;
#
# # With php-fpm (or other unix sockets):
# fastcgi_pass unix:/run/php/php7.4-fpm.sock;
# # With php-cgi (or other tcp sockets):
# fastcgi_pass 127.0.0.1:9000;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# Virtual Host configuration for example.com
#
# You can move that to a different file under sites-available/ and symlink that
# to sites-enabled/ to enable it.
#
#server {
# listen 80;
# listen [::]:80;
#
# server_name example.com;
#
# root /var/www/example.com;
# index index.html;
#
# location / {
# try_files $uri $uri/ =404;
# }
#}

34
src/nginx/nginx.conf Normal file
View File

@ -0,0 +1,34 @@
user www-data;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
gzip on;
include /etc/nginx/conf.d/*.conf;
}

View File

@ -115,3 +115,7 @@ code {
#admin-main h2 {
@apply text-xl font-semibold;
}
hr {
@apply border-foreground-light/10 dark:border-foreground-dark/10;
}

View File

@ -231,6 +231,8 @@ export const WebSocketEvents = [
export type WebSocketDataType = {
event: (typeof WebSocketEvents)[number];
message?: string;
service?: ParsedDeploymentServiceConfig;
server?: NormalizedServerObject;
};
export type WebSocketType = {

View File

@ -0,0 +1,44 @@
import { dlopen, ptr, FFIType } from "bun:ffi";
import { AppData } from "../data/app-data";
const libc = dlopen("libc.so.6", {
socket: {
args: [FFIType.i32, FFIType.i32, FFIType.i32],
returns: FFIType.i32,
},
bind: {
args: [FFIType.i32, FFIType.ptr, FFIType.i32],
returns: FFIType.i32,
},
close: { args: [FFIType.i32], returns: FFIType.i32 },
});
const AF_INET = 2;
const SOCK_STREAM = 1;
const INADDR_ANY = 0;
function makeSockaddr(port: number): Buffer {
const buf = Buffer.alloc(16);
buf.writeUInt16LE(AF_INET, 0);
buf.writeUInt16BE(port, 2);
buf.writeUInt32BE(INADDR_ANY, 4);
return buf;
}
export default function getNextAvailablePort(
startPort = AppData["DynamicPortStart"],
maxPort = 65535,
): number {
for (let port = startPort; port <= maxPort; port++) {
const fd = libc.symbols.socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) continue;
const addr = makeSockaddr(port);
const result = libc.symbols.bind(fd, ptr(addr), addr.byteLength);
libc.symbols.close(fd);
if (result === 0) return port;
}
throw new Error(`No available port found in range ${startPort}-${maxPort}`);
}

2
twui

@ -1 +1 @@
Subproject commit 9f8527fc4d851c1fecd6600bd60c490de998676f
Subproject commit 2c101420ccc7dc1c56eb7bb7c41f2cca83af334a