asw-v01: archive deferred content (packs, site, lab, legacy examples)
- 2.1: packs/ -> archive/packs/ - 2.2: site/ -> archive/site/ - 2.3: src/lab/ -> archive/lab/ - 2.4: examples/ -> archive/examples-legacy/ (SSI-based)
This commit is contained in:
parent
416fe2f180
commit
e47a9f4401
173 changed files with 11 additions and 5 deletions
348
archive/packs/python/asw_server.py
Normal file
348
archive/packs/python/asw_server.py
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
ASW Dev Server — http.server with ASW-styled error pages.
|
||||
|
||||
Drop this in any directory and run it instead of `python -m http.server`.
|
||||
All error responses (403, 404, 500, etc.) are styled with ASW CSS.
|
||||
|
||||
Usage:
|
||||
python asw_server.py # serve on port 8000
|
||||
python asw_server.py 3000 # custom port
|
||||
python asw_server.py 3000 /dir # custom port + directory
|
||||
|
||||
The server inlines ASW styles so no external dependencies are needed.
|
||||
"""
|
||||
|
||||
import http.server
|
||||
import sys
|
||||
import os
|
||||
import socket
|
||||
import urllib.parse
|
||||
import html
|
||||
|
||||
|
||||
# ── Minimal ASW inline styles ─────────────────────────────────────────────────
|
||||
# Reduced version for standalone error pages — no external deps.
|
||||
|
||||
ASW_INLINE = """
|
||||
:root {
|
||||
--asw-bg: #0d1117;
|
||||
--asw-bg-elevated: #161b22;
|
||||
--asw-bg-overlay: #1c2128;
|
||||
--asw-text: #e6edf3;
|
||||
--asw-text-secondary: #8b949e;
|
||||
--asw-text-muted: #484f58;
|
||||
--asw-accent: #3fb950;
|
||||
--asw-border: #30363d;
|
||||
--asw-font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
--asw-font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', ui-monospace, monospace;
|
||||
}
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html { font-size: 16px; }
|
||||
body {
|
||||
background: var(--asw-bg);
|
||||
color: var(--asw-text);
|
||||
font-family: var(--asw-font-body);
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-bottom: 1px solid var(--asw-border);
|
||||
background: var(--asw-bg-elevated);
|
||||
}
|
||||
nav a { color: var(--asw-text); text-decoration: none; font-weight: 600; }
|
||||
nav a:hover { color: var(--asw-accent); }
|
||||
.badge {
|
||||
font-family: var(--asw-font-mono);
|
||||
font-size: 0.75rem;
|
||||
color: var(--asw-text-muted);
|
||||
background: var(--asw-bg-overlay);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid var(--asw-border);
|
||||
}
|
||||
main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
.error-card {
|
||||
max-width: 42ch;
|
||||
text-align: center;
|
||||
}
|
||||
.error-code {
|
||||
font-family: var(--asw-font-mono);
|
||||
font-size: 6rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: var(--asw-text-muted);
|
||||
letter-spacing: -0.05em;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1.5rem;
|
||||
color: var(--asw-text);
|
||||
}
|
||||
p { margin: 0 0 0.75rem; color: var(--asw-text-secondary); }
|
||||
a.button {
|
||||
display: inline-block;
|
||||
margin-top: 1.5rem;
|
||||
padding: 0.5rem 1.25rem;
|
||||
background: transparent;
|
||||
color: var(--asw-accent);
|
||||
border: 1px solid var(--asw-accent);
|
||||
border-radius: 0.375rem;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
a.button:hover { background: rgba(63, 185, 80, 0.1); }
|
||||
code {
|
||||
font-family: var(--asw-font-mono);
|
||||
font-size: 0.85em;
|
||||
background: var(--asw-bg-overlay);
|
||||
padding: 0.1em 0.35em;
|
||||
border-radius: 0.2rem;
|
||||
color: var(--asw-text-secondary);
|
||||
}
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--asw-text-muted);
|
||||
border-top: 1px solid var(--asw-border);
|
||||
}
|
||||
footer a { color: var(--asw-text-muted); }
|
||||
|
||||
/* Directory listing */
|
||||
.dir-listing { max-width: 72ch; width: 100%; }
|
||||
.dir-listing h1 { font-size: 1.25rem; margin-bottom: 1.5rem; text-align: left; }
|
||||
.dir-listing h1 code { font-size: 1rem; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { padding: 0.5rem 0.75rem; text-align: left; border-bottom: 1px solid var(--asw-border); }
|
||||
th { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--asw-text-muted); }
|
||||
td a { color: var(--asw-accent); text-decoration: none; }
|
||||
td a:hover { text-decoration: underline; }
|
||||
.size { color: var(--asw-text-muted); font-family: var(--asw-font-mono); font-size: 0.8rem; }
|
||||
.parent { color: var(--asw-text-muted); }
|
||||
"""
|
||||
|
||||
|
||||
ERROR_MESSAGES = {
|
||||
400: ("Bad Request", "The server couldn't understand the request."),
|
||||
401: ("Unauthorized", "Authentication is required to access this resource."),
|
||||
403: ("Forbidden", "You don't have permission to access this path."),
|
||||
404: ("Not Found", "The file or directory you requested doesn't exist."),
|
||||
405: ("Method Not Allowed", "That HTTP method isn't supported here."),
|
||||
408: ("Request Timeout", "The request took too long to process."),
|
||||
500: ("Server Error", "Something went wrong on the server."),
|
||||
501: ("Not Implemented", "The server doesn't support that feature."),
|
||||
503: ("Service Unavailable", "The server is temporarily unable to handle requests."),
|
||||
}
|
||||
|
||||
|
||||
def error_page(code: int, path: str = "") -> bytes:
|
||||
title, message = ERROR_MESSAGES.get(code, ("Error", "An error occurred."))
|
||||
path_hint = f"<p>Path: <code>{html.escape(path)}</code></p>" if path else ""
|
||||
body = f"""<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{code} — {title}</title>
|
||||
<style>{ASW_INLINE}</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/">⌂ Dev Server</a>
|
||||
<span class="badge">http.server</span>
|
||||
</nav>
|
||||
<main>
|
||||
<div class="error-card">
|
||||
<p class="error-code">{code}</p>
|
||||
<h1>{title}</h1>
|
||||
<p>{message}</p>
|
||||
{path_hint}
|
||||
<a class="button" href="/">← Back to root</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer>ASW Dev Server · <a href="https://github.com/trentuna/agentic-semantic-web">agentic-semantic-web</a></footer>
|
||||
</body>
|
||||
</html>"""
|
||||
return body.encode("utf-8")
|
||||
|
||||
|
||||
def dir_listing_page(path: str, display_path: str, entries: list) -> bytes:
|
||||
rows = []
|
||||
if display_path != "/":
|
||||
parent = "/" + "/".join(display_path.strip("/").split("/")[:-1])
|
||||
rows.append(f'<tr><td class="parent"><a href="{parent or "/"}">../</a></td><td class="size">—</td></tr>')
|
||||
|
||||
for name, is_dir, size in sorted(entries, key=lambda e: (not e[1], e[0].lower())):
|
||||
link_name = html.escape(name) + ("/" if is_dir else "")
|
||||
href = urllib.parse.quote(name) + ("/" if is_dir else "")
|
||||
size_str = "—" if is_dir else _human_size(size)
|
||||
rows.append(f'<tr><td><a href="{href}">{link_name}</a></td><td class="size">{size_str}</td></tr>')
|
||||
|
||||
rows_html = "\n ".join(rows)
|
||||
body = f"""<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Index of {html.escape(display_path)}</title>
|
||||
<style>{ASW_INLINE}</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/">⌂ Dev Server</a>
|
||||
<span class="badge">http.server</span>
|
||||
</nav>
|
||||
<main>
|
||||
<div class="dir-listing">
|
||||
<h1>Index of <code>{html.escape(display_path)}</code></h1>
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Size</th></tr></thead>
|
||||
<tbody>
|
||||
{rows_html}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
<footer>ASW Dev Server · <a href="https://github.com/trentuna/agentic-semantic-web">agentic-semantic-web</a></footer>
|
||||
</body>
|
||||
</html>"""
|
||||
return body.encode("utf-8")
|
||||
|
||||
|
||||
def _human_size(n: int) -> str:
|
||||
for unit in ("B", "KB", "MB", "GB"):
|
||||
if n < 1024:
|
||||
return f"{n:.0f} {unit}" if unit == "B" else f"{n:.1f} {unit}"
|
||||
n /= 1024
|
||||
return f"{n:.1f} TB"
|
||||
|
||||
|
||||
class ASWHandler(http.server.SimpleHTTPRequestHandler):
|
||||
"""SimpleHTTPRequestHandler with ASW-styled error pages and directory listings."""
|
||||
|
||||
server_version = "ASWServer/1.0"
|
||||
|
||||
def send_error(self, code, message=None, explain=None):
|
||||
"""Override to send ASW-styled error pages."""
|
||||
try:
|
||||
short, long = self.responses[code]
|
||||
except KeyError:
|
||||
short, long = "???", "???"
|
||||
if message is None:
|
||||
message = short
|
||||
if explain is None:
|
||||
explain = long
|
||||
|
||||
# Log to terminal
|
||||
self.log_error("code %d, message %s", code, message)
|
||||
|
||||
# Build ASW error page
|
||||
path = getattr(self, "path", "")
|
||||
content = error_page(code, path)
|
||||
|
||||
self.send_response(code, message)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(content)))
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
if self.command != "HEAD" and code >= 200 and code not in (204, 304):
|
||||
self.wfile.write(content)
|
||||
|
||||
def list_directory(self, path):
|
||||
"""Override to send ASW-styled directory listing."""
|
||||
try:
|
||||
names = os.listdir(path)
|
||||
except OSError:
|
||||
self.send_error(403, "No permission to list directory")
|
||||
return None
|
||||
|
||||
display_path = urllib.parse.unquote(self.path)
|
||||
entries = []
|
||||
for name in names:
|
||||
full = os.path.join(path, name)
|
||||
is_dir = os.path.isdir(full)
|
||||
size = 0
|
||||
if not is_dir:
|
||||
try:
|
||||
size = os.path.getsize(full)
|
||||
except OSError:
|
||||
pass
|
||||
entries.append((name, is_dir, size))
|
||||
|
||||
content = dir_listing_page(path, display_path, entries)
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(content)))
|
||||
self.end_headers()
|
||||
self.wfile.write(content)
|
||||
return None
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
"""Slightly cleaner terminal output."""
|
||||
print(f" {self.address_string()} — {fmt % args}")
|
||||
|
||||
|
||||
def main():
|
||||
port = 8000
|
||||
directory = "."
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
try:
|
||||
port = int(sys.argv[1])
|
||||
except ValueError:
|
||||
print(f"Usage: {sys.argv[0]} [port] [directory]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if len(sys.argv) > 2:
|
||||
directory = sys.argv[2]
|
||||
|
||||
directory = os.path.abspath(directory)
|
||||
|
||||
handler = lambda *args, **kwargs: ASWHandler(*args, directory=directory, **kwargs)
|
||||
|
||||
# Find a free port if needed (dev convenience)
|
||||
for attempt in range(10):
|
||||
try:
|
||||
server = http.server.HTTPServer(("", port + attempt), handler)
|
||||
actual_port = port + attempt
|
||||
break
|
||||
except OSError:
|
||||
if attempt == 9:
|
||||
print(f"Could not bind to ports {port}–{port+9}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
hostname = socket.gethostname()
|
||||
print(f"\n ASW Dev Server")
|
||||
print(f" ──────────────────────────────────")
|
||||
print(f" Serving: {directory}")
|
||||
print(f" Local: http://localhost:{actual_port}")
|
||||
print(f" Network: http://{hostname}:{actual_port}")
|
||||
print(f" ──────────────────────────────────")
|
||||
print(f" Press Ctrl+C to stop\n")
|
||||
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\n Stopped.")
|
||||
server.server_close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue