Skip to content

Commit 894a50c

Browse files
committed
chore: estate-wide metadata and submodule sync (2026-04-20)
1 parent 8507330 commit 894a50c

72 files changed

Lines changed: 5948 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"cmt_scan":[{"also_scan_build_root":true,"build_root":"lib/bs","scan_dirs":["src"]}],"dirs":["src"],"generated":[],"pkgs":[["@rescript/react","/var/mnt/eclipse/repos/nextgen-databases/quandledb/frontend/node_modules/.deno/@rescript+react@0.14.2/node_modules/@rescript/react"]],"version":2}

quandledb/frontend/lib/bs/build.ninja

Whitespace-only changes.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"version": "12.2.0",
3+
"bsc_path": "/var/mnt/eclipse/repos/nextgen-databases/quandledb/frontend/node_modules/.deno/@rescript+linux-x64@12.2.0/node_modules/@rescript/linux-x64/bin/bsc.exe",
4+
"bsc_hash": "ca03c46847ff55e2a2115049975f546b655fceff6fc10e34645da2e812314070",
5+
"rescript_config_hash": "79ad88940d912192046b850f2bd286dcfe87c6153de832d852da542baaea544c",
6+
"runtime_path": "/var/mnt/eclipse/repos/nextgen-databases/quandledb/frontend/node_modules/.deno/@rescript+runtime@12.2.0/node_modules/@rescript/runtime",
7+
"generated_at": "1775993930151"
8+
}
15.3 KB
Binary file not shown.
183 Bytes
Binary file not shown.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
4+
// HTTP API client for QuandleDB server
5+
6+
let baseUrl = ""
7+
8+
// Type-safe Fetch API bindings (no Obj.magic)
9+
type fetchResponse
10+
@val external fetch: (string, 'a) => promise<fetchResponse> = "fetch"
11+
@get external responseOk: fetchResponse => bool = "ok"
12+
@get external responseStatus: fetchResponse => int = "status"
13+
@send external responseJson: fetchResponse => promise<Js.Json.t> = "json"
14+
@send external responseText: fetchResponse => promise<string> = "text"
15+
16+
let fetchJson = async (url: string): result<Js.Json.t, string> => {
17+
try {
18+
let resp = await fetch(url, {"method": "GET"})
19+
if responseOk(resp) {
20+
let data = await responseJson(resp)
21+
Ok(data)
22+
} else {
23+
let body = await responseText(resp)
24+
Error(`HTTP ${Belt.Int.toString(responseStatus(resp))}: ${body}`)
25+
}
26+
} catch {
27+
| exn => Error(Js.String2.make(exn))
28+
}
29+
}
30+
31+
let fetchKnots = async (~filters: Types.filters=Types.emptyFilters): result<
32+
Types.knotListResponse,
33+
string,
34+
> => {
35+
let params = []
36+
37+
switch filters.crossingNumber {
38+
| Some(cn) =>
39+
let _ = Js.Array2.push(params, "crossing_number=" ++ Belt.Int.toString(cn))
40+
| None => ()
41+
}
42+
43+
switch filters.genus {
44+
| Some(g) =>
45+
let _ = Js.Array2.push(params, "genus=" ++ Belt.Int.toString(g))
46+
| None => ()
47+
}
48+
49+
if filters.nameSearch != "" {
50+
let _ = Js.Array2.push(
51+
params,
52+
"name=" ++ Js.Global.encodeURIComponent(filters.nameSearch),
53+
)
54+
}
55+
56+
let queryStr = if Belt.Array.length(params) > 0 {
57+
"?" ++ Js.Array2.joinWith(params, "&")
58+
} else {
59+
""
60+
}
61+
62+
let result = await fetchJson(baseUrl ++ "/api/knots" ++ queryStr)
63+
switch result {
64+
| Ok(json) => Decoders.decodeKnotList(json)
65+
| Error(e) => Error(e)
66+
}
67+
}
68+
69+
let fetchKnot = async (name: string): result<Types.knot, string> => {
70+
let result = await fetchJson(
71+
baseUrl ++ "/api/knots/" ++ Js.Global.encodeURIComponent(name),
72+
)
73+
switch result {
74+
| Ok(json) => Decoders.decodeKnot(json)
75+
| Error(e) => Error(e)
76+
}
77+
}
78+
79+
let fetchStatistics = async (): result<Types.statistics, string> => {
80+
let result = await fetchJson(baseUrl ++ "/api/statistics")
81+
switch result {
82+
| Ok(json) => Decoders.decodeStatistics(json)
83+
| Error(e) => Error(e)
84+
}
85+
}
86+
87+
let postQuery = async (
88+
~src: string,
89+
~format: string="auto",
90+
~maxRows: int=1000,
91+
): result<Types.queryResponse, Types.queryError> => {
92+
try {
93+
let bodyObj = Js.Dict.empty()
94+
Js.Dict.set(bodyObj, "query", Js.Json.string(src))
95+
Js.Dict.set(bodyObj, "format", Js.Json.string(format))
96+
Js.Dict.set(bodyObj, "max_rows", Js.Json.number(Belt.Int.toFloat(maxRows)))
97+
let bodyStr = Js.Json.stringify(Js.Json.object_(bodyObj))
98+
99+
let resp = await fetch(baseUrl ++ "/api/query", {
100+
"method": "POST",
101+
"headers": {"Content-Type": "application/json"},
102+
"body": bodyStr,
103+
})
104+
let json = await responseJson(resp)
105+
106+
if responseOk(resp) {
107+
switch Decoders.decodeQueryResponse(json) {
108+
| Ok(r) => Ok(r)
109+
| Error(e) =>
110+
Error({Types.errorKind: "decode_error", message: e, line: None, col: None})
111+
}
112+
} else {
113+
Error(Decoders.decodeQueryError(json))
114+
}
115+
} catch {
116+
| exn =>
117+
Error({
118+
Types.errorKind: "network_error",
119+
message: Js.String2.make(exn),
120+
line: None,
121+
col: None,
122+
})
123+
}
124+
}
18.3 KB
Binary file not shown.
99 Bytes
Binary file not shown.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
4+
// QuandleDB — top-level application component
5+
// Manages routing, state, and API interaction.
6+
7+
type model = {
8+
route: Route.t,
9+
stats: Types.remoteData<Types.statistics, string>,
10+
knots: Types.remoteData<Types.knotListResponse, string>,
11+
knotDetail: Types.remoteData<Types.knot, string>,
12+
filters: Types.filters,
13+
}
14+
15+
type msg =
16+
| NavigateTo(Route.t)
17+
| UrlChanged
18+
| GotStatistics(result<Types.statistics, string>)
19+
| GotKnots(result<Types.knotListResponse, string>)
20+
| GotKnotDetail(result<Types.knot, string>)
21+
| SetFilters(Types.filters)
22+
23+
@val external pushState: (Js.Nullable.t<string>, string, string) => unit = "history.pushState"
24+
@val external addWindowEventListener: (string, 'a => unit) => unit = "window.addEventListener"
25+
@val external removeWindowEventListener: (string, 'a => unit) => unit = "window.removeEventListener"
26+
27+
@react.component
28+
let make = () => {
29+
let (model, dispatch) = React.useReducer((model: model, msg: msg) =>
30+
switch msg {
31+
| NavigateTo(route) => {
32+
pushState(Js.Nullable.null, "", Route.toPath(route))
33+
{...model, route}
34+
}
35+
| UrlChanged => {...model, route: Route.fromUrl()}
36+
| GotStatistics(Ok(stats)) => {...model, stats: Success(stats)}
37+
| GotStatistics(Error(err)) => {...model, stats: Failure(err)}
38+
| GotKnots(Ok(knots)) => {...model, knots: Success(knots)}
39+
| GotKnots(Error(err)) => {...model, knots: Failure(err)}
40+
| GotKnotDetail(Ok(knot)) => {...model, knotDetail: Success(knot)}
41+
| GotKnotDetail(Error(err)) => {...model, knotDetail: Failure(err)}
42+
| SetFilters(filters) => {...model, filters, knots: Loading}
43+
}
44+
, {
45+
route: Route.fromUrl(),
46+
stats: NotAsked,
47+
knots: NotAsked,
48+
knotDetail: NotAsked,
49+
filters: Types.emptyFilters,
50+
})
51+
52+
// Listen for popstate (browser back/forward)
53+
React.useEffect0(() => {
54+
let handler = _ => dispatch(UrlChanged)
55+
addWindowEventListener("popstate", handler)
56+
Some(() => removeWindowEventListener("popstate", handler))
57+
})
58+
59+
// Fetch data when route changes
60+
React.useEffect1(() => {
61+
switch model.route {
62+
| Dashboard =>
63+
if model.stats == NotAsked {
64+
let _ = {
65+
open Promise
66+
Api.fetchStatistics()->then(result => {
67+
dispatch(GotStatistics(result))
68+
resolve()
69+
})
70+
}
71+
}
72+
| KnotList =>
73+
if model.knots == NotAsked {
74+
let _ = {
75+
open Promise
76+
Api.fetchKnots()->then(result => {
77+
dispatch(GotKnots(result))
78+
resolve()
79+
})
80+
}
81+
}
82+
| KnotDetail(name) => {
83+
let _ = {
84+
open Promise
85+
Api.fetchKnot(name)->then(result => {
86+
dispatch(GotKnotDetail(result))
87+
resolve()
88+
})
89+
}
90+
}
91+
| Query => ()
92+
| NotFound => ()
93+
}
94+
None
95+
}, [model.route])
96+
97+
// Refetch knots when filters change
98+
React.useEffect1(() => {
99+
if model.route == KnotList {
100+
let _ = {
101+
open Promise
102+
Api.fetchKnots(~filters=model.filters)->then(result => {
103+
dispatch(GotKnots(result))
104+
resolve()
105+
})
106+
}
107+
}
108+
None
109+
}, [model.filters])
110+
111+
let navigate = (route: Route.t) => dispatch(NavigateTo(route))
112+
113+
<div className="app">
114+
<View_Helpers.NavBar currentRoute={model.route} onNavigate=navigate />
115+
<main className="content">
116+
{switch model.route {
117+
| Dashboard => <Page_Dashboard stats={model.stats} onNavigate=navigate />
118+
| KnotList =>
119+
<Page_KnotList
120+
knots={model.knots}
121+
filters={model.filters}
122+
onFilterChange={f => dispatch(SetFilters(f))}
123+
onNavigate=navigate
124+
/>
125+
| KnotDetail(_) =>
126+
<Page_KnotDetail knot={model.knotDetail} onNavigate=navigate />
127+
| Query => <Page_Query />
128+
| NotFound =>
129+
<div className="not-found">
130+
<h1> {React.string("404 - Not Found")} </h1>
131+
<p> {React.string("The page you are looking for does not exist.")} </p>
132+
<button className="btn-primary" onClick={_ => navigate(Dashboard)}>
133+
{React.string("Go Home")}
134+
</button>
135+
</div>
136+
}}
137+
</main>
138+
<footer className="app-footer">
139+
<p> {React.string("QuandleDB — Powered by Skein.jl")} </p>
140+
</footer>
141+
</div>
142+
}
42 KB
Binary file not shown.

0 commit comments

Comments
 (0)