diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000..f35edc5
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,5 @@
+[build]
+target = "wasm32-unknown-unknown"
+
+[target.wasm32-unknown-unknown]
+rustflags = ["-C", "link-arg=-s"]
diff --git a/.gitignore b/.gitignore
index 87bf182..f21c759 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,20 @@
+# Environment files
.env
.env.bak
+
+# Rust build artifacts
+target/
+Cargo.lock
+
+# Editor files
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS files
+.DS_Store
+Thumbs.db
+
+# Note: We commit dist/ since we build locally for faster CI
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..66cdbb1
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "railwayka-landing"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+wasm-bindgen = "0.2.104"
+wasm-bindgen-futures = "0.4.54"
+
+[dependencies.web-sys]
+version = "0.3.81"
+features = [
+ "console",
+ "Document",
+ "Element",
+ "HtmlElement",
+ "Node",
+ "Window",
+ "Location",
+]
+
+[profile.release]
+opt-level = "z"
+lto = true
+codegen-units = 1
diff --git a/Dockerfile b/Dockerfile
index 7696acc..14be65c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,4 +3,4 @@ ARG IMAGE_VER=1.27-alpine
FROM --platform=$BUILDPLATFORM ${IMAGE}:${IMAGE_VER}
-COPY html /usr/share/nginx/html
+COPY dist /usr/share/nginx/html
diff --git a/README.md b/README.md
index c38465b..36cd935 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,22 @@
# railwayka.ru Landing Page
-Business card website for railwayka.ru infrastructure, deployed via [dcape](https://github.com/dopos/dcape) GitOps platform.
+Modern business card website for railwayka.ru infrastructure, built with **Rust + WebAssembly** and deployed via [dcape](https://github.com/dopos/dcape) GitOps platform.
## Features
-- Modern dark theme with animated stars background
+- 🦀 **Rust + WebAssembly** - High-performance, compiled to WASM
+- Modern dark theme with clean design
- Service cards showcasing current and planned services
- Technology stack descriptions
-- GitOps workflow visualization
-- Konami code easter egg
-- Responsive design
+- Fully responsive layout
+- Minimal bundle size (~19KB WASM)
## Technology Stack
+- **Rust**: Core application logic
+- **WebAssembly**: Fast, portable binary format
+- **wasm-bindgen**: Rust-WASM bindings
+- **web-sys**: Web API bindings for Rust
- **Nginx**: Alpine-based web server
- **Docker**: Containerized deployment
- **Traefik**: Automatic HTTPS via Let's Encrypt
@@ -44,32 +48,75 @@ make docker-build
make up
```
-## Local Development
+## Building from Source
-To run locally:
+### Prerequisites
+
+- Rust (latest stable) - install from [rustup.rs](https://rustup.rs/)
+- wasm-bindgen-cli - `cargo install wasm-bindgen-cli`
+- wasm32 target - `rustup target add wasm32-unknown-unknown`
+
+### Build Steps
```bash
-# Serve files with any web server
-cd html
+# 1. Build Rust to WASM
+cargo build --release --target wasm32-unknown-unknown
+
+# 2. Generate JS bindings
+wasm-bindgen --out-dir dist --target web \
+ target/wasm32-unknown-unknown/release/railwayka_landing.wasm
+
+# 3. Copy static assets
+cp index.html style.css dist/
+
+# 4. Serve locally
+cd dist
python3 -m http.server 8000
# Visit http://localhost:8000
```
-## Structure
+### Quick Build Script
+
+For convenience, you can use this one-liner:
+
+```bash
+cargo build --release --target wasm32-unknown-unknown && \
+wasm-bindgen --out-dir dist --target web target/wasm32-unknown-unknown/release/railwayka_landing.wasm && \
+cp index.html style.css dist/
+```
+
+## Project Structure
```
.
-├── Makefile # Dcape app Makefile
-├── docker-compose.yml # Service definition
-├── Dockerfile # Custom nginx image
-├── .woodpecker.yml # CI/CD pipeline
-├── html/ # Static website files
+├── Cargo.toml # Rust project manifest
+├── .cargo/
+│ └── config.toml # Rust build configuration
+├── src/
+│ └── lib.rs # Main Rust/WASM code
+├── index.html # HTML shell
+├── style.css # Styling
+├── dist/ # Built artifacts (committed to git)
│ ├── index.html
│ ├── style.css
-│ └── script.js
-└── README.md
+│ ├── railwayka_landing.js
+│ ├── railwayka_landing_bg.wasm
+│ └── *.d.ts
+├── html-old/ # Archived old HTML version
+├── Makefile # Dcape app Makefile
+├── docker-compose.yml # Service definition
+├── Dockerfile # Nginx deployment
+└── .woodpecker.yml # CI/CD pipeline
```
+## Why Rust + WASM?
+
+- **Performance**: Near-native execution speed
+- **Size**: Optimized bundle (~19KB WASM + 9KB JS)
+- **Type Safety**: Rust's type system prevents common bugs
+- **Modern**: Cutting-edge web technology
+- **Learning**: Great opportunity to explore WASM ecosystem
+
## License
MIT
diff --git a/dist/index.html b/dist/index.html
new file mode 100644
index 0000000..a152394
--- /dev/null
+++ b/dist/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+ railwayka.ru
+
+
+
+
+
+
+
+
diff --git a/dist/railwayka_landing.d.ts b/dist/railwayka_landing.d.ts
new file mode 100644
index 0000000..66b17eb
--- /dev/null
+++ b/dist/railwayka_landing.d.ts
@@ -0,0 +1,33 @@
+/* tslint:disable */
+/* eslint-disable */
+export function main(): void;
+
+export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
+
+export interface InitOutput {
+ readonly memory: WebAssembly.Memory;
+ readonly main: () => void;
+ readonly __wbindgen_export_0: (a: number) => void;
+ readonly __wbindgen_start: () => void;
+}
+
+export type SyncInitInput = BufferSource | WebAssembly.Module;
+/**
+* Instantiates the given `module`, which can either be bytes or
+* a precompiled `WebAssembly.Module`.
+*
+* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
+*
+* @returns {InitOutput}
+*/
+export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
+
+/**
+* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
+* for everything else, calls `WebAssembly.instantiate` directly.
+*
+* @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated.
+*
+* @returns {Promise}
+*/
+export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise;
diff --git a/dist/railwayka_landing.js b/dist/railwayka_landing.js
new file mode 100644
index 0000000..9c1168b
--- /dev/null
+++ b/dist/railwayka_landing.js
@@ -0,0 +1,275 @@
+let wasm;
+
+let heap = new Array(128).fill(undefined);
+
+heap.push(undefined, null, true, false);
+
+function getObject(idx) { return heap[idx]; }
+
+let heap_next = heap.length;
+
+function addHeapObject(obj) {
+ if (heap_next === heap.length) heap.push(heap.length + 1);
+ const idx = heap_next;
+ heap_next = heap[idx];
+
+ heap[idx] = obj;
+ return idx;
+}
+
+function handleError(f, args) {
+ try {
+ return f.apply(this, args);
+ } catch (e) {
+ wasm.__wbindgen_export_0(addHeapObject(e));
+ }
+}
+
+function isLikeNone(x) {
+ return x === undefined || x === null;
+}
+
+let cachedUint8ArrayMemory0 = null;
+
+function getUint8ArrayMemory0() {
+ if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
+ cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
+ }
+ return cachedUint8ArrayMemory0;
+}
+
+let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
+
+cachedTextDecoder.decode();
+
+const MAX_SAFARI_DECODE_BYTES = 2146435072;
+let numBytesDecoded = 0;
+function decodeText(ptr, len) {
+ numBytesDecoded += len;
+ if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
+ cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
+ cachedTextDecoder.decode();
+ numBytesDecoded = len;
+ }
+ return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
+}
+
+function getStringFromWasm0(ptr, len) {
+ ptr = ptr >>> 0;
+ return decodeText(ptr, len);
+}
+
+function dropObject(idx) {
+ if (idx < 132) return;
+ heap[idx] = heap_next;
+ heap_next = idx;
+}
+
+function takeObject(idx) {
+ const ret = getObject(idx);
+ dropObject(idx);
+ return ret;
+}
+
+export function main() {
+ wasm.main();
+}
+
+const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']);
+
+async function __wbg_load(module, imports) {
+ if (typeof Response === 'function' && module instanceof Response) {
+ if (typeof WebAssembly.instantiateStreaming === 'function') {
+ try {
+ return await WebAssembly.instantiateStreaming(module, imports);
+
+ } catch (e) {
+ const validResponse = module.ok && EXPECTED_RESPONSE_TYPES.has(module.type);
+
+ if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
+ console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
+
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ const bytes = await module.arrayBuffer();
+ return await WebAssembly.instantiate(bytes, imports);
+
+ } else {
+ const instance = await WebAssembly.instantiate(module, imports);
+
+ if (instance instanceof WebAssembly.Instance) {
+ return { instance, module };
+
+ } else {
+ return instance;
+ }
+ }
+}
+
+function __wbg_get_imports() {
+ const imports = {};
+ imports.wbg = {};
+ imports.wbg.__wbg_appendChild_87a6cc0aeb132c06 = function() { return handleError(function (arg0, arg1) {
+ const ret = getObject(arg0).appendChild(getObject(arg1));
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_body_8822ca55cb3730d2 = function(arg0) {
+ const ret = getObject(arg0).body;
+ return isLikeNone(ret) ? 0 : addHeapObject(ret);
+ };
+ imports.wbg.__wbg_call_13410aac570ffff7 = function() { return handleError(function (arg0, arg1) {
+ const ret = getObject(arg0).call(getObject(arg1));
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_createElement_4909dfa2011f2abe = function() { return handleError(function (arg0, arg1, arg2) {
+ const ret = getObject(arg0).createElement(getStringFromWasm0(arg1, arg2));
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_document_7d29d139bd619045 = function(arg0) {
+ const ret = getObject(arg0).document;
+ return isLikeNone(ret) ? 0 : addHeapObject(ret);
+ };
+ imports.wbg.__wbg_instanceof_Window_12d20d558ef92592 = function(arg0) {
+ let result;
+ try {
+ result = getObject(arg0) instanceof Window;
+ } catch (_) {
+ result = false;
+ }
+ const ret = result;
+ return ret;
+ };
+ imports.wbg.__wbg_log_6c7b5f4f00b8ce3f = function(arg0) {
+ console.log(getObject(arg0));
+ };
+ imports.wbg.__wbg_newnoargs_254190557c45b4ec = function(arg0, arg1) {
+ const ret = new Function(getStringFromWasm0(arg0, arg1));
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_setAttribute_d1baf9023ad5696f = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
+ getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
+ }, arguments) };
+ imports.wbg.__wbg_setclassName_c8bccad917b973f4 = function(arg0, arg1, arg2) {
+ getObject(arg0).className = getStringFromWasm0(arg1, arg2);
+ };
+ imports.wbg.__wbg_setinnerHTML_34e240d6b8e8260c = function(arg0, arg1, arg2) {
+ getObject(arg0).innerHTML = getStringFromWasm0(arg1, arg2);
+ };
+ imports.wbg.__wbg_settextContent_b55fe2f5f1399466 = function(arg0, arg1, arg2) {
+ getObject(arg0).textContent = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2);
+ };
+ imports.wbg.__wbg_static_accessor_GLOBAL_8921f820c2ce3f12 = function() {
+ const ret = typeof global === 'undefined' ? null : global;
+ return isLikeNone(ret) ? 0 : addHeapObject(ret);
+ };
+ imports.wbg.__wbg_static_accessor_GLOBAL_THIS_f0a4409105898184 = function() {
+ const ret = typeof globalThis === 'undefined' ? null : globalThis;
+ return isLikeNone(ret) ? 0 : addHeapObject(ret);
+ };
+ imports.wbg.__wbg_static_accessor_SELF_995b214ae681ff99 = function() {
+ const ret = typeof self === 'undefined' ? null : self;
+ return isLikeNone(ret) ? 0 : addHeapObject(ret);
+ };
+ imports.wbg.__wbg_static_accessor_WINDOW_cde3890479c675ea = function() {
+ const ret = typeof window === 'undefined' ? null : window;
+ return isLikeNone(ret) ? 0 : addHeapObject(ret);
+ };
+ imports.wbg.__wbg_wbindgenisundefined_c4b71d073b92f3c5 = function(arg0) {
+ const ret = getObject(arg0) === undefined;
+ return ret;
+ };
+ imports.wbg.__wbg_wbindgenrethrow_01815c9239d70cc2 = function(arg0) {
+ throw takeObject(arg0);
+ };
+ imports.wbg.__wbg_wbindgenthrow_451ec1a8469d7eb6 = function(arg0, arg1) {
+ throw new Error(getStringFromWasm0(arg0, arg1));
+ };
+ imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) {
+ // Cast intrinsic for `Ref(String) -> Externref`.
+ const ret = getStringFromWasm0(arg0, arg1);
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
+ const ret = getObject(arg0);
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
+ takeObject(arg0);
+ };
+
+ return imports;
+}
+
+function __wbg_init_memory(imports, memory) {
+
+}
+
+function __wbg_finalize_init(instance, module) {
+ wasm = instance.exports;
+ __wbg_init.__wbindgen_wasm_module = module;
+ cachedUint8ArrayMemory0 = null;
+
+
+ wasm.__wbindgen_start();
+ return wasm;
+}
+
+function initSync(module) {
+ if (wasm !== undefined) return wasm;
+
+
+ if (typeof module !== 'undefined') {
+ if (Object.getPrototypeOf(module) === Object.prototype) {
+ ({module} = module)
+ } else {
+ console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
+ }
+ }
+
+ const imports = __wbg_get_imports();
+
+ __wbg_init_memory(imports);
+
+ if (!(module instanceof WebAssembly.Module)) {
+ module = new WebAssembly.Module(module);
+ }
+
+ const instance = new WebAssembly.Instance(module, imports);
+
+ return __wbg_finalize_init(instance, module);
+}
+
+async function __wbg_init(module_or_path) {
+ if (wasm !== undefined) return wasm;
+
+
+ if (typeof module_or_path !== 'undefined') {
+ if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
+ ({module_or_path} = module_or_path)
+ } else {
+ console.warn('using deprecated parameters for the initialization function; pass a single object instead')
+ }
+ }
+
+ if (typeof module_or_path === 'undefined') {
+ module_or_path = new URL('railwayka_landing_bg.wasm', import.meta.url);
+ }
+ const imports = __wbg_get_imports();
+
+ if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
+ module_or_path = fetch(module_or_path);
+ }
+
+ __wbg_init_memory(imports);
+
+ const { instance, module } = await __wbg_load(await module_or_path, imports);
+
+ return __wbg_finalize_init(instance, module);
+}
+
+export { initSync };
+export default __wbg_init;
diff --git a/dist/railwayka_landing_bg.wasm b/dist/railwayka_landing_bg.wasm
new file mode 100644
index 0000000..37de7d2
Binary files /dev/null and b/dist/railwayka_landing_bg.wasm differ
diff --git a/dist/railwayka_landing_bg.wasm.d.ts b/dist/railwayka_landing_bg.wasm.d.ts
new file mode 100644
index 0000000..13cde68
--- /dev/null
+++ b/dist/railwayka_landing_bg.wasm.d.ts
@@ -0,0 +1,6 @@
+/* tslint:disable */
+/* eslint-disable */
+export const memory: WebAssembly.Memory;
+export const main: () => void;
+export const __wbindgen_export_0: (a: number) => void;
+export const __wbindgen_start: () => void;
diff --git a/dist/style.css b/dist/style.css
new file mode 100644
index 0000000..0f8af89
--- /dev/null
+++ b/dist/style.css
@@ -0,0 +1,299 @@
+/* Reset and base styles */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --bg-dark: #0a0e17;
+ --bg-card: #1a1f2e;
+ --bg-card-hover: #252a3a;
+ --text-primary: #e0e6ed;
+ --text-secondary: #9ca3af;
+ --accent: #3b82f6;
+ --accent-hover: #2563eb;
+ --rust: #f74c00;
+ --border: #2d3748;
+ --success: #10b981;
+ --planned: #6366f1;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', sans-serif;
+ background: var(--bg-dark);
+ color: var(--text-primary);
+ line-height: 1.6;
+ min-height: 100vh;
+}
+
+/* Loading screen */
+#loading {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: var(--bg-dark);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 9999;
+}
+
+.loader {
+ font-size: 1.2rem;
+ color: var(--text-secondary);
+ animation: pulse 2s ease-in-out infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 0.5; }
+ 50% { opacity: 1; }
+}
+
+.error {
+ padding: 2rem;
+ text-align: center;
+ color: var(--rust);
+}
+
+/* Container */
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 2rem;
+}
+
+/* Hero Section */
+.hero {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ background: linear-gradient(135deg, #0a0e17 0%, #1a1f2e 100%);
+ position: relative;
+ overflow: hidden;
+}
+
+.hero::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: radial-gradient(circle at 50% 50%, rgba(59, 130, 246, 0.1) 0%, transparent 50%);
+}
+
+.hero .container {
+ position: relative;
+ z-index: 1;
+}
+
+.hero-title {
+ font-size: clamp(3rem, 8vw, 6rem);
+ font-weight: 700;
+ margin-bottom: 1rem;
+ background: linear-gradient(135deg, var(--accent) 0%, var(--planned) 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.hero-subtitle {
+ font-size: clamp(1.2rem, 3vw, 1.5rem);
+ color: var(--text-secondary);
+ margin-bottom: 2rem;
+ max-width: 600px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.hero-badge {
+ display: inline-block;
+ padding: 0.75rem 1.5rem;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 2rem;
+ font-size: 1rem;
+ color: var(--text-primary);
+ animation: float 3s ease-in-out infinite;
+}
+
+@keyframes float {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-10px); }
+}
+
+/* Section Styles */
+section {
+ padding: 5rem 0;
+}
+
+.section-title {
+ font-size: 2.5rem;
+ margin-bottom: 3rem;
+ text-align: center;
+ color: var(--text-primary);
+}
+
+/* Services Section */
+.services {
+ background: var(--bg-dark);
+}
+
+.services-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 1.5rem;
+}
+
+.service-card {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 0.75rem;
+ padding: 1.5rem;
+ transition: all 0.3s ease;
+ text-decoration: none;
+ color: inherit;
+ display: block;
+}
+
+.service-card:hover {
+ background: var(--bg-card-hover);
+ border-color: var(--accent);
+ transform: translateY(-4px);
+ box-shadow: 0 10px 25px rgba(59, 130, 246, 0.2);
+}
+
+.service-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 0.75rem;
+}
+
+.service-card h3 {
+ font-size: 1.25rem;
+ color: var(--text-primary);
+ flex: 1;
+}
+
+.status-badge {
+ font-size: 1.5rem;
+ flex-shrink: 0;
+ margin-left: 0.5rem;
+}
+
+.service-description {
+ color: var(--text-secondary);
+ font-size: 0.95rem;
+ line-height: 1.6;
+}
+
+/* Tech Section */
+.tech {
+ background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-card) 100%);
+}
+
+.tech-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 2rem;
+}
+
+.tech-item {
+ text-align: center;
+ padding: 2rem 1rem;
+}
+
+.tech-item h3 {
+ font-size: 1.5rem;
+ margin-bottom: 0.75rem;
+ color: var(--accent);
+}
+
+.tech-item p {
+ color: var(--text-secondary);
+ font-size: 0.95rem;
+}
+
+/* Footer */
+.footer {
+ background: var(--bg-card);
+ border-top: 1px solid var(--border);
+ padding: 3rem 0;
+ text-align: center;
+}
+
+.footer p {
+ color: var(--text-secondary);
+ margin-bottom: 1.5rem;
+}
+
+.rust-love {
+ color: var(--rust);
+ font-weight: 600;
+}
+
+.footer-links {
+ display: flex;
+ justify-content: center;
+ gap: 2rem;
+ flex-wrap: wrap;
+}
+
+.footer-links a {
+ color: var(--accent);
+ text-decoration: none;
+ transition: color 0.3s ease;
+ font-size: 0.95rem;
+}
+
+.footer-links a:hover {
+ color: var(--accent-hover);
+ text-decoration: underline;
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+ .container {
+ padding: 0 1.5rem;
+ }
+
+ section {
+ padding: 3rem 0;
+ }
+
+ .services-grid,
+ .tech-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .hero {
+ min-height: 80vh;
+ }
+
+ .footer-links {
+ flex-direction: column;
+ gap: 1rem;
+ }
+}
+
+/* Smooth scrolling */
+html {
+ scroll-behavior: smooth;
+}
+
+/* Selection color */
+::selection {
+ background: var(--accent);
+ color: white;
+}
+
+::-moz-selection {
+ background: var(--accent);
+ color: white;
+}
diff --git a/html/index.html b/html-old/index.html
similarity index 100%
rename from html/index.html
rename to html-old/index.html
diff --git a/html/script.js b/html-old/script.js
similarity index 100%
rename from html/script.js
rename to html-old/script.js
diff --git a/html/style.css b/html-old/style.css
similarity index 100%
rename from html/style.css
rename to html-old/style.css
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..a152394
--- /dev/null
+++ b/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+ railwayka.ru
+
+
+
+
+
+
+
+
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..9658e58
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,240 @@
+use wasm_bindgen::prelude::*;
+use web_sys::{console, Document, Element, HtmlElement, Window};
+
+#[wasm_bindgen(start)]
+pub fn main() -> Result<(), JsValue> {
+ console::log_1(&"🚀 railwayka.ru WASM landing page initializing...".into());
+
+ let window = web_sys::window().expect("no global window exists");
+ let document = window.document().expect("should have a document on window");
+
+ // Clear body and build page
+ if let Some(body) = document.body() {
+ body.set_inner_html("");
+ build_page(&document, &body)?;
+ }
+
+ console::log_1(&"✅ Landing page ready!".into());
+ Ok(())
+}
+
+fn build_page(document: &Document, body: &HtmlElement) -> Result<(), JsValue> {
+ // Hero Section
+ let hero = create_hero(document)?;
+ body.append_child(&hero)?;
+
+ // Services Section
+ let services = create_services_section(document)?;
+ body.append_child(&services)?;
+
+ // Tech Stack Section
+ let tech = create_tech_section(document)?;
+ body.append_child(&tech)?;
+
+ // Footer
+ let footer = create_footer(document)?;
+ body.append_child(&footer)?;
+
+ Ok(())
+}
+
+fn create_hero(document: &Document) -> Result {
+ let section = document.create_element("section")?;
+ section.set_class_name("hero");
+
+ let container = document.create_element("div")?;
+ container.set_class_name("container");
+
+ let title = document.create_element("h1")?;
+ title.set_text_content(Some("railwayka.ru"));
+ title.set_class_name("hero-title");
+
+ let subtitle = document.create_element("p")?;
+ subtitle.set_text_content(Some("Modern self-hosted infrastructure powered by GitOps"));
+ subtitle.set_class_name("hero-subtitle");
+
+ let badge = document.create_element("div")?;
+ badge.set_class_name("hero-badge");
+ badge.set_text_content(Some("🦀 Powered by Rust + WebAssembly"));
+
+ container.append_child(&title)?;
+ container.append_child(&subtitle)?;
+ container.append_child(&badge)?;
+ section.append_child(&container)?;
+
+ Ok(section)
+}
+
+fn create_services_section(document: &Document) -> Result {
+ let section = document.create_element("section")?;
+ section.set_class_name("services");
+
+ let container = document.create_element("div")?;
+ container.set_class_name("container");
+
+ let heading = document.create_element("h2")?;
+ heading.set_text_content(Some("Services"));
+ heading.set_class_name("section-title");
+ container.append_child(&heading)?;
+
+ let grid = document.create_element("div")?;
+ grid.set_class_name("services-grid");
+
+ // Live services
+ let services_live = vec![
+ ("Git Repository", "Gitea - Self-hosted Git server with web UI", "🔴", "https://git.dc.railwayka.ru"),
+ ("CI/CD", "Woodpecker CI - Automated builds and deployments", "🔴", "https://cicd.dc.railwayka.ru"),
+ ("Container Management", "Portainer - Docker management interface", "🔴", "https://port.dc.railwayka.ru"),
+ ];
+
+ for (name, desc, status, url) in services_live {
+ let card = create_service_card(document, name, desc, status, Some(url))?;
+ grid.append_child(&card)?;
+ }
+
+ // Planned services
+ let services_planned = vec![
+ ("Matrix Server", "Decentralized messaging and communication", "🔵"),
+ ("Blog", "Technical writing and knowledge sharing", "🔵"),
+ ("Password Manager", "Vaultwarden - Self-hosted password vault", "🔵"),
+ ("Cloud Storage", "Nextcloud - File sync and sharing", "🔵"),
+ ("Monitoring", "Grafana + Prometheus - Infrastructure monitoring", "🔵"),
+ ];
+
+ for (name, desc, status) in services_planned {
+ let card = create_service_card(document, name, desc, status, None)?;
+ grid.append_child(&card)?;
+ }
+
+ container.append_child(&grid)?;
+ section.append_child(&container)?;
+
+ Ok(section)
+}
+
+fn create_service_card(
+ document: &Document,
+ name: &str,
+ description: &str,
+ status: &str,
+ url: Option<&str>,
+) -> Result {
+ let card = if let Some(link) = url {
+ let a = document.create_element("a")?;
+ a.set_attribute("href", link)?;
+ a.set_attribute("target", "_blank")?;
+ a.set_attribute("rel", "noopener noreferrer")?;
+ a
+ } else {
+ document.create_element("div")?
+ };
+ card.set_class_name("service-card");
+
+ let header = document.create_element("div")?;
+ header.set_class_name("service-header");
+
+ let title = document.create_element("h3")?;
+ title.set_text_content(Some(name));
+
+ let badge = document.create_element("span")?;
+ badge.set_class_name("status-badge");
+ badge.set_text_content(Some(status));
+
+ header.append_child(&title)?;
+ header.append_child(&badge)?;
+
+ let desc = document.create_element("p")?;
+ desc.set_text_content(Some(description));
+ desc.set_class_name("service-description");
+
+ card.append_child(&header)?;
+ card.append_child(&desc)?;
+
+ Ok(card)
+}
+
+fn create_tech_section(document: &Document) -> Result {
+ let section = document.create_element("section")?;
+ section.set_class_name("tech");
+
+ let container = document.create_element("div")?;
+ container.set_class_name("container");
+
+ let heading = document.create_element("h2")?;
+ heading.set_text_content(Some("Technology Stack"));
+ heading.set_class_name("section-title");
+ container.append_child(&heading)?;
+
+ let grid = document.create_element("div")?;
+ grid.set_class_name("tech-grid");
+
+ let technologies = vec![
+ ("Dcape", "GitOps platform for containerized applications"),
+ ("Docker", "Containerization and orchestration"),
+ ("Traefik", "Automatic HTTPS and reverse proxy"),
+ ("PowerDNS", "Authoritative DNS server"),
+ ("PostgreSQL", "Shared database infrastructure"),
+ ("Rust + WASM", "High-performance web applications"),
+ ];
+
+ for (name, desc) in technologies {
+ let item = create_tech_item(document, name, desc)?;
+ grid.append_child(&item)?;
+ }
+
+ container.append_child(&grid)?;
+ section.append_child(&container)?;
+
+ Ok(section)
+}
+
+fn create_tech_item(document: &Document, name: &str, description: &str) -> Result {
+ let item = document.create_element("div")?;
+ item.set_class_name("tech-item");
+
+ let title = document.create_element("h3")?;
+ title.set_text_content(Some(name));
+
+ let desc = document.create_element("p")?;
+ desc.set_text_content(Some(description));
+
+ item.append_child(&title)?;
+ item.append_child(&desc)?;
+
+ Ok(item)
+}
+
+fn create_footer(document: &Document) -> Result {
+ let footer = document.create_element("footer")?;
+ footer.set_class_name("footer");
+
+ let container = document.create_element("div")?;
+ container.set_class_name("container");
+
+ let text = document.create_element("p")?;
+ text.set_inner_html("Built with 🦀 Rust + WebAssembly • Deployed via GitOps");
+
+ let links = document.create_element("div")?;
+ links.set_class_name("footer-links");
+
+ let link_data = vec![
+ ("Traefik Dashboard", "https://dc.railwayka.ru/dashboard/"),
+ ("Gitea", "https://git.dc.railwayka.ru"),
+ ("Woodpecker CI", "https://cicd.dc.railwayka.ru"),
+ ];
+
+ for (name, url) in link_data {
+ let link = document.create_element("a")?;
+ link.set_attribute("href", url)?;
+ link.set_attribute("target", "_blank")?;
+ link.set_attribute("rel", "noopener noreferrer")?;
+ link.set_text_content(Some(name));
+ links.append_child(&link)?;
+ }
+
+ container.append_child(&text)?;
+ container.append_child(&links)?;
+ footer.append_child(&container)?;
+
+ Ok(footer)
+}
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..0f8af89
--- /dev/null
+++ b/style.css
@@ -0,0 +1,299 @@
+/* Reset and base styles */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --bg-dark: #0a0e17;
+ --bg-card: #1a1f2e;
+ --bg-card-hover: #252a3a;
+ --text-primary: #e0e6ed;
+ --text-secondary: #9ca3af;
+ --accent: #3b82f6;
+ --accent-hover: #2563eb;
+ --rust: #f74c00;
+ --border: #2d3748;
+ --success: #10b981;
+ --planned: #6366f1;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', sans-serif;
+ background: var(--bg-dark);
+ color: var(--text-primary);
+ line-height: 1.6;
+ min-height: 100vh;
+}
+
+/* Loading screen */
+#loading {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: var(--bg-dark);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 9999;
+}
+
+.loader {
+ font-size: 1.2rem;
+ color: var(--text-secondary);
+ animation: pulse 2s ease-in-out infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 0.5; }
+ 50% { opacity: 1; }
+}
+
+.error {
+ padding: 2rem;
+ text-align: center;
+ color: var(--rust);
+}
+
+/* Container */
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 2rem;
+}
+
+/* Hero Section */
+.hero {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ background: linear-gradient(135deg, #0a0e17 0%, #1a1f2e 100%);
+ position: relative;
+ overflow: hidden;
+}
+
+.hero::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: radial-gradient(circle at 50% 50%, rgba(59, 130, 246, 0.1) 0%, transparent 50%);
+}
+
+.hero .container {
+ position: relative;
+ z-index: 1;
+}
+
+.hero-title {
+ font-size: clamp(3rem, 8vw, 6rem);
+ font-weight: 700;
+ margin-bottom: 1rem;
+ background: linear-gradient(135deg, var(--accent) 0%, var(--planned) 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.hero-subtitle {
+ font-size: clamp(1.2rem, 3vw, 1.5rem);
+ color: var(--text-secondary);
+ margin-bottom: 2rem;
+ max-width: 600px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.hero-badge {
+ display: inline-block;
+ padding: 0.75rem 1.5rem;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 2rem;
+ font-size: 1rem;
+ color: var(--text-primary);
+ animation: float 3s ease-in-out infinite;
+}
+
+@keyframes float {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-10px); }
+}
+
+/* Section Styles */
+section {
+ padding: 5rem 0;
+}
+
+.section-title {
+ font-size: 2.5rem;
+ margin-bottom: 3rem;
+ text-align: center;
+ color: var(--text-primary);
+}
+
+/* Services Section */
+.services {
+ background: var(--bg-dark);
+}
+
+.services-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 1.5rem;
+}
+
+.service-card {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 0.75rem;
+ padding: 1.5rem;
+ transition: all 0.3s ease;
+ text-decoration: none;
+ color: inherit;
+ display: block;
+}
+
+.service-card:hover {
+ background: var(--bg-card-hover);
+ border-color: var(--accent);
+ transform: translateY(-4px);
+ box-shadow: 0 10px 25px rgba(59, 130, 246, 0.2);
+}
+
+.service-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 0.75rem;
+}
+
+.service-card h3 {
+ font-size: 1.25rem;
+ color: var(--text-primary);
+ flex: 1;
+}
+
+.status-badge {
+ font-size: 1.5rem;
+ flex-shrink: 0;
+ margin-left: 0.5rem;
+}
+
+.service-description {
+ color: var(--text-secondary);
+ font-size: 0.95rem;
+ line-height: 1.6;
+}
+
+/* Tech Section */
+.tech {
+ background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-card) 100%);
+}
+
+.tech-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 2rem;
+}
+
+.tech-item {
+ text-align: center;
+ padding: 2rem 1rem;
+}
+
+.tech-item h3 {
+ font-size: 1.5rem;
+ margin-bottom: 0.75rem;
+ color: var(--accent);
+}
+
+.tech-item p {
+ color: var(--text-secondary);
+ font-size: 0.95rem;
+}
+
+/* Footer */
+.footer {
+ background: var(--bg-card);
+ border-top: 1px solid var(--border);
+ padding: 3rem 0;
+ text-align: center;
+}
+
+.footer p {
+ color: var(--text-secondary);
+ margin-bottom: 1.5rem;
+}
+
+.rust-love {
+ color: var(--rust);
+ font-weight: 600;
+}
+
+.footer-links {
+ display: flex;
+ justify-content: center;
+ gap: 2rem;
+ flex-wrap: wrap;
+}
+
+.footer-links a {
+ color: var(--accent);
+ text-decoration: none;
+ transition: color 0.3s ease;
+ font-size: 0.95rem;
+}
+
+.footer-links a:hover {
+ color: var(--accent-hover);
+ text-decoration: underline;
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+ .container {
+ padding: 0 1.5rem;
+ }
+
+ section {
+ padding: 3rem 0;
+ }
+
+ .services-grid,
+ .tech-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .hero {
+ min-height: 80vh;
+ }
+
+ .footer-links {
+ flex-direction: column;
+ gap: 1rem;
+ }
+}
+
+/* Smooth scrolling */
+html {
+ scroll-behavior: smooth;
+}
+
+/* Selection color */
+::selection {
+ background: var(--accent);
+ color: white;
+}
+
+::-moz-selection {
+ background: var(--accent);
+ color: white;
+}