-16
menu/.sqlx/query-9eecf9b43e5458bc95fc45fe8e48d6da5edb50cdbf1e7a478faf310d6b9022ad.json
-16
menu/.sqlx/query-9eecf9b43e5458bc95fc45fe8e48d6da5edb50cdbf1e7a478faf310d6b9022ad.json
···
1
1
-
{
2
2
-
"db_name": "PostgreSQL",
3
3
-
"query": "INSERT INTO direct (\"from\", \"to\", \"owner\") VALUES ($1, $2, $3) ON CONFLICT (\"from\") DO UPDATE SET \"to\" = EXCLUDED.to, \"owner\" = EXCLUDED.owner",
4
4
-
"describe": {
5
5
-
"columns": [],
6
6
-
"parameters": {
7
7
-
"Left": [
8
8
-
"Varchar",
9
9
-
"Varchar",
10
10
-
"Varchar"
11
11
-
]
12
12
-
},
13
13
-
"nullable": []
14
14
-
},
15
15
-
"hash": "9eecf9b43e5458bc95fc45fe8e48d6da5edb50cdbf1e7a478faf310d6b9022ad"
16
16
-
}
+25
menu/.sqlx/query-b8fd68f2b748b100b6b594a1ce38ed07de8be8f5aefa059a2800b94f5b485a3d.json
+25
menu/.sqlx/query-b8fd68f2b748b100b6b594a1ce38ed07de8be8f5aefa059a2800b94f5b485a3d.json
···
1
1
+
{
2
2
+
"db_name": "PostgreSQL",
3
3
+
"query": "\n WITH insertion AS (\n INSERT INTO direct (\"from\", \"to\", \"owner\")\n VALUES ($1, $2, $3)\n ON CONFLICT (\"from\")\n DO UPDATE SET \"to\" = EXCLUDED.to, \"owner\" = EXCLUDED.owner WHERE direct.to = $4\n RETURNING direct.from\n )\n SELECT direct.to FROM direct\n WHERE direct.from NOT IN (SELECT insertion.from FROM insertion) AND direct.from = $1\n ",
4
4
+
"describe": {
5
5
+
"columns": [
6
6
+
{
7
7
+
"ordinal": 0,
8
8
+
"name": "to",
9
9
+
"type_info": "Varchar"
10
10
+
}
11
11
+
],
12
12
+
"parameters": {
13
13
+
"Left": [
14
14
+
"Varchar",
15
15
+
"Varchar",
16
16
+
"Varchar",
17
17
+
"Text"
18
18
+
]
19
19
+
},
20
20
+
"nullable": [
21
21
+
false
22
22
+
]
23
23
+
},
24
24
+
"hash": "b8fd68f2b748b100b6b594a1ce38ed07de8be8f5aefa059a2800b94f5b485a3d"
25
25
+
}
+3
menu/.sqlx/query-b8fd68f2b748b100b6b594a1ce38ed07de8be8f5aefa059a2800b94f5b485a3d.json.license
+3
menu/.sqlx/query-b8fd68f2b748b100b6b594a1ce38ed07de8be8f5aefa059a2800b94f5b485a3d.json.license
+15
menu/.sqlx/query-caeb329e3f6c545eac6589ce5bc0bff894842512f8278db02ed7a50c193a92ea.json
+15
menu/.sqlx/query-caeb329e3f6c545eac6589ce5bc0bff894842512f8278db02ed7a50c193a92ea.json
···
1
1
+
{
2
2
+
"db_name": "PostgreSQL",
3
3
+
"query": "DELETE FROM direct WHERE direct.from = $1 AND direct.to = $2",
4
4
+
"describe": {
5
5
+
"columns": [],
6
6
+
"parameters": {
7
7
+
"Left": [
8
8
+
"Text",
9
9
+
"Text"
10
10
+
]
11
11
+
},
12
12
+
"nullable": []
13
13
+
},
14
14
+
"hash": "caeb329e3f6c545eac6589ce5bc0bff894842512f8278db02ed7a50c193a92ea"
15
15
+
}
+3
menu/.sqlx/query-caeb329e3f6c545eac6589ce5bc0bff894842512f8278db02ed7a50c193a92ea.json.license
+3
menu/.sqlx/query-caeb329e3f6c545eac6589ce5bc0bff894842512f8278db02ed7a50c193a92ea.json.license
+127
menu/Cargo.lock
+127
menu/Cargo.lock
···
489
489
]
490
490
491
491
[[package]]
492
492
+
name = "html-escape"
493
493
+
version = "0.2.13"
494
494
+
source = "registry+https://github.com/rust-lang/crates.io-index"
495
495
+
checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
496
496
+
dependencies = [
497
497
+
"utf8-width",
498
498
+
]
499
499
+
500
500
+
[[package]]
492
501
name = "http"
493
502
version = "1.4.0"
494
503
source = "registry+https://github.com/rust-lang/crates.io-index"
···
520
529
"http-body",
521
530
"pin-project-lite",
522
531
]
532
532
+
533
533
+
[[package]]
534
534
+
name = "http-range-header"
535
535
+
version = "0.4.2"
536
536
+
source = "registry+https://github.com/rust-lang/crates.io-index"
537
537
+
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
523
538
524
539
[[package]]
525
540
name = "httparse"
···
673
688
]
674
689
675
690
[[package]]
691
691
+
name = "include_dir"
692
692
+
version = "0.7.4"
693
693
+
source = "registry+https://github.com/rust-lang/crates.io-index"
694
694
+
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
695
695
+
dependencies = [
696
696
+
"include_dir_macros",
697
697
+
]
698
698
+
699
699
+
[[package]]
700
700
+
name = "include_dir_macros"
701
701
+
version = "0.7.4"
702
702
+
source = "registry+https://github.com/rust-lang/crates.io-index"
703
703
+
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
704
704
+
dependencies = [
705
705
+
"proc-macro2",
706
706
+
"quote",
707
707
+
]
708
708
+
709
709
+
[[package]]
676
710
name = "indexmap"
677
711
version = "2.12.1"
678
712
source = "registry+https://github.com/rust-lang/crates.io-index"
···
788
822
version = "0.1.0"
789
823
dependencies = [
790
824
"axum",
825
825
+
"html-escape",
826
826
+
"include_dir",
827
827
+
"percent-encoding",
791
828
"serde",
792
829
"sqlx",
793
830
"tokio",
831
831
+
"tower",
794
832
"tower-http",
795
833
"tower-layer",
834
834
+
"tower-serve-static",
796
835
]
797
836
798
837
[[package]]
···
800
839
version = "0.3.17"
801
840
source = "registry+https://github.com/rust-lang/crates.io-index"
802
841
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
842
842
+
843
843
+
[[package]]
844
844
+
name = "mime_guess"
845
845
+
version = "2.0.5"
846
846
+
source = "registry+https://github.com/rust-lang/crates.io-index"
847
847
+
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
848
848
+
dependencies = [
849
849
+
"mime",
850
850
+
"unicase",
851
851
+
]
803
852
804
853
[[package]]
805
854
name = "mio"
···
907
956
version = "2.3.2"
908
957
source = "registry+https://github.com/rust-lang/crates.io-index"
909
958
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
959
959
+
960
960
+
[[package]]
961
961
+
name = "pin-project"
962
962
+
version = "1.1.10"
963
963
+
source = "registry+https://github.com/rust-lang/crates.io-index"
964
964
+
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
965
965
+
dependencies = [
966
966
+
"pin-project-internal",
967
967
+
]
968
968
+
969
969
+
[[package]]
970
970
+
name = "pin-project-internal"
971
971
+
version = "1.1.10"
972
972
+
source = "registry+https://github.com/rust-lang/crates.io-index"
973
973
+
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
974
974
+
dependencies = [
975
975
+
"proc-macro2",
976
976
+
"quote",
977
977
+
"syn",
978
978
+
]
910
979
911
980
[[package]]
912
981
name = "pin-project-lite"
···
1593
1662
]
1594
1663
1595
1664
[[package]]
1665
1665
+
name = "tokio-util"
1666
1666
+
version = "0.7.18"
1667
1667
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1668
1668
+
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
1669
1669
+
dependencies = [
1670
1670
+
"bytes",
1671
1671
+
"futures-core",
1672
1672
+
"futures-sink",
1673
1673
+
"pin-project-lite",
1674
1674
+
"tokio",
1675
1675
+
]
1676
1676
+
1677
1677
+
[[package]]
1596
1678
name = "tower"
1597
1679
version = "0.5.2"
1598
1680
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1616
1698
dependencies = [
1617
1699
"bitflags",
1618
1700
"bytes",
1701
1701
+
"futures-core",
1702
1702
+
"futures-util",
1619
1703
"http",
1704
1704
+
"http-body",
1705
1705
+
"http-body-util",
1706
1706
+
"http-range-header",
1707
1707
+
"httpdate",
1708
1708
+
"mime",
1709
1709
+
"mime_guess",
1710
1710
+
"percent-encoding",
1620
1711
"pin-project-lite",
1712
1712
+
"tokio",
1713
1713
+
"tokio-util",
1621
1714
"tower-layer",
1622
1715
"tower-service",
1716
1716
+
"tracing",
1623
1717
]
1624
1718
1625
1719
[[package]]
···
1629
1723
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
1630
1724
1631
1725
[[package]]
1726
1726
+
name = "tower-serve-static"
1727
1727
+
version = "0.1.1"
1728
1728
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1729
1729
+
checksum = "148022c3d604d85a3b558ef7154aa90aaec5f9506beae64f6ad4e856306d287f"
1730
1730
+
dependencies = [
1731
1731
+
"bytes",
1732
1732
+
"futures-util",
1733
1733
+
"http",
1734
1734
+
"http-body",
1735
1735
+
"http-body-util",
1736
1736
+
"include_dir",
1737
1737
+
"mime",
1738
1738
+
"mime_guess",
1739
1739
+
"percent-encoding",
1740
1740
+
"pin-project",
1741
1741
+
"tokio",
1742
1742
+
"tokio-util",
1743
1743
+
"tower-service",
1744
1744
+
]
1745
1745
+
1746
1746
+
[[package]]
1632
1747
name = "tower-service"
1633
1748
version = "0.3.3"
1634
1749
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1673
1788
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
1674
1789
1675
1790
[[package]]
1791
1791
+
name = "unicase"
1792
1792
+
version = "2.9.0"
1793
1793
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1794
1794
+
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
1795
1795
+
1796
1796
+
[[package]]
1676
1797
name = "unicode-bidi"
1677
1798
version = "0.3.18"
1678
1799
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1716
1837
"percent-encoding",
1717
1838
"serde",
1718
1839
]
1840
1840
+
1841
1841
+
[[package]]
1842
1842
+
name = "utf8-width"
1843
1843
+
version = "0.1.8"
1844
1844
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1845
1845
+
checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
1719
1846
1720
1847
[[package]]
1721
1848
name = "utf8_iter"
+6
-1
menu/Cargo.toml
+6
-1
menu/Cargo.toml
···
9
9
10
10
[dependencies]
11
11
axum = { version = "0.8.8", features = ["macros"] }
12
12
+
html-escape = "0.2.13"
13
13
+
include_dir = "0.7.4"
14
14
+
percent-encoding = "2.3.2"
12
15
serde = { version = "1.0.228", features = ["derive"] }
13
16
sqlx = { version = "0.8.6", features = ["runtime-tokio", "tls-rustls-ring-webpki", "postgres", "uuid", "macros"] }
14
17
tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread"] }
15
15
-
tower-http = { version = "0.6.8", features = ["normalize-path"] }
18
18
+
tower = "0.5.2"
19
19
+
tower-http = { version = "0.6.8", features = ["fs", "normalize-path"] }
16
20
tower-layer = "0.3.3"
21
21
+
tower-serve-static = "0.1.1"
+24
menu/src/html/create/conflict.html
+24
menu/src/html/create/conflict.html
···
1
1
+
<!--
2
2
+
SPDX-FileCopyrightText: 2026 Freshly Baked Cake
3
3
+
4
4
+
SPDX-License-Identifier: MIT
5
5
+
-->
6
6
+
7
7
+
<html>
8
8
+
<head>
9
9
+
<title>Add shortlink</title>
10
10
+
<link rel="stylesheet" href="/_/public/logo.css"/>
11
11
+
<link rel="stylesheet" href="/_/public/colors.css"/>
12
12
+
<link rel="stylesheet" href="/_/public/actions.css"/>
13
13
+
</head>
14
14
+
15
15
+
<body>
16
16
+
<div id="logo"><div><span><span class="failure">Couldn't create</span> shortlink</span><img src="/_/public/logo.svg"></div></div>
17
17
+
<p>You can't create a link from <a href="/{from:url}">{host}/{from}</a> to <a href="{to:attribute}">{to}</a> because <a href="/{from:url}">{host}/{from}</a> already goes to <a href="{current:attribute}">{current}</a>.</p>
18
18
+
<ul>
19
19
+
<li><a href="/_/create/do?from={from:url}&to={to:url}¤t={current:url}">Overwrite it</a></li>
20
20
+
<li><a href="/_/create?from={from:url}&to={to:url}">Back to editor</a></li>
21
21
+
<li><a href="/">All shortlinks</a></li>
22
22
+
</ul>
23
23
+
</body>
24
24
+
</html>
+23
menu/src/html/create/failure.html
+23
menu/src/html/create/failure.html
···
1
1
+
<!--
2
2
+
SPDX-FileCopyrightText: 2026 Freshly Baked Cake
3
3
+
4
4
+
SPDX-License-Identifier: MIT
5
5
+
-->
6
6
+
7
7
+
<html>
8
8
+
<head>
9
9
+
<title>Add shortlink</title>
10
10
+
<link rel="stylesheet" href="/_/public/logo.css"/>
11
11
+
<link rel="stylesheet" href="/_/public/colors.css"/>
12
12
+
<link rel="stylesheet" href="/_/public/actions.css"/>
13
13
+
</head>
14
14
+
15
15
+
<body>
16
16
+
<div id="logo"><div><span><span class="failure">Couldn't create</span> shortlink</span><img src="/_/public/logo.svg"></div></div>
17
17
+
<p>There was an internal error when creating your shortlink. You may try again. If you keep experiencing this error, please contact Minion or Coded.</p>
18
18
+
<ul>
19
19
+
<li><a href="/_/create?from={from:url}&to={to:url}">Back to editor</a></li>
20
20
+
<li><a href="/">All shortlinks</a></li>
21
21
+
</ul>
22
22
+
</body>
23
23
+
</html>
+43
menu/src/html/create/success.html
+43
menu/src/html/create/success.html
···
1
1
+
<!--
2
2
+
SPDX-FileCopyrightText: 2026 Freshly Baked Cake
3
3
+
4
4
+
SPDX-License-Identifier: MIT
5
5
+
-->
6
6
+
7
7
+
<html>
8
8
+
<head>
9
9
+
<title>Add shortlink</title>
10
10
+
<link rel="stylesheet" href="/_/public/logo.css"/>
11
11
+
<link rel="stylesheet" href="/_/public/colors.css"/>
12
12
+
<link rel="stylesheet" href="/_/public/actions.css"/>
13
13
+
</head>
14
14
+
15
15
+
<body>
16
16
+
<div id="logo"><div><span><span class="success">Created</span> shortlink</span><img src="/_/public/logo.svg"></div></div>
17
17
+
<p>Your new shortlink is up!</p>
18
18
+
<p><a href="/{from:url}">{host}/{from}</a> now goes to <a href="{to:attribute}">{to}</a></p>
19
19
+
<ul>
20
20
+
<li>Access it at <a href="/{from:url}">{host}/{from}</a> (<span id="copy" text="{host:attribute}/{from:attribute}">copy</span>)</li>
21
21
+
<li><a href="/_/create">Create another one</a></li>
22
22
+
<li><a href="/_/create?from={from:url}&to={to:url}¤t={to:url}">Edit it</a></li>
23
23
+
<li><a href="/_/delete/do?from={from:url}¤t={to:url}">Delete it</a></li>
24
24
+
<li><a href="/">All shortlinks</a></li>
25
25
+
</ul>
26
26
+
<script>
27
27
+
const copy = document.getElementById("copy");
28
28
+
29
29
+
copy.onclick = () => {
30
30
+
document.execCommand("copy");
31
31
+
};
32
32
+
33
33
+
copy.addEventListener("copy", event => {
34
34
+
event.preventDefault();
35
35
+
if (event.clipboardData) {
36
36
+
event.clipboardData.setData("text/plain", copy.getAttribute("text"));
37
37
+
copy.classList.add('complete');
38
38
+
copy.textContent = 'copied';
39
39
+
}
40
40
+
});
41
41
+
</script>
42
42
+
</body>
43
43
+
</html>
+13
-7
menu/src/html/create.html
+13
-7
menu/src/html/create.html
···
7
7
<html>
8
8
<head>
9
9
<title>Add shortlink</title>
10
10
+
<link rel="stylesheet" href="/_/public/logo.css"/>
11
11
+
<link rel="stylesheet" href="/_/public/forms.css"/>
10
12
</head>
11
13
12
14
<body>
13
13
-
<form action="" method="post">
14
14
-
<div>
15
15
-
<label for="from">When I go to {host}/</label><!--
16
16
-
--><input type="text" name="from" id="from" placeholder="kagi" value="{path}" required />
17
17
-
</div>
15
15
+
<div id="logo"><div><span>Create shortlink</span><img src="/_/public/logo.svg"></div></div>
16
16
+
<form action="/_/create/do" method="get">
18
17
<div>
19
19
-
<label for="to">Send me to </label>
20
20
-
<input type="text" name="to" id="to" placeholder="https://kagi.com" required />
18
18
+
<div>
19
19
+
<label for="from">When I go to {host}/</label><!--
20
20
+
--><input type="text" name="from" id="from" placeholder="kagi" value="{from:attribute}" required />...
21
21
+
</div>
22
22
+
<div>
23
23
+
...<label for="to">send me to </label>
24
24
+
<input type="url" name="to" id="to" value="{to:attribute}" placeholder="https://kagi.com" required />
25
25
+
</div>
21
26
</div>
27
27
+
<input type="hidden" name="current" id="current" value="{current:attribute}" />
22
28
<input type="submit" value="Add shortlink" />
23
29
</form>
24
30
</body>
+22
menu/src/html/delete/failure.html
+22
menu/src/html/delete/failure.html
···
1
1
+
<!--
2
2
+
SPDX-FileCopyrightText: 2026 Freshly Baked Cake
3
3
+
4
4
+
SPDX-License-Identifier: MIT
5
5
+
-->
6
6
+
7
7
+
<html>
8
8
+
<head>
9
9
+
<title>Add shortlink</title>
10
10
+
<link rel="stylesheet" href="/_/public/logo.css"/>
11
11
+
<link rel="stylesheet" href="/_/public/colors.css"/>
12
12
+
<link rel="stylesheet" href="/_/public/actions.css"/>
13
13
+
</head>
14
14
+
15
15
+
<body>
16
16
+
<div id="logo"><div><span><span class="failure">Couldn't delete</span> shortlink</span><img src="/_/public/logo.svg"></div></div>
17
17
+
<p>Shortlink deletions fail if the shortcut was changed before you asked for it to be deleted. Please try again from the "All shortlinks" page...</p>
18
18
+
<ul>
19
19
+
<li><a href="/">All shortlinks</a></li>
20
20
+
</ul>
21
21
+
</body>
22
22
+
</html>
+23
menu/src/html/delete/success.html
+23
menu/src/html/delete/success.html
···
1
1
+
<!--
2
2
+
SPDX-FileCopyrightText: 2026 Freshly Baked Cake
3
3
+
4
4
+
SPDX-License-Identifier: MIT
5
5
+
-->
6
6
+
7
7
+
<html>
8
8
+
<head>
9
9
+
<title>Add shortlink</title>
10
10
+
<link rel="stylesheet" href="/_/public/logo.css"/>
11
11
+
<link rel="stylesheet" href="/_/public/colors.css"/>
12
12
+
<link rel="stylesheet" href="/_/public/actions.css"/>
13
13
+
</head>
14
14
+
15
15
+
<body>
16
16
+
<div id="logo"><div><span><span class="success">Deleted</span> shortlink</span><img src="/_/public/logo.svg"></div></div>
17
17
+
<p><a href="/{from:url}">{host}/{from}</a> no longer goes to <a href="{current:attribute}">{current}</a></p>
18
18
+
<ul>
19
19
+
<li><a href="/_/create?from={from:url}&to={current:url}">Recreate it</a></li>
20
20
+
<li><a href="/">All shortlinks</a></li>
21
21
+
</ul>
22
22
+
</body>
23
23
+
</html>
+22
menu/src/html/public/actions.css
+22
menu/src/html/public/actions.css
···
1
1
+
/*
2
2
+
* SPDX-FileCopyrightText: 2026 Freshly Baked Cake
3
3
+
*
4
4
+
* SPDX-License-Identifier: MIT
5
5
+
*/
6
6
+
7
7
+
#copy {
8
8
+
text-decoration: underline;
9
9
+
color: blue;
10
10
+
11
11
+
&:hover {
12
12
+
cursor: pointer;
13
13
+
}
14
14
+
15
15
+
&.complete {
16
16
+
color: purple;
17
17
+
}
18
18
+
19
19
+
&:active {
20
20
+
color: red;
21
21
+
}
22
22
+
}
+13
menu/src/html/public/colors.css
+13
menu/src/html/public/colors.css
+52
menu/src/html/public/forms.css
+52
menu/src/html/public/forms.css
···
1
1
+
/*
2
2
+
* SPDX-FileCopyrightText: 2026 Freshly Baked Cake
3
3
+
*
4
4
+
* SPDX-License-Identifier: MIT
5
5
+
*/
6
6
+
7
7
+
form {
8
8
+
display: flex;
9
9
+
flex-direction: column;
10
10
+
align-items: center;
11
11
+
}
12
12
+
13
13
+
form > div {
14
14
+
padding: 0.25em;
15
15
+
}
16
16
+
17
17
+
form > div > div {
18
18
+
padding: 0.1em;
19
19
+
}
20
20
+
21
21
+
input {
22
22
+
border: 0.1em solid black;
23
23
+
border-radius: 0.25em;
24
24
+
height: 1.5em;
25
25
+
font-size: 1em;
26
26
+
}
27
27
+
28
28
+
input:invalid:not(:placeholder-shown):not(:focus), input:user-invalid {
29
29
+
border: 0.1em solid red;
30
30
+
}
31
31
+
32
32
+
form div:has(> input) {
33
33
+
&::after {
34
34
+
content: "";
35
35
+
height: 0.75em;
36
36
+
font-size: 0.75em;
37
37
+
display: block;
38
38
+
text-align: center;
39
39
+
}
40
40
+
41
41
+
&:has(:is(input:invalid:not(:placeholder-shown):not(:focus), input:user-invalid)) {
42
42
+
&::after {
43
43
+
height: auto;
44
44
+
content: "You must fill this out";
45
45
+
color: red;
46
46
+
}
47
47
+
48
48
+
&:has(input[type="url"])::after {
49
49
+
content: "You must enter a URL (including http:// or https://)";
50
50
+
}
51
51
+
}
52
52
+
}
+53
menu/src/html/public/logo.css
+53
menu/src/html/public/logo.css
···
1
1
+
/*
2
2
+
* SPDX-FileCopyrightText: 2026 Freshly Baked Cake
3
3
+
*
4
4
+
* SPDX-License-Identifier: MIT
5
5
+
*/
6
6
+
7
7
+
@import url('https://fonts.googleapis.com/css2?family=Delius&display=swap'); /* TODO: vendor this font */
8
8
+
9
9
+
html {
10
10
+
display: flex;
11
11
+
flex-direction: column;
12
12
+
align-items: center;
13
13
+
min-height: 100%;
14
14
+
width: 100%;
15
15
+
}
16
16
+
17
17
+
body {
18
18
+
display: flex;
19
19
+
flex-direction: column;
20
20
+
align-items: center;
21
21
+
margin-top: 0;
22
22
+
min-height: 100%;
23
23
+
width: 80%;
24
24
+
background: #e0faff;
25
25
+
font-size: 1.5rem;
26
26
+
font-family: sans-serif;
27
27
+
}
28
28
+
29
29
+
#logo {
30
30
+
background: #144f5f;
31
31
+
border-radius: 0 0 0.5rem 0.5rem;
32
32
+
height: 5rem;
33
33
+
display: flex;
34
34
+
justify-content: center;
35
35
+
width: 100%;
36
36
+
margin-bottom: 1rem;
37
37
+
}
38
38
+
39
39
+
#logo > div {
40
40
+
height: 100%;
41
41
+
}
42
42
+
43
43
+
#logo > div > span {
44
44
+
color: #64e4ff;
45
45
+
font-family: "Delius", cursive;
46
46
+
font-size: 3rem;
47
47
+
line-height: 5rem;
48
48
+
vertical-align: top;
49
49
+
}
50
50
+
51
51
+
#logo > div > img {
52
52
+
height: 100%;
53
53
+
}
+37
menu/src/html/public/logo.svg
+37
menu/src/html/public/logo.svg
···
1
1
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="2048" height="2048"><defs><linearGradient id="13594572358447137094" x1="1023.9999999999964" y1="1290.6144863048703" x2="1855.3785056818951" y2="1290.6144863048703" gradientTransform="matrix(0.001202822,0,0,0.001875367,-1.231689288,-1.920375772)"><stop stop-color="#64e3ff" /><stop offset="1" stop-color="#196b9f" /></linearGradient></defs><g>
2
2
+
<rect fill="#144e5e" x="0" y="0" width="2048" height="2048"/>
3
3
+
<g>
4
4
+
<g>
5
5
+
<path d="M1023.9999999999964,1023.9999999999859 C1023.9999999999964,1023.9999999999859 1147.2733646355885,1557.2289726097542 1147.2733646355885,1557.2289726097542 C1147.2733646355885,1557.2289726097542 1726.371496179532,1557.2289726097542 1726.371496179532,1557.2289726097542 C1726.371496179532,1557.2289726097542 1855.3785056818951,1023.9999999999859 1855.3785056818951,1023.9999999999859 C1855.3785056818951,1023.9999999999859 1023.9999999999964,1023.9999999999859 1023.9999999999964,1023.9999999999859 Z" fill="url('#13594572358447137094')"/>
6
6
+
</g>
7
7
+
<g>
8
8
+
<g transform="matrix(1,0,0,1,415.689252841,0)">
9
9
+
<path d="M659.0535042299737,1023.9999999999941 C659.0535042299737,1023.9999999999941 633.2521023295005,960.9299064655045 633.2521023295005,960.9299064655045 C633.2521023295005,960.9299064655045 659.0535042299732,889.2593456308584 659.0535042299732,889.2593456308584 C659.0535042299732,889.2593456308584 711.7114725606918,889.2593456308589 711.7114725606918,889.2593456308589 C711.7114725606918,889.2593456308589 689.6329435194218,826.1892520963702 689.6329435194218,826.1892520963702 C689.6329435194218,826.1892520963702 711.7114725606918,754.5186912617241 711.7114725606918,754.5186912617241 C711.7114725606918,754.5186912617241 784.36161869832,754.5186912617221 784.36161869832,754.5186912617221 C784.36161869832,754.5186912617221 767.4193922119493,691.4485977272336 767.4193922119493,691.4485977272336 C767.4193922119493,691.4485977272336 784.36161869832,619.7780368925883 784.36161869832,619.7780368925883 C784.36161869832,619.7780368925883 867.8557362803406,619.7780368925883 867.8557362803406,619.7780368925883 C867.8557362803406,619.7780368925883 856.8164717597062,556.7079433580998 856.8164717597062,556.7079433580998 C856.8164717597062,556.7079433580998 867.8557362803401,485.0373825234544 867.8557362803401,485.0373825234544 C867.8557362803401,485.0373825234544 1180.144263719638,485.0373825234544 1180.144263719638,485.0373825234544 C1180.144263719638,485.0373825234544 1191.183528240273,552.4077097080215 1191.183528240273,552.4077097080215 C1191.183528240273,552.4077097080215 1180.144263719638,619.7780368925883 1180.144263719638,619.7780368925883 C1180.144263719638,619.7780368925883 1263.6383813016655,619.7780368925883 1263.6383813016655,619.7780368925883 C1263.6383813016655,619.7780368925883 1280.580607788036,691.4485977272334 1280.580607788036,691.4485977272334 C1280.580607788036,691.4485977272334 1263.638381301666,754.5186912617221 1263.638381301666,754.5186912617221 C1263.638381301666,754.5186912617221 1336.288527439287,754.5186912617239 1336.288527439287,754.5186912617239 C1336.288527439287,754.5186912617239 1358.3670564805566,826.1892520963697 1358.3670564805566,826.1892520963697 C1358.3670564805566,826.1892520963697 1336.288527439287,889.2593456308589 1336.288527439287,889.2593456308589 C1336.288527439287,889.2593456308589 1388.946495770003,889.2593456308584 1388.946495770003,889.2593456308584 C1388.946495770003,889.2593456308584 1414.7478976704756,960.929906465504 1414.7478976704756,960.929906465504 C1414.7478976704756,960.929906465504 1388.946495770003,1023.9999999999934 1388.946495770003,1023.9999999999934 C1388.946495770003,1023.9999999999934 659.0535042299737,1023.9999999999941 659.0535042299737,1023.9999999999941 Z" fill="#fae59c"/>
10
10
+
</g>
11
11
+
<g transform="matrix(1,0,0,1,415.689252841,0)">
12
12
+
<g>
13
13
+
<path d="M856.8164717597062,556.7079433580998 C856.8164717597062,556.7079433580998 1191.183528240273,552.4077097080215 1191.183528240273,552.4077097080215 C1191.183528240273,552.4077097080215 1180.1442637196387,619.7780368925883 1180.1442637196387,619.7780368925883 C1180.1442637196387,619.7780368925883 867.8557362803406,619.7780368925883 867.8557362803406,619.7780368925883 C867.8557362803406,619.7780368925883 856.8164717597062,556.7079433580998 856.8164717597062,556.7079433580998 Z" fill="#e7cc6e"/>
14
14
+
</g>
15
15
+
<g>
16
16
+
<path d="M767.4193922119489,691.4485977272334 C767.4193922119489,691.4485977272334 1280.580607788036,691.4485977272332 1280.580607788036,691.4485977272332 C1280.580607788036,691.4485977272332 1263.6383813016655,754.5186912617219 1263.6383813016655,754.5186912617219 C1263.6383813016655,754.5186912617219 784.36161869832,754.5186912617223 784.36161869832,754.5186912617221 C784.3616186983197,754.5186912617219 767.4193922119489,691.4485977272334 767.4193922119489,691.4485977272334 Z" fill="#e7cc6e"/>
17
17
+
</g>
18
18
+
<g>
19
19
+
<path d="M689.6329435194216,826.1892520963697 C689.6329435194216,826.1892520963697 1358.3670564805561,826.1892520963695 1358.3670564805561,826.1892520963695 C1358.3670564805561,826.1892520963695 1336.2885274392868,889.2593456308587 1336.2885274392868,889.2593456308587 C1336.2885274392868,889.2593456308587 711.7114725606916,889.2593456308589 711.7114725606916,889.2593456308589 C711.7114725606916,889.2593456308589 689.6329435194216,826.1892520963697 689.6329435194216,826.1892520963697 Z" fill="#e7cc6e"/>
20
20
+
</g>
21
21
+
<g>
22
22
+
<path d="M633.2521023295001,960.929906465504 C633.2521023295001,960.929906465504 1414.7478976704751,960.9299064655036 1414.7478976704751,960.9299064655036 C1414.7478976704751,960.9299064655036 1388.9464957700027,1023.9999999999934 1388.9464957700027,1023.9999999999934 C1388.9464957700027,1023.9999999999934 659.0535042299734,1023.9999999999936 659.0535042299734,1023.9999999999936 C659.0535042299734,1023.9999999999936 633.2521023295001,960.929906465504 633.2521023295001,960.929906465504 Z" fill="#e7cc6e"/>
23
23
+
</g>
24
24
+
</g>
25
25
+
</g>
26
26
+
<g>
27
27
+
<g>
28
28
+
<path d="M122.52614469399941,1023.9999999999859 C122.52614469399941,1023.9999999999859 893.8839192448595,1023.9999999999859 893.8839192448595,1023.9999999999859" fill="none" stroke="#64e4ff" stroke-width="50" stroke-linecap="round" stroke-linejoin="round"/>
29
29
+
</g>
30
30
+
<g>
31
31
+
<path d="M893.8839192448594,1024 C893.8839192448594,1024 656.0843589301605,1261.8 656.0843589301605,1261.8" fill="none" stroke="#64e4ff" stroke-width="50" stroke-linecap="round" stroke-linejoin="round"/>
32
32
+
</g>
33
33
+
<g>
34
34
+
<path d="M893.8839192448594,1023.9999999999858 C893.8839192448594,1023.9999999999858 656.0843589301605,786.2004396852869 656.0843589301605,786.2004396852869" fill="none" stroke="#64e4ff" stroke-width="50" stroke-linecap="round" stroke-linejoin="round"/>
35
35
+
</g>
36
36
+
</g>
37
37
+
</g></g></svg>
+3
menu/src/html/public/logo.svg.license
+3
menu/src/html/public/logo.svg.license
+334
-54
menu/src/main.rs
+334
-54
menu/src/main.rs
···
1
1
// SPDX-FileCopyrightText: 2026 Freshly Baked Cake
2
2
//
3
3
// SPDX-License-Identifier: MIT
4
4
-
5
4
use axum::{
6
6
-
Form, Router, ServiceExt,
5
5
+
Router, ServiceExt,
7
6
body::Body,
8
7
extract::{Path, Query, Request},
9
8
http::{HeaderMap, Response, StatusCode},
10
10
-
response::{Html, IntoResponse, Redirect},
11
11
-
routing::{get, post},
9
9
+
response::{ErrorResponse, Html, IntoResponse, Redirect, Result},
10
10
+
routing::get,
12
11
};
13
13
-
use serde::Deserialize;
12
12
+
use include_dir::{Dir, include_dir};
13
13
+
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
14
14
use sqlx::{Connection, PgConnection};
15
15
+
16
16
+
#[cfg(debug_assertions)]
17
17
+
use std::fs;
18
18
+
15
19
use std::{collections::HashMap, env, ops::DerefMut, sync::OnceLock};
16
20
use tokio::{
17
21
sync::Mutex,
18
22
time::{Duration, sleep},
19
23
};
20
20
-
use tower_http::normalize_path::NormalizePathLayer;
24
24
+
use tower_http::{self, normalize_path::NormalizePathLayer};
21
25
use tower_layer::Layer;
26
26
+
use tower_serve_static;
27
27
+
28
28
+
static PUBLIC_DIR: Dir<'static> = include_dir!("src/html/public");
22
29
23
23
-
fn template_html(html: &str, replacements: &HashMap<&str, Option<&str>>) -> String {
24
24
-
let mut result = html.to_owned();
25
25
-
for (&text, &maybe_replacement) in replacements {
26
26
-
if let Some(replacement) = maybe_replacement {
27
27
-
result = result.replace(&("{".to_owned() + text + "}"), replacement)
28
28
-
}
30
30
+
#[cfg(debug_assertions)]
31
31
+
static DEVELOPMENT: OnceLock<bool> = OnceLock::new();
32
32
+
33
33
+
fn template_html<'a>(
34
34
+
mut html: String,
35
35
+
replacements: HashMap<&str, Box<dyn 'a + FnOnce() -> Option<&'a str>>>,
36
36
+
) -> String {
37
37
+
for (text, maybe_replacement) in replacements {
38
38
+
let replacement = maybe_replacement().unwrap_or_else(|| "");
39
39
+
html = html.replace(
40
40
+
&("{".to_owned() + text + "}"),
41
41
+
&html_escape::encode_text(replacement),
42
42
+
);
43
43
+
html = html.replace(
44
44
+
&("{".to_owned() + text + ":attribute}"),
45
45
+
&html_escape::encode_quoted_attribute(replacement),
46
46
+
);
47
47
+
html = html.replace(
48
48
+
&("{".to_owned() + text + ":url}"),
49
49
+
&utf8_percent_encode(replacement, NON_ALPHANUMERIC).to_string(),
50
50
+
);
29
51
}
30
52
31
31
-
result
53
53
+
html
54
54
+
}
55
55
+
56
56
+
/// include_str, but if DEVELOPMENT then the string is dynamically fetched for easy reloading
57
57
+
/// to support this, the string is *always* owned.
58
58
+
#[cfg(debug_assertions)]
59
59
+
macro_rules! include_String_dynamic {
60
60
+
($file:expr $(,)?) => {
61
61
+
if (*DEVELOPMENT.get().unwrap()) {
62
62
+
fs::read_to_string("src/".to_string() + $file)
63
63
+
.expect(format!("Unable to read file {}", $file).as_str())
64
64
+
} else {
65
65
+
include_str!($file).to_owned()
66
66
+
}
67
67
+
};
68
68
+
}
69
69
+
#[cfg(not(debug_assertions))]
70
70
+
macro_rules! include_String_dynamic {
71
71
+
($file:expr $(,)?) => {
72
72
+
include_str!($file).to_owned()
73
73
+
};
32
74
}
33
75
34
76
#[derive(Debug)]
···
76
118
}
77
119
78
120
async fn get_redirect_base(go: &str) -> Redirect {
79
79
-
get_redirect("/_/create?path=", go).await
121
121
+
get_redirect("/_/create?from=", go).await
80
122
}
81
123
82
124
async fn get_redirect_search(go: &str) -> Redirect {
83
125
get_redirect("https://kagi.com/search?q=", go).await
84
126
}
85
127
86
86
-
async fn handle_root() -> String {
87
87
-
"Hello, world!".to_string()
128
128
+
async fn handle_root(
129
129
+
headers: HeaderMap,
130
130
+
#[cfg(debug_assertions)] Query(params): Query<HashMap<String, String>>,
131
131
+
) -> Result<String> {
132
132
+
ensure_authenticated(
133
133
+
&headers,
134
134
+
#[cfg(debug_assertions)]
135
135
+
¶ms,
136
136
+
)?;
137
137
+
Ok("Hello, world!".to_string())
88
138
}
89
139
90
140
async fn handle_base(Path(go): Path<String>) -> Redirect {
···
99
149
}
100
150
}
101
151
152
152
+
#[derive(Clone)]
153
153
+
enum StaticPageType {
154
154
+
Create,
155
155
+
CreateSuccess,
156
156
+
CreateConflict,
157
157
+
CreateFailure,
158
158
+
DeleteSuccess,
159
159
+
DeleteFailure,
160
160
+
}
161
161
+
162
162
+
async fn handle_static_page<'a>(
163
163
+
page_type: StaticPageType,
164
164
+
params: &'a HashMap<String, String>,
165
165
+
headers: &'a HeaderMap,
166
166
+
) -> Result<Html<String>> {
167
167
+
let auth_required = match page_type {
168
168
+
_ => true,
169
169
+
};
170
170
+
171
171
+
let html = match page_type {
172
172
+
StaticPageType::Create => include_String_dynamic!("./html/create.html"),
173
173
+
StaticPageType::CreateSuccess => include_String_dynamic!("./html/create/success.html"),
174
174
+
StaticPageType::CreateConflict => include_String_dynamic!("./html/create/conflict.html"),
175
175
+
StaticPageType::CreateFailure => include_String_dynamic!("./html/create/failure.html"),
176
176
+
StaticPageType::DeleteSuccess => include_String_dynamic!("./html/delete/success.html"),
177
177
+
StaticPageType::DeleteFailure => include_String_dynamic!("./html/delete/failure.html"),
178
178
+
};
179
179
+
180
180
+
let username = if auth_required {
181
181
+
Some(ensure_authenticated(
182
182
+
headers,
183
183
+
#[cfg(debug_assertions)]
184
184
+
params,
185
185
+
)?)
186
186
+
} else {
187
187
+
None
188
188
+
};
189
189
+
190
190
+
let mut replacements: HashMap<&str, Box<dyn 'a + FnOnce() -> Option<&'a str>>> = HashMap::new();
191
191
+
replacements.insert(
192
192
+
"host",
193
193
+
Box::new(|| {
194
194
+
Some(clean_host(
195
195
+
headers
196
196
+
.get("host")
197
197
+
.and_then(|header| Some(header.to_str().unwrap_or_else(|_| "go")))
198
198
+
.unwrap_or_else(|| "go"),
199
199
+
))
200
200
+
}),
201
201
+
);
202
202
+
replacements.insert(
203
203
+
"from",
204
204
+
Box::new(|| params.get("from").and_then(|from| Some(from.as_str()))),
205
205
+
);
206
206
+
replacements.insert(
207
207
+
"to",
208
208
+
Box::new(|| params.get("to").and_then(|to| Some(to.as_str()))),
209
209
+
);
210
210
+
replacements.insert(
211
211
+
"current",
212
212
+
Box::new(|| {
213
213
+
params
214
214
+
.get("current")
215
215
+
.and_then(|current| Some(current.as_str()))
216
216
+
}),
217
217
+
);
218
218
+
replacements.insert("username", Box::new(move || username));
219
219
+
220
220
+
let result = template_html(html, replacements);
221
221
+
Ok(Html(result))
222
222
+
}
223
223
+
102
224
async fn handle_create_page(
103
225
Query(params): Query<HashMap<String, String>>,
104
226
headers: HeaderMap,
105
105
-
) -> Html<String> {
106
106
-
Html(template_html(
107
107
-
include_str!("./html/create.html"),
108
108
-
&HashMap::from([
109
109
-
(
110
110
-
"host",
111
111
-
Some(clean_host(
112
112
-
headers
113
113
-
.get("host")
114
114
-
.and_then(|header| Some(header.to_str().unwrap_or_else(|_| "go")))
115
115
-
.unwrap_or_else(|| "go"),
116
116
-
)),
117
117
-
),
118
118
-
(
119
119
-
"path",
120
120
-
params.get("path").and_then(|path| Some(path.as_str())),
121
121
-
),
122
122
-
]),
123
123
-
))
227
227
+
) -> Result<Html<String>> {
228
228
+
handle_static_page(StaticPageType::Create, ¶ms, &headers).await
229
229
+
}
230
230
+
async fn handle_create_success_page(
231
231
+
Query(params): Query<HashMap<String, String>>,
232
232
+
headers: HeaderMap,
233
233
+
) -> Result<Html<String>> {
234
234
+
handle_static_page(StaticPageType::CreateSuccess, ¶ms, &headers).await
235
235
+
}
236
236
+
async fn handle_create_conflict_page(
237
237
+
Query(params): Query<HashMap<String, String>>,
238
238
+
headers: HeaderMap,
239
239
+
) -> Result<Html<String>> {
240
240
+
handle_static_page(StaticPageType::CreateConflict, ¶ms, &headers).await
241
241
+
}
242
242
+
async fn handle_create_failure_page(
243
243
+
Query(params): Query<HashMap<String, String>>,
244
244
+
headers: HeaderMap,
245
245
+
) -> Result<impl IntoResponse> {
246
246
+
handle_static_page(StaticPageType::CreateFailure, ¶ms, &headers)
247
247
+
.await
248
248
+
.and_then(|html| Ok((StatusCode::INTERNAL_SERVER_ERROR, html)))
249
249
+
}
250
250
+
async fn handle_delete_success_page(
251
251
+
Query(params): Query<HashMap<String, String>>,
252
252
+
headers: HeaderMap,
253
253
+
) -> Result<Html<String>> {
254
254
+
handle_static_page(StaticPageType::DeleteSuccess, ¶ms, &headers).await
255
255
+
}
256
256
+
async fn handle_delete_failure_page(
257
257
+
Query(params): Query<HashMap<String, String>>,
258
258
+
headers: HeaderMap,
259
259
+
) -> Result<Html<String>> {
260
260
+
handle_static_page(StaticPageType::DeleteFailure, ¶ms, &headers).await
124
261
}
125
262
126
126
-
#[derive(Deserialize)]
127
127
-
struct Create {
128
128
-
from: String,
129
129
-
to: String,
263
263
+
struct NotAuthenticated;
264
264
+
impl IntoResponse for NotAuthenticated {
265
265
+
fn into_response(self) -> axum::response::Response {
266
266
+
return (StatusCode::UNAUTHORIZED, "Access over Tailscale only").into_response();
267
267
+
}
268
268
+
}
269
269
+
270
270
+
fn ensure_authenticated<'a>(
271
271
+
headers: &'a HeaderMap,
272
272
+
#[cfg(debug_assertions)] params: &'a HashMap<String, String>,
273
273
+
) -> Result<&'a str, ErrorResponse> {
274
274
+
if let Some(user) = headers
275
275
+
.get("X-Webauth-Login")
276
276
+
.and_then(|header| header.to_str().ok())
277
277
+
{
278
278
+
return Ok(user);
279
279
+
}
280
280
+
281
281
+
#[cfg(debug_assertions)]
282
282
+
{
283
283
+
if *DEVELOPMENT.get().unwrap() {
284
284
+
if let Some(user) = params.get("dev_auth_as") {
285
285
+
return Ok(user);
286
286
+
}
287
287
+
}
288
288
+
}
289
289
+
290
290
+
Err(NotAuthenticated {}.into())
130
291
}
131
292
132
293
#[axum::debug_handler]
133
133
-
async fn handle_create_post(headers: HeaderMap, Form(create): Form<Create>) -> Response<Body> {
134
134
-
let Some(Ok(owner)) = headers
135
135
-
.get("X-Webauth-Login")
136
136
-
.and_then(|header| Some(header.to_str()))
137
137
-
else {
138
138
-
return (StatusCode::UNAUTHORIZED, "Access over Tailscale only").into_response();
139
139
-
};
294
294
+
async fn handle_create_do(
295
295
+
headers: HeaderMap,
296
296
+
Query(params): Query<HashMap<String, String>>,
297
297
+
) -> Result<Response<Body>> {
298
298
+
let owner = ensure_authenticated(
299
299
+
&headers,
300
300
+
#[cfg(debug_assertions)]
301
301
+
¶ms,
302
302
+
)?;
303
303
+
304
304
+
let from = params.get("from").ok_or("Missing from query")?;
305
305
+
let to = params.get("to").ok_or("Missing to query")?;
306
306
+
307
307
+
println!("Attempting to make go/{} -> {}", from, to);
140
308
141
309
let create_call = sqlx::query!(
142
142
-
r#"INSERT INTO direct ("from", "to", "owner") VALUES ($1, $2, $3) ON CONFLICT ("from") DO UPDATE SET "to" = EXCLUDED.to, "owner" = EXCLUDED.owner"#,
143
143
-
create.from.to_lowercase(),
144
144
-
create.to,
310
310
+
r#"
311
311
+
WITH insertion AS (
312
312
+
INSERT INTO direct ("from", "to", "owner")
313
313
+
VALUES ($1, $2, $3)
314
314
+
ON CONFLICT ("from")
315
315
+
DO UPDATE SET "to" = EXCLUDED.to, "owner" = EXCLUDED.owner WHERE direct.to = $4
316
316
+
RETURNING direct.from
317
317
+
)
318
318
+
SELECT direct.to FROM direct
319
319
+
WHERE direct.from NOT IN (SELECT insertion.from FROM insertion) AND direct.from = $1
320
320
+
"#, // Insert our URL, return a row with the same from that weren't updated (i.e. a conflict)
321
321
+
from.to_lowercase(),
322
322
+
to,
145
323
owner,
324
324
+
params.get("current"),
325
325
+
)
326
326
+
.fetch_optional(
327
327
+
STATE
328
328
+
.get()
329
329
+
.expect("Server must be initialized before processing connections")
330
330
+
.sqlx_connection
331
331
+
.lock()
332
332
+
.await
333
333
+
.deref_mut(),
334
334
+
)
335
335
+
.await;
336
336
+
337
337
+
if let Ok(None) = &create_call {
338
338
+
Ok(Redirect::to(&format!(
339
339
+
"/_/create/success?from={}&to={}",
340
340
+
utf8_percent_encode(&from, NON_ALPHANUMERIC).to_string(),
341
341
+
utf8_percent_encode(&to, NON_ALPHANUMERIC).to_string(),
342
342
+
))
343
343
+
.into_response())
344
344
+
} else if let Ok(Some(conflict)) = create_call {
345
345
+
Ok(Redirect::to(&format!(
346
346
+
"/_/create/conflict?from={}&to={}¤t={}",
347
347
+
utf8_percent_encode(&from, NON_ALPHANUMERIC).to_string(),
348
348
+
utf8_percent_encode(&to, NON_ALPHANUMERIC).to_string(),
349
349
+
utf8_percent_encode(&conflict.to, NON_ALPHANUMERIC).to_string(),
350
350
+
))
351
351
+
.into_response())
352
352
+
} else {
353
353
+
Ok(Redirect::to(&format!(
354
354
+
"/_/create/failure?from={}&to={}",
355
355
+
utf8_percent_encode(&from, NON_ALPHANUMERIC).to_string(),
356
356
+
utf8_percent_encode(&to, NON_ALPHANUMERIC).to_string(),
357
357
+
))
358
358
+
.into_response())
359
359
+
}
360
360
+
}
361
361
+
362
362
+
#[axum::debug_handler]
363
363
+
async fn handle_delete_do(
364
364
+
headers: HeaderMap,
365
365
+
Query(params): Query<HashMap<String, String>>,
366
366
+
) -> Result<Response<Body>> {
367
367
+
ensure_authenticated(
368
368
+
&headers,
369
369
+
#[cfg(debug_assertions)]
370
370
+
¶ms,
371
371
+
)?;
372
372
+
373
373
+
let from = params.get("from").ok_or("Missing from query")?;
374
374
+
let current = params.get("current").ok_or("Missing current query")?;
375
375
+
376
376
+
println!("Attempting to delete go/{} -> {}", from, current);
377
377
+
378
378
+
let delete_call = sqlx::query!(
379
379
+
r#"DELETE FROM direct WHERE direct.from = $1 AND direct.to = $2"#,
380
380
+
from.to_lowercase(),
381
381
+
current,
146
382
)
147
383
.execute(
148
384
STATE
···
155
391
)
156
392
.await;
157
393
158
158
-
if create_call.is_ok() {
159
159
-
Redirect::to(&format!("/_/created?path={}", create.from)).into_response()
394
394
+
if let Ok(delete_result) = &delete_call
395
395
+
&& delete_result.rows_affected() > 0
396
396
+
{
397
397
+
Ok(Redirect::to(&format!(
398
398
+
"/_/delete/success?from={}¤t={}",
399
399
+
utf8_percent_encode(&from, NON_ALPHANUMERIC).to_string(),
400
400
+
utf8_percent_encode(¤t, NON_ALPHANUMERIC).to_string(),
401
401
+
))
402
402
+
.into_response())
160
403
} else {
161
161
-
Redirect::to(&format!("/_/createFailed?path={}", create.from)).into_response()
404
404
+
Ok(Redirect::to(&format!(
405
405
+
"/_/delete/failure?from={}&to={}",
406
406
+
utf8_percent_encode(&from, NON_ALPHANUMERIC).to_string(),
407
407
+
utf8_percent_encode(¤t, NON_ALPHANUMERIC).to_string(),
408
408
+
))
409
409
+
.into_response())
162
410
}
163
411
}
164
412
···
168
416
169
417
#[tokio::main]
170
418
async fn main() {
419
419
+
#[cfg(debug_assertions)]
420
420
+
{
421
421
+
DEVELOPMENT
422
422
+
.set(env::var("DEVELOPMENT").is_ok_and(|value| value == "true"))
423
423
+
.unwrap();
424
424
+
}
171
425
let mut connection = {
172
426
let mut maybe_connection;
173
427
let mut tries = 3;
···
205
459
})
206
460
.expect("Consistency issue: failed to set STATE - was it already set?");
207
461
208
208
-
let router = Router::new()
462
462
+
let mut router = Router::new();
463
463
+
464
464
+
#[cfg(not(debug_assertions))]
465
465
+
{
466
466
+
router = router.nest_service("/_/public", tower_serve_static::ServeDir::new(&PUBLIC_DIR));
467
467
+
}
468
468
+
469
469
+
#[cfg(debug_assertions)]
470
470
+
{
471
471
+
if *DEVELOPMENT.get().unwrap() {
472
472
+
router = router.nest_service(
473
473
+
"/_/public",
474
474
+
tower_http::services::ServeDir::new("src/html/public"),
475
475
+
);
476
476
+
} else {
477
477
+
router =
478
478
+
router.nest_service("/_/public", tower_serve_static::ServeDir::new(&PUBLIC_DIR));
479
479
+
}
480
480
+
}
481
481
+
482
482
+
router = router
209
483
.route("/", get(handle_root))
210
484
.route("/_/create", get(handle_create_page))
211
211
-
.route("/_/create", post(handle_create_post))
485
485
+
.route("/_/create/success", get(handle_create_success_page))
486
486
+
.route("/_/create/conflict", get(handle_create_conflict_page))
487
487
+
.route("/_/create/failure", get(handle_create_failure_page))
488
488
+
.route("/_/create/do", get(handle_create_do))
489
489
+
.route("/_/delete/do", get(handle_delete_do))
490
490
+
.route("/_/delete/success", get(handle_delete_success_page))
491
491
+
.route("/_/delete/failure", get(handle_delete_failure_page))
212
492
.route("/_/search", get(handle_search))
213
493
.route("/_/{*route}", get(handle_404))
214
494
.route("/{*go}", get(handle_base));
+3
-14
packetmix/homes/coded/niri.nix
+3
-14
packetmix/homes/coded/niri.nix
···
22
22
"DP-1" = {
23
23
position = {
24
24
x = 5760;
25
25
-
y = 2160;
25
25
+
y = 0;
26
26
};
27
27
mode = {
28
28
width = 3840;
···
34
34
"DP-2" = {
35
35
position = {
36
36
x = 1920;
37
37
-
y = 2160;
37
37
+
y = 0;
38
38
};
39
39
mode = {
40
40
width = 3840;
···
43
43
};
44
44
scale = 1;
45
45
};
46
46
-
"LG Electronics LG TV SSCR2 0x01010101" = {
47
47
-
position = {
48
48
-
x = 1920;
49
49
-
y = 0;
50
50
-
};
51
51
-
mode = {
52
52
-
width = 3840;
53
53
-
height = 2160;
54
54
-
refresh = 60.;
55
55
-
};
56
56
-
};
57
46
"Dell Inc. DELL S2422HG BTTCK83" = {
58
47
position = {
59
48
x = 0;
60
60
-
y = 2700;
49
49
+
y = 540;
61
50
};
62
51
mode = {
63
52
width = 1920;
+6
packetmix/systems/shorthair/flatpak.nix
+6
packetmix/systems/shorthair/flatpak.nix
+26
-1
packetmix/systems/teal/fava.nix
+26
-1
packetmix/systems/teal/fava.nix
···
40
40
plugin "fava.plugins.link_documents"
41
41
42
42
plugin "beancount.plugins.pedantic"
43
43
-
plugin "beancount.plugins.unrealized" "Unrealized"
43
43
+
plugin "beancount.plugins.implicit_prices"
44
44
+
45
45
+
plugin "beancount_share.share" "{
46
46
+
'mark_name': 'share',
47
47
+
'meta_name': 'shared',
48
48
+
'account_debtors': 'Assets:People',
49
49
+
'account_creditors': 'Liabilities:People',
50
50
+
'open_date': None,
51
51
+
'quantize': '0.01'
52
52
+
}"
53
53
+
'';
54
54
+
}
55
55
+
{
56
56
+
slug = "hyperneutrino";
57
57
+
name = "HyperNeutrino";
58
58
+
beancountOptions.operating_currency = "CAD";
59
59
+
favaOptions = {
60
60
+
invert-income-liabilities-equity = "true";
61
61
+
auto-reload = "true";
62
62
+
fiscal-year-end = "03-31";
63
63
+
};
64
64
+
extraConfig = ''
65
65
+
plugin "fava.plugins.tag_discovered_documents"
66
66
+
plugin "fava.plugins.link_documents"
67
67
+
68
68
+
plugin "beancount.plugins.pedantic"
44
69
plugin "beancount.plugins.implicit_prices"
45
70
46
71
plugin "beancount_share.share" "{