new site
This commit is contained in:
commit
788526b029
186
.gitignore
vendored
Normal file
186
.gitignore
vendored
Normal file
@ -0,0 +1,186 @@
|
||||
# Created by venv; see https://docs.python.org/3/library/venv.html
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
.Python
|
||||
[Bb]in
|
||||
[Ii]nclude
|
||||
[Ll]ib
|
||||
[Ll]ib64
|
||||
[Ll]ocal
|
||||
[Ss]cripts
|
||||
pyvenv.cfg
|
||||
.venv
|
||||
pip-selfcheck.json
|
4
config.toml
Normal file
4
config.toml
Normal file
@ -0,0 +1,4 @@
|
||||
[server]
|
||||
host = "0.0.0.0"
|
||||
port = 8080
|
||||
debug = true
|
39
main.py
Normal file
39
main.py
Normal file
@ -0,0 +1,39 @@
|
||||
from flask import Flask, request, url_for, render_template
|
||||
from rich.logging import RichHandler
|
||||
import logging
|
||||
from waitress import serve
|
||||
import toml
|
||||
import werkzeug.serving
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Load configuration from config.toml
|
||||
config = toml.load("config.toml")
|
||||
|
||||
# Configure Rich logging
|
||||
logging.basicConfig(level=logging.DEBUG, handlers=[RichHandler()])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@app.before_request
|
||||
def log_request_info():
|
||||
logger.debug(f"Request: {request.method} {request.url}")
|
||||
|
||||
|
||||
@app.route("/hello")
|
||||
def hello():
|
||||
return render_template("hello.html")
|
||||
|
||||
@app.route("/")
|
||||
def home():
|
||||
return render_template("index.html")
|
||||
|
||||
|
||||
def run_server():
|
||||
app.debug = config["server"]["debug"]
|
||||
logger.debug("Starting server with Flask")
|
||||
app.run(host=config["server"]["host"], port=config["server"]["port"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_server()
|
5
static/fan.svg
Normal file
5
static/fan.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 3.48154C7.29535 3.48154 3.48148 7.29541 3.48148 12.0001C3.48148 16.7047 7.29535 20.5186 12 20.5186C16.7046 20.5186 20.5185 16.7047 20.5185 12.0001C20.5185 7.29541 16.7046 3.48154 12 3.48154ZM2 12.0001C2 6.47721 6.47715 2.00006 12 2.00006C17.5228 2.00006 22 6.47721 22 12.0001C22 17.5229 17.5228 22.0001 12 22.0001C6.47715 22.0001 2 17.5229 2 12.0001Z"/>
|
||||
<path d="M12 11.3C11.8616 11.3 11.7262 11.3411 11.6111 11.418C11.496 11.4949 11.4063 11.6042 11.3533 11.7321C11.3003 11.86 11.2864 12.0008 11.3134 12.1366C11.3405 12.2724 11.4071 12.3971 11.505 12.495C11.6029 12.5929 11.7277 12.6596 11.8634 12.6866C11.9992 12.7136 12.14 12.6997 12.2679 12.6467C12.3958 12.5937 12.5051 12.504 12.582 12.3889C12.6589 12.2738 12.7 12.1385 12.7 12C12.7 11.8144 12.6262 11.6363 12.495 11.505C12.3637 11.3738 12.1857 11.3 12 11.3ZM12.35 5.00002C15.5 5.00002 15.57 7.49902 13.911 8.32502C13.6028 8.50778 13.3403 8.75856 13.1438 9.05822C12.9473 9.35787 12.8218 9.69847 12.777 10.054C13.1117 10.1929 13.4073 10.4116 13.638 10.691C16.2 9.29102 19 9.84401 19 12.35C19 15.5 16.494 15.57 15.675 13.911C15.4869 13.6029 15.232 13.341 14.9291 13.1448C14.6262 12.9485 14.283 12.8228 13.925 12.777C13.7844 13.1108 13.566 13.406 13.288 13.638C14.688 16.221 14.128 19 11.622 19C8.5 19 8.423 16.494 10.082 15.668C10.3852 15.4828 10.644 15.2332 10.84 14.9368C11.036 14.6404 11.1644 14.3046 11.216 13.953C10.8729 13.8188 10.5711 13.5967 10.341 13.309C7.758 14.695 5 14.149 5 11.65C5 8.50002 7.478 8.42302 8.304 10.082C8.48945 10.3888 8.74199 10.6496 9.04265 10.8448C9.34332 11.0399 9.68431 11.1645 10.04 11.209C10.1748 10.8721 10.3971 10.5772 10.684 10.355C9.291 7.80001 9.844 5.00002 12.336 5.00002H12.35Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
23
static/fan2.svg
Normal file
23
static/fan2.svg
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg version="1.1" id="Layer_1" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800px" height="800px"
|
||||
viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve">
|
||||
<title>Fan</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Page-1" sketch:type="MSPage">
|
||||
<g id="Fan" transform="translate(1.000000, 1.000000)" sketch:type="MSLayerGroup">
|
||||
<circle id="Oval_1_" sketch:type="MSShapeGroup" fill="none" stroke="#6B6C6E" stroke-width="2" cx="31" cy="31" r="31">
|
||||
</circle>
|
||||
<path id="Shape" sketch:type="MSShapeGroup" fill="none" stroke="#6B6C6E" stroke-width="2" d="M51,22c-6.2,0-9.8,4.5-14.7,7.1
|
||||
c-0.6-1.6-1.9-2.8-3.5-3.3c2.6-5,7-8.7,7-14.9C39.8,6,35.9,4,31,4s-8.8,2.1-8.8,6.9c0,6.2,4.4,9.9,7,14.9
|
||||
c-1.6,0.5-2.8,1.8-3.4,3.3c-4.9-2.6-8.5-7.1-14.7-7.1c-4.9,0-7,4.1-7,8.9s2.1,9,7,9c6.2,0,9.8-4.5,14.7-7.1
|
||||
c0.6,1.6,1.8,2.9,3.4,3.4c-2.6,4.9-6.9,8.6-6.9,14.8c0,4.9,3.9,6.9,8.8,6.9s8.8-2.1,8.8-6.9c0-6.2-4.3-9.9-6.9-14.8
|
||||
c1.6-0.6,2.9-1.8,3.4-3.4c4.9,2.6,8.5,7.1,14.7,7.1c4.9,0,7-4.1,7-9S55.9,22,51,22L51,22z"/>
|
||||
<ellipse id="Oval" sketch:type="MSShapeGroup" fill="none" stroke="#6B6C6E" stroke-width="2" cx="30.9" cy="31" rx="1.9" ry="2">
|
||||
</ellipse>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
361
static/script.js
Normal file
361
static/script.js
Normal file
@ -0,0 +1,361 @@
|
||||
const container = document.getElementById('patch-container');
|
||||
const svgOverlay = document.getElementById('cable-overlay');
|
||||
|
||||
const portsState = {};
|
||||
|
||||
const LED_COLORS = [
|
||||
// Gruvbox dark bright colors
|
||||
'#fb4934', // red
|
||||
'#b8bb26', // green
|
||||
'#fabd2f', // yellow
|
||||
'#83a598', // blue
|
||||
'#d3869b', // purple
|
||||
'#8ec07c', // aqua
|
||||
'#fe8019', // orange
|
||||
];
|
||||
|
||||
const CABLE_COLORS = [
|
||||
// Gruvbox light dark colors
|
||||
'#9d0006', // red
|
||||
'#79740e', // green
|
||||
'#b57614', // yellow
|
||||
'#076678', // blue
|
||||
'#8f3f71', // purple
|
||||
'#427b58', // aqua
|
||||
'#af3a03', // orange
|
||||
];
|
||||
|
||||
let cables = [];
|
||||
|
||||
let dragSource = null;
|
||||
let tempCablePath = null;
|
||||
|
||||
const NUM_SEGMENTS = 64;
|
||||
const GRAVITY = 0.6;
|
||||
const DAMPING = 0.4;
|
||||
const CONSTRAINT_ITERATIONS = 15;
|
||||
|
||||
const SWAY_AMPLITUDE = 0.05;
|
||||
const SWAY_FREQUENCY = 0.002;
|
||||
|
||||
function getPortCenter(portEl) {
|
||||
const rect = portEl.getBoundingClientRect();
|
||||
return {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2
|
||||
};
|
||||
}
|
||||
|
||||
/**********************************************
|
||||
* LED logic
|
||||
**********************************************/
|
||||
function initPortState(portId) {
|
||||
if (!portsState[portId]) {
|
||||
portsState[portId] = {
|
||||
connectionCount: 0,
|
||||
ledColor: null,
|
||||
ledIsLit: false,
|
||||
nextBlinkTime: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
function addConnection(portId) {
|
||||
initPortState(portId);
|
||||
const st = portsState[portId];
|
||||
st.connectionCount++;
|
||||
if (st.connectionCount === 1) {
|
||||
// choose a color if not set
|
||||
if (!st.ledColor) {
|
||||
st.ledColor = LED_COLORS[Math.floor(Math.random() * LED_COLORS.length)];
|
||||
}
|
||||
// schedule next blink
|
||||
st.nextBlinkTime = performance.now();
|
||||
}
|
||||
}
|
||||
function removeConnection(portId) {
|
||||
initPortState(portId);
|
||||
const st = portsState[portId];
|
||||
if (st.connectionCount > 0) {
|
||||
st.connectionCount--;
|
||||
}
|
||||
if (st.connectionCount <= 0) {
|
||||
st.connectionCount = 0;
|
||||
st.ledIsLit = false;
|
||||
updatePortLED(portId);
|
||||
}
|
||||
}
|
||||
/** Check if we should toggle LED, then update DOM. */
|
||||
function updateLEDs(now) {
|
||||
for (const portId in portsState) {
|
||||
const st = portsState[portId];
|
||||
if (st.connectionCount > 0) {
|
||||
if (now >= st.nextBlinkTime) {
|
||||
st.ledIsLit = !st.ledIsLit;
|
||||
const interval = 50 + Math.random() * 150; // 200..800 ms
|
||||
st.nextBlinkTime = now + interval;
|
||||
}
|
||||
}
|
||||
updatePortLED(portId);
|
||||
}
|
||||
}
|
||||
function updatePortLED(portId) {
|
||||
const st = portsState[portId];
|
||||
const portEl = document.getElementById(portId);
|
||||
if (!portEl) return;
|
||||
const ledEl = portEl.querySelector('.led');
|
||||
if (!ledEl) return;
|
||||
|
||||
if (st.connectionCount > 0 && st.ledColor) {
|
||||
ledEl.style.background = st.ledIsLit ? st.ledColor : '#222';
|
||||
} else {
|
||||
ledEl.style.background = '#222';
|
||||
}
|
||||
}
|
||||
|
||||
/**********************************************
|
||||
* Cable creation & removal
|
||||
**********************************************/
|
||||
function randomColor() {
|
||||
return CABLE_COLORS[Math.floor(Math.random() * CABLE_COLORS.length)];
|
||||
}
|
||||
function createCablePath(color) {
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
path.setAttribute('stroke', color);
|
||||
path.setAttribute('fill', 'none');
|
||||
path.setAttribute('stroke-width', '3');
|
||||
path.classList.add('cable-path');
|
||||
// We want to click cables to remove them
|
||||
path.addEventListener('click', onCableClick);
|
||||
svgOverlay.appendChild(path);
|
||||
return path;
|
||||
}
|
||||
function createRope(sourceEl, targetEl, numSegments) {
|
||||
const s = getPortCenter(sourceEl);
|
||||
const t = getPortCenter(targetEl);
|
||||
const rope = [];
|
||||
for (let i = 0; i <= numSegments; i++) {
|
||||
const alpha = i / numSegments;
|
||||
rope.push({
|
||||
x: s.x + (t.x - s.x) * alpha,
|
||||
y: s.y + (t.y - s.y) * alpha,
|
||||
vx: 0, vy: 0
|
||||
});
|
||||
}
|
||||
return rope;
|
||||
}
|
||||
function connect(sourceId, targetId, color) {
|
||||
if (!sourceId || !targetId || sourceId === targetId) return;
|
||||
const sourceEl = document.getElementById(sourceId);
|
||||
const targetEl = document.getElementById(targetId);
|
||||
if (!sourceEl || !targetEl) return;
|
||||
|
||||
color = color || randomColor();
|
||||
const rope = createRope(sourceEl, targetEl, NUM_SEGMENTS);
|
||||
const dx = rope[rope.length - 1].x - rope[0].x;
|
||||
const dy = rope[rope.length - 1].y - rope[0].y;
|
||||
const totalDist = Math.sqrt(dx * dx + dy * dy);
|
||||
const restLength = totalDist / NUM_SEGMENTS;
|
||||
|
||||
const pathEl = createCablePath(color);
|
||||
const swayOffset = Math.random() * 1000;
|
||||
|
||||
cables.push({
|
||||
sourceId, targetId,
|
||||
pathEl, ropeSegments: rope,
|
||||
restLength, color, swayOffset
|
||||
});
|
||||
|
||||
addConnection(sourceId);
|
||||
addConnection(targetId);
|
||||
}
|
||||
function removeCable(cable) {
|
||||
svgOverlay.removeChild(cable.pathEl);
|
||||
cables = cables.filter(c => c !== cable);
|
||||
removeConnection(cable.sourceId);
|
||||
removeConnection(cable.targetId);
|
||||
}
|
||||
function onCableClick(e) {
|
||||
const pathEl = e.currentTarget;
|
||||
const cable = cables.find(c => c.pathEl === pathEl);
|
||||
if (cable) removeCable(cable);
|
||||
}
|
||||
|
||||
/**********************************************
|
||||
* Dragging logic (user can draw cables)
|
||||
**********************************************/
|
||||
function handlePortMouseDown(e) {
|
||||
e.preventDefault();
|
||||
dragSource = e.currentTarget;
|
||||
const color = randomColor();
|
||||
tempCablePath = createCablePath(color);
|
||||
tempCablePath.style.pointerEvents = 'none';
|
||||
}
|
||||
function handleMouseMove(e) {
|
||||
if (!dragSource || !tempCablePath) return;
|
||||
const s = getPortCenter(dragSource);
|
||||
const mx = e.clientX, my = e.clientY;
|
||||
const midX = (s.x + mx) / 2, midY = (s.y + my) / 2;
|
||||
const d = `
|
||||
M ${s.x},${s.y}
|
||||
C ${midX},${s.y}
|
||||
${midX},${my}
|
||||
${mx},${my}
|
||||
`;
|
||||
tempCablePath.setAttribute('d', d);
|
||||
}
|
||||
function handleMouseUp(e) {
|
||||
|
||||
const dropTargetEl = document.elementFromPoint(e.clientX, e.clientY);
|
||||
|
||||
const portEl = dropTargetEl && dropTargetEl.closest('.port');
|
||||
|
||||
if (portEl) {
|
||||
|
||||
const sourceId = dragSource.id;
|
||||
const targetId = portEl.id;
|
||||
if (sourceId !== targetId) {
|
||||
const color = tempCablePath.getAttribute('stroke');
|
||||
svgOverlay.removeChild(tempCablePath);
|
||||
tempCablePath = null;
|
||||
connect(sourceId, targetId, color);
|
||||
} else {
|
||||
|
||||
svgOverlay.removeChild(tempCablePath);
|
||||
tempCablePath = null;
|
||||
}
|
||||
} else {
|
||||
svgOverlay.removeChild(tempCablePath);
|
||||
tempCablePath = null;
|
||||
}
|
||||
dragSource = null;
|
||||
}
|
||||
|
||||
/***********************************************
|
||||
* NAS drive-lights random blinking
|
||||
***********************************************/
|
||||
let driveLights = [];
|
||||
function initDriveLights() {
|
||||
driveLights = document.querySelectorAll('.drive-light');
|
||||
driveLights.forEach(light => {
|
||||
|
||||
light.dataset.nextBlinkTime = '0';
|
||||
});
|
||||
}
|
||||
function updateDriveLights(timestamp) {
|
||||
driveLights.forEach(light => {
|
||||
let nextBlink = parseFloat(light.dataset.nextBlinkTime) || 0;
|
||||
if (timestamp >= nextBlink) {
|
||||
|
||||
light.classList.toggle('lit');
|
||||
|
||||
const interval = 300 + Math.random() * 500; // 300..800 ms
|
||||
light.dataset.nextBlinkTime = (timestamp + interval).toString();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**********************************************
|
||||
* Main animation loop
|
||||
**********************************************/
|
||||
function animate(timestamp) {
|
||||
|
||||
updateLEDs(timestamp);
|
||||
updateDriveLights(timestamp);
|
||||
|
||||
|
||||
const dt = 1.0;
|
||||
cables.forEach(cable => {
|
||||
const rope = cable.ropeSegments;
|
||||
const n = rope.length;
|
||||
if (n < 2) return;
|
||||
|
||||
const sourceEl = document.getElementById(cable.sourceId);
|
||||
const targetEl = document.getElementById(cable.targetId);
|
||||
if (!sourceEl || !targetEl) return;
|
||||
|
||||
const s = getPortCenter(sourceEl);
|
||||
const t = getPortCenter(targetEl);
|
||||
|
||||
// anchors
|
||||
rope[0].x = s.x; rope[0].y = s.y;
|
||||
rope[0].vx = 0; rope[0].vy = 0;
|
||||
rope[n - 1].x = t.x; rope[n - 1].y = t.y;
|
||||
rope[n - 1].vx = 0; rope[n - 1].vy = 0;
|
||||
|
||||
// 1) gravity + wind for interior segments
|
||||
for (let i = 1; i < n - 1; i++) {
|
||||
rope[i].vy += GRAVITY;
|
||||
const wave = Math.sin((timestamp + cable.swayOffset + i * 50) * SWAY_FREQUENCY);
|
||||
rope[i].vx += wave * SWAY_AMPLITUDE;
|
||||
|
||||
rope[i].vx *= DAMPING;
|
||||
rope[i].vy *= DAMPING;
|
||||
rope[i].x += rope[i].vx * dt;
|
||||
rope[i].y += rope[i].vy * dt;
|
||||
}
|
||||
|
||||
// 2) constraints
|
||||
for (let iter = 0; iter < CONSTRAINT_ITERATIONS; iter++) {
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
let p1 = rope[i], p2 = rope[i + 1];
|
||||
let dx = p2.x - p1.x, dy = p2.y - p1.y;
|
||||
let dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist < 0.001) dist = 0.001;
|
||||
let diff = (dist - cable.restLength) / dist;
|
||||
if (i !== 0) {
|
||||
p1.x += 0.5 * dx * diff;
|
||||
p1.y += 0.5 * dy * diff;
|
||||
}
|
||||
if (i + 1 !== n - 1) {
|
||||
p2.x -= 0.5 * dx * diff;
|
||||
p2.y -= 0.5 * dy * diff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) recalc velocities
|
||||
for (let i = 1; i < n - 1; i++) {
|
||||
rope[i].vx = rope[i].x - (rope[i].x - rope[i].vx * dt);
|
||||
rope[i].vy = rope[i].y - (rope[i].y - rope[i].vy * dt);
|
||||
}
|
||||
|
||||
// 4) update path
|
||||
let pathD = `M ${rope[0].x},${rope[0].y}`;
|
||||
for (let i = 1; i < n; i++) {
|
||||
pathD += ` L ${rope[i].x},${rope[i].y}`;
|
||||
}
|
||||
cable.pathEl.setAttribute('d', pathD);
|
||||
});
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********************************************
|
||||
* Startup
|
||||
**********************************************/
|
||||
initDriveLights();
|
||||
document.querySelectorAll('.port').forEach(portEl => {
|
||||
portEl.addEventListener('mousedown', handlePortMouseDown);
|
||||
});
|
||||
container.addEventListener('mousemove', handleMouseMove);
|
||||
container.addEventListener('mouseup', handleMouseUp);
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
|
||||
|
||||
connect('r1s0_portA', 'r1s2_portA',randomColor());
|
||||
connect('r1s2_portB', 'r2s1_portA',randomColor());
|
||||
connect('r1s2_portC', 'r1s3_portA',randomColor());
|
||||
|
||||
connect('r2s1_portB', 'r2s2_portS',randomColor());
|
||||
connect('r2s2_portA', 'r2s3_portA',randomColor());
|
||||
connect('r2s2_portB', 'r3s1_portA',randomColor());
|
||||
connect('r2s2_portC', 'r3s2_portA',randomColor());
|
||||
connect('r2s2_portD', 'r3s3_portA',randomColor());
|
||||
connect('r2s1_portD', 'r2s1_portG',randomColor());
|
||||
connect('r2s1_portK', 'r3s2_portB',randomColor());
|
||||
|
||||
|
||||
|
323
static/style.css
Normal file
323
static/style.css
Normal file
@ -0,0 +1,323 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0
|
||||
}
|
||||
|
||||
/* Basic reset */
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #1d2021;
|
||||
overflow: hidden;
|
||||
/* No default scrollbars */
|
||||
font-family: "JetBrains Mono", serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
color: #ebdbb2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 36px;
|
||||
color: #ebdbb2;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 28px;
|
||||
color: #ebdbb2;
|
||||
}
|
||||
|
||||
/*******************************************
|
||||
* Main Container (holds racks + cables)
|
||||
*******************************************/
|
||||
#patch-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* SVG overlay for cables, on top of racks */
|
||||
#cable-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
/* put cables on top */
|
||||
pointer-events: none;
|
||||
/* let clicks pass through by default */
|
||||
}
|
||||
|
||||
/* Each cable path can be clicked on => remove */
|
||||
.cable-path {
|
||||
pointer-events: auto;
|
||||
/* re-enable clicks for cable paths specifically */
|
||||
cursor: pointer;
|
||||
transition: stroke 0.2s;
|
||||
}
|
||||
|
||||
.cable-path:hover {
|
||||
stroke: #cc241d !important;
|
||||
}
|
||||
|
||||
/*******************************************
|
||||
* Data center (the racks)
|
||||
*******************************************/
|
||||
#datacenter {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
/* If you want horizontal scrolling if there are many racks, use:
|
||||
overflow-x: auto;
|
||||
*/
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
z-index: 1;
|
||||
/* behind cable overlay */
|
||||
}
|
||||
|
||||
.rack {
|
||||
width: 16em;
|
||||
/* each rack’s width */
|
||||
/* fill all available vertical space */
|
||||
background: #282828;
|
||||
border: 2px solid #3c3836;
|
||||
margin: 10px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* stack servers top to bottom */
|
||||
}
|
||||
|
||||
.rack-title {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
color: #ebdbb2;
|
||||
margin: 8px 0;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.server {
|
||||
background: #32302f;
|
||||
border: 1px solid #3c3836;
|
||||
margin: 6px;
|
||||
/* spacing within the rack */
|
||||
}
|
||||
|
||||
.server-under-construction {
|
||||
/* Diagonal stripes:
|
||||
repeating-linear-gradient(angle, color1 start, color1 end, color2 start, color2 end) */
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
#282828 0,
|
||||
#282828 12px,
|
||||
#32302f 12px,
|
||||
#32302f 24px
|
||||
);
|
||||
color:#000;
|
||||
border: 2px dashed #282828; /* Give it a dashed border for effect */
|
||||
}
|
||||
|
||||
.server-row {
|
||||
padding: 5px 8px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.text-row {
|
||||
font-size: 14px;
|
||||
color: #ebdbb2;
|
||||
}
|
||||
|
||||
.port-row {
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
/* If you want a spacer between servers, you can use a simple div with .spacer. */
|
||||
.spacer {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/*******************************************
|
||||
* Port + LED styling
|
||||
*******************************************/
|
||||
.port {
|
||||
position: relative;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background: #32302f;
|
||||
border: 2px solid #504945;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.port:hover {
|
||||
background: #7c6f64;
|
||||
border-color: #ebdbb2;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: #b16286;
|
||||
}
|
||||
|
||||
/* visited link */
|
||||
a:visited {
|
||||
color: #8f3f71;
|
||||
}
|
||||
|
||||
/* mouse over link */
|
||||
a:hover {
|
||||
color: #d3869b;
|
||||
}
|
||||
|
||||
/* selected link */
|
||||
a:active {
|
||||
color: #b16286;
|
||||
}
|
||||
|
||||
.led {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
width: .4rem;
|
||||
height: .4rem;
|
||||
background: #1d2021;
|
||||
/* off state */
|
||||
border: 2px solid #504945;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 2px #000;
|
||||
}
|
||||
|
||||
.temp-row {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
.temp-label {
|
||||
color: #eee;
|
||||
font-size: 11px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.temp-bar {
|
||||
flex: 1;
|
||||
height: 10px;
|
||||
background: linear-gradient(to right, #f80, #f00);
|
||||
box-shadow: 0 0 6px rgba(255,80,0,0.5);
|
||||
animation: flickerHeat 1.5s infinite ease-in-out alternate;
|
||||
}
|
||||
@keyframes flickerHeat {
|
||||
0% { opacity: 1; transform: scaleX(1); }
|
||||
100% { opacity: 0.7; transform: scaleX(1.05); }
|
||||
}
|
||||
|
||||
/***************************************************
|
||||
* POWER USAGE LIGHTS (Random Blink)
|
||||
***************************************************/
|
||||
.power-lights-row {
|
||||
gap: 4px;
|
||||
}
|
||||
.power-light {
|
||||
width: 10px; height: 10px;
|
||||
background: #222;
|
||||
border: 2px solid #555;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 2px #000;
|
||||
}
|
||||
.power-light.lit {
|
||||
background: #0f0; /* or something bright to indicate usage */
|
||||
}
|
||||
|
||||
/************************************************
|
||||
* FAN SPACER: Animated fans
|
||||
************************************************/
|
||||
.fans {
|
||||
background: #7c6f64;
|
||||
margin: 6px;
|
||||
/* spacing within the rack */
|
||||
}
|
||||
|
||||
.fan-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.fan {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
border-radius: 50%;
|
||||
background-image: url('fan.svg');
|
||||
filter: invert(29%) sepia(3%) saturate(1342%) hue-rotate(338deg) brightness(93%) contrast(90%);
|
||||
animation: spin 1s linear infinite;
|
||||
transform-origin: center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
transition: all 1s linear;
|
||||
}
|
||||
|
||||
.fan:hover {
|
||||
animation: 5s;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************
|
||||
* NAS SPACER: Random blinking drive lights
|
||||
************************************************/
|
||||
.nas {
|
||||
background: #504945;
|
||||
border: 1px solid #7c6f64;
|
||||
margin: 6px;
|
||||
/* spacing within the rack */
|
||||
}
|
||||
|
||||
.drive-lights-row {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
padding: 5px 0;
|
||||
flex-wrap: wrap;
|
||||
/* if many lights, wrap to next line */
|
||||
}
|
||||
|
||||
.drive-light {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #1d2021;
|
||||
/* off by default */
|
||||
border: 2px solid #504945;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 2px #000;
|
||||
}
|
||||
|
||||
.drive-light.lit {
|
||||
background: #b8bb26;
|
||||
/* or any color you like */
|
||||
}
|
101
templates/hello.html
Normal file
101
templates/hello.html
Normal file
@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DWS</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<script src="https://unpkg.com/patchbay.js/dist/patchbay.umd.js" defer></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="patch-container">
|
||||
<!-- The cable overlay on top -->
|
||||
<svg id="cable-overlay"></svg>
|
||||
|
||||
<!-- The racks, side by side -->
|
||||
<div id="datacenter">
|
||||
|
||||
<!-- RACK 1 -->
|
||||
<div class="rack" id="rack1">
|
||||
<!-- Server 1 -->
|
||||
<div class="server">
|
||||
<div class="server-row text-row"><h1>DWS</h1></div>
|
||||
<div class="server-row port-row">
|
||||
<div class="port" id="r1s1_portA"><div class="led"></div></div>
|
||||
<div class="port" id="r1s1_portB"><div class="led"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server 2 -->
|
||||
<div class="server">
|
||||
<div class="server-row text-row">Server 2 (Compute)</div>
|
||||
<div class="server-row port-row">
|
||||
<div class="port" id="r1s2_portA"><div class="led"></div></div>
|
||||
<div class="port" id="r1s2_portB"><div class="led"></div></div>
|
||||
<div class="port" id="r1s2_portC"><div class="led"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<!-- Server 3 -->
|
||||
<div class="server">
|
||||
<div class="server-row text-row">Server 3 (DB)</div>
|
||||
<div class="server-row port-row">
|
||||
<div class="port" id="r1s3_portA"><div class="led"></div></div>
|
||||
<div class="port" id="r1s3_portB"><div class="led"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RACK 2 -->
|
||||
<div class="rack" id="rack2">
|
||||
<div class="rack-title">Rack 2</div>
|
||||
|
||||
<!-- Server 1 -->
|
||||
<div class="server">
|
||||
<div class="server-row text-row">Server 1 (WEB)</div>
|
||||
<div class="server-row port-row">
|
||||
<div class="port" id="r2s1_portA"><div class="led"></div></div>
|
||||
<div class="port" id="r2s1_portB"><div class="led"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server 2 -->
|
||||
<div class="server">
|
||||
<div class="server-row text-row">Server 2 (Backup)</div>
|
||||
<div class="server-row port-row">
|
||||
<div class="port" id="r2s2_portA"><div class="led"></div></div>
|
||||
<div class="port" id="r2s2_portB"><div class="led"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RACK 3 -->
|
||||
<div class="rack" id="rack3">
|
||||
<div class="rack-title">Rack 3</div>
|
||||
<div class="server">
|
||||
<div class="server-row text-row">Server 1 (Misc)</div>
|
||||
<div class="server-row port-row">
|
||||
<div class="port" id="r3s1_portA"><div class="led"></div></div>
|
||||
<div class="port" id="r3s1_portB"><div class="led"></div></div>
|
||||
<div class="port" id="r3s1_portC"><div class="led"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add more racks as needed -->
|
||||
</div>
|
||||
</div>
|
||||
<script src="{{ url_for('static', filename='script.js') }}" defer></script>
|
||||
</body>
|
||||
</html>
|
240
templates/index.html
Normal file
240
templates/index.html
Normal file
@ -0,0 +1,240 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DWS</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<script src="https://unpkg.com/patchbay.js/dist/patchbay.umd.js" defer></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
|
||||
rel="stylesheet">
|
||||
<style>
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="patch-container">
|
||||
<svg id="cable-overlay"></svg>
|
||||
|
||||
<div id="datacenter">
|
||||
|
||||
<div class="rack" id="rack1">
|
||||
|
||||
<div class="server">
|
||||
<div class="server-row text-row">
|
||||
<h1>DWS</h1>
|
||||
<div class="port" id="r1s0_portA">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="server">
|
||||
<div class="server-row text-row"><b>It's your internet.</b></div>
|
||||
<div class="server-row text-row"><b>Take it back.</b></div>
|
||||
<div class="server-row port-row">
|
||||
<div class="port" id="r1s2_portA">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
<div class="port" id="r1s2_portB">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
<div class="port" id="r1s2_portC">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="spacer"></div>
|
||||
|
||||
<div class="server">
|
||||
<div class="server-row">
|
||||
<h2>About</h2>
|
||||
<div class="port" id="r1s3_portA">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
<div class="port" id="r1s3_portB">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="server-row text-row">DWS is built around the idea of giving the user control of the
|
||||
internet again.</div>
|
||||
<div class="server-row text-row">We do this by building tools, providing services, and ideas that
|
||||
focus on local first, openness, and privacy.</div>
|
||||
<div class="fan-row">
|
||||
<div class="fan"></div>
|
||||
<div class="fan"></div>
|
||||
<div class="fan"></div>
|
||||
</div>
|
||||
<div class="server-row text-row">Joining us is easy. Host your own software, spread the word, and
|
||||
make the internet yours.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rack" id="rack2">
|
||||
<div class="server nas">
|
||||
<div class="drive-lights-row">
|
||||
<div class="drive-light"></div>
|
||||
<div class="drive-light"></div>
|
||||
<div class="drive-light"></div>
|
||||
<div class="drive-light"></div>
|
||||
<div class="drive-light"></div>
|
||||
<div class="drive-light"></div>
|
||||
|
||||
</div>
|
||||
<div class="server-row">
|
||||
<div class="port" id="r2s1_portA">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
<div class="port-row">
|
||||
<div class="port" id="r2s1_portB">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
<div class="port" id="r2s1_portC">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
<div class="port" id="r2s1_portD">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
<div class="port" id="r2s1_portE">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
<div class="port" id="r2s1_portF">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-row">
|
||||
<div></div>
|
||||
<div class="port-row">
|
||||
<div class="port" id="r2s1_portG">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
<div class="port" id="r2s1_portH">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
<div class="port" id="r2s1_portI">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
<div class="port" id="r2s1_portJ">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
<div class="port" id="r2s1_portK">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="server">
|
||||
<div class="server-row">
|
||||
<h2>Services</h2>
|
||||
<div class="port" id="r2s2_portS">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-row text-row ">Sesame 2FA <div class="port" id="r2s2_portA">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-row text-row">Spectra <div class="port" id="r2s2_portB">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-row text-row">KAT <div class="port" id="r2s2_portC">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-row text-row">Custom Hosting & Storage <div class="port" id="r2s2_portD">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server">
|
||||
<div class="server-row">
|
||||
<h3>Sesame 2FA</h3>
|
||||
<div class="port-row">
|
||||
<div class="port" id="r2s3_portA">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
<div class="port" id="r2s3_portB">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-row text-row"><div>A open source 2FA app that lets you inspect, edit, and create your own 2FA tokens. All stored encrypted on the iOS Keychain.</div></div>
|
||||
<div class="server-row text-row"><div>Find out more at <a href="https://sesame.dws.rip/">the Sesame site.</a></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="rack" id="rack3">
|
||||
<div class="server">
|
||||
<div class="server-row">
|
||||
<h3>Spectra</h3>
|
||||
<div class="port-row">
|
||||
<div class="port" id="r3s1_portA">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
<div class="port" id="r3s1_portB">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-row text-row"><div>Fast and simple photo porfolio hosting. DWS offers both selfhosted and cloud offerings of Spectra, with BYOD (domain) support and for early signups, <b>unlimited storage</b>, all at minimal cost.</div></div>
|
||||
<div class="server-row text-row"><div>Find out more at <a href="https://git.dws.rip/dubey/spectra">DWS Git</a></div></div>
|
||||
<div class="server-row text-row">Or contact us at admin (at) dws.rip for hosting and pricing</div>
|
||||
</div>
|
||||
<div class="server server-under-construction">
|
||||
<div class="server-row">
|
||||
<h3>KAT</h3>
|
||||
<div class="port-row">
|
||||
<div class="port" id="r3s2_portA">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
<div class="port" id="r3s2_portB">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-row text-row">
|
||||
<div>COMING SOON: A orchestration solution brought to you by DWS.</div>
|
||||
<div class="fan"></div>
|
||||
</div>
|
||||
<div class="server-row text-row"><div>Opinionated, fast, single or multi-machine. This is the orchestration system for hackers.</div></div>
|
||||
<div class="server-row text-row">EST. Summer 2025.</div>
|
||||
</div>
|
||||
<div class="server">
|
||||
<div class="server-row">
|
||||
<h3>Custom Hosting</h3>
|
||||
<div class="port-row">
|
||||
<div class="port" id="r3s3_portA">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
<div class="port" id="r3s3_portB">
|
||||
<div class="led"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-row text-row"><div>Need help with something unlisted?</div></div>
|
||||
<div class="server-row text-row">Contact us at admin (at) dws.rip for hosting and pricing</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script src="{{ url_for('static', filename='script.js') }}" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user