commit 788526b0298c5e72869c9f57190e1e2561edf565 Author: Tanishq Dubey Date: Sun Jan 26 16:58:18 2025 -0500 new site diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dcb5080 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..24548cd --- /dev/null +++ b/config.toml @@ -0,0 +1,4 @@ +[server] +host = "0.0.0.0" +port = 8080 +debug = true \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..1821d8e --- /dev/null +++ b/main.py @@ -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() diff --git a/static/fan.svg b/static/fan.svg new file mode 100644 index 0000000..4cbd888 --- /dev/null +++ b/static/fan.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/static/fan2.svg b/static/fan2.svg new file mode 100644 index 0000000..c7d5395 --- /dev/null +++ b/static/fan2.svg @@ -0,0 +1,23 @@ + + + + + +Fan +Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..b815e4e --- /dev/null +++ b/static/script.js @@ -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()); + + + diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..db6baa1 --- /dev/null +++ b/static/style.css @@ -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 */ +} \ No newline at end of file diff --git a/templates/hello.html b/templates/hello.html new file mode 100644 index 0000000..d73c750 --- /dev/null +++ b/templates/hello.html @@ -0,0 +1,101 @@ + + + + + + + +DWS + + + + + + + + + +
+ + + + +
+ + +
+ +
+

DWS

+
+
+
+
+
+ + +
+
Server 2 (Compute)
+
+
+
+
+
+
+ +
+ + +
+
Server 3 (DB)
+
+
+
+
+
+
+ + +
+
Rack 2
+ + +
+
Server 1 (WEB)
+
+
+
+
+
+ + +
+
Server 2 (Backup)
+
+
+
+
+
+
+ + +
+
Rack 3
+
+
Server 1 (Misc)
+
+
+
+
+
+
+
+ + +
+
+ + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..4c66f90 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,240 @@ + + + + + + + + + DWS + + + + + + + + + + +
+ + +
+ +
+ +
+
+

DWS

+
+
+
+
+
+ +
+
It's your internet.
+
Take it back.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+

About

+
+
+
+
+
+
+ +
+
DWS is built around the idea of giving the user control of the + internet again.
+
We do this by building tools, providing services, and ideas that + focus on local first, openness, and privacy.
+
+
+
+
+
+
Joining us is easy. Host your own software, spread the word, and + make the internet yours.
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+

Services

+
+
+
+
+
Sesame 2FA
+
+
+
+
Spectra
+
+
+
+
KAT
+
+
+
+
Custom Hosting & Storage
+
+
+
+
+
+
+

Sesame 2FA

+
+
+
+
+
+
+
+
+
+
A open source 2FA app that lets you inspect, edit, and create your own 2FA tokens. All stored encrypted on the iOS Keychain.
+
Find out more at the Sesame site.
+
+
+ + +
+
+
+

Spectra

+
+
+
+
+
+
+
+
+
+
Fast and simple photo porfolio hosting. DWS offers both selfhosted and cloud offerings of Spectra, with BYOD (domain) support and for early signups, unlimited storage, all at minimal cost.
+
Find out more at DWS Git
+
Or contact us at admin (at) dws.rip for hosting and pricing
+
+
+
+

KAT

+
+
+
+
+
+
+
+
+
+
+
COMING SOON: A orchestration solution brought to you by DWS.
+
+
+
Opinionated, fast, single or multi-machine. This is the orchestration system for hackers.
+
EST. Summer 2025.
+
+
+
+

Custom Hosting

+
+
+
+
+
+
+
+
+
+
Need help with something unlisted?
+
Contact us at admin (at) dws.rip for hosting and pricing
+
+
+ +
+
+ + + + \ No newline at end of file