syntropy 0.43.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/TODO.md +60 -0
- data/examples/blog/app/posts/[id]/edit.rb +1 -1
- data/examples/blog/app/posts/[id]/index.rb +3 -3
- data/examples/gitshabbos/Dockerfile +25 -0
- data/examples/gitshabbos/Gemfile +3 -0
- data/examples/gitshabbos/README.md +40 -0
- data/examples/gitshabbos/app/[repo]/commits.rb +23 -0
- data/examples/gitshabbos/app/[repo]/index.rb +32 -0
- data/examples/gitshabbos/app/[repo]/raw+.rb +11 -0
- data/examples/gitshabbos/app/[repo]/tree+.rb +37 -0
- data/examples/gitshabbos/app/_layout/default.rb +14 -0
- data/examples/gitshabbos/app/_layout/repo.rb +27 -0
- data/examples/gitshabbos/app/_layout/style.css +466 -0
- data/examples/gitshabbos/app/_lib/git_repository.rb +95 -0
- data/examples/gitshabbos/app/_repos.rb +10 -0
- data/examples/gitshabbos/app/git/[repo]/git-receive-pack.rb +0 -0
- data/examples/gitshabbos/app/git/[repo]/git-upload-pack.rb +21 -0
- data/examples/gitshabbos/app/git/[repo]/info/refs.rb +19 -0
- data/examples/gitshabbos/app/git/[repo]/objects/index+.rb +37 -0
- data/examples/gitshabbos/app/git/index+.rb +37 -0
- data/examples/gitshabbos/app/index.rb +18 -0
- data/examples/gitshabbos/config/Caddyfile +5 -0
- data/examples/gitshabbos/config/development.rb +11 -0
- data/examples/gitshabbos/config/production.rb +5 -0
- data/examples/gitshabbos/config/test.rb +5 -0
- data/examples/gitshabbos/docker-compose.yml +71 -0
- data/examples/gitshabbos/test/test_app.rb +14 -0
- data/examples/mcp-oauth/app/oauth/consent.rb +1 -1
- data/examples/mcp-oauth/app/signin.rb +1 -1
- data/lib/syntropy/app.rb +4 -4
- data/lib/syntropy/applets/builtin/default_error_handler.rb +1 -1
- data/lib/syntropy/controller_extensions.rb +1 -1
- data/lib/syntropy/errors.rb +2 -0
- data/lib/syntropy/http/server_connection.rb +1 -1
- data/lib/syntropy/json_api.rb +1 -1
- data/lib/syntropy/markdown.rb +1 -1
- data/lib/syntropy/request/request_info.rb +11 -2
- data/lib/syntropy/request/response.rb +46 -4
- data/lib/syntropy/request/validation.rb +2 -1
- data/lib/syntropy/routing_tree.rb +16 -7
- data/lib/syntropy/version.rb +1 -1
- data/test/fixtures/app/mod/bar/index+.rb +5 -1
- data/test/fixtures/app/params/[foo].rb +1 -1
- data/test/test_app.rb +29 -0
- data/test/test_response.rb +115 -0
- data/test/test_routing_tree.rb +80 -25
- data/test/test_server.rb +3 -3
- metadata +25 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 277eab69875c267230dd9c936c17d0bfcb4ee53a3b8baa5a3bea632a8a1517d4
|
|
4
|
+
data.tar.gz: 753ca178d4d5be205ee2d71ee08509b1f8745db80bba1b23785e08a0c7608265
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f80177d7e93d797a0ef12644e62265b198275ceac4c0621dc10ab692806bb0f98eb99c4082c84fcada831deaee27d86b1fe9323228fa745b609e4ad18ba94987
|
|
7
|
+
data.tar.gz: 296a00998843af0e1a900ff51419613c6fdb6b4e68c0cd6e14d557ed04c9598e5826c50925954b65d56e8353efdfeb3fb7cea87406d4fe76f829ea092ef47f1b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
# 0.44.0 2026-11-05
|
|
2
|
+
|
|
3
|
+
- Rename `Request#json_pretty_response` to `Request#respond_json_pretty`
|
|
4
|
+
- Rename `Request#method` to `Request#http_method`
|
|
5
|
+
- Add `Request#respond_sse` and `Request#send_event` methods
|
|
6
|
+
- Add `Error.bad_request` helper method
|
|
7
|
+
- Prevent multiple redundant `return nil` statements at end of router proc code
|
|
8
|
+
- Implement `Request#rel_path` for wildcard routes
|
|
9
|
+
|
|
1
10
|
# 0.43.0 2026-10-07
|
|
2
11
|
|
|
3
12
|
- Fix logging
|
data/TODO.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
## Immediate
|
|
2
|
+
|
|
3
|
+
- Datastar-specific APIs:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
req.respond_sse {
|
|
7
|
+
req.send_datastar_event(patch_signals: { _states: { '/time' => Time.now.to_i } })
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
# and also without a block (for non-streaming datastar events)
|
|
11
|
+
req.respond_sse(
|
|
12
|
+
'datastar-patch-signals' => 'signals { _states: { '/time': #{Time.now.to_i} } }'
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# more sugar:
|
|
16
|
+
req.respond_datastar(
|
|
17
|
+
patch_signals: { _states: { '/time' => Time.now.to_i } }
|
|
18
|
+
)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- gitshabbos
|
|
22
|
+
- main (README) page `/[repo]`
|
|
23
|
+
- commits page `/[repo]/commits`
|
|
24
|
+
- file page `/[repo]/tree/**`
|
|
25
|
+
- raw page `/[repo]/raw/**`
|
|
26
|
+
- git protocol `/[repo]/git/**`
|
|
27
|
+
|
|
1
28
|
## Extralite doc site
|
|
2
29
|
|
|
3
30
|
- Similar to Papercraft site
|
|
@@ -98,6 +125,39 @@ Verbose output (`-v`):
|
|
|
98
125
|
- CLI command `syntropy background`
|
|
99
126
|
- Run automatically when running `syntropy serve` in dev mode
|
|
100
127
|
|
|
128
|
+
```sql
|
|
129
|
+
create table jobs (
|
|
130
|
+
id blob primary key not null,
|
|
131
|
+
due_stamp integer,
|
|
132
|
+
interval integer,
|
|
133
|
+
retry_count integer not null,
|
|
134
|
+
state integer not null, -- pending/running/done
|
|
135
|
+
payload jsonb not null
|
|
136
|
+
) strict;
|
|
137
|
+
|
|
138
|
+
create index idx_jobs_pending on jobs (state) where state = 1;
|
|
139
|
+
|
|
140
|
+
create table job_receipts (
|
|
141
|
+
id integer primary key autoincrement,
|
|
142
|
+
stamp integer not null,
|
|
143
|
+
job_id blob not null,
|
|
144
|
+
retry_count integer not null,
|
|
145
|
+
payload jsonb not null,
|
|
146
|
+
result jsonb not null
|
|
147
|
+
) strict;
|
|
148
|
+
|
|
149
|
+
create index idx_job_receipts_stamp on job_receipts (stamp);
|
|
150
|
+
create index idx_job_receipts_job_id on job_receipts (job_id);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Create a UUID (not really conforming to UUID v4 RFC 4122, just 16 random bytes):
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
require 'securerandom'
|
|
157
|
+
|
|
158
|
+
uuid = SecureRandom.random_bytes(16)
|
|
159
|
+
```
|
|
160
|
+
|
|
101
161
|
## Admin Dashboard
|
|
102
162
|
|
|
103
163
|
- Server state / stats
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export dispatch_by_http_method
|
|
5
5
|
|
|
6
6
|
def get(req)
|
|
7
|
-
id = req.route_params[
|
|
7
|
+
id = req.route_params[:id].to_i
|
|
8
8
|
post = @posts.get(id)
|
|
9
9
|
raise Syntropy::Error.not_found if !post
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ def post(req)
|
|
|
15
15
|
data = req.get_form_data
|
|
16
16
|
return delete(req) if data['method'] == 'delete'
|
|
17
17
|
|
|
18
|
-
id = req.route_params[
|
|
18
|
+
id = req.route_params[:id].to_i
|
|
19
19
|
title = req.validate(data['title'], String, /.+/)
|
|
20
20
|
body = req.validate(data['body'], String, /.+/)
|
|
21
21
|
|
|
@@ -27,7 +27,7 @@ def post(req)
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def delete(req)
|
|
30
|
-
id = req.route_params[
|
|
30
|
+
id = req.route_params[:id].to_i
|
|
31
31
|
|
|
32
32
|
deleted = @posts.delete(id)
|
|
33
33
|
raise BadRequestError, 'Failed to delete post' if deleted != 1
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
FROM ruby:4.0-slim AS base
|
|
2
|
+
WORKDIR /syntropy
|
|
3
|
+
RUN apt-get update -qq && apt-get install -y \
|
|
4
|
+
curl \
|
|
5
|
+
build-essential \
|
|
6
|
+
libpq-dev \
|
|
7
|
+
libsqlite3-dev \
|
|
8
|
+
libssl-dev \
|
|
9
|
+
libyaml-dev \
|
|
10
|
+
pkg-config && \
|
|
11
|
+
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
|
12
|
+
|
|
13
|
+
ENV BUNDLE_PATH="/usr/local/bundle"
|
|
14
|
+
COPY Gemfile Gemfile.lock ./
|
|
15
|
+
RUN bundle install
|
|
16
|
+
|
|
17
|
+
RUN groupadd --system --gid 1000 syntropy && \
|
|
18
|
+
useradd syntropy --uid 1000 --gid 1000 --create-home --shell /bin/bash
|
|
19
|
+
USER 1000:1000
|
|
20
|
+
|
|
21
|
+
EXPOSE 1234
|
|
22
|
+
|
|
23
|
+
# Start the main process
|
|
24
|
+
# CMD ["bash"]
|
|
25
|
+
CMD ["bundle", "exec", "syntropy", "serve"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# My awesome Syntropy app
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
$ bundle install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Running the web server
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
$ bundle exec syntropy serve
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Starting the app console
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
$ bundle exec syntropy console
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Running tests
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
$ bundle exec syntropy tests
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Usage with Docker Compose
|
|
28
|
+
|
|
29
|
+
You can also run the app on Docker Compose:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# run the app server
|
|
33
|
+
$ docker compose up
|
|
34
|
+
|
|
35
|
+
# run the console
|
|
36
|
+
$ docker compose run --remove-orphans console
|
|
37
|
+
|
|
38
|
+
# run tests
|
|
39
|
+
$ docker compose run --remove-orphans test
|
|
40
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
layout = import '../_layout/repo'
|
|
2
|
+
repos = import '../../_repos'
|
|
3
|
+
|
|
4
|
+
template = layout.apply { |commits:, **props|
|
|
5
|
+
commits {
|
|
6
|
+
ul {
|
|
7
|
+
commits.each {
|
|
8
|
+
li it
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export ->(req) {
|
|
15
|
+
repo = repos[req.route_params[:repo]]
|
|
16
|
+
raise Error.not_found if !repo
|
|
17
|
+
|
|
18
|
+
commits = repo.commits[0..15]
|
|
19
|
+
title = repo.name
|
|
20
|
+
repo_url = File.join('/', repo.name)
|
|
21
|
+
|
|
22
|
+
req.respond_html(template.render(title:, repo:, repo_url:, commits:))
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
layout = import '../_layout/repo'
|
|
2
|
+
repos = import '../../_repos'
|
|
3
|
+
|
|
4
|
+
template = layout.apply { |md:, files:, **props|
|
|
5
|
+
main {
|
|
6
|
+
markdown(md)
|
|
7
|
+
}
|
|
8
|
+
sidebar {
|
|
9
|
+
ul {
|
|
10
|
+
files.each { |r, u|
|
|
11
|
+
li { a r, href: u }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export ->(req) {
|
|
18
|
+
repo = repos[req.route_params[:repo]]
|
|
19
|
+
raise Error.not_found if !repo
|
|
20
|
+
|
|
21
|
+
item = repo.get('README.md')
|
|
22
|
+
md = item ? @machine.file_read(item[:fn]).force_encoding('UTF-8') : ''
|
|
23
|
+
|
|
24
|
+
files = repo.list.each_with_object({}) { |r, h|
|
|
25
|
+
h[r[:ref]] = File.join(req.path, 'tree', r[:ref])
|
|
26
|
+
}.sort_by { |k, v| k }
|
|
27
|
+
|
|
28
|
+
title = repo.name
|
|
29
|
+
repo_url = req.path
|
|
30
|
+
|
|
31
|
+
req.respond_html(template.render(title:, repo:, repo_url:, md:, files:))
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
repos = import '../../_repos'
|
|
2
|
+
|
|
3
|
+
export ->(req) {
|
|
4
|
+
repo = repos[req.route_params[:repo]]
|
|
5
|
+
raise Error.not_found if !repo
|
|
6
|
+
|
|
7
|
+
item = repo.get(req.rel_path)
|
|
8
|
+
raise Error.not_found if !item
|
|
9
|
+
|
|
10
|
+
req.respond(@machine.file_read(item[:fn]), 'Content-Type' => item[:mime_type])
|
|
11
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
layout = import '../_layout/repo'
|
|
2
|
+
repos = import '../../_repos'
|
|
3
|
+
|
|
4
|
+
template = layout.apply { |title:, item:, content:, url_raw:, **props|
|
|
5
|
+
p(class: 'single-file') { strong title }
|
|
6
|
+
p { a 'Download raw file', href: url_raw }
|
|
7
|
+
|
|
8
|
+
file_content {
|
|
9
|
+
if item[:fn] =~ /\.(png|jpe?g|gif|svg)\z/i
|
|
10
|
+
img(src: url_raw, alt: '')
|
|
11
|
+
elsif item[:binary]
|
|
12
|
+
p 'Binary file, not shown.'
|
|
13
|
+
else
|
|
14
|
+
pre {
|
|
15
|
+
code {
|
|
16
|
+
content.lines.each.each { span it, class: 'line' }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export ->(req) {
|
|
24
|
+
repo = repos[req.route_params[:repo]]
|
|
25
|
+
raise Error.not_found if !repo
|
|
26
|
+
|
|
27
|
+
item = repo.get(req.rel_path)
|
|
28
|
+
raise Error.not_found if !item
|
|
29
|
+
content = @machine.file_read(item[:fn]).force_encoding('UTF-8')
|
|
30
|
+
|
|
31
|
+
title = "#{repo.name} - #{req.rel_path}"
|
|
32
|
+
url_raw = File.join('/', req.route_params[:repo], 'raw', req.rel_path)
|
|
33
|
+
|
|
34
|
+
repo_url = File.join('/', repo.name)
|
|
35
|
+
|
|
36
|
+
req.respond_html(template.render(title:, repo:, repo_url:, item:, content:, url_raw:))
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export template { |**props|
|
|
2
|
+
html {
|
|
3
|
+
head {
|
|
4
|
+
title "Gitshabbos - #{props[:title]}"
|
|
5
|
+
meta charset: 'utf-8'
|
|
6
|
+
meta name: 'viewport', content: 'width=device-width, initial-scale=1.0'
|
|
7
|
+
link rel: "stylesheet", type: "text/css", href: "/assets/style.css"
|
|
8
|
+
}
|
|
9
|
+
body {
|
|
10
|
+
render_children(**props)
|
|
11
|
+
auto_refresh!
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
default = import './default'
|
|
2
|
+
|
|
3
|
+
export default.apply { |repo:, **props|
|
|
4
|
+
nav {
|
|
5
|
+
a '← index', href: '/'
|
|
6
|
+
text ' | '
|
|
7
|
+
a 'about', href: props[:repo_url]
|
|
8
|
+
text ' | '
|
|
9
|
+
a 'commits', href: File.join(props[:repo_url], 'commits')
|
|
10
|
+
}
|
|
11
|
+
if props[:header] != false
|
|
12
|
+
header {
|
|
13
|
+
h3 repo.name
|
|
14
|
+
p repo.description
|
|
15
|
+
p { code "git clone https://git.noteflakes.com/#{repo.name}/.git" }
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
content {
|
|
19
|
+
render_children(**props)
|
|
20
|
+
}
|
|
21
|
+
footer {
|
|
22
|
+
p {
|
|
23
|
+
text 'Powered by '
|
|
24
|
+
a 'Syntropy', href: 'https://github.com/digital-fabric/syntropy'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|