TRUSS-2 working on moving to Rust
This commit is contained in:
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Rust build output
|
||||
/target
|
||||
|
||||
# Generated static output (dynamic server mode is the intended workflow)
|
||||
/output
|
||||
|
||||
# Python artifacts left over from the previous implementation
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Editor/OS noise
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
746
Cargo.lock
generated
Normal file
746
Cargo.lock
generated
Normal file
@@ -0,0 +1,746 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "memo-map"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minijinja"
|
||||
version = "2.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86886cf6dbf4e614b19c9a1eec9775f021869d7eadde0fc73921a81b90c9b4c9"
|
||||
dependencies = [
|
||||
"memo-map",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truss"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"minijinja",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "truss"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.8"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
regex = "1"
|
||||
minijinja = { version = "2", features = ["loader", "json"] }
|
||||
|
||||
45
README.md
Normal file
45
README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Truss
|
||||
|
||||
A small, dynamically-rendered kanban server for the Arcline TODO board.
|
||||
|
||||
The server reads `TODO.md` and the per-track board files, renders the board with
|
||||
Rust + Axum + Minijinja, and persists card changes back to the same Markdown files.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
cargo run
|
||||
```
|
||||
|
||||
Then open <http://localhost:5000>.
|
||||
|
||||
## Board files
|
||||
|
||||
- `TODO.md` is the master board.
|
||||
- `TODO-msp.md` is the MSP / IT track.
|
||||
- `TODO-os.md` is the OS / open-source track.
|
||||
|
||||
The MSP and OS board files are auto-generated from `TODO.md` on startup if they
|
||||
do not already exist.
|
||||
|
||||
## HTTP API
|
||||
|
||||
| Method | Route | Description |
|
||||
| ------ | ----- | ----------- |
|
||||
| `GET` | `/` | Board index |
|
||||
| `GET` | `/{board}` | Rendered board (`msp` or `os`) |
|
||||
| `GET` | `/api/{board}/board` | Board data as JSON |
|
||||
| `POST` | `/api/{board}/cards` | Create a card |
|
||||
| `PUT` | `/api/{board}/cards/{card_id}` | Update a card |
|
||||
| `DELETE` | `/api/{board}/cards/{card_id}` | Delete a card |
|
||||
| `POST` | `/api/{board}/cards/{card_id}/move` | Move a card to another column |
|
||||
| `POST` | `/api/{board}/reload` | Reload a board from disk |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
cargo fmt --check
|
||||
```
|
||||
|
||||
21
TODO-msp.md
21
TODO-msp.md
@@ -22,6 +22,10 @@ Codenames are architectural/masonry terms — fitting for a company named after
|
||||
|
||||
| Codename | Initiative | Track | Status |
|
||||
|
||||
| Codename | Initiative | Track | Status |
|
||||
|
||||
| Codename | Initiative | Track | Status |
|
||||
|
||||
| Codename | Initiative | Track | Status |
|
||||
|-------------|-----------------------------------------------|-------|-------------|
|
||||
| Keystone | Customer portal + billing overhaul | IT | In Progress |
|
||||
@@ -42,6 +46,19 @@ Codenames are architectural/masonry terms — fitting for a company named after
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -57,17 +74,17 @@ Codenames are architectural/masonry terms — fitting for a company named after
|
||||
- [ ] `[IT]` **Lintel** — restructure docs.arcline.it into per-tool sections <!-- id:85851b3d-ecd3-46ea-9870-4cc515670b13 -->
|
||||
- [ ] `[IT]` **Plinth** — build referral program signup + tracking flow <!-- id:b4c0421a-3201-424f-adce-f8399d848c05 -->
|
||||
- [ ] `[IT/OS]` **Voussoir** — spec `arcline-migrate` cPanel import mapping <!-- id:86df261e-e1c8-48af-9aa9-551a0da4fe14 -->
|
||||
- [ ] `[IT]` — Case studies page — draft first 2 client write-ups <!-- id:88242e2b-7218-42bd-9790-bd33e7649f3e -->
|
||||
- [ ] `[IT]` — Game server plans — add automated backup scheduling to portal <!-- id:c4aae5af-f5f9-418b-b078-a6534c69f27e -->
|
||||
- [ ] `[IT]` — Case studies page — draft first 2 client write-ups <!-- id:88242e2b-7218-42bd-9790-bd33e7649f3e -->
|
||||
|
||||
## In Progress
|
||||
|
||||
- [ ] `[IT]` **Keystone** — customer portal (`arcline-portal`) SSL + ticketing UI <!-- id:d4ccf112-a52e-42c4-9ff5-0b986969cfb5 -->
|
||||
- [ ] `[IT]` **Keystone** — Stripe billing (`arcline-billing`) subscription sync <!-- id:e059813f-0c1f-4294-859d-b0b7d36ec5db -->
|
||||
|
||||
## In Review
|
||||
|
||||
- [ ] `[IT]` **Springer** — status page generator (`arcline-status`) — awaiting Blake's sign-off <!-- id:cfb2ee5e-ffe4-4c0a-9a2e-9e4204979ffd -->
|
||||
- [ ] `[IT]` **Keystone** — customer portal (`arcline-portal`) SSL + ticketing UI <!-- id:d4ccf112-a52e-42c4-9ff5-0b986969cfb5 -->
|
||||
|
||||
## Done
|
||||
|
||||
|
||||
Binary file not shown.
601
build.py
601
build.py
@@ -1,601 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Arcline Kanban — multi-board Kanban with CRUD over markdown files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, jsonify, redirect, render_template_string, request, url_for
|
||||
from markupsafe import Markup
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
TEMPLATES_DIR = ROOT / "templates"
|
||||
OUTPUT_DIR = ROOT / "output"
|
||||
|
||||
# Board registry — name → file path
|
||||
BOARDS: dict[str, Path] = {
|
||||
"msp": ROOT / "TODO-msp.md",
|
||||
"os": ROOT / "TODO-os.md",
|
||||
}
|
||||
|
||||
app = Flask(__name__, template_folder=str(TEMPLATES_DIR))
|
||||
|
||||
# Per-board in-memory state — loaded on first access, synced on write
|
||||
_boards_data: dict[str, dict] = {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Board helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _get_board(name: str) -> dict | None:
|
||||
"""Return in-memory data for *name*, loading from its file if needed."""
|
||||
if name not in BOARDS:
|
||||
return None
|
||||
if name not in _boards_data:
|
||||
_boards_data[name] = parse_todo(BOARDS[name])
|
||||
return _boards_data[name]
|
||||
|
||||
|
||||
def _find_card(board: str, card_id: str) -> tuple[dict, list[dict]] | None:
|
||||
"""Return (card, parent_column_cards_list) or None within *board*."""
|
||||
data = _get_board(board)
|
||||
if data is None:
|
||||
return None
|
||||
for col in data["columns"]:
|
||||
for card in col["cards"]:
|
||||
if card["id"] == card_id:
|
||||
return card, col["cards"]
|
||||
return None
|
||||
|
||||
|
||||
def _ensure_board_files() -> None:
|
||||
"""Create any missing board markdown files from TODO.md."""
|
||||
original = ROOT / "TODO.md"
|
||||
if not original.exists():
|
||||
return
|
||||
|
||||
for name, path in BOARDS.items():
|
||||
if path.exists():
|
||||
continue
|
||||
data = parse_todo(original)
|
||||
|
||||
# Customize title per board
|
||||
if name == "msp":
|
||||
data["_header_raw"] = data["_header_raw"].replace(
|
||||
"# Arcline — TODO", "# Arcline MSP — TODO"
|
||||
).replace(
|
||||
"In-house Kanban board for **Arcline IT** ",
|
||||
"Kanban board for **Arcline MSP** — IT / managed-services track. ",
|
||||
)
|
||||
# Filter cards: keep IT and IT/OS
|
||||
for col in data["columns"]:
|
||||
col["cards"] = [
|
||||
c for c in col["cards"]
|
||||
if "IT" in c.get("track", "")
|
||||
]
|
||||
# Filter legend: keep IT, IT/OS
|
||||
data["legend"] = [
|
||||
r for r in data["legend"]
|
||||
if "IT" in r.get("track", "") or "OS" not in r.get("track", "")
|
||||
]
|
||||
elif name == "os":
|
||||
data["_header_raw"] = data["_header_raw"].replace(
|
||||
"# Arcline — TODO", "# Arcline OS — TODO"
|
||||
).replace(
|
||||
"In-house Kanban board for **Arcline IT** ",
|
||||
"Kanban board for **Arcline OS** — open-source / GPL track. ",
|
||||
)
|
||||
# Filter cards: keep OS and IT/OS
|
||||
for col in data["columns"]:
|
||||
col["cards"] = [
|
||||
c for c in col["cards"]
|
||||
if "OS" in c.get("track", "")
|
||||
]
|
||||
# Filter legend: keep OS, IT/OS
|
||||
data["legend"] = [
|
||||
r for r in data["legend"]
|
||||
if "OS" in r.get("track", "") or "IT" not in r.get("track", "")
|
||||
]
|
||||
|
||||
# Regenerate legend raw from filtered legend list
|
||||
if data["legend"]:
|
||||
legend_header = "| Codename | Initiative | Track | Status |"
|
||||
legend_sep = "|-------------|-----------------------------------------------|-------|-------------|"
|
||||
legend_rows = [
|
||||
f"| {r['codename']:<12} | {r['initiative']:<45} | {r['track']:<5} | {r['status']:<11} |"
|
||||
for r in data["legend"]
|
||||
]
|
||||
data["_legend_raw"] = "\n".join([legend_header, legend_sep] + legend_rows)
|
||||
|
||||
# Update notes
|
||||
data["_notes_raw"] = "## Notes\n\n- Board auto-generated from the master TODO.md\n"
|
||||
|
||||
_save_board(data, path)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parser (parses a TODO.md file into structured data)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def parse_todo(path: Path) -> dict:
|
||||
"""Parse a TODO.md file into a structured dictionary."""
|
||||
text = path.read_text()
|
||||
return {
|
||||
"header": _parse_header(text),
|
||||
"legend": _parse_legend(text),
|
||||
"columns": _parse_columns(text),
|
||||
"notes": _parse_notes(text),
|
||||
"_header_raw": _extract_header_raw(text),
|
||||
"_pre_legend_raw": _extract_pre_legend_raw(text),
|
||||
"_legend_raw": _extract_legend_raw(text),
|
||||
"_post_legend_raw": _extract_post_legend_raw(text),
|
||||
"_notes_raw": _extract_notes_raw(text),
|
||||
}
|
||||
|
||||
|
||||
def _parse_header(text: str) -> dict:
|
||||
lines = text.splitlines()
|
||||
title = ""
|
||||
subtitle = ""
|
||||
flow_line = ""
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("# ") and not title:
|
||||
title = stripped[2:].strip()
|
||||
for j in range(i + 1, min(i + 8, len(lines))):
|
||||
nxt = lines[j].strip()
|
||||
if nxt.startswith("#") or nxt.startswith("---"):
|
||||
break
|
||||
if not nxt:
|
||||
if subtitle:
|
||||
break
|
||||
continue
|
||||
if not subtitle:
|
||||
subtitle = nxt
|
||||
else:
|
||||
subtitle += " " + nxt
|
||||
elif "Backlog" in stripped and "In Progress" in stripped:
|
||||
flow_line = stripped
|
||||
break
|
||||
|
||||
return {"title": title, "subtitle": subtitle, "flow_line": flow_line}
|
||||
|
||||
|
||||
def _parse_legend(text: str) -> list[dict]:
|
||||
legends: list[dict] = []
|
||||
in_table = False
|
||||
|
||||
for line in text.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("| Codename"):
|
||||
in_table = True
|
||||
continue
|
||||
if in_table:
|
||||
if not stripped.startswith("|"):
|
||||
break
|
||||
if "---" in stripped:
|
||||
continue
|
||||
cells = [c.strip() for c in stripped.split("|")[1:-1]]
|
||||
if len(cells) >= 4:
|
||||
legends.append({
|
||||
"codename": cells[0],
|
||||
"initiative": cells[1],
|
||||
"track": cells[2],
|
||||
"status": cells[3],
|
||||
})
|
||||
return legends
|
||||
|
||||
|
||||
def _parse_columns(text: str) -> list[dict]:
|
||||
columns: list[dict] = []
|
||||
current_col: dict | None = None
|
||||
|
||||
for line in text.splitlines():
|
||||
stripped = line.strip()
|
||||
|
||||
if stripped.startswith("## Backlog"):
|
||||
current_col = {"name": "Backlog", "cards": []}
|
||||
columns.append(current_col)
|
||||
elif stripped.startswith("## In Progress"):
|
||||
current_col = {"name": "In Progress", "cards": []}
|
||||
columns.append(current_col)
|
||||
elif stripped.startswith("## In Review"):
|
||||
current_col = {"name": "In Review", "cards": []}
|
||||
columns.append(current_col)
|
||||
elif stripped.startswith("## Done"):
|
||||
current_col = {"name": "Done", "cards": []}
|
||||
columns.append(current_col)
|
||||
elif stripped.startswith("## ") and current_col and current_col["name"] != "Done":
|
||||
break
|
||||
|
||||
if current_col and _is_card_line(stripped):
|
||||
card = _parse_card(stripped)
|
||||
if card:
|
||||
current_col["cards"].append(card)
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
def _is_card_line(line: str) -> bool:
|
||||
return line.startswith("- [ ]") or line.startswith("- [x]")
|
||||
|
||||
|
||||
def _parse_card(line: str) -> dict | None:
|
||||
id_match = re.search(r"<!--\s*id:(\S+)\s*-->", line)
|
||||
card_id = id_match.group(1) if id_match else str(uuid.uuid4())
|
||||
clean_line = re.sub(r"<!--\s*id:\S+\s*-->", "", line).strip()
|
||||
|
||||
checked = clean_line.startswith("- [x]")
|
||||
rest = re.sub(r"^- \[[ x]\]\s*", "", clean_line)
|
||||
|
||||
track_match = re.match(r"`\[([^\]]+)\]`", rest)
|
||||
track = track_match.group(1) if track_match else ""
|
||||
if track_match:
|
||||
rest = rest[track_match.end():].strip()
|
||||
|
||||
codename_match = re.match(r"\*\*([^*]+)\*\*", rest)
|
||||
codename = codename_match.group(1) if codename_match else ""
|
||||
if codename_match:
|
||||
rest = rest[codename_match.end():].strip()
|
||||
|
||||
rest = re.sub(r"^—\s*", "", rest)
|
||||
description = rest.strip()
|
||||
|
||||
return {
|
||||
"id": card_id,
|
||||
"track": track,
|
||||
"codename": codename,
|
||||
"description": description,
|
||||
"checked": checked,
|
||||
}
|
||||
|
||||
|
||||
def _parse_notes(text: str) -> list[str]:
|
||||
notes: list[str] = []
|
||||
in_notes = False
|
||||
|
||||
for line in text.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("## Notes"):
|
||||
in_notes = True
|
||||
continue
|
||||
if in_notes:
|
||||
if stripped.startswith("## "):
|
||||
break
|
||||
if stripped.startswith("- "):
|
||||
notes.append(stripped[2:].strip())
|
||||
|
||||
return notes
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Raw-section extractors
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _extract_header_raw(text: str) -> str:
|
||||
lines = text.splitlines()
|
||||
out: list[str] = []
|
||||
for line in lines:
|
||||
if line.strip().startswith("## Codename Legend"):
|
||||
break
|
||||
out.append(line)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def _extract_pre_legend_raw(text: str) -> str:
|
||||
lines = text.splitlines()
|
||||
out: list[str] = []
|
||||
in_section = False
|
||||
for line in lines:
|
||||
if line.strip().startswith("## Codename Legend"):
|
||||
in_section = True
|
||||
if in_section:
|
||||
out.append(line)
|
||||
if line.strip().startswith("|"):
|
||||
break
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def _extract_legend_raw(text: str) -> str:
|
||||
lines = text.splitlines()
|
||||
out: list[str] = []
|
||||
in_table = False
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("| Codename"):
|
||||
in_table = True
|
||||
out.append(line)
|
||||
continue
|
||||
if in_table:
|
||||
if stripped.startswith("|"):
|
||||
out.append(line)
|
||||
else:
|
||||
if stripped == "":
|
||||
out.append(line)
|
||||
else:
|
||||
break
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def _extract_post_legend_raw(text: str) -> str:
|
||||
lines = text.splitlines()
|
||||
out: list[str] = []
|
||||
found_legend_end = False
|
||||
found_sep = False
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if found_legend_end and not found_sep:
|
||||
if stripped == "---":
|
||||
found_sep = True
|
||||
out.append(line)
|
||||
elif stripped == "":
|
||||
out.append(line)
|
||||
continue
|
||||
if found_sep:
|
||||
if stripped.startswith("## "):
|
||||
break
|
||||
out.append(line)
|
||||
continue
|
||||
if stripped.startswith("## Codename Legend"):
|
||||
found_legend_end = False
|
||||
if stripped.startswith("|"):
|
||||
found_legend_end = True
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def _extract_notes_raw(text: str) -> str:
|
||||
lines = text.splitlines()
|
||||
out: list[str] = []
|
||||
in_notes = False
|
||||
for line in lines:
|
||||
if line.strip().startswith("## Notes"):
|
||||
in_notes = True
|
||||
if in_notes:
|
||||
out.append(line)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Serializer
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _card_to_md(card: dict) -> str:
|
||||
checked = "x" if card["checked"] else " "
|
||||
parts = [f"- [{checked}]"]
|
||||
if card.get("track"):
|
||||
parts.append(f"`[{card['track']}]`")
|
||||
if card.get("codename"):
|
||||
parts.append(f"**{card['codename']}**")
|
||||
if card.get("description"):
|
||||
parts.append(f"— {card['description']}")
|
||||
line = " ".join(parts)
|
||||
if card.get("id"):
|
||||
line += f" <!-- id:{card['id']} -->"
|
||||
return line
|
||||
|
||||
|
||||
def _save_board(data: dict, path: Path | None = None) -> None:
|
||||
"""Persist board data to its markdown file."""
|
||||
if path is None:
|
||||
return
|
||||
|
||||
sections: list[str] = []
|
||||
|
||||
sections.append(data["_header_raw"].rstrip())
|
||||
sections.append("")
|
||||
sections.append(data["_pre_legend_raw"].rstrip())
|
||||
sections.append("")
|
||||
sections.append(data["_legend_raw"].rstrip())
|
||||
sections.append("")
|
||||
sections.append(data["_post_legend_raw"].rstrip())
|
||||
|
||||
for col in data["columns"]:
|
||||
sections.append("")
|
||||
sections.append(f"## {col['name']}")
|
||||
sections.append("")
|
||||
for card in col["cards"]:
|
||||
sections.append(_card_to_md(card))
|
||||
|
||||
sections.append("")
|
||||
sections.append(data["_notes_raw"].rstrip())
|
||||
sections.append("")
|
||||
|
||||
path.write_text("\n".join(sections) + "\n")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Jinja2 filter
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.template_filter("inline_md")
|
||||
def _inline_markdown(text: str) -> Markup:
|
||||
text = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", text)
|
||||
text = re.sub(r"\*([^*]+)\*", r"<em>\1</em>", text)
|
||||
text = re.sub(r"\[([^\]]+)]\(([^)]+)\)", r'<a href="\2">\1</a>', text)
|
||||
return Markup(text)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Routes — pages
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.route("/")
|
||||
def home():
|
||||
"""Board selector."""
|
||||
board_list = [
|
||||
{"slug": slug, "title": _get_board(slug)["header"]["title"] if _get_board(slug) else slug.upper()}
|
||||
for slug in BOARDS
|
||||
]
|
||||
return render_template_string(
|
||||
(TEMPLATES_DIR / "home.html").read_text(),
|
||||
boards=board_list,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/<board>")
|
||||
def board_page(board: str):
|
||||
"""Render a specific Kanban board."""
|
||||
data = _get_board(board)
|
||||
if data is None:
|
||||
return redirect(url_for("home"))
|
||||
return render_template_string(
|
||||
(TEMPLATES_DIR / "board.html").read_text(),
|
||||
board_slug=board,
|
||||
**{k: v for k, v in data.items() if not k.startswith("_")},
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Routes — REST API (per-board)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.route("/api/<board>/board")
|
||||
def api_board(board: str):
|
||||
data = _get_board(board)
|
||||
if data is None:
|
||||
return jsonify({"ok": False, "error": "Unknown board"}), 404
|
||||
return jsonify({
|
||||
"header": data["header"],
|
||||
"legend": data["legend"],
|
||||
"columns": data["columns"],
|
||||
"notes": data["notes"],
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/<board>/cards", methods=["POST"])
|
||||
def create_card(board: str):
|
||||
data = _get_board(board)
|
||||
if data is None:
|
||||
return jsonify({"ok": False, "error": "Unknown board"}), 404
|
||||
|
||||
payload = request.get_json(silent=True) or {}
|
||||
column_name = payload.get("column", "Backlog")
|
||||
card = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"track": payload.get("track", ""),
|
||||
"codename": payload.get("codename", ""),
|
||||
"description": payload.get("description", ""),
|
||||
"checked": bool(payload.get("checked", False)),
|
||||
}
|
||||
|
||||
for col in data["columns"]:
|
||||
if col["name"] == column_name:
|
||||
col["cards"].append(card)
|
||||
_save_board(data, BOARDS[board])
|
||||
return jsonify({"ok": True, "card": card}), 201
|
||||
|
||||
return jsonify({"ok": False, "error": f"Unknown column: {column_name}"}), 400
|
||||
|
||||
|
||||
@app.route("/api/<board>/cards/<card_id>", methods=["PUT"])
|
||||
def update_card(board: str, card_id: str):
|
||||
data = _get_board(board)
|
||||
if data is None:
|
||||
return jsonify({"ok": False, "error": "Unknown board"}), 404
|
||||
|
||||
payload = request.get_json(silent=True) or {}
|
||||
result = _find_card(board, card_id)
|
||||
if result is None:
|
||||
return jsonify({"ok": False, "error": "Card not found"}), 404
|
||||
|
||||
card, _cards_list = result
|
||||
for field in ("track", "codename", "description", "checked"):
|
||||
if field in payload:
|
||||
card[field] = payload[field]
|
||||
|
||||
_save_board(data, BOARDS[board])
|
||||
return jsonify({"ok": True, "card": card})
|
||||
|
||||
|
||||
@app.route("/api/<board>/cards/<card_id>/move", methods=["POST"])
|
||||
def move_card(board: str, card_id: str):
|
||||
data = _get_board(board)
|
||||
if data is None:
|
||||
return jsonify({"ok": False, "error": "Unknown board"}), 404
|
||||
|
||||
payload = request.get_json(silent=True) or {}
|
||||
target_column = payload.get("column")
|
||||
if not target_column:
|
||||
return jsonify({"ok": False, "error": "Missing 'column' field"}), 400
|
||||
|
||||
result = _find_card(board, card_id)
|
||||
if result is None:
|
||||
return jsonify({"ok": False, "error": "Card not found"}), 404
|
||||
|
||||
card, source_list = result
|
||||
target_list = None
|
||||
for col in data["columns"]:
|
||||
if col["name"] == target_column:
|
||||
target_list = col["cards"]
|
||||
break
|
||||
|
||||
if target_list is None:
|
||||
return jsonify({"ok": False, "error": f"Unknown column: {target_column}"}), 400
|
||||
|
||||
source_list.remove(card)
|
||||
target_list.append(card)
|
||||
_save_board(data, BOARDS[board])
|
||||
return jsonify({"ok": True, "card": card})
|
||||
|
||||
|
||||
@app.route("/api/<board>/cards/<card_id>", methods=["DELETE"])
|
||||
def delete_card(board: str, card_id: str):
|
||||
data = _get_board(board)
|
||||
if data is None:
|
||||
return jsonify({"ok": False, "error": "Unknown board"}), 404
|
||||
|
||||
result = _find_card(board, card_id)
|
||||
if result is None:
|
||||
return jsonify({"ok": False, "error": "Card not found"}), 404
|
||||
|
||||
card, cards_list = result
|
||||
cards_list.remove(card)
|
||||
_save_board(data, BOARDS[board])
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@app.route("/api/<board>/reload", methods=["POST"])
|
||||
def reload_board(board: str):
|
||||
if board not in BOARDS:
|
||||
return jsonify({"ok": False, "error": "Unknown board"}), 404
|
||||
_boards_data.pop(board, None)
|
||||
_get_board(board)
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main() -> None:
|
||||
_ensure_board_files()
|
||||
|
||||
if "--build" in sys.argv:
|
||||
env = app.jinja_env
|
||||
template = env.from_string((TEMPLATES_DIR / "board.html").read_text())
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
for slug in BOARDS:
|
||||
data = _get_board(slug)
|
||||
if data is None:
|
||||
continue
|
||||
html = template.render(
|
||||
board_slug=slug,
|
||||
**{k: v for k, v in data.items() if not k.startswith("_")},
|
||||
)
|
||||
(OUTPUT_DIR / f"{slug}.html").write_text(html)
|
||||
print(f"✅ Rendered {slug} → {OUTPUT_DIR / f'{slug}.html'}")
|
||||
else:
|
||||
print("🚀 Serving Kanban boards at http://127.0.0.1:5000")
|
||||
for slug in BOARDS:
|
||||
print(f" /{slug}")
|
||||
app.run(debug=True, host="127.0.0.1", port=5000)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,789 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Arcline — TODO</title>
|
||||
<style>
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Reset & base */
|
||||
/* ------------------------------------------------------------------ */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #f4f5f7;
|
||||
--surface: #ffffff;
|
||||
--border: #dfe1e6;
|
||||
--text: #172b4d;
|
||||
--muted: #6b778c;
|
||||
--track-it: #0052cc;
|
||||
--track-os: #36b37e;
|
||||
--track-both: #6554c0;
|
||||
--backlog: #dfe1e6;
|
||||
--inprogress: #ffc400;
|
||||
--inreview: #00b8d9;
|
||||
--done: #36b37e;
|
||||
--card-shadow: 0 1px 3px rgba(9,30,66,0.08);
|
||||
--card-hover: 0 2px 6px rgba(9,30,66,0.14);
|
||||
--radius: 8px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Header */
|
||||
/* ------------------------------------------------------------------ */
|
||||
header {
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 24px 32px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
header h1 .logo {
|
||||
background: linear-gradient(135deg, #0052cc, #6554c0);
|
||||
color: #fff;
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
header .subtitle {
|
||||
color: var(--muted);
|
||||
margin-top: 4px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Legend */
|
||||
/* ------------------------------------------------------------------ */
|
||||
.legend-bar {
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 12px 32px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.legend-bar strong { color: var(--text); }
|
||||
|
||||
.track-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
.track-it { background: var(--track-it); }
|
||||
.track-os { background: var(--track-os); }
|
||||
.track-both { background: var(--track-both); }
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Legend table */
|
||||
/* ------------------------------------------------------------------ */
|
||||
.legend-table-wrap {
|
||||
padding: 20px 32px 16px;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.legend-table-wrap h2 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 10px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
table.legend {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
table.legend th {
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 2px solid var(--border);
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
table.legend td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
table.legend tr:last-child td { border-bottom: none; }
|
||||
|
||||
.status-pill {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
.status-backlog { background: var(--backlog); color: #42526e; }
|
||||
.status-inprogress { background: var(--inprogress); color: #172b4d; }
|
||||
.status-inreview { background: var(--inreview); }
|
||||
.status-done { background: var(--done); }
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Board */
|
||||
/* ------------------------------------------------------------------ */
|
||||
.board {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 24px 32px;
|
||||
overflow-x: auto;
|
||||
min-height: calc(100vh - 340px);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
max-width: 380px;
|
||||
background: #ebecf0;
|
||||
border-radius: var(--radius);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.column.done { background: #e3fcef; }
|
||||
|
||||
.column-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 8px 8px;
|
||||
}
|
||||
|
||||
.column-header h2 {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.column-count {
|
||||
background: rgba(9,30,66,0.08);
|
||||
color: var(--muted);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Cards */
|
||||
/* ------------------------------------------------------------------ */
|
||||
.card-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border-radius: 6px;
|
||||
padding: 12px 14px;
|
||||
box-shadow: var(--card-shadow);
|
||||
cursor: default;
|
||||
transition: box-shadow 0.15s ease, transform 0.15s ease;
|
||||
border-left: 3px solid var(--border);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--card-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.card.checked {
|
||||
opacity: 0.65;
|
||||
text-decoration: line-through;
|
||||
text-decoration-color: var(--muted);
|
||||
}
|
||||
|
||||
.card.track-it { border-left-color: var(--track-it); }
|
||||
.card.track-os { border-left-color: var(--track-os); }
|
||||
.card.track-both { border-left-color: var(--track-both); }
|
||||
|
||||
.card .card-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card .codename {
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.card .description {
|
||||
font-size: 0.82rem;
|
||||
color: var(--muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Notes */
|
||||
/* ------------------------------------------------------------------ */
|
||||
.notes {
|
||||
padding: 20px 32px 32px;
|
||||
background: var(--surface);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.notes h2 {
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 8px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.notes ul {
|
||||
list-style: disc inside;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Footer */
|
||||
/* ------------------------------------------------------------------ */
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--track-it);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Responsive */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@media (max-width: 900px) {
|
||||
.board { flex-direction: column; padding: 16px; }
|
||||
.column { max-width: 100%; }
|
||||
header, .legend-bar, .legend-table-wrap, .notes { padding-left: 16px; padding-right: 16px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ================================================================= -->
|
||||
<!-- Header -->
|
||||
<!-- ================================================================= -->
|
||||
<header>
|
||||
<h1>
|
||||
<span class="logo">⌂</span>
|
||||
Arcline — TODO
|
||||
</h1>
|
||||
<p class="subtitle">In-house Kanban board for <strong>Arcline IT</strong> (<a href="https://arcline.it">arcline.it</a>) and <strong>The Arcline Project</strong> (<a href="https://arclineproject.org">arclineproject.org</a>).</p>
|
||||
</header>
|
||||
|
||||
<!-- ================================================================= -->
|
||||
<!-- Legend bar -->
|
||||
<!-- ================================================================= -->
|
||||
<div class="legend-bar">
|
||||
<span><strong>Track:</strong></span>
|
||||
<span class="track-badge track-it">IT</span> arcline.it
|
||||
<span class="track-badge track-os">OS</span> arclineproject.org
|
||||
<span class="track-badge track-both">IT/OS</span> shared
|
||||
<span style="margin-left:auto">Move cards between sections as they progress: `Backlog → In Progress → In Review → Done`.</span>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================= -->
|
||||
<!-- Codename Legend table -->
|
||||
<!-- ================================================================= -->
|
||||
|
||||
<div class="legend-table-wrap">
|
||||
<h2>Codename Legend</h2>
|
||||
<table class="legend">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Codename</th>
|
||||
<th>Initiative</th>
|
||||
<th>Track</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td><strong>Keystone</strong></td>
|
||||
<td>Customer portal + billing overhaul</td>
|
||||
<td>
|
||||
|
||||
<span class="track-badge track-it">IT</span>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span class="status-pill status-inprogress">In Progress</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><strong>Cornerstone</strong></td>
|
||||
<td>Arcline OS alpha → public ISO release</td>
|
||||
<td>
|
||||
|
||||
<span class="track-badge track-os">OS</span>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span class="status-pill status-inprogress">In Progress</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><strong>Buttress</strong></td>
|
||||
<td>`arcline-audit` hardening & CVE scan integration</td>
|
||||
<td>
|
||||
|
||||
<span class="track-badge track-os">OS</span>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span class="status-pill status-backlog">Backlog</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><strong>Voussoir</strong></td>
|
||||
<td>Migration tool (cPanel/Plesk → Arcline)</td>
|
||||
<td>
|
||||
|
||||
<span class="track-badge track-both">IT/OS</span>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span class="status-pill status-backlog">Backlog</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><strong>Springer</strong></td>
|
||||
<td>Status page + uptime monitoring rollout</td>
|
||||
<td>
|
||||
|
||||
<span class="track-badge track-it">IT</span>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span class="status-pill status-inreview">In Review</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><strong>Rampart</strong></td>
|
||||
<td>Firewall/nftables default-deny profile pass</td>
|
||||
<td>
|
||||
|
||||
<span class="track-badge track-os">OS</span>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span class="status-pill status-backlog">Backlog</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><strong>Lintel</strong></td>
|
||||
<td>Docs site (docs.arcline.it) restructure</td>
|
||||
<td>
|
||||
|
||||
<span class="track-badge track-it">IT</span>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span class="status-pill status-backlog">Backlog</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><strong>Plinth</strong></td>
|
||||
<td>Referral program launch</td>
|
||||
<td>
|
||||
|
||||
<span class="track-badge track-it">IT</span>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span class="status-pill status-backlog">Backlog</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><strong>Abutment</strong></td>
|
||||
<td>Workstation (KDE) build target</td>
|
||||
<td>
|
||||
|
||||
<span class="track-badge track-os">OS</span>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span class="status-pill status-backlog">Backlog</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ================================================================= -->
|
||||
<!-- Board columns -->
|
||||
<!-- ================================================================= -->
|
||||
<div class="board">
|
||||
|
||||
<div class="column">
|
||||
<div class="column-header">
|
||||
<h2>Backlog</h2>
|
||||
<span class="column-count">9</span>
|
||||
</div>
|
||||
<ul class="card-list">
|
||||
|
||||
<li class="card track-os">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-os">OS</span>
|
||||
|
||||
|
||||
<span class="codename">Buttress</span>
|
||||
|
||||
</div>
|
||||
<p class="description">wire `arcline-audit` into CI for nightly CVE scans</p>
|
||||
</li>
|
||||
|
||||
<li class="card track-os">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-os">OS</span>
|
||||
|
||||
|
||||
<span class="codename">Rampart</span>
|
||||
|
||||
</div>
|
||||
<p class="description">finalize default-deny nftables ruleset, document opt-in exceptions</p>
|
||||
</li>
|
||||
|
||||
<li class="card track-os">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-os">OS</span>
|
||||
|
||||
|
||||
<span class="codename">Abutment</span>
|
||||
|
||||
</div>
|
||||
<p class="description">scope KDE Plasma workstation ISO target</p>
|
||||
</li>
|
||||
|
||||
<li class="card track-it">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-it">IT</span>
|
||||
|
||||
|
||||
<span class="codename">Lintel</span>
|
||||
|
||||
</div>
|
||||
<p class="description">restructure docs.arcline.it into per-tool sections</p>
|
||||
</li>
|
||||
|
||||
<li class="card track-it">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-it">IT</span>
|
||||
|
||||
|
||||
<span class="codename">Plinth</span>
|
||||
|
||||
</div>
|
||||
<p class="description">build referral program signup + tracking flow</p>
|
||||
</li>
|
||||
|
||||
<li class="card track-both">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-both">IT/OS</span>
|
||||
|
||||
|
||||
<span class="codename">Voussoir</span>
|
||||
|
||||
</div>
|
||||
<p class="description">spec `arcline-migrate` cPanel import mapping</p>
|
||||
</li>
|
||||
|
||||
<li class="card track-it">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-it">IT</span>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="description">Case studies page — draft first 2 client write-ups</p>
|
||||
</li>
|
||||
|
||||
<li class="card track-os">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-os">OS</span>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="description">Grafana dashboard pack — ship default node_exporter + Loki dashboards</p>
|
||||
</li>
|
||||
|
||||
<li class="card track-it">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-it">IT</span>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="description">Game server plans — add automated backup scheduling to portal</p>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<div class="column-header">
|
||||
<h2>In Progress</h2>
|
||||
<span class="column-count">4</span>
|
||||
</div>
|
||||
<ul class="card-list">
|
||||
|
||||
<li class="card track-it">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-it">IT</span>
|
||||
|
||||
|
||||
<span class="codename">Keystone</span>
|
||||
|
||||
</div>
|
||||
<p class="description">customer portal (`arcline-portal`) SSL + ticketing UI</p>
|
||||
</li>
|
||||
|
||||
<li class="card track-it">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-it">IT</span>
|
||||
|
||||
|
||||
<span class="codename">Keystone</span>
|
||||
|
||||
</div>
|
||||
<p class="description">Stripe billing (`arcline-billing`) subscription sync</p>
|
||||
</li>
|
||||
|
||||
<li class="card track-os">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-os">OS</span>
|
||||
|
||||
|
||||
<span class="codename">Cornerstone</span>
|
||||
|
||||
</div>
|
||||
<p class="description">ISO build pipeline (Debian Bookworm base)</p>
|
||||
</li>
|
||||
|
||||
<li class="card track-os">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-os">OS</span>
|
||||
|
||||
|
||||
<span class="codename">Cornerstone</span>
|
||||
|
||||
</div>
|
||||
<p class="description">btrfs snapshot rollback QA pass</p>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<div class="column-header">
|
||||
<h2>In Review</h2>
|
||||
<span class="column-count">2</span>
|
||||
</div>
|
||||
<ul class="card-list">
|
||||
|
||||
<li class="card track-it">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-it">IT</span>
|
||||
|
||||
|
||||
<span class="codename">Springer</span>
|
||||
|
||||
</div>
|
||||
<p class="description">status page generator (`arcline-status`) — awaiting Blake's sign-off</p>
|
||||
</li>
|
||||
|
||||
<li class="card track-os">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-os">OS</span>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="description">`arcline-dns` — multi-resolver propagation checker — code review open</p>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="column done">
|
||||
<div class="column-header">
|
||||
<h2>Done</h2>
|
||||
<span class="column-count">4</span>
|
||||
</div>
|
||||
<ul class="card-list">
|
||||
|
||||
<li class="card track-it checked">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-it">IT</span>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="description">Site relaunch — arcline.it copy + pricing page</p>
|
||||
</li>
|
||||
|
||||
<li class="card track-os checked">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-os">OS</span>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="description">Site launch — arclineproject.org landing page</p>
|
||||
</li>
|
||||
|
||||
<li class="card track-os checked">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-os">OS</span>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="description">Toolchain announcement — 11 Go tools listed publicly</p>
|
||||
</li>
|
||||
|
||||
<li class="card track-it checked">
|
||||
<div class="card-meta">
|
||||
|
||||
|
||||
<span class="track-badge track-it">IT</span>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="description">Acquired Saibeo Solutions, onboarded Ed Do as CTO</p>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ================================================================= -->
|
||||
<!-- Notes -->
|
||||
<!-- ================================================================= -->
|
||||
|
||||
<div class="notes">
|
||||
<h2>Notes</h2>
|
||||
<ul>
|
||||
|
||||
<li>Keep `[IT]` (arcline.it, revenue-facing) and `[OS]` (arclineproject.org, GPL/open-source)</li>
|
||||
|
||||
<li>New codenames go in the legend table before the card is created, not after.</li>
|
||||
|
||||
<li>Source repos live on `git.arcline.it` — link commits/PRs in card text once work starts.</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ================================================================= -->
|
||||
<!-- Footer -->
|
||||
<!-- ================================================================= -->
|
||||
<footer>
|
||||
Built with Python & Jinja2 ·
|
||||
<a href="https://arcline.it">arcline.it</a> ·
|
||||
<a href="https://arclineproject.org">arclineproject.org</a>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1037
src/main.rs
Normal file
1037
src/main.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -585,7 +585,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% set status_lower = item.status.lower().replace(' ', '') %}
|
||||
{% set status_lower = item.status | lower | replace(' ', '') %}
|
||||
<span class="status-pill status-{{ status_lower }}">{{ item.status }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -611,7 +611,7 @@
|
||||
</div>
|
||||
<ul class="card-list" id="list-{{ col.name | replace(' ', '-') | lower }}">
|
||||
{% for card in col.cards %}
|
||||
<li class="card track-{{ ('both' if '/' in card.track else card.track.lower()) }}{% if card.checked %} checked{% endif %}"
|
||||
<li class="card track-{{ ('both' if '/' in card.track else card.track | lower) }}{% if card.checked %} checked{% endif %}"
|
||||
data-id="{{ card.id }}"
|
||||
data-column="{{ col.name }}"
|
||||
draggable="true"
|
||||
@@ -623,7 +623,7 @@
|
||||
</div>
|
||||
<div class="card-meta">
|
||||
{% if card.track %}
|
||||
{% set track_class = 'both' if '/' in card.track else card.track.lower() %}
|
||||
{% set track_class = 'both' if '/' in card.track else card.track | lower %}
|
||||
<span class="track-badge track-{{ track_class }}">{{ card.track }}</span>
|
||||
{% endif %}
|
||||
{% if card.codename %}
|
||||
@@ -659,7 +659,7 @@
|
||||
<!-- Footer -->
|
||||
<!-- ================================================================= -->
|
||||
<footer>
|
||||
Built with Python & Jinja2 ·
|
||||
Built with Rust & Minijinja ·
|
||||
<a href="https://arcline.it">arcline.it</a> ·
|
||||
<a href="https://arclineproject.org">arclineproject.org</a>
|
||||
</footer>
|
||||
@@ -978,7 +978,7 @@ async function handleDrop(e) {
|
||||
if (sourceColumn === targetColumn) return;
|
||||
|
||||
try {
|
||||
await api(`/api/cards/${cardId}/move`, {
|
||||
await api(`/api/{{ board_slug }}/cards/${cardId}/move`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ column: targetColumn })
|
||||
|
||||
Reference in New Issue
Block a user