Commit

Author:

Hash:

Timestamp:

+1957 -2337 +/-49 browse

Kevin Schoon [me@kevinschoon.com]

d6a94c7be254f4ce9fe35b3c1c80a2e557b5ec9d

Sun, 17 Mar 2024 08:56:50 +0000 (1.3 years ago)

Replace capnp with tarpc
Replace capnp with tarpc

This replaces the capnp RPC implementation with tarpc. Tarpc requires far less
boilerplate and simplifies the codebase considerably.
1diff --git a/Cargo.lock b/Cargo.lock
2index eb34e63..814f8c7 100644
3--- a/Cargo.lock
4+++ b/Cargo.lock
5 @@ -505,8 +505,6 @@ dependencies = [
6 "ayllu_database",
7 "ayllu_git",
8 "ayllu_rpc",
9- "capnp",
10- "capnp-rpc",
11 "cc",
12 "clap 4.5.3",
13 "comrak",
14 @@ -528,6 +526,7 @@ dependencies = [
15 "serde_json",
16 "sqlx",
17 "tabwriter",
18+ "tarpc",
19 "tera",
20 "time",
21 "time-macros",
22 @@ -556,8 +555,6 @@ dependencies = [
23 "ayllu_database",
24 "ayllu_git",
25 "ayllu_rpc",
26- "capnp",
27- "capnp-rpc",
28 "clap 4.5.3",
29 "clap_complete",
30 "nickel-lang-core",
31 @@ -580,8 +577,6 @@ dependencies = [
32 "ayllu_api",
33 "ayllu_config",
34 "ayllu_rpc",
35- "capnp",
36- "capnp-rpc",
37 "clap 4.5.3",
38 "clap_complete",
39 "futures",
40 @@ -603,8 +598,6 @@ dependencies = [
41 "ayllu_config",
42 "ayllu_database",
43 "ayllu_rpc",
44- "capnp",
45- "capnp-rpc",
46 "clap 4.5.3",
47 "futures",
48 "rand",
49 @@ -622,11 +615,9 @@ dependencies = [
50 name = "ayllu_api"
51 version = "0.2.1"
52 dependencies = [
53- "capnp",
54- "capnp-futures",
55- "capnp-rpc",
56- "capnpc",
57 "serde",
58+ "tarpc",
59+ "thiserror",
60 ]
61
62 [[package]]
63 @@ -665,10 +656,8 @@ name = "ayllu_rpc"
64 version = "0.2.1"
65 dependencies = [
66 "async-trait",
67- "capnp",
68- "capnp-rpc",
69- "capnpc",
70 "futures",
71+ "tarpc",
72 "tokio",
73 "tokio-util",
74 "tracing",
75 @@ -834,45 +823,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
76 checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
77
78 [[package]]
79- name = "capnp"
80- version = "0.18.13"
81- source = "registry+https://github.com/rust-lang/crates.io-index"
82- checksum = "384b671a5b39eadb909b0c2b81d22a400d446526e651e64be9eb6674b33644a4"
83- dependencies = [
84- "embedded-io",
85- ]
86-
87- [[package]]
88- name = "capnp-futures"
89- version = "0.18.2"
90- source = "registry+https://github.com/rust-lang/crates.io-index"
91- checksum = "b8697b857f5b014ff378f02817426d3b6fb90a69f32e330fe010f24fe10cf8f1"
92- dependencies = [
93- "capnp",
94- "futures",
95- ]
96-
97- [[package]]
98- name = "capnp-rpc"
99- version = "0.18.0"
100- source = "registry+https://github.com/rust-lang/crates.io-index"
101- checksum = "94b87a9e0f74600628e227d39b79ef8652c558a408999ac46ba22b19dbad0010"
102- dependencies = [
103- "capnp",
104- "capnp-futures",
105- "futures",
106- ]
107-
108- [[package]]
109- name = "capnpc"
110- version = "0.18.1"
111- source = "registry+https://github.com/rust-lang/crates.io-index"
112- checksum = "a642faaaa78187e70bdcc0014c593c213553cfeda3b15054426d0d596048b131"
113- dependencies = [
114- "capnp",
115- ]
116-
117- [[package]]
118 name = "cc"
119 version = "1.0.90"
120 source = "registry+https://github.com/rust-lang/crates.io-index"
121 @@ -1427,6 +1377,18 @@ dependencies = [
122 ]
123
124 [[package]]
125+ name = "educe"
126+ version = "0.4.23"
127+ source = "registry+https://github.com/rust-lang/crates.io-index"
128+ checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f"
129+ dependencies = [
130+ "enum-ordinalize",
131+ "proc-macro2",
132+ "quote",
133+ "syn 1.0.109",
134+ ]
135+
136+ [[package]]
137 name = "either"
138 version = "1.10.0"
139 source = "registry+https://github.com/rust-lang/crates.io-index"
140 @@ -1436,12 +1398,6 @@ dependencies = [
141 ]
142
143 [[package]]
144- name = "embedded-io"
145- version = "0.5.0"
146- source = "registry+https://github.com/rust-lang/crates.io-index"
147- checksum = "658bbadc628dc286b9ae02f0cb0f5411c056eb7487b72f0083203f115de94060"
148-
149- [[package]]
150 name = "ena"
151 version = "0.14.2"
152 source = "registry+https://github.com/rust-lang/crates.io-index"
153 @@ -1551,6 +1507,19 @@ dependencies = [
154 ]
155
156 [[package]]
157+ name = "enum-ordinalize"
158+ version = "3.1.15"
159+ source = "registry+https://github.com/rust-lang/crates.io-index"
160+ checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee"
161+ dependencies = [
162+ "num-bigint",
163+ "num-traits",
164+ "proc-macro2",
165+ "quote",
166+ "syn 2.0.52",
167+ ]
168+
169+ [[package]]
170 name = "env_logger"
171 version = "0.8.4"
172 source = "registry+https://github.com/rust-lang/crates.io-index"
173 @@ -3639,6 +3608,49 @@ dependencies = [
174 ]
175
176 [[package]]
177+ name = "opentelemetry"
178+ version = "0.18.0"
179+ source = "registry+https://github.com/rust-lang/crates.io-index"
180+ checksum = "69d6c3d7288a106c0a363e4b0e8d308058d56902adefb16f4936f417ffef086e"
181+ dependencies = [
182+ "opentelemetry_api",
183+ "opentelemetry_sdk",
184+ ]
185+
186+ [[package]]
187+ name = "opentelemetry_api"
188+ version = "0.18.0"
189+ source = "registry+https://github.com/rust-lang/crates.io-index"
190+ checksum = "c24f96e21e7acc813c7a8394ee94978929db2bcc46cf6b5014fc612bf7760c22"
191+ dependencies = [
192+ "futures-channel",
193+ "futures-util",
194+ "indexmap 1.9.3",
195+ "js-sys",
196+ "once_cell",
197+ "pin-project-lite",
198+ "thiserror",
199+ ]
200+
201+ [[package]]
202+ name = "opentelemetry_sdk"
203+ version = "0.18.0"
204+ source = "registry+https://github.com/rust-lang/crates.io-index"
205+ checksum = "1ca41c4933371b61c2a2f214bf16931499af4ec90543604ec828f7a625c09113"
206+ dependencies = [
207+ "async-trait",
208+ "crossbeam-channel",
209+ "futures-channel",
210+ "futures-executor",
211+ "futures-util",
212+ "once_cell",
213+ "opentelemetry_api",
214+ "percent-encoding",
215+ "rand",
216+ "thiserror",
217+ ]
218+
219+ [[package]]
220 name = "overload"
221 version = "0.1.1"
222 source = "registry+https://github.com/rust-lang/crates.io-index"
223 @@ -5203,6 +5215,41 @@ dependencies = [
224 ]
225
226 [[package]]
227+ name = "tarpc"
228+ version = "0.34.0"
229+ source = "registry+https://github.com/rust-lang/crates.io-index"
230+ checksum = "93a1870169fb9490fb3b37df7f50782986475c33cb90955f9f9b9ae659124200"
231+ dependencies = [
232+ "anyhow",
233+ "fnv",
234+ "futures",
235+ "humantime",
236+ "opentelemetry",
237+ "pin-project",
238+ "rand",
239+ "serde",
240+ "static_assertions",
241+ "tarpc-plugins",
242+ "thiserror",
243+ "tokio",
244+ "tokio-serde",
245+ "tokio-util",
246+ "tracing",
247+ "tracing-opentelemetry",
248+ ]
249+
250+ [[package]]
251+ name = "tarpc-plugins"
252+ version = "0.13.1"
253+ source = "registry+https://github.com/rust-lang/crates.io-index"
254+ checksum = "ad8302bea2fb8a2b01b025d23414b0b4ed32a783b95e5d818c3320a8bc4baada"
255+ dependencies = [
256+ "proc-macro2",
257+ "quote",
258+ "syn 1.0.109",
259+ ]
260+
261+ [[package]]
262 name = "tempfile"
263 version = "3.10.1"
264 source = "registry+https://github.com/rust-lang/crates.io-index"
265 @@ -5439,6 +5486,22 @@ dependencies = [
266 ]
267
268 [[package]]
269+ name = "tokio-serde"
270+ version = "0.8.0"
271+ source = "registry+https://github.com/rust-lang/crates.io-index"
272+ checksum = "911a61637386b789af998ee23f50aa30d5fd7edcec8d6d3dedae5e5815205466"
273+ dependencies = [
274+ "bincode",
275+ "bytes",
276+ "educe",
277+ "futures-core",
278+ "futures-sink",
279+ "pin-project",
280+ "serde",
281+ "serde_json",
282+ ]
283+
284+ [[package]]
285 name = "tokio-stream"
286 version = "0.1.15"
287 source = "registry+https://github.com/rust-lang/crates.io-index"
288 @@ -5460,6 +5523,7 @@ dependencies = [
289 "futures-io",
290 "futures-sink",
291 "pin-project-lite",
292+ "slab",
293 "tokio",
294 "tracing",
295 ]
296 @@ -5622,6 +5686,19 @@ dependencies = [
297 ]
298
299 [[package]]
300+ name = "tracing-opentelemetry"
301+ version = "0.18.0"
302+ source = "registry+https://github.com/rust-lang/crates.io-index"
303+ checksum = "21ebb87a95ea13271332df069020513ab70bdb5637ca42d6e492dc3bbbad48de"
304+ dependencies = [
305+ "once_cell",
306+ "opentelemetry",
307+ "tracing",
308+ "tracing-core",
309+ "tracing-subscriber",
310+ ]
311+
312+ [[package]]
313 name = "tracing-subscriber"
314 version = "0.3.18"
315 source = "registry+https://github.com/rust-lang/crates.io-index"
316 diff --git a/README.md b/README.md
317index fbd911c..810ac35 100644
318--- a/README.md
319+++ b/README.md
320 @@ -81,7 +81,6 @@ Ayllu is written in [rust](https://www.rust-lang.org/) and you'll need its
321 compiler to build the project. Additionally you also must have
322
323 - [git](https://git-scm.com)
324- - [capnproto](https://capnproto.org/install.html)
325 - [sqlx-cli](https://github.com/launchbadge/sqlx/tree/main/sqlx-cli)
326 - [libgit2](https://libgit2.org/)
327
328 diff --git a/ayllu-build/Cargo.toml b/ayllu-build/Cargo.toml
329index 72373d8..0cdede2 100644
330--- a/ayllu-build/Cargo.toml
331+++ b/ayllu-build/Cargo.toml
332 @@ -21,8 +21,6 @@ rand = "0.8.5"
333 serde = { version = "1.0", features = ["derive"] }
334 tracing = "0.1.40"
335 tracing-subscriber = "0.3.18"
336- capnp-rpc = "0.18.0"
337- capnp = "0.18.9"
338 async-trait = "0.1.74"
339 sqlx = { version = "0.7.3", features = ["sqlite", "macros"] }
340 tokio = { version = "1.34.0", features = ["full"] }
341 diff --git a/ayllu-build/src/rpc_server.rs b/ayllu-build/src/rpc_server.rs
342index 10fe521..b703541 100644
343--- a/ayllu-build/src/rpc_server.rs
344+++ b/ayllu-build/src/rpc_server.rs
345 @@ -1,49 +1,55 @@
346+ use std::path::Path;
347+
348 use anyhow::Result;
349- use capnp::{capability::Promise, Error as CapnpError};
350+ use tracing::log::info;
351
352- use crate::config::Builder as Config;
353- use ayllu_api::build_capnp::worker::{
354- Client, ListManifestsParams, ListManifestsResults, ReadManifestParams, ReadManifestResults,
355- Server, SubmitParams, SubmitResults,
356+ use ayllu_api::{
357+ build::{Event, Manifest, Server},
358+ error::ApiError,
359 };
360-
361 use ayllu_database::{Builder, Wrapper as Database};
362- use ayllu_rpc::Server as RpcHelper;
363+ use ayllu_rpc::{
364+ futures::prelude::*,
365+ init_socket, spawn,
366+ tarpc::{
367+ context::Context,
368+ serde_transport::unix,
369+ server::{BaseChannel, Channel},
370+ tokio_serde::formats::Bincode,
371+ },
372+ };
373+
374+ use crate::config::Builder as Config;
375
376+ #[derive(Clone)]
377 #[allow(dead_code)]
378 struct ServerImpl {
379 db: Database,
380 }
381
382 impl Server for ServerImpl {
383- fn submit(
384- &mut self,
385- _params: SubmitParams,
386- _results: SubmitResults,
387- ) -> Promise<(), CapnpError> {
388- Promise::err(CapnpError::unimplemented(
389- "method worker::Server::submit not implemented".to_string(),
390- ))
391+ #[doc = r" submit a new event which may result in manifests being run"]
392+ async fn submit(self, _context: Context, _event: Event) -> Result<(), ApiError> {
393+ todo!()
394 }
395
396- fn list_manifests(
397- &mut self,
398- _: ListManifestsParams,
399- _: ListManifestsResults,
400- ) -> ::capnp::capability::Promise<(), ::capnp::Error> {
401- ::capnp::capability::Promise::err(::capnp::Error::unimplemented(
402- "method worker::Server::list_manifests not implemented".to_string(),
403- ))
404+ #[doc = r" list manifests available on the worker"]
405+ async fn list_manifests(
406+ self,
407+ _context: Context,
408+ _offset: i64,
409+ _limit: i64,
410+ ) -> Result<Vec<Manifest>, ApiError> {
411+ todo!()
412 }
413
414- fn read_manifest(
415- &mut self,
416- _: ReadManifestParams,
417- _: ReadManifestResults,
418- ) -> ::capnp::capability::Promise<(), ::capnp::Error> {
419- ::capnp::capability::Promise::err(::capnp::Error::unimplemented(
420- "method worker::Server::read_manifest not implemented".to_string(),
421- ))
422+ #[doc = r" read a manifest including it's status and all job output"]
423+ async fn read_manifest(
424+ self,
425+ _context: Context,
426+ _manifest_id: String,
427+ ) -> Result<Manifest, ApiError> {
428+ todo!()
429 }
430 }
431
432 @@ -53,8 +59,22 @@ pub async fn serve(cfg: &Config) -> Result<()> {
433 .migrations("./migrations")
434 .build()
435 .await?;
436- let server = ServerImpl { db };
437- let runtime = RpcHelper::<Client, ServerImpl>::new(&cfg.address, server);
438- runtime.serve().await?;
439+ info!("build server listening @ {}", cfg.address);
440+ let socket_path = Path::new(&cfg.address);
441+ init_socket(socket_path)?;
442+ let mut listener = unix::listen(socket_path, Bincode::default).await?;
443+ listener.config_mut().max_frame_length(usize::MAX);
444+ listener
445+ // Ignore accept errors.
446+ .filter_map(|r| future::ready(r.ok()))
447+ .map(BaseChannel::with_defaults)
448+ .map(move |channel| {
449+ let server = ServerImpl { db: db.clone() };
450+ channel.execute(server.serve()).for_each(spawn)
451+ })
452+ // Max 10 channels.
453+ .buffer_unordered(10)
454+ .for_each(|_| async {})
455+ .await;
456 Ok(())
457 }
458 diff --git a/ayllu-mail/Cargo.toml b/ayllu-mail/Cargo.toml
459index 855fa64..dc22d8c 100644
460--- a/ayllu-mail/Cargo.toml
461+++ b/ayllu-mail/Cargo.toml
462 @@ -16,9 +16,7 @@ clap = { version = "4.4.6", features = ["derive"] }
463 tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
464 tracing = "0.1.37"
465 tokio = { version = "1.33.0", features = ["full"] }
466- capnp-rpc = "0.18.0"
467 futures = "0.3.28"
468- capnp = "0.18.1"
469 melib = "0.8.2"
470 mailpot = { git = "https://ayllu-forge.org/forks/mailpot", branch = "ayllu-dev"}
471 clap_complete = "4.4.5"
472 diff --git a/ayllu-mail/src/server.rs b/ayllu-mail/src/server.rs
473index d77357f..efc135d 100644
474--- a/ayllu-mail/src/server.rs
475+++ b/ayllu-mail/src/server.rs
476 @@ -1,262 +1,234 @@
477 use std::error::Error as StdError;
478+ use std::path::Path;
479
480- use anyhow::{format_err, Result};
481- use capnp::{capability::Promise, Error as CapnpError};
482- use capnp_rpc::pry;
483+ use anyhow::Result;
484 use mailpot::{
485 models::{DbVal, Post},
486- Connection, Error as MailpotError,
487+ Configuration, Connection, Error as MailpotError,
488 };
489 use melib::Envelope;
490- use tracing::log;
491+ use tracing::log::info;
492
493 use crate::config::Config;
494- use ayllu_api::mail_capnp::server::{
495- Client, ExportParams, ExportResults, ListThreadsParams, ListThreadsResults, ListsParams,
496- ListsResults, ReadPostParams, ReadPostResults, ReadThreadParams, ReadThreadResults, Server,
497+
498+ use ayllu_api::{
499+ error::ApiError,
500+ mail::{MailingList, Message, Server as MailServer, Thread},
501+ };
502+
503+ use ayllu_rpc::{
504+ futures::prelude::*,
505+ init_socket, spawn,
506+ tarpc::{
507+ context::Context,
508+ serde_transport::unix,
509+ server::{BaseChannel, Channel},
510+ tokio_serde::formats::Bincode,
511+ },
512 };
513- use ayllu_rpc::Server as RpcHelper;
514
515 fn is_patch(subject: &str) -> bool {
516 subject.contains("[PATCH]") || subject.contains("[patch]")
517 }
518
519+ fn to_api_error(e: MailpotError) -> ApiError {
520+ ApiError::Message(e.to_string())
521+ }
522+
523+ fn to_message(post: &DbVal<Post>) -> Message {
524+ let envelope = Envelope::from_bytes(&post.message, None).unwrap();
525+ let text = envelope.body_bytes(&post.message).text();
526+ let body = String::from_utf8(post.message.to_vec()).unwrap();
527+ Message {
528+ id: post.pk,
529+ message_id: post.message_id.clone(),
530+ timestamp: post.timestamp,
531+ from: envelope.from[0].get_email(),
532+ address: post.address.clone(),
533+ body,
534+ text,
535+ is_patch: envelope.subject.map_or(false, |subject| is_patch(&subject)),
536+ }
537+ }
538+
539+ #[derive(Clone)]
540 struct ServerImpl {
541- db: Connection,
542+ mailpot_config: Configuration,
543 }
544
545- impl Server for ServerImpl {
546- fn list_threads(
547- &mut self,
548- params: ListThreadsParams,
549- results: ListThreadsResults,
550- ) -> Promise<(), CapnpError> {
551- let list_id = pry!(pry!(params.get()).get_id()).to_string().unwrap();
552- let list_threads = |mut results: ListThreadsResults| {
553- let list = self
554- .db
555- .list_by_id(&list_id)?
556- .ok_or(format_err!("no mailing list with id: {}", &list_id))?;
557- let posts = self.db.list_posts(list.pk, None)?;
558- // TODO: add a new database method to only return posts without
559- // reply headers to save cpu cycles
560- let posts: Vec<&DbVal<Post>> = posts
561- .iter()
562- .filter(|post| {
563- let envelope = Envelope::from_bytes(&post.0.message, None).unwrap();
564- envelope.in_reply_to.is_none()
565- })
566- .collect();
567- let mut threads = results.get().init_threads(posts.len() as u32);
568- for (i, post) in posts.iter().enumerate() {
569- let envelope = Envelope::from_bytes(&post.0.message, None).unwrap();
570- let message_text = envelope.body_bytes(&post.0.message).text();
571- let replies = self.db.list_thread(list.pk, &post.message_id)?;
572- let mut thread = threads.reborrow().get(i as u32);
573- thread.reborrow().set_n_replies(replies.len() as i64);
574- let mut first = thread.get_first().unwrap();
575- first.set_from(envelope.from[0].get_email().as_str().into());
576- first.set_id(post.pk);
577- first.set_address(post.0.address.as_str().into());
578- first.set_message_id(post.0.message_id.as_str().into());
579- first.set_timestamp(post.0.timestamp as i64);
580- if let Some(subject) = envelope.subject {
581- first.set_subject(subject.as_str().into());
582- first.set_is_patch(is_patch(&subject))
583- }
584- let message = String::from_utf8(post.0.message.clone()).unwrap();
585- first.set_body(message.as_str().into());
586- first.set_text(message_text.as_str().into());
587- }
588- Ok::<(), MailpotError>(())
589- };
590- match list_threads(results) {
591- Ok(_) => Promise::ok(()),
592- Err(e) => Promise::err(CapnpError::failed(e.to_string())),
593- }
594+ // BUG: these are all read-only functions but mailpot requires trusted()
595+
596+ impl MailServer for ServerImpl {
597+ #[doc = r" return all managed mailing lists"]
598+ async fn lists(self, _context: Context) -> Result<Vec<MailingList>, ApiError> {
599+ let db =
600+ Connection::open_or_create_db(self.mailpot_config.clone()).map_err(to_api_error)?.trusted();
601+ Ok(db
602+ .lists()
603+ .map_err(|e| ApiError::Message(e.to_string()))?
604+ .iter()
605+ .map(|db_val| MailingList {
606+ id: db_val.id.clone(),
607+ name: db_val.name.clone(),
608+ request_address: db_val.request_subaddr().clone(),
609+ address: db_val.address.clone(),
610+ description: db_val
611+ .description
612+ .as_ref()
613+ .map_or(String::new(), |desc| desc.clone()),
614+ topics: db_val.topics.clone(),
615+ })
616+ .collect())
617 }
618
619- fn read_thread(
620- &mut self,
621- params: ReadThreadParams,
622- results: ReadThreadResults,
623- ) -> Promise<(), CapnpError> {
624- let params = pry!(params.get());
625- let list_id = pry!(pry!(params.get_id()).to_string());
626- let message_id = pry!(pry!(params.get_message_id()).to_string());
627- let read_thread = |mut results: ReadThreadResults| {
628- let list = self
629- .db
630- .list_by_id(&list_id)?
631- .ok_or(format_err!("no mailing list with id: {}", list_id))?;
632- let first_post = self
633- .db
634- .list_post_by_message_id(list.pk, &message_id)?
635- .ok_or(format_err!("cannot find post: {}", message_id))?;
636- let replies = self.db.list_thread(list.pk, &message_id)?;
637- let mut threads = results.get().init_thread(replies.len() as u32 + 1);
638- let mut first = threads.reborrow().get(0);
639- first.set_id(first_post.0.pk);
640- first.set_address(first_post.0.address.as_str().into());
641- first.set_message_id(first_post.0.message_id.as_str().into());
642- let envelope = Envelope::from_bytes(&first_post.0.message, None).unwrap();
643- let message_text = envelope.body_bytes(&first_post.0.message).text();
644- first.set_text(message_text.as_str().into());
645- let message_body = String::from_utf8(first_post.0.message).unwrap();
646- first.set_body(message_body.as_str().into());
647- first.set_timestamp(first_post.0.timestamp as i64);
648- if let Some(subject) = envelope.subject {
649- first.set_subject(subject.as_str().into());
650- first.set_is_patch(is_patch(subject.as_str()));
651- }
652- log::info!("read thread with {} replies", replies.len());
653- for (i, reply) in replies.iter().enumerate() {
654- let mut next = threads.reborrow().get(i as u32 + 1);
655- let envelope = Envelope::from_bytes(&reply.1.message, None).unwrap();
656- next.set_id(reply.1.pk);
657- next.set_address(reply.1.address.as_str().into());
658- next.set_message_id(reply.1 .0.message_id.as_str().into());
659- let message_body = String::from_utf8(reply.1.message.to_vec()).unwrap();
660- next.set_body(message_body.as_str().into());
661- let message_text = envelope.body_bytes(&reply.1.message).text();
662- if let Some(subject) = envelope.subject {
663- next.set_subject(subject.as_str().into());
664- next.set_is_patch(is_patch(subject.as_str()));
665- }
666- next.set_text(message_text.as_str().into());
667- next.set_timestamp(envelope.timestamp as i64);
668- }
669- Ok::<(), MailpotError>(())
670- };
671- match read_thread(results) {
672- Ok(_) => Promise::ok(()),
673- Err(e) => Promise::err(CapnpError::failed(e.to_string())),
674+ #[doc = r" list all of the threads associated with a mailing list"]
675+ async fn list_threads(
676+ self,
677+ _context: Context,
678+ id: String,
679+ _offset: i64,
680+ _limit: i64,
681+ ) -> Result<Vec<Thread>, ApiError> {
682+ let db =
683+ Connection::open_or_create_db(self.mailpot_config.clone()).map_err(to_api_error)?.trusted();
684+ let list = db
685+ .list_by_id(&id)
686+ .map_err(to_api_error)?
687+ .ok_or(ApiError::NotFound(format!(
688+ "no mailing list with id {} exists",
689+ id
690+ )))?;
691+ let posts = db.list_posts(list.pk, None).map_err(to_api_error)?;
692+ // TODO: add a new database method to only return posts without
693+ // reply headers to save cpu cycles
694+ let posts: Vec<&DbVal<Post>> = posts
695+ .iter()
696+ .filter(|post| {
697+ let envelope = Envelope::from_bytes(&post.0.message, None).unwrap();
698+ envelope.in_reply_to.is_none()
699+ })
700+ .collect();
701+ let mut threads: Vec<Thread> = Vec::with_capacity(posts.len());
702+ for post in posts.iter() {
703+ let replies = db
704+ .list_thread(list.pk, &post.message_id)
705+ .map_err(to_api_error)?;
706+ let first = to_message(post);
707+ let has_patch = first.is_patch;
708+ threads.push(Thread {
709+ first,
710+ n_replies: replies.len() as i64,
711+ has_patch,
712+ });
713 }
714+ Ok(threads)
715 }
716
717- fn read_post(
718- &mut self,
719- params: ReadPostParams,
720- results: ReadPostResults,
721- ) -> Promise<(), CapnpError> {
722- let params = pry!(params.get());
723- let list_id = pry!(pry!(params.get_id()).to_string());
724- let message_id = pry!(pry!(params.get_message_id()).to_string());
725- let read_post = |mut results: ReadPostResults| {
726- let list = self
727- .db
728- .list_by_id(&list_id)?
729- .ok_or(format_err!("no mailing list with id: {}", list_id))?;
730- let post = self
731- .db
732- .list_post_by_message_id(list.pk, &message_id)?
733- .ok_or(format_err!("failed to find post with id: {}", message_id))?;
734- let mut message = results.get();
735- let envelope = Envelope::from_bytes(&post.message, None).unwrap();
736- message.set_id(post.pk);
737- message.set_address(post.address.as_str().into());
738- if let Some(from) = post.envelope_from.as_ref() {
739- message.set_from(from.as_str().into());
740- }
741- let message_text = envelope.body_bytes(&post.message).text();
742- let message_body = String::from_utf8(post.message.to_vec()).unwrap();
743- message.set_message_id(envelope.message_id.to_string().as_str().into());
744- message.set_body(message_body.as_str().into());
745- message.set_text(message_text.as_str().into());
746- message.set_timestamp(envelope.timestamp as i64);
747- if let Some(subject) = envelope.subject {
748- message.set_subject(subject.as_str().into());
749- message.set_is_patch(is_patch(subject.as_str()));
750- }
751-
752- Ok::<(), MailpotError>(())
753- };
754- match read_post(results) {
755- Ok(_) => Promise::ok(()),
756- Err(e) => Promise::err(CapnpError::failed(e.to_string())),
757+ #[doc = r" list all of the responses associated with a post starting from messageId"]
758+ async fn read_thread(
759+ self,
760+ _context: Context,
761+ id: String,
762+ message_id: String,
763+ _offset: i64,
764+ _limit: i64,
765+ ) -> Result<Vec<Message>, ApiError> {
766+ let db =
767+ Connection::open_or_create_db(self.mailpot_config.clone()).map_err(to_api_error)?.trusted();
768+ let list = db
769+ .list_by_id(&id)
770+ .map_err(to_api_error)?
771+ .ok_or(ApiError::NotFound(format!(
772+ "no mailing list with id {} exists",
773+ id
774+ )))?;
775+ let mut posts: Vec<Message> = Vec::new();
776+ // Read the initial post
777+ let first_post = db
778+ .list_post_by_message_id(list.pk, &message_id)
779+ .map_err(to_api_error)?
780+ .ok_or(ApiError::NotFound(format!(
781+ "no post with id {} exists",
782+ list.pk
783+ )))?;
784+ posts.push(to_message(&first_post));
785+ // then all of it's replies
786+ let replies = db.list_thread(list.pk, &message_id).map_err(to_api_error)?;
787+ for (_, post, _, _) in replies.iter() {
788+ posts.push(to_message(post));
789 }
790+ Ok(posts)
791 }
792
793- fn export(&mut self, params: ExportParams, results: ExportResults) -> Promise<(), CapnpError> {
794- let params = pry!(params.get());
795- let list_id = pry!(params.get_id()).to_string().unwrap();
796- let message_id = if params.has_message_id() {
797- let message_id = pry!(params.get_message_id()).to_string().unwrap();
798- Some(message_id)
799- } else {
800- None
801- };
802- let read_mbox = |mut results: ExportResults| {
803- let list = self
804- .db
805- .list_by_id(&list_id)?
806- .ok_or(format_err!("no mailing list with id: {}", list_id))?;
807- let buf = self.db.export_mbox(
808- list.pk,
809- message_id.as_deref(),
810- params.get_as_thread(),
811- )?;
812- results.get().set_thread(&buf);
813- Ok::<(), MailpotError>(())
814- };
815- match read_mbox(results) {
816- Ok(_) => Promise::ok(()),
817- Err(e) => Promise::err(CapnpError::failed(e.to_string())),
818- }
819+ #[doc = r" read a single post"]
820+ async fn read_post(
821+ self,
822+ _context: Context,
823+ id: String,
824+ message_id: String,
825+ ) -> Result<Message, ApiError> {
826+ let db =
827+ Connection::open_or_create_db(self.mailpot_config.clone()).map_err(to_api_error)?.trusted();
828+ let list = db
829+ .list_by_id(&id)
830+ .map_err(to_api_error)?
831+ .ok_or(ApiError::NotFound(format!(
832+ "no mailing list with id {} exists",
833+ id
834+ )))?;
835+ let post = db
836+ .list_post_by_message_id(list.pk, &message_id)
837+ .map_err(to_api_error)?
838+ .ok_or(ApiError::NotFound(format!(
839+ "no post with id {}",
840+ message_id
841+ )))?;
842+ Ok(to_message(&post))
843 }
844
845- fn lists(
846- &mut self,
847- _: ListsParams,
848- results: ListsResults,
849- ) -> ::capnp::capability::Promise<(), ::capnp::Error> {
850- let read_lists = |mut results: ListsResults| {
851- let lists = self.db.lists()?;
852- let mut lists_array = results.get().init_lists(lists.len() as u32);
853- for (i, list) in lists.iter().enumerate() {
854- lists_array
855- .reborrow()
856- .get(i as u32)
857- .set_id(list.id.as_str().into());
858- lists_array
859- .reborrow()
860- .get(i as u32)
861- .set_name(list.name.as_str().into());
862- lists_array
863- .reborrow()
864- .get(i as u32)
865- .set_address(list.address.as_str().into());
866- lists_array
867- .reborrow()
868- .get(i as u32)
869- .set_request_address(list.request_subaddr().as_str().into());
870- if let Some(description) = &list.description {
871- lists_array
872- .reborrow()
873- .get(i as u32)
874- .set_name(description.as_str().into());
875- }
876- let mut topics = lists_array
877- .reborrow()
878- .get(i as u32)
879- .init_topics(list.topics.len() as u32);
880- for (j, topic) in list.topics.iter().enumerate() {
881- topics.set(j as u32, topic.as_str().into());
882- }
883- }
884- Ok::<(), MailpotError>(())
885- };
886- match read_lists(results) {
887- Ok(_) => Promise::ok(()),
888- Err(e) => Promise::err(CapnpError::failed(e.to_string())),
889- }
890+ #[doc = r" export one or more messages in mbox format"]
891+ async fn export(
892+ self,
893+ _context: Context,
894+ id: String,
895+ message_id: Option<String>,
896+ as_thread: bool,
897+ ) -> Result<Vec<u8>, ApiError> {
898+ let db =
899+ Connection::open_or_create_db(self.mailpot_config.clone()).map_err(to_api_error)?.trusted();
900+ let list = db
901+ .list_by_id(&id)
902+ .map_err(to_api_error)?
903+ .ok_or(ApiError::NotFound(format!(
904+ "no mailing list with id {} exists",
905+ id
906+ )))?;
907+ let buf = db
908+ .export_mbox(list.pk, message_id.as_deref(), as_thread)
909+ .map_err(to_api_error)?;
910+ Ok(buf)
911 }
912 }
913
914 pub async fn serve(cfg: &Config) -> Result<(), Box<dyn StdError>> {
915- let db = Connection::open_or_create_db(cfg.mailpot_config())?.trusted();
916- let server = ServerImpl { db };
917- let runtime = RpcHelper::<Client, ServerImpl>::new(&cfg.mail.socket_path, server);
918- runtime.serve().await?;
919+ let socket_path = Path::new(&cfg.mail.socket_path);
920+ init_socket(socket_path)?;
921+ info!("mail server listening @ {:?}", socket_path);
922+ let mut listener = unix::listen(socket_path, Bincode::default).await?;
923+ listener.config_mut().max_frame_length(usize::MAX);
924+ listener
925+ // Ignore accept errors.
926+ .filter_map(|r| future::ready(r.ok()))
927+ .map(BaseChannel::with_defaults)
928+ .map(move |channel| {
929+ let server = ServerImpl {
930+ mailpot_config: cfg.mailpot_config(),
931+ };
932+ channel.execute(server.serve()).for_each(spawn)
933+ })
934+ // Max 10 channels.
935+ .buffer_unordered(10)
936+ .for_each(|_| async {})
937+ .await;
938 Ok(())
939 }
940 diff --git a/ayllu-xmpp/Cargo.toml b/ayllu-xmpp/Cargo.toml
941index 54105eb..7a5e342 100644
942--- a/ayllu-xmpp/Cargo.toml
943+++ b/ayllu-xmpp/Cargo.toml
944 @@ -19,8 +19,6 @@ anyhow = "1.0.75"
945 tokio-xmpp = "3.4.0"
946 futures = "0.3.28"
947 rand = "0.8.5"
948- capnp = "0.18.3"
949- capnp-rpc = "0.18.0"
950 tracing = { version = "0.1.40", features = ["log"] }
951 async-trait = "0.1.74"
952 sqlx = { version = "0.7.3", features = [ "sqlite", "macros", ] }
953 diff --git a/ayllu-xmpp/src/main.rs b/ayllu-xmpp/src/main.rs
954index 3b8b21c..7a6a30a 100644
955--- a/ayllu-xmpp/src/main.rs
956+++ b/ayllu-xmpp/src/main.rs
957 @@ -76,8 +76,6 @@ fn main() -> Result<()> {
958 .build()
959 .unwrap()
960 .block_on(async move {
961- // Capnp RPC server implementation with a read-only database
962- // connection
963 LocalSet::new().run_until(rpc_server::serve(config)).await
964 })
965 .unwrap();
966 diff --git a/ayllu-xmpp/src/rpc_server.rs b/ayllu-xmpp/src/rpc_server.rs
967index 122bff0..9d4f638 100644
968--- a/ayllu-xmpp/src/rpc_server.rs
969+++ b/ayllu-xmpp/src/rpc_server.rs
970 @@ -1,101 +1,102 @@
971 use std::error::Error as StdError;
972+ use std::path::Path;
973
974- use capnp::{capability::Promise, Error as CapnpError};
975- use capnp_rpc::pry;
976- use tracing::log;
977+ use tracing::log::info;
978
979 use crate::config::Xmpp as Config;
980 use crate::database_ext::XmppExt;
981- use ayllu_api::xmpp_capnp::server::{
982- Client, MessagesParams, MessagesResults, Server, StatsParams, StatsResults,
983+ use ayllu_api::{
984+ error::ApiError,
985+ xmpp::{ChannelStat, Message, Server},
986+ };
987+ use ayllu_database::{Builder, Wrapper as Database};
988+ use ayllu_rpc::{
989+ futures::prelude::*,
990+ init_socket, spawn,
991+ tarpc::{
992+ context::Context,
993+ serde_transport::unix,
994+ server::{BaseChannel, Channel},
995+ tokio_serde::formats::Bincode,
996+ },
997 };
998- use ayllu_database::{Wrapper as Database, Builder};
999- use ayllu_rpc::Server as RpcHelper;
1000
1001+ #[derive(Clone)]
1002 struct ServerImpl {
1003 db: Database,
1004 }
1005
1006 impl Server for ServerImpl {
1007- fn stats(&mut self, params: StatsParams, mut results: StatsResults) -> Promise<(), CapnpError> {
1008- let db = self.db.clone();
1009- let mut channels: Vec<String> = Vec::new();
1010- for channel_name in pry!(pry!(params.get()).get_channels()) {
1011- let channel_name = pry!(channel_name).to_string().unwrap();
1012- channels.push(channel_name);
1013+ #[doc = r" return stats for the given channel names"]
1014+ async fn stats(
1015+ self,
1016+ _context: Context,
1017+ channels: Vec<String>,
1018+ ) -> Result<Vec<ChannelStat>, ApiError> {
1019+ info!("looking up channels: {:?}", channels);
1020+ let stats = self.db.read_stats(channels).await.unwrap();
1021+ // sqlx can't use WHERE $ IN (..) ..
1022+ let mut results: Vec<ChannelStat> = Vec::with_capacity(stats.len());
1023+ for count in stats.iter() {
1024+ info!(
1025+ "got channel: name={} users={} msgs={}",
1026+ count.0, count.1, count.2,
1027+ );
1028+ results.push(ChannelStat {
1029+ name: count.0.clone(),
1030+ users_online: count.1,
1031+ num_messages: count.2,
1032+ });
1033 }
1034- Promise::from_future(async move {
1035- log::info!("looking up channels: {:?}", channels);
1036- let stats = db.read_stats(channels).await.unwrap();
1037- // sqlx can't use WHERE $ IN (..) ..
1038- let mut results = results.get().init_stats(stats.len() as u32);
1039- for (i, count) in stats.iter().enumerate() {
1040- log::info!(
1041- "got channel: name={} users={} msgs={}",
1042- count.0,
1043- count.1,
1044- count.2
1045- );
1046- results
1047- .reborrow()
1048- .get(i as u32)
1049- .set_name(count.0.as_str().into());
1050- results.reborrow().get(i as u32).set_users_online(count.1);
1051- results.reborrow().get(i as u32).set_n_messages(count.2);
1052- }
1053- Ok(())
1054- })
1055+ Ok(results)
1056 }
1057
1058- fn messages(
1059- &mut self,
1060- params: MessagesParams,
1061- mut results: MessagesResults,
1062- ) -> Promise<(), CapnpError> {
1063- let db = self.db.clone();
1064- let params = pry!(params.get());
1065- let channel_name = pry!(params.get_channel_name()).to_string().unwrap();
1066- let last_message = if params.has_last_message() {
1067- Some(pry!(params.get_last_message()).to_string().unwrap())
1068- } else {
1069- None
1070- };
1071- let limit = params.get_limit();
1072- Promise::from_future(async move {
1073- log::info!("looking up messages for channel: {:?}", channel_name);
1074- let messages = db
1075- .list_messages(&channel_name, last_message.as_deref(), limit)
1076- .await
1077- .unwrap();
1078- log::info!("db returned {} messages", messages.len());
1079- let mut results = results.get().init_messages(messages.len() as u32);
1080- for (i, message) in messages.iter().enumerate() {
1081- results
1082- .reborrow()
1083- .get(i as u32)
1084- .set_message_id(message.xmpp_id.as_str().into());
1085- results
1086- .reborrow()
1087- .get(i as u32)
1088- .set_timestamp(message.xmpp_timestamp.as_str().into());
1089- results
1090- .reborrow()
1091- .get(i as u32)
1092- .set_nickname(message.xmpp_nickname.as_str().into());
1093- results
1094- .reborrow()
1095- .get(i as u32)
1096- .set_body(message.xmpp_body.as_str().into());
1097- }
1098- Ok(())
1099- })
1100+ #[doc = r" return public messages for the channel"]
1101+ async fn messages(
1102+ self,
1103+ _context: Context,
1104+ channel_name: String,
1105+ last_message: Option<String>,
1106+ limit: i64,
1107+ ) -> Result<Vec<Message>, ApiError> {
1108+ info!("looking up messages for channel: {:?}", channel_name);
1109+ let messages = self
1110+ .db
1111+ .list_messages(&channel_name, last_message.as_deref(), limit)
1112+ .await
1113+ .unwrap();
1114+ info!("db returned {} messages", messages.len());
1115+ let mut results: Vec<Message> = Vec::with_capacity(messages.len());
1116+ for db_message in messages.iter() {
1117+ results.push(Message {
1118+ message_id: db_message.xmpp_id.clone(),
1119+ timestamp: db_message.xmpp_timestamp.clone(),
1120+ nickname: db_message.xmpp_nickname.clone(),
1121+ body: db_message.xmpp_body.clone(),
1122+ })
1123+ }
1124+ Ok(results)
1125 }
1126 }
1127
1128 pub async fn serve(cfg: Config) -> Result<(), Box<dyn StdError>> {
1129 let db = Builder::default().url(&cfg.database.path).build().await?;
1130- let server = ServerImpl { db };
1131- let runtime = RpcHelper::<Client, ServerImpl>::new(&cfg.socket_path, server);
1132- runtime.serve().await?;
1133+ let socket_path = Path::new(&cfg.socket_path);
1134+ init_socket(socket_path)?;
1135+ info!("xmpp server listening @ {:?}", socket_path);
1136+ let mut listener = unix::listen(socket_path, Bincode::default).await?;
1137+ listener.config_mut().max_frame_length(usize::MAX);
1138+ listener
1139+ // Ignore accept errors.
1140+ .filter_map(|r| future::ready(r.ok()))
1141+ .map(BaseChannel::with_defaults)
1142+ .map(move |channel| {
1143+ let server = ServerImpl { db: db.clone() };
1144+ channel.execute(server.serve()).for_each(spawn)
1145+ })
1146+ // Max 10 channels.
1147+ .buffer_unordered(10)
1148+ .for_each(|_| async {})
1149+ .await;
1150 Ok(())
1151 }
1152 diff --git a/ayllu/Cargo.toml b/ayllu/Cargo.toml
1153index 2b1b749..4188ab4 100644
1154--- a/ayllu/Cargo.toml
1155+++ b/ayllu/Cargo.toml
1156 @@ -58,10 +58,9 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
1157 tracing = "0.1.40"
1158 tower = { version = "0.4.13", features = ["util", "timeout", "tracing"] }
1159 mime = "0.3.17"
1160- capnp-rpc = "0.18.0"
1161- capnp = "0.18.1"
1162 async-trait = "0.1.74"
1163 webfinger = "0.5.1"
1164+ tarpc = { version = "0.34.0", features = ["full"] }
1165
1166 # NOTE: this must be cautiously updated along with sqlx and rusqlite.
1167 [dependencies.libsqlite3-sys]
1168 diff --git a/ayllu/src/job_server/commands.rs b/ayllu/src/job_server/commands.rs
1169index da7c748..2f7db87 100644
1170--- a/ayllu/src/job_server/commands.rs
1171+++ b/ayllu/src/job_server/commands.rs
1172 @@ -1,6 +1,7 @@
1173 use std::fs::canonicalize;
1174 use std::io::Write;
1175 use std::path::{Path, PathBuf};
1176+ use std::time::{Duration as StdDuration, SystemTime};
1177
1178 use anyhow::{format_err, Result};
1179 use tabwriter::TabWriter;
1180 @@ -8,10 +9,10 @@ use time::Duration;
1181
1182 use crate::config::Config;
1183 use crate::time::friendly;
1184- use ayllu_api::jobs_capnp::server::Client;
1185- use ayllu_api::models::{Job, Kind};
1186+ use ayllu_api::jobs::{Kind, ServerClient as JobsClient};
1187 use ayllu_git::git_dir;
1188- use ayllu_rpc::Client as RpcHelper;
1189+
1190+ use tarpc::{context, serde_transport::unix, tokio_serde::formats::Bincode};
1191
1192 fn name(repo_path: Option<String>) -> String {
1193 match repo_path {
1194 @@ -40,6 +41,12 @@ fn resolve_path(path: Option<&PathBuf>) -> Result<PathBuf> {
1195 }
1196 }
1197
1198+ async fn jobs_client(socket_path: &str) -> Result<JobsClient> {
1199+ let conn = unix::connect(Path::new(socket_path), Bincode::default).await?;
1200+ let client = JobsClient::new(tarpc::client::Config::default(), conn).spawn();
1201+ Ok(client)
1202+ }
1203+
1204 /// Run a job against a specific repository path, if kind is not specified all
1205 /// jobs will be ran.
1206 pub async fn run_one(
1207 @@ -52,20 +59,16 @@ pub async fn run_one(
1208 vec![kind.to_string().into()]
1209 });
1210 let repo_path = resolve_path(repo_path.as_ref())?;
1211- RpcHelper::new(&config.jobs_socket_path)
1212- .invoke(move |client: Client| async move {
1213- for kind in kinds {
1214- let mut request = client.submit_request();
1215- // FIXME: set_repo_path must take Path/PathBuf
1216- request
1217- .get()
1218- .set_repo_path(repo_path.display().to_string().as_str().into());
1219- request.get().set_kind(kind.into());
1220- request.send().promise.await?;
1221- }
1222- Ok(())
1223- })
1224- .await?;
1225+ let client = jobs_client(&config.jobs_socket_path).await?;
1226+ for kind in kinds {
1227+ let mut ctx = context::current();
1228+ ctx.deadline = SystemTime::now()
1229+ .checked_add(StdDuration::from_secs(1200))
1230+ .unwrap();
1231+ client
1232+ .submit(ctx, kind, repo_path.display().to_string())
1233+ .await?;
1234+ }
1235 Ok(())
1236 }
1237
1238 @@ -73,31 +76,12 @@ pub async fn list(config: Config, repo_path: Option<PathBuf>) -> Result<()> {
1239 let repo_path = repo_path
1240 .map(|repo_path| resolve_path(Some(repo_path).as_ref()))
1241 .transpose()?;
1242- let jobs = RpcHelper::new(&config.jobs_socket_path)
1243- .invoke(move |client: Client| async move {
1244- let mut request = client.list_request();
1245- if let Some(repo_path) = repo_path {
1246- // FIXME: set_repo_path must take Path/PathBuf
1247- request
1248- .get()
1249- .set_repo_path(repo_path.display().to_string().as_str().into());
1250- }
1251- let result = request.send().promise.await?;
1252- let jobs: Vec<Job> = result
1253- .get()?
1254- .get_jobs()?
1255- .iter()
1256- .map(|item| Job {
1257- id: item.get_id(),
1258- repo_path: String::from_utf8(item.get_repo_path().unwrap().0.to_vec()).unwrap(),
1259- created_at: item.get_created_at(),
1260- runtime: item.get_runtime(),
1261- kind: Kind::from(item.get_kind().unwrap()),
1262- commits: item.get_commits(),
1263- })
1264- .collect();
1265- Ok(jobs)
1266- })
1267+ let client = jobs_client(&config.jobs_socket_path).await?;
1268+ let jobs = client
1269+ .list(
1270+ context::current(),
1271+ repo_path.map(|path| path.display().to_string()),
1272+ )
1273 .await?;
1274 let mut tw = TabWriter::new(vec![]);
1275 writeln!(&mut tw, "name\ttime\truntime\tkind\tcommits")?;
1276 @@ -119,16 +103,9 @@ pub async fn list(config: Config, repo_path: Option<PathBuf>) -> Result<()> {
1277
1278 pub async fn purge(config: Config, repo_path: Option<PathBuf>) -> Result<()> {
1279 let repo_path = resolve_path(repo_path.as_ref())?;
1280- RpcHelper::new(&config.jobs_socket_path)
1281- .invoke(move |client: Client| async move {
1282- let mut request = client.purge_request();
1283- // FIXME: set_repo_path must take Path/PathBuf
1284- request
1285- .get()
1286- .set_repo_path(repo_path.display().to_string().as_str().into());
1287- request.send().promise.await?;
1288- Ok(())
1289- })
1290+ let client = jobs_client(&config.jobs_socket_path).await?;
1291+ client
1292+ .purge(context::current(), repo_path.display().to_string())
1293 .await?;
1294 Ok(())
1295 }
1296 diff --git a/ayllu/src/job_server/jobs/cloc.rs b/ayllu/src/job_server/jobs/cloc.rs
1297index 73a1d64..df3f8c5 100644
1298--- a/ayllu/src/job_server/jobs/cloc.rs
1299+++ b/ayllu/src/job_server/jobs/cloc.rs
1300 @@ -1,3 +1,4 @@
1301+ use std::path::Path;
1302 use std::sync::Arc;
1303 use std::time::{Duration, SystemTime};
1304
1305 @@ -5,52 +6,47 @@ use anyhow::Result;
1306 use tokei::{Config as TokeiConfig, Languages};
1307
1308 use crate::database_ext::{jobs::JobsExt, langauges::LanguagesExt};
1309- use ayllu_api::models::{Job as JobModel, Kind};
1310+ use ayllu_api::jobs::Kind;
1311 use ayllu_database::Wrapper as Database;
1312 use ayllu_git::Wrapper as Repository;
1313
1314- pub struct Job {
1315- pub repository: Repository,
1316- pub database: Arc<Database>,
1317- pub model: JobModel,
1318- }
1319-
1320- impl Job {
1321- pub async fn invoke(&self, commits: Vec<String>) -> Result<Duration> {
1322- let start = SystemTime::now();
1323- let repo_path = self.repository.path();
1324- for (i, commit) in commits.iter().enumerate() {
1325- if i % 50 == 0 && i != 0 {
1326- log::info!("processed {}/{} commits", i, commits.len());
1327- }
1328- let mut stats: Vec<(&str, i64)> = Vec::new();
1329- let git_hash = commit.to_string();
1330- self.repository
1331- .with_worktree("/tmp", Some(git_hash.as_str()), |tree_path| {
1332- let config = TokeiConfig::default();
1333- let mut languages = Languages::new();
1334- let paths = &[tree_path];
1335- let exclude = &[];
1336- languages.get_statistics(paths, exclude, &config);
1337- for language in languages {
1338- let name = language.0.name();
1339- let loc = language.1.code;
1340- stats.push((name, loc.try_into().unwrap()));
1341- }
1342- Ok(())
1343- })?;
1344- self.database
1345- .language_create(&repo_path, git_hash.as_str(), stats)
1346- .await?;
1347- self.database
1348- .create_hash(
1349- &repo_path,
1350- git_hash.as_str(),
1351- Kind::Cloc.to_string().as_str(),
1352- self.model.id,
1353- )
1354- .await?;
1355+ pub async fn invoke(
1356+ db: Arc<Database>,
1357+ repo_path: &Path,
1358+ commits: Vec<String>,
1359+ job_id: i64,
1360+ ) -> Result<Duration> {
1361+ let repository = Repository::new(repo_path)?;
1362+ let start = SystemTime::now();
1363+ let repo_path = repository.path();
1364+ for (i, commit) in commits.iter().enumerate() {
1365+ if i % 50 == 0 && i != 0 {
1366+ log::info!("processed {}/{} commits", i, commits.len());
1367 }
1368- Ok(start.elapsed().unwrap())
1369+ let mut stats: Vec<(&str, i64)> = Vec::new();
1370+ let git_hash = commit.to_string();
1371+ repository.with_worktree("/tmp", Some(git_hash.as_str()), |tree_path| {
1372+ let config = TokeiConfig::default();
1373+ let mut languages = Languages::new();
1374+ let paths = &[tree_path];
1375+ let exclude = &[];
1376+ languages.get_statistics(paths, exclude, &config);
1377+ for language in languages {
1378+ let name = language.0.name();
1379+ let loc = language.1.code;
1380+ stats.push((name, loc.try_into().unwrap()));
1381+ }
1382+ Ok(())
1383+ })?;
1384+ db.language_create(&repo_path, git_hash.as_str(), stats)
1385+ .await?;
1386+ db.create_hash(
1387+ &repo_path,
1388+ git_hash.as_str(),
1389+ Kind::Cloc.to_string().as_str(),
1390+ job_id,
1391+ )
1392+ .await?;
1393 }
1394+ Ok(start.elapsed().unwrap())
1395 }
1396 diff --git a/ayllu/src/job_server/jobs/contributors.rs b/ayllu/src/job_server/jobs/contributors.rs
1397index cc5e9a4..555e4a0 100644
1398--- a/ayllu/src/job_server/jobs/contributors.rs
1399+++ b/ayllu/src/job_server/jobs/contributors.rs
1400 @@ -1,11 +1,12 @@
1401 use std::sync::Arc;
1402+ use std::path::Path;
1403 use std::time::{Duration, SystemTime};
1404 use time::OffsetDateTime;
1405
1406 use anyhow::Result;
1407
1408 use crate::database_ext::{contributors::ContributorsExt, jobs::JobsExt};
1409- use ayllu_api::models::{Job as JobModel, Kind};
1410+ use ayllu_api::jobs::Kind;
1411 use ayllu_database::Wrapper as Database;
1412 use ayllu_git::Wrapper as Repository;
1413
1414 @@ -18,53 +19,48 @@ struct Contribution {
1415 lines_removed: usize,
1416 }
1417
1418- pub struct Job {
1419- pub repository: Repository,
1420- pub database: Arc<Database>,
1421- pub model: JobModel,
1422- }
1423-
1424- impl Job {
1425- pub async fn invoke(&self, commits: Vec<String>) -> Result<Duration> {
1426- let start = SystemTime::now();
1427- let repo_path = self.repository.path();
1428- for (i, commit) in commits.iter().enumerate() {
1429- if i % 50 == 0 && i != 0 {
1430- log::info!("processed {}/{} commits", i, commits.len());
1431- }
1432- let mut contribution = Contribution::default();
1433- let git_hash = commit.to_string();
1434- let commit = self.repository.commit(Some(git_hash.clone()))?.unwrap();
1435- let stats = self.repository.stats(git_hash.clone().as_str())?;
1436- contribution.name = commit.author_name;
1437- contribution.email = commit.author_email;
1438- contribution.time = commit.epoch;
1439- contribution.lines_added = stats.insertions;
1440- contribution.lines_removed = stats.deletions;
1441- let timestamp = OffsetDateTime::from_unix_timestamp(contribution.time).unwrap();
1442- let author_id = self
1443- .database
1444- .upsert_author(contribution.name.as_str(), contribution.email.as_str())
1445- .await?;
1446- self.database
1447- .contribution_add(
1448- author_id,
1449- &repo_path,
1450- git_hash.as_str(),
1451- timestamp,
1452- contribution.lines_added as i64,
1453- contribution.lines_removed as i64,
1454- )
1455- .await?;
1456- self.database
1457- .create_hash(
1458- &repo_path,
1459- git_hash.as_str(),
1460- Kind::Contributors.to_string().as_str(),
1461- self.model.id,
1462- )
1463- .await?;
1464+ pub async fn invoke(
1465+ db: Arc<Database>,
1466+ repo_path: &Path,
1467+ commits: Vec<String>,
1468+ job_id: i64,
1469+ ) -> Result<Duration> {
1470+ let repository = Repository::new(repo_path)?;
1471+ let start = SystemTime::now();
1472+ let repo_path = repository.path();
1473+ for (i, commit) in commits.iter().enumerate() {
1474+ if i % 50 == 0 && i != 0 {
1475+ log::info!("processed {}/{} commits", i, commits.len());
1476 }
1477- Ok(start.elapsed().unwrap())
1478+ let mut contribution = Contribution::default();
1479+ let git_hash = commit.to_string();
1480+ let commit = repository.commit(Some(git_hash.clone()))?.unwrap();
1481+ let stats = repository.stats(git_hash.clone().as_str())?;
1482+ contribution.name = commit.author_name;
1483+ contribution.email = commit.author_email;
1484+ contribution.time = commit.epoch;
1485+ contribution.lines_added = stats.insertions;
1486+ contribution.lines_removed = stats.deletions;
1487+ let timestamp = OffsetDateTime::from_unix_timestamp(contribution.time).unwrap();
1488+ let author_id = db
1489+ .upsert_author(contribution.name.as_str(), contribution.email.as_str())
1490+ .await?;
1491+ db.contribution_add(
1492+ author_id,
1493+ &repo_path,
1494+ git_hash.as_str(),
1495+ timestamp,
1496+ contribution.lines_added as i64,
1497+ contribution.lines_removed as i64,
1498+ )
1499+ .await?;
1500+ db.create_hash(
1501+ &repo_path,
1502+ git_hash.as_str(),
1503+ Kind::Contributors.to_string().as_str(),
1504+ job_id,
1505+ )
1506+ .await?;
1507 }
1508+ Ok(start.elapsed().unwrap())
1509 }
1510 diff --git a/ayllu/src/job_server/jobs/mod.rs b/ayllu/src/job_server/jobs/mod.rs
1511index eb3ca38..9a63152 100644
1512--- a/ayllu/src/job_server/jobs/mod.rs
1513+++ b/ayllu/src/job_server/jobs/mod.rs
1514 @@ -1,37 +1,2 @@
1515- use std::sync::Arc;
1516- use std::time::Duration;
1517-
1518- use anyhow::Result;
1519-
1520- use ayllu_api::models::{Job, Kind};
1521- use ayllu_database::Wrapper as Database;
1522- use ayllu_git::Wrapper as Repository;
1523-
1524- mod cloc;
1525- mod contributors;
1526-
1527- pub async fn invoke(
1528- commits: Vec<String>,
1529- job: Job,
1530- repository: Repository,
1531- database: Arc<Database>,
1532- ) -> Result<Duration> {
1533- match job.kind {
1534- Kind::Contributors => {
1535- let job = contributors::Job {
1536- repository,
1537- database,
1538- model: job,
1539- };
1540- job.invoke(commits).await
1541- }
1542- Kind::Cloc => {
1543- let job = cloc::Job {
1544- repository,
1545- database,
1546- model: job,
1547- };
1548- job.invoke(commits).await
1549- }
1550- }
1551- }
1552+ pub mod cloc;
1553+ pub mod contributors;
1554 diff --git a/ayllu/src/job_server/mod.rs b/ayllu/src/job_server/mod.rs
1555index 0cceca5..374fac6 100644
1556--- a/ayllu/src/job_server/mod.rs
1557+++ b/ayllu/src/job_server/mod.rs
1558 @@ -1,7 +1,48 @@
1559+ use std::error::Error;
1560+ use std::path::Path;
1561+
1562+ use tarpc::serde_transport::unix;
1563+
1564+ use tracing::info;
1565+
1566+ use crate::config::Config;
1567+ use ayllu_api::jobs::Server;
1568+ use ayllu_database::Builder;
1569+ use ayllu_rpc::{
1570+ futures::prelude::*,
1571+ init_socket, spawn,
1572+ tarpc::{
1573+ server::{BaseChannel, Channel},
1574+ tokio_serde::formats::Bincode,
1575+ },
1576+ };
1577+
1578 pub use commands::*;
1579- pub use server::serve;
1580
1581 mod commands;
1582 mod jobs;
1583 mod runner;
1584 mod server;
1585+
1586+ pub async fn serve(cfg: &Config) -> Result<(), Box<dyn Error>> {
1587+ let db = Builder::default().url(&cfg.database.path).build().await?;
1588+ // run server
1589+ let socket_path = Path::new(&cfg.jobs_socket_path);
1590+ init_socket(socket_path)?;
1591+ info!("job server listening @ {:?}", socket_path);
1592+ let mut listener = unix::listen(socket_path, Bincode::default).await?;
1593+ listener.config_mut().max_frame_length(usize::MAX);
1594+ listener
1595+ // Ignore accept errors.
1596+ .filter_map(|r| future::ready(r.ok()))
1597+ .map(BaseChannel::with_defaults)
1598+ .map(|channel| {
1599+ let server = server::Server::new(db.clone().into());
1600+ channel.execute(server.serve()).for_each(spawn)
1601+ })
1602+ // Max 10 channels.
1603+ .buffer_unordered(10)
1604+ .for_each(|_| async {})
1605+ .await;
1606+ Ok(())
1607+ }
1608 diff --git a/ayllu/src/job_server/runner.rs b/ayllu/src/job_server/runner.rs
1609index b5a7570..3c3fd09 100644
1610--- a/ayllu/src/job_server/runner.rs
1611+++ b/ayllu/src/job_server/runner.rs
1612 @@ -1,12 +1,13 @@
1613 use std::path::Path;
1614 use std::sync::Arc;
1615+ use std::time::Duration;
1616
1617 use anyhow::Result;
1618 use tracing::log;
1619
1620 use crate::database_ext::jobs::JobsExt;
1621- use crate::job_server::jobs::invoke;
1622- use ayllu_api::models::{Job, Kind};
1623+ use crate::job_server::jobs;
1624+ use ayllu_api::jobs::{Job, Kind};
1625 use ayllu_database::Wrapper as Database;
1626 use ayllu_git::Wrapper as Repository;
1627
1628 @@ -25,7 +26,21 @@ impl Runner {
1629 }
1630 }
1631
1632- async fn commits(&self, kind: Kind, repository: &Repository) -> Result<Vec<String>> {
1633+ pub async fn invoke(
1634+ &self,
1635+ commits: Vec<String>,
1636+ job: Job,
1637+ repo_path: &Path,
1638+ ) -> Result<Duration> {
1639+ let database = self.database.clone();
1640+ match job.kind {
1641+ Kind::Contributors => jobs::contributors::invoke(database, repo_path, commits, job.id).await,
1642+ Kind::Cloc => jobs::cloc::invoke(database, repo_path, commits, job.id).await,
1643+ }
1644+ }
1645+
1646+ async fn commits(&self, kind: Kind, repo_path: &Path) -> Result<Vec<String>> {
1647+ let repository = Repository::new(repo_path)?;
1648 let repo_path = repository.path();
1649 let latest_db_hash = self
1650 .database
1651 @@ -64,9 +79,12 @@ impl Runner {
1652
1653 let mut job = self.job.clone();
1654 job.id = job_id;
1655+ let repo_path = job.repo_path.clone();
1656
1657- let repository = Repository::new(Path::new(&job.repo_path))?;
1658- let commits = self.commits(job.kind.clone(), &repository).await?;
1659+ // let repository = Repository::new(Path::new(&job.repo_path))?;
1660+ let commits = self
1661+ .commits(job.kind.clone(), Path::new(&repo_path))
1662+ .await?;
1663 log::info!("processing {} commits", commits.len());
1664 if commits.is_empty() {
1665 // nothing to do
1666 @@ -76,7 +94,7 @@ impl Runner {
1667 return Ok(());
1668 }
1669
1670- let runtime = invoke(commits, job, repository, self.database.clone()).await?;
1671+ let runtime = self.invoke(commits, job, Path::new(&repo_path)).await?;
1672
1673 let duration = runtime.as_millis() as i64;
1674 log::info!("runtime {}ms", duration);
1675 diff --git a/ayllu/src/job_server/server.rs b/ayllu/src/job_server/server.rs
1676index c66afcb..3975b5c 100644
1677--- a/ayllu/src/job_server/server.rs
1678+++ b/ayllu/src/job_server/server.rs
1679 @@ -1,112 +1,65 @@
1680- use std::error::Error as StdError;
1681 use std::sync::Arc;
1682
1683- use capnp::{capability::Promise, Error};
1684- use capnp_rpc::pry;
1685-
1686- use crate::config::Config;
1687 use crate::database_ext::jobs::JobsExt;
1688 use crate::job_server::runner::Runner;
1689- use ayllu_api::jobs_capnp::server::{
1690- Client, ListParams, ListResults, PurgeParams, PurgeResults, Server, SubmitParams, SubmitResults,
1691- };
1692- use ayllu_api::models::{Job, Kind};
1693- use ayllu_database::{Wrapper as Database, Builder};
1694- use ayllu_rpc::Server as CapnpServerHelper;
1695-
1696+ use ayllu_api::jobs::{Job, Kind, Server as Api};
1697+ use ayllu_database::Wrapper as Database;
1698+ use ayllu_rpc::tarpc::context::Context;
1699
1700 #[derive(Clone)]
1701- pub struct ServerImpl {
1702+ pub struct Server {
1703 db: Arc<Database>,
1704 }
1705
1706- impl Server for ServerImpl {
1707- fn submit(
1708- &mut self,
1709- params: SubmitParams,
1710- _: SubmitResults,
1711- ) -> ::capnp::capability::Promise<(), ::capnp::Error> {
1712- let repo_path = pry!(pry!(pry!(params.get()).get_repo_path()).to_string());
1713- let kind = match pry!(pry!(params.get()).get_kind()) {
1714- ayllu_api::jobs_capnp::Kind::Contributors => String::from("contributors"),
1715- ayllu_api::jobs_capnp::Kind::Cloc => String::from("cloc"),
1716- };
1717- let db = self.db.clone();
1718- Promise::from_future(async move {
1719- let job = Job {
1720- id: 0,
1721- repo_path: repo_path.clone(),
1722- created_at: 0,
1723- runtime: 0,
1724- kind: Kind::from(kind),
1725- commits: 0,
1726- };
1727- match Runner::new(job, db).run().await {
1728- Ok(()) => Ok(()),
1729- Err(e) => Err(Error {
1730- kind: capnp::ErrorKind::Failed,
1731- extra: e.to_string(),
1732- }),
1733- }
1734- })
1735+ impl Server {
1736+ pub fn new(db: Arc<Database>) -> Self {
1737+ Server { db }
1738 }
1739+ }
1740
1741- fn list(
1742- &mut self,
1743- _: ListParams,
1744- mut result: ListResults,
1745- ) -> ::capnp::capability::Promise<(), ::capnp::Error> {
1746+ impl Api for Server {
1747+ #[doc = r" Submit a new job to be run in the background"]
1748+ async fn submit(self, _context: Context, kind: Kind, repo_path: String) {
1749 let db = self.db.clone();
1750- Promise::from_future(async move {
1751- match db.list_jobs(None).await {
1752- Ok(jobs) => {
1753- let mut jobs_result = result.get().init_jobs(jobs.len() as u32);
1754- for (i, job) in jobs.iter().enumerate() {
1755- let mut job_result = jobs_result.reborrow().get(i as u32);
1756- job_result.set_created_at(job.created_at.unix_timestamp());
1757- if job.repo_path.is_some() {
1758- let repo_path = job.repo_path.as_ref().unwrap();
1759- job_result.set_repo_path(repo_path.as_str().into())
1760- }
1761- if job.runtime.is_some() {
1762- job_result.set_runtime(job.runtime.unwrap() as i64)
1763- }
1764- job_result.set_commits(job.commits as i64);
1765- job_result.set_kind(Kind::from(job.kind.clone()).into());
1766- job_result.set_success(job.success.is_some_and(|success| success))
1767- }
1768- Ok(())
1769- }
1770- Err(e) => Err(Error {
1771- kind: capnp::ErrorKind::Failed,
1772- extra: e.to_string(),
1773- }),
1774- }
1775- })
1776+ let job = Job {
1777+ id: 0,
1778+ created_at: 0,
1779+ kind,
1780+ repo_path,
1781+ runtime: 0,
1782+ success: false,
1783+ commits: 0,
1784+ };
1785+ Runner::new(job, db).run().await.expect("failed to run job");
1786 }
1787
1788- fn purge(
1789- &mut self,
1790- params: PurgeParams,
1791- _: PurgeResults,
1792- ) -> ::capnp::capability::Promise<(), ::capnp::Error> {
1793- let repo_path = pry!(pry!(pry!(params.get()).get_repo_path()).to_string());
1794+ #[doc = r" List submitted and complete jobs"]
1795+ async fn list(self, _context: Context, repo_path: Option<String>) -> Vec<Job> {
1796 let db = self.db.clone();
1797- Promise::from_future(async move {
1798- match db.purge(&repo_path).await {
1799- Ok(_) => Ok(()),
1800- Err(e) => Err(Error {
1801- kind: capnp::ErrorKind::Failed,
1802- extra: e.to_string(),
1803- }),
1804- }
1805- })
1806+ let jobs = db
1807+ .list_jobs(repo_path.as_deref())
1808+ .await
1809+ .expect("failed to list jobs");
1810+ jobs.iter()
1811+ .map(|job| Job {
1812+ id: job.id,
1813+ created_at: job.created_at.unix_timestamp(),
1814+ kind: job.kind.clone().into(),
1815+ // FIXME
1816+ repo_path: job
1817+ .repo_path
1818+ .clone()
1819+ .expect("the database only supports absolute paths"),
1820+ runtime: job.runtime.map_or(0, |n| n as i64),
1821+ success: job.success.unwrap_or(false),
1822+ commits: job.commits as i64,
1823+ })
1824+ .collect()
1825 }
1826- }
1827
1828- pub async fn serve(cfg: &Config) -> Result<(), Box<dyn StdError>> {
1829- let db = Builder::default().url(&cfg.database.path).build().await?;
1830- let server = ServerImpl { db: Arc::new(db) };
1831- let runtime = CapnpServerHelper::<Client, ServerImpl>::new(&cfg.jobs_socket_path, server);
1832- runtime.serve().await.map_err(|e| e.into())
1833+ #[doc = r" Purge all jobs associated with the given repository"]
1834+ async fn purge(self, _context: Context, repo_path: String) {
1835+ let db = self.db.clone();
1836+ db.purge(&repo_path).await.expect("failed to purge jobs");
1837+ }
1838 }
1839 diff --git a/ayllu/src/web2/error.rs b/ayllu/src/web2/error.rs
1840index 38d078e..1f7d718 100644
1841--- a/ayllu/src/web2/error.rs
1842+++ b/ayllu/src/web2/error.rs
1843 @@ -5,9 +5,10 @@ use std::io::Error as IoError;
1844 use axum::{body::Body, response::IntoResponse, response::Response};
1845 use tera::Error as TeraError;
1846
1847+ use ayllu_api::error::ApiError;
1848 use ayllu_database::Error as SqlError;
1849 use ayllu_git::Error as GitError;
1850- use ayllu_rpc::Error as RpcError;
1851+ use ayllu_rpc::tarpc::client::RpcError;
1852
1853 /// Error maps known error types into errors that can be translated into HTTP
1854 /// status codes, e.g. Io::NotFound -> 404
1855 @@ -32,6 +33,12 @@ impl From<Box<dyn StdError>> for Error {
1856 }
1857 }
1858
1859+ impl From<ApiError> for Error {
1860+ fn from(value: ApiError) -> Self {
1861+ Error::Message(format!("RPC [ayllu]: {:?}", value))
1862+ }
1863+ }
1864+
1865 impl From<RpcError> for Error {
1866 fn from(value: RpcError) -> Self {
1867 Error::Message(format!("RPC: {:?}", value))
1868 diff --git a/ayllu/src/web2/middleware/rpc_initiator.rs b/ayllu/src/web2/middleware/rpc_initiator.rs
1869index cdc455c..0ef7534 100644
1870--- a/ayllu/src/web2/middleware/rpc_initiator.rs
1871+++ b/ayllu/src/web2/middleware/rpc_initiator.rs
1872 @@ -1,3 +1,4 @@
1873+ use std::path::Path;
1874 use std::sync::Arc;
1875
1876 use crate::web2::terautil::{Loader, Options};
1877 @@ -15,7 +16,8 @@ use tracing::log;
1878 use crate::config::Config;
1879 use crate::web2::config::Config as ClientConfig;
1880 use crate::web2::extractors::config::ConfigReader;
1881- use ayllu_rpc::Client;
1882+ use ayllu_api::{mail::ServerClient as MailClient, xmpp::ServerClient as XmppClient};
1883+ use ayllu_rpc::tarpc::{client, serde_transport::unix, tokio_serde::formats::Bincode};
1884
1885 pub type State = Arc<(Config, Vec<(String, Tera)>, &'static [Kind])>;
1886
1887 @@ -25,18 +27,12 @@ pub enum Kind {
1888 }
1889
1890 #[derive(Clone, Default)]
1891+ /// Initiator contains zero or more active RPC connections to various plugin
1892+ /// servers. Pages that mark plugins as required will always have them
1893+ /// configured and they can be safely unwrapped.
1894 pub struct Initiator {
1895- mail: Option<Client>,
1896- xmpp: Option<Client>,
1897- }
1898-
1899- impl Initiator {
1900- pub fn client(self, kind: Kind) -> Option<Client> {
1901- match kind {
1902- Kind::Mail => self.mail,
1903- Kind::Xmpp => self.xmpp,
1904- }
1905- }
1906+ pub mail: Option<MailClient>,
1907+ pub xmpp: Option<XmppClient>,
1908 }
1909
1910 fn plugin_not_enabled(
1911 @@ -72,22 +68,28 @@ fn plugin_not_enabled(
1912 .unwrap()
1913 }
1914
1915- /// configure an optinoal plugin for use in a handler
1916+ /// configure an optional plugin for use in a handler
1917 pub async fn optional(
1918 extract::State(cfg): extract::State<Arc<Config>>,
1919 mut req: extract::Request,
1920 next: Next,
1921 ) -> Response {
1922- req.extensions_mut().insert(Initiator {
1923- mail: cfg
1924- .mail
1925- .as_ref()
1926- .map(|mail_cfg| Client::new(&mail_cfg.socket_path)),
1927- xmpp: cfg
1928- .xmpp
1929- .as_ref()
1930- .map(|xmpp_cfg| Client::new(&xmpp_cfg.socket_path)),
1931- });
1932+ let mut initator = Initiator::default();
1933+ if let Some(mail_cfg) = cfg.mail.as_ref() {
1934+ // FIXME
1935+ let conn = unix::connect(Path::new(&mail_cfg.socket_path), Bincode::default)
1936+ .await
1937+ .unwrap();
1938+ initator.mail = Some(MailClient::new(client::Config::default(), conn).spawn());
1939+ };
1940+ if let Some(xmpp_cfg) = cfg.xmpp.as_ref() {
1941+ // FIXME
1942+ let conn = unix::connect(Path::new(&xmpp_cfg.socket_path), Bincode::default)
1943+ .await
1944+ .unwrap();
1945+ initator.xmpp = Some(XmppClient::new(client::Config::default(), conn).spawn());
1946+ };
1947+ req.extensions_mut().insert(initator);
1948 next.run(req).await
1949 }
1950
1951 @@ -104,13 +106,21 @@ pub async fn required(
1952 match kind {
1953 Kind::Mail => match &state.0.mail {
1954 Some(mail_cfg) => {
1955- initiator.mail = Some(Client::new(&mail_cfg.socket_path));
1956+ // FIXME
1957+ let conn = unix::connect(Path::new(&mail_cfg.socket_path), Bincode::default)
1958+ .await
1959+ .unwrap();
1960+ initiator.mail = Some(MailClient::new(client::Config::default(), conn).spawn());
1961 }
1962 None => return plugin_not_enabled("ayllu-mail", &state.0, client_config, &state.1),
1963 },
1964 Kind::Xmpp => match &state.0.xmpp {
1965 Some(xmpp_cfg) => {
1966- initiator.xmpp = Some(Client::new(&xmpp_cfg.socket_path));
1967+ // FIXME
1968+ let conn = unix::connect(Path::new(&xmpp_cfg.socket_path), Bincode::default)
1969+ .await
1970+ .unwrap();
1971+ initiator.xmpp = Some(XmppClient::new(client::Config::default(), conn).spawn());
1972 }
1973 None => return plugin_not_enabled("ayllu-xmpp", &state.0, client_config, &state.1),
1974 },
1975 diff --git a/ayllu/src/web2/routes/mail.rs b/ayllu/src/web2/routes/mail.rs
1976index e6a4977..199f1de 100644
1977--- a/ayllu/src/web2/routes/mail.rs
1978+++ b/ayllu/src/web2/routes/mail.rs
1979 @@ -7,14 +7,11 @@ use axum::{
1980 };
1981 use serde::{Deserialize, Serialize};
1982
1983- use crate::languages::Hint;
1984 use crate::web2::error::Error;
1985- use crate::web2::highlight::Highlighter;
1986- use crate::web2::middleware::rpc_initiator::{Initiator, Kind as InitiatorKind};
1987+ use crate::web2::middleware::rpc_initiator::Initiator;
1988 use crate::web2::middleware::template::Template;
1989 use crate::web2::navigation;
1990- use ayllu_api::mail_capnp::server::Client as MailClient;
1991- use ayllu_rpc::Client;
1992+ use ayllu_rpc::tarpc::context;
1993
1994 #[derive(Deserialize)]
1995 pub struct Params {
1996 @@ -55,73 +52,15 @@ struct List {
1997 pub request_address: String,
1998 }
1999
2000- async fn read_list(mail_client: Client, list_id: String) -> Result<Option<List>, Error> {
2001- let list = mail_client
2002- .invoke(move |c: MailClient| async move {
2003- let req = c.lists_request();
2004- let result = req.send().promise.await?;
2005- let rpc_lists = result.get()?.get_lists()?;
2006- for list in rpc_lists.iter() {
2007- let id = list.get_id()?.to_string().unwrap();
2008- let topics: Vec<String> = list
2009- .get_topics()?
2010- .iter()
2011- .map(|topic| topic.unwrap().to_string().unwrap())
2012- .collect();
2013- if list_id == id {
2014- return Ok(Some(List {
2015- id: list_id.clone(),
2016- name: list.get_name()?.to_string().unwrap(),
2017- description: None,
2018- address: list.get_address()?.to_string().unwrap(),
2019- topics,
2020- request_address: list.get_request_address()?.to_string().unwrap(),
2021- }));
2022- }
2023- }
2024- Ok(None)
2025- })
2026- .await?;
2027- Ok(list)
2028- }
2029-
2030 pub async fn lists(
2031 Extension(initiator): Extension<Initiator>,
2032 Extension((templates, mut ctx)): Extension<Template>,
2033 ) -> Result<Html<String>, Error> {
2034 ctx.insert("title", "lists");
2035 ctx.insert("nav_elements", &navigation::global("mail", true));
2036- let mail_client = initiator.client(InitiatorKind::Mail).unwrap();
2037- let lists = mail_client
2038- .invoke(move |c: MailClient| async move {
2039- let mut lists: Vec<List> = Vec::new();
2040- let req = c.lists_request();
2041- let result = req.send().promise.await?;
2042- let rpc_lists = result.get()?.get_lists()?;
2043- for list in rpc_lists.iter() {
2044- let description = if list.has_description() {
2045- Some(list.get_description()?.to_string().unwrap())
2046- } else {
2047- None
2048- };
2049- let topics: Vec<String> = list
2050- .get_topics()?
2051- .iter()
2052- .map(|topic| topic.unwrap().to_string().unwrap())
2053- .collect();
2054- lists.push(List {
2055- id: list.get_id()?.to_string().unwrap(),
2056- name: list.get_name()?.to_string().unwrap(),
2057- description,
2058- address: list.get_name()?.to_string().unwrap(),
2059- topics,
2060- request_address: list.get_request_address()?.to_string().unwrap(),
2061- })
2062- }
2063- Ok(lists)
2064- })
2065- .await?;
2066- ctx.insert("lists", &lists);
2067+ let mail_client = initiator.mail.unwrap();
2068+ let rpc_lists = mail_client.lists(context::current()).await??;
2069+ ctx.insert("lists", &rpc_lists);
2070 // ctx.insert("lists", &cfg.mail.unwrap().lists);
2071 let body = templates.render("lists.html", &ctx)?;
2072 Ok(Html(body))
2073 @@ -134,8 +73,9 @@ pub async fn threads(
2074 Extension(initiator): Extension<Initiator>,
2075 Extension((templates, mut ctx)): Extension<Template>,
2076 ) -> Result<Html<String>, Error> {
2077- let client = initiator.client(InitiatorKind::Mail).unwrap();
2078- let mailing_list = read_list(client.clone(), params.list_id.clone()).await?;
2079+ let client = initiator.mail.unwrap();
2080+ let lists = client.lists(context::current()).await??;
2081+ let mailing_list = lists.iter().find(|list| list.id == params.list_id);
2082 if let Some(mailing_list) = mailing_list {
2083 ctx.insert("title", &format!("list {}", mailing_list.name));
2084 ctx.insert("nav_elements", &navigation::global("mail", true));
2085 @@ -143,29 +83,11 @@ pub async fn threads(
2086 ctx.insert("title", &format!("list {}", mailing_list.address));
2087 ctx.insert("nav_elements", &navigation::global("mail", true));
2088 ctx.insert("list", &mailing_list);
2089- let mut threads = client
2090- .invoke(move |c: MailClient| async move {
2091- let mut threads: Vec<Thread> = Vec::new();
2092- let mut req = c.list_threads_request();
2093- req.get().set_id(params.list_id.as_str().into());
2094- let result = req.send().promise.await?;
2095- let rpc_threads = result.get()?.get_threads()?;
2096- for thread in rpc_threads.iter() {
2097- let message = thread.get_first()?;
2098- let message_id = message.get_message_id()?.to_string().unwrap();
2099- threads.push(Thread {
2100- id: message.get_id(),
2101- message_id,
2102- from: message.get_from()?.to_string().unwrap(),
2103- subject: message.get_subject()?.to_string().unwrap(),
2104- n_replies: thread.get_n_replies(),
2105- timestamp: thread.get_first().unwrap().get_timestamp(),
2106- })
2107- }
2108- Ok(threads)
2109- })
2110- .await?;
2111- threads.sort_by(|first, second| second.timestamp.cmp(&first.timestamp));
2112+ let threads = client
2113+ .list_threads(context::current(), params.list_id, 0, 0)
2114+ .await??;
2115+ // FIXME
2116+ // threads.sort_by(|first, second| second.timestamp.cmp(&first.timestamp));
2117 ctx.insert("threads", &threads);
2118 ctx.insert("request_address", &mailing_list.request_address);
2119 let body = templates.render("threads.html", &ctx)?;
2120 @@ -181,56 +103,31 @@ pub async fn threads(
2121 pub async fn thread(
2122 Path(params): Path<Params>,
2123 Extension(initiator): Extension<Initiator>,
2124- Extension(highlighter): Extension<Highlighter>,
2125+ // Extension(highlighter): Extension<Highlighter>,
2126 Extension((templates, mut ctx)): Extension<Template>,
2127 ) -> Result<Html<String>, Error> {
2128- let client = initiator.client(InitiatorKind::Mail).unwrap();
2129- let mailing_list = read_list(client.clone(), params.list_id.clone()).await?;
2130+ let client = initiator.mail.unwrap();
2131+ let lists = client.lists(context::current()).await??;
2132+ let mailing_list = lists.iter().find(|list| list.id == params.list_id);
2133 if let Some(mailing_list) = mailing_list {
2134- let thread_id = params.thread_id.clone();
2135 let messages = client
2136- .invoke(move |c: MailClient| async move {
2137- let mut messages: Vec<Message> = Vec::new();
2138- let mut req = c.read_thread_request();
2139- req.get().set_id(params.list_id.as_str().into());
2140- req.get().set_message_id(thread_id.unwrap().as_str().into());
2141- let result = req.send().promise.await?;
2142- for message in result.get()?.get_thread()? {
2143- let text = message.get_text().unwrap().to_string().unwrap();
2144- let text = if message.get_is_patch() {
2145- let (_, diff) = highlighter.highlight(
2146- &text,
2147- None,
2148- None,
2149- Some(Hint::from("DIFF")),
2150- false,
2151- );
2152- diff
2153- } else {
2154- text
2155- };
2156- messages.push(Message {
2157- id: message.get_id().to_string(),
2158- subject: message.get_subject()?.to_string().unwrap(),
2159- message_id: message.get_message_id()?.to_string().unwrap(),
2160- created_at: message.get_timestamp(),
2161- from_address: message.get_address()?.to_string().unwrap(),
2162- body: message.get_body()?.to_string().unwrap(),
2163- text,
2164- is_patch: message.get_is_patch(),
2165- })
2166- }
2167- Ok(messages)
2168- })
2169- .await?;
2170- let subject = messages.first().map(|message| message.subject.clone());
2171+ .read_thread(
2172+ context::current(),
2173+ params.thread_id.as_ref().unwrap().clone(),
2174+ params.message_id.unwrap().clone(),
2175+ 1000,
2176+ 0,
2177+ )
2178+ .await??;
2179+ // FIXME
2180+ // let subject = messages.first().map(|message| message.subject.clone());
2181 ctx.insert("title", &format!("list {}", mailing_list.address));
2182 ctx.insert("nav_elements", &navigation::global("mail", true));
2183 ctx.insert("list", &mailing_list);
2184 ctx.insert("list_id", &mailing_list.id);
2185 ctx.insert("thread_id", &params.thread_id.clone());
2186 ctx.insert("messages", &messages);
2187- ctx.insert("subject", &subject);
2188+ // ctx.insert("subject", &subject);
2189 let body = templates.render("thread.html", &ctx)?;
2190 Ok(Html(body))
2191 } else {
2192 @@ -244,41 +141,19 @@ pub async fn thread(
2193 pub async fn message(
2194 Path(params): Path<Params>,
2195 Extension(initiator): Extension<Initiator>,
2196- Extension(highlighter): Extension<Highlighter>,
2197 Extension((templates, mut ctx)): Extension<Template>,
2198 ) -> Result<Html<String>, Error> {
2199- let client = initiator.client(InitiatorKind::Mail).unwrap();
2200- let mailing_list = read_list(client.clone(), params.list_id.clone()).await?;
2201+ let client = initiator.mail.unwrap();
2202+ let lists = client.lists(context::current()).await??;
2203+ let mailing_list = lists.iter().find(|list| list.id == params.list_id);
2204 if let Some(mailing_list) = mailing_list {
2205 let message = client
2206- .invoke(move |c: MailClient| async move {
2207- let mut req = c.read_post_request();
2208- req.get().set_id(params.list_id.as_str().into());
2209- req.get()
2210- .set_message_id(params.message_id.unwrap().as_str().into());
2211- // TODO: detect a missing message and return 404 appropriately
2212- let result = req.send().promise.await?;
2213- let message = result.get()?;
2214- let text = message.get_text().unwrap().to_string().unwrap();
2215- let text = if message.get_is_patch() {
2216- let (_, diff) =
2217- highlighter.highlight(&text, None, None, Some(Hint::from("DIFF")), false);
2218- diff
2219- } else {
2220- text
2221- };
2222- Ok(Message {
2223- id: message.get_id().to_string(),
2224- subject: message.get_subject()?.to_string().unwrap(),
2225- message_id: message.get_message_id()?.to_string().unwrap(),
2226- created_at: message.get_timestamp(),
2227- body: message.get_body()?.to_string().unwrap(),
2228- text,
2229- from_address: message.get_address()?.to_string().unwrap(),
2230- is_patch: message.get_is_patch(),
2231- })
2232- })
2233- .await?;
2234+ .read_post(
2235+ context::current(),
2236+ params.list_id,
2237+ params.message_id.unwrap(),
2238+ )
2239+ .await??;
2240 ctx.insert("title", &format!("list {}", mailing_list.name));
2241 ctx.insert("nav_elements", &navigation::global("mail", true));
2242 ctx.insert("list", &mailing_list);
2243 @@ -299,24 +174,19 @@ pub async fn export(
2244 Path(params): Path<Params>,
2245 Extension(initiator): Extension<Initiator>,
2246 ) -> Result<Response, Error> {
2247- let client = initiator.client(InitiatorKind::Mail).unwrap();
2248- let mailing_list = read_list(client.clone(), params.list_id.clone()).await?;
2249+ let client = initiator.mail.unwrap();
2250+ let lists = client.lists(context::current()).await??;
2251+ let mailing_list = lists.iter().find(|list| list.id == params.list_id);
2252 if let Some(mailing_list) = mailing_list {
2253- let list_id = mailing_list.id.clone();
2254- let thread = client
2255- .invoke(move |c: MailClient| async move {
2256- let mut req = c.export_request();
2257- req.get().set_id(list_id.as_str().into());
2258- if let Some(message_id) = params.message_id {
2259- req.get().set_message_id(message_id.as_str().into());
2260- req.get().set_as_thread(true);
2261- }
2262- let result = req.send().promise.await?;
2263- let post = result.get()?.get_thread()?;
2264- Ok(post.to_vec())
2265- })
2266- .await?;
2267- let mut response = Bytes::from(thread).into_response();
2268+ let blob = client
2269+ .export(
2270+ context::current(),
2271+ mailing_list.id.clone(),
2272+ params.message_id,
2273+ true,
2274+ )
2275+ .await??;
2276+ let mut response = Bytes::from(blob).into_response();
2277 response
2278 .headers_mut()
2279 .insert(CONTENT_TYPE, "application/mbox".parse().unwrap());
2280 diff --git a/ayllu/src/web2/routes/repo.rs b/ayllu/src/web2/routes/repo.rs
2281index 31b8779..aa98bb7 100644
2282--- a/ayllu/src/web2/routes/repo.rs
2283+++ b/ayllu/src/web2/routes/repo.rs
2284 @@ -22,13 +22,15 @@ use crate::web2::charts;
2285 use crate::web2::error::Error;
2286 use crate::web2::highlight::TreeSitterAdapter;
2287 use crate::web2::middleware::repository::Preamble;
2288- use crate::web2::middleware::rpc_initiator::{Initiator, Kind as InitiatorKind};
2289+ use crate::web2::middleware::rpc_initiator::Initiator;
2290 use crate::web2::middleware::template::Template;
2291 use crate::web2::navigation;
2292 use crate::web2::util;
2293- use ayllu_api::xmpp_capnp::server::Client as XmppClient;
2294+
2295+ use ayllu_api::xmpp::ChannelStat;
2296 use ayllu_database::Wrapper as Database;
2297 use ayllu_git::{ChatKind, Commit, Config as GitConfig, TreeEntry, Wrapper};
2298+ use ayllu_rpc::tarpc::context;
2299
2300 const README_FILES: [&str; 6] = [
2301 "readme.md",
2302 @@ -117,75 +119,20 @@ pub struct ChatLink {
2303 pub users_online: Option<i64>,
2304 }
2305
2306- async fn managed_chats(cfg: &Config, git_cfg: &GitConfig, initiator: Initiator) -> Vec<ChatLink> {
2307- let managed_xmpp_channels: Vec<String> = cfg.xmpp.clone().map_or(Vec::new(), |link| {
2308- link.channels.iter().map(|item| item.jid.clone()).collect()
2309- });
2310- let mut links: Vec<ChatLink> = Vec::new();
2311- if git_cfg.chat.is_some() {
2312- let mut to_lookup: Vec<String> = Vec::new();
2313- for link in git_cfg.chat.clone().unwrap() {
2314- match link.kind {
2315- ChatKind::Xmpp => {
2316- if managed_xmpp_channels.contains(&link.url) {
2317- to_lookup.push(link.url.clone())
2318- } else {
2319- links.push(ChatLink {
2320- kind: format!("{:?}", link.kind),
2321- url: link.url.clone(),
2322- description: None,
2323- users_online: None,
2324- })
2325- };
2326- }
2327- ChatKind::Irc => links.push(ChatLink {
2328- kind: format!("{:?}", link.kind),
2329- url: link.url.clone(),
2330- description: None,
2331- users_online: None,
2332- }),
2333- }
2334- }
2335- let xmpp_client = initiator.client(InitiatorKind::Xmpp);
2336- if let Some(xmpp_client) = xmpp_client {
2337- match xmpp_client
2338- .invoke(move |client: XmppClient| async move {
2339- let mut remote_channels: Vec<ChatLink> = Vec::new();
2340- let mut request = client.stats_request();
2341- let mut channels = request
2342- .get()
2343- .init_channels(managed_xmpp_channels.len() as u32);
2344- for (i, lookup) in to_lookup.iter().enumerate() {
2345- channels.set(i as u32, lookup.as_str().into());
2346- }
2347- let status = request.send().promise.await?;
2348- let results = status.get()?;
2349- let stats = results.get_stats()?;
2350- for stat in stats {
2351- remote_channels.push(ChatLink {
2352- kind: format!("{:?}", ChatKind::Xmpp),
2353- url: stat.get_name()?.to_string().unwrap(),
2354- description: None,
2355- users_online: Some(stat.get_users_online()),
2356- })
2357- }
2358- Ok(remote_channels)
2359- })
2360- .await
2361- {
2362- Ok(remote_links) => {
2363- links.extend(remote_links);
2364- }
2365- Err(err) => {
2366- log::error!(
2367- "failed to resolve discussion group status: {:?}",
2368- err.to_string()
2369- )
2370- }
2371- };
2372- }
2373+ fn chat_links(cfg: &GitConfig) -> Vec<ChatLink> {
2374+ if let Some(chats) = cfg.chat.as_ref() {
2375+ chats
2376+ .iter()
2377+ .map(|chat| ChatLink {
2378+ kind: chat.kind.to_string(),
2379+ url: chat.url.clone(),
2380+ description: None,
2381+ users_online: None,
2382+ })
2383+ .collect()
2384+ } else {
2385+ vec![]
2386 }
2387- links
2388 }
2389
2390 #[derive(Serialize, Clone)]
2391 @@ -240,37 +187,27 @@ pub async fn serve(
2392 }
2393 }
2394
2395- let chat_links = managed_chats(&cfg, &preamble.config, initiator).await;
2396- ctx.insert("chat_links", &chat_links);
2397-
2398- // find any associated mailing lists associated with this repository
2399- /*
2400- let email_links = preamble.config.clone().mail.map(|mail| {
2401- let links: Vec<EmailLink> = mail
2402- .iter()
2403- .map(|address| {
2404- let git_address = address.0.clone();
2405- let description = cfg
2406- .mail
2407- .clone()
2408- .and_then(|mail_cfg| {
2409- mail_cfg
2410- .lists
2411- .iter()
2412- .find(|entry| entry.address == git_address)
2413- .map(|entry| entry.address.clone())
2414- });
2415- EmailLink {
2416- url: address.0.clone(),
2417- description,
2418+ let mut chats = chat_links(&preamble.config);
2419+ if !chats.is_empty() && initiator.xmpp.is_some() {
2420+ let stats = initiator
2421+ .xmpp
2422+ .unwrap()
2423+ .stats(
2424+ context::current(),
2425+ chats.iter().map(|chat| chat.url.clone()).collect(),
2426+ )
2427+ .await??;
2428+ for chat in &mut chats {
2429+ for stat in &stats {
2430+ // FIXME name --> url wrong?
2431+ if stat.name == chat.url {
2432+ chat.users_online = Some(stat.users_online);
2433 }
2434- })
2435- .collect();
2436- links
2437- });
2438+ }
2439+ }
2440+ }
2441
2442- ctx.insert("email_links", &email_links);
2443- */
2444+ ctx.insert("chats", &chats);
2445
2446 let materialized = make_view(
2447 &db,
2448 @@ -316,11 +253,7 @@ pub async fn serve(
2449 ctx.insert("show_details", &preamble.file_path.is_none());
2450 ctx.insert(
2451 "nav_elements",
2452- &navigation::primary(
2453- "project",
2454- &preamble.collection_name,
2455- &preamble.repo_name,
2456- ),
2457+ &navigation::primary("project", &preamble.collection_name, &preamble.repo_name),
2458 );
2459 ctx.insert("commit_count", &materialized.commit_count);
2460 ctx.insert("latest_commit", &preamble.latest_commit);
2461 diff --git a/ayllu/src/web2/routes/xmpp.rs b/ayllu/src/web2/routes/xmpp.rs
2462index 749bdce..7131691 100644
2463--- a/ayllu/src/web2/routes/xmpp.rs
2464+++ b/ayllu/src/web2/routes/xmpp.rs
2465 @@ -6,11 +6,10 @@ use serde::{Deserialize, Serialize};
2466
2467 use crate::config::Config;
2468 use crate::web2::error::Error;
2469- use crate::web2::extractors::config::ConfigReader;
2470- use crate::web2::middleware::rpc_initiator::{Initiator, Kind as InitiatorKind};
2471+ use crate::web2::middleware::rpc_initiator::Initiator;
2472 use crate::web2::middleware::template::Template;
2473 use crate::web2::navigation;
2474- use ayllu_api::xmpp_capnp::server::Client as XmppClient;
2475+ use ayllu_rpc::tarpc::context;
2476
2477 #[derive(Debug, Serialize)]
2478 struct ChannelWithStats {
2479 @@ -34,23 +33,9 @@ pub async fn channels(
2480 ) -> Result<Html<String>, Error> {
2481 ctx.insert("title", "Discussions");
2482 ctx.insert("nav_elements", &navigation::global("xmpp", true));
2483- let xmpp_client = initiator.client(InitiatorKind::Xmpp).unwrap();
2484- let channels = xmpp_client
2485- .invoke(move |c: XmppClient| async move {
2486- let mut channels: Vec<ChannelWithStats> = Vec::new();
2487- let req = c.stats_request();
2488- let stats = req.send().promise.await?;
2489- for stat in stats.get()?.get_stats()? {
2490- channels.push(ChannelWithStats {
2491- name: stat.get_name()?.to_string().unwrap(),
2492- n_users: stat.get_users_online(),
2493- n_messages: stat.get_n_messages(),
2494- online: true,
2495- })
2496- }
2497- Ok(channels)
2498- })
2499- .await?;
2500+ let client = initiator.xmpp.unwrap();
2501+ // FIXME
2502+ let channels = client.stats(context::current(), vec![]).await??;
2503 ctx.insert("channels", &channels);
2504 let body = templates.render("channels.html", &ctx)?;
2505 Ok(Html(body))
2506 @@ -65,35 +50,21 @@ pub struct ChannelParams {
2507 pub async fn channel(
2508 Path(params): Path<ChannelParams>,
2509 Extension(_cfg): Extension<Config>,
2510- ConfigReader(config): ConfigReader,
2511 Extension((templates, mut ctx)): Extension<Template>,
2512 Extension(initiator): Extension<Initiator>,
2513 ) -> Result<Html<String>, Error> {
2514 ctx.insert("title", "lists");
2515 ctx.insert("nav_elements", &navigation::global("xmpp", true));
2516 ctx.insert("channel", &params.channel);
2517- let xmpp_client = initiator.client(InitiatorKind::Xmpp).unwrap();
2518- let messages = xmpp_client
2519- .invoke(move |c: XmppClient| async move {
2520- let mut xmpp_messages: Vec<XmppMessage> = Vec::new();
2521- let mut req = c.messages_request();
2522- req.get().set_channel_name(params.channel.as_str().into());
2523- req.get().set_limit(config.items_per_page as i64);
2524- if let Some(id) = params.last_message {
2525- req.get().set_last_message(id.as_str().into());
2526- }
2527- let messages = req.send().promise.await?;
2528- for message in messages.get()?.get_messages()? {
2529- xmpp_messages.push(XmppMessage {
2530- id: message.get_message_id()?.to_string().unwrap(),
2531- nickname: message.get_nickname()?.to_string().unwrap(),
2532- timestamp: message.get_timestamp()?.to_string().unwrap(),
2533- body: message.get_body()?.to_string().unwrap(),
2534- })
2535- }
2536- Ok(xmpp_messages)
2537- })
2538- .await?;
2539+ let client = initiator.xmpp.unwrap();
2540+ let messages = client
2541+ .messages(
2542+ context::current(),
2543+ params.channel,
2544+ params.last_message,
2545+ 1000,
2546+ )
2547+ .await??;
2548 ctx.insert("messages", &messages);
2549 // ctx.insert("lists", &cfg.mail.unwrap().lists);
2550 let body = templates.render("channel.html", &ctx)?;
2551 diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml
2552index c83cb5f..b849c96 100644
2553--- a/crates/api/Cargo.toml
2554+++ b/crates/api/Cargo.toml
2555 @@ -6,10 +6,6 @@ edition = "2021"
2556 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
2557
2558 [dependencies]
2559- capnp = "0.18.1"
2560- capnp-futures = "0.18.0"
2561- capnp-rpc = "0.18.0"
2562 serde = { version = "1.0", features = ["derive"] }
2563-
2564- [build-dependencies]
2565- capnpc = "0.18.0"
2566+ tarpc = { version = "0.34.0", features = ["full"] }
2567+ thiserror = "1.0.58"
2568 diff --git a/crates/api/README.md b/crates/api/README.md
2569index e119d65..7eeb16a 100644
2570--- a/crates/api/README.md
2571+++ b/crates/api/README.md
2572 @@ -1,6 +1,6 @@
2573 # api
2574
2575- capnp plugin interfaces for ayllu.
2576+ Tarpc plugin interfaces for ayllu.
2577
2578 There are two types of plugins:
2579
2580 diff --git a/crates/api/build.rs b/crates/api/build.rs
2581deleted file mode 100644
2582index b491b06..0000000
2583--- a/crates/api/build.rs
2584+++ /dev/null
2585 @@ -1,10 +0,0 @@
2586- fn main() {
2587- capnpc::CompilerCommand::new()
2588- .src_prefix("v1")
2589- .file("v1/build.capnp")
2590- .file("v1/jobs.capnp")
2591- .file("v1/mail.capnp")
2592- .file("v1/xmpp.capnp")
2593- .run()
2594- .expect("compiling schema");
2595- }
2596 diff --git a/crates/api/src/build.rs b/crates/api/src/build.rs
2597new file mode 100644
2598index 0000000..ef6ab84
2599--- /dev/null
2600+++ b/crates/api/src/build.rs
2601 @@ -0,0 +1,76 @@
2602+ use serde::{Deserialize, Serialize};
2603+ use tarpc::service;
2604+
2605+ use crate::error::ApiError;
2606+
2607+ #[derive(Debug, Deserialize, Serialize)]
2608+ /// Defines some kind of event that occurs within a repository
2609+ pub enum Kind {
2610+ Commit,
2611+ Tag,
2612+ }
2613+
2614+ #[derive(Debug, Deserialize, Serialize)]
2615+ pub struct Event {
2616+ pub repository_url: String,
2617+ pub commit_hash: String,
2618+ pub kind: Kind,
2619+ }
2620+
2621+ #[derive(Debug, Deserialize, Serialize)]
2622+ /// array of samples taken to construct informative charts in the UI
2623+ pub struct Samples {
2624+ load_1m: Vec<f32>,
2625+ load_5m: Vec<f32>,
2626+ load_15m: Vec<f32>,
2627+ disk_total_bytes: Vec<i64>,
2628+ disk_free_bytes: Vec<i64>,
2629+ net_sent_bytes: Vec<i64>,
2630+ net_received_bytes: Vec<i64>,
2631+ net_sent_packets: Vec<i64>,
2632+ net_received_packets: Vec<i64>,
2633+ mem_total_bytes: Vec<i64>,
2634+ mem_free_bytes: Vec<i64>,
2635+ mem_available_bytes: Vec<i64>,
2636+ }
2637+
2638+ #[derive(Debug, Deserialize, Serialize)]
2639+ pub struct Step {
2640+ pub id: i64,
2641+ pub input: String,
2642+ pub shell: String,
2643+ }
2644+
2645+ #[derive(Debug, Deserialize, Serialize)]
2646+ pub struct Job {
2647+ pub id: i64,
2648+ pub name: String,
2649+ pub steps: Vec<Step>,
2650+ }
2651+
2652+ #[derive(Debug, Deserialize, Serialize)]
2653+ pub struct Manifest {
2654+ pub id: i64,
2655+ repository_url: String,
2656+ git_hash: String,
2657+ /// DOT representation of DAG rendered via manifest
2658+ dot_chart: String,
2659+ created_at: i64,
2660+ started_at: i64,
2661+ finished_at: i64,
2662+ success: bool,
2663+ // not included on manfest list calls otherwise ordered in the way
2664+ // that they are evaluated via dependency graph
2665+ jobs: Vec<Job>,
2666+ }
2667+
2668+ #[service]
2669+ /// running service that will respond to events
2670+ pub trait Server {
2671+ /// submit a new event which may result in manifests being run
2672+ async fn submit(event: Event) -> Result<(), ApiError>;
2673+ /// list manifests available on the worker
2674+ async fn list_manifests(offset: i64, limit: i64) -> Result<Vec<Manifest>, ApiError>;
2675+ /// read a manifest including it's status and all job output
2676+ async fn read_manifest(manifest_id: String) -> Result<Manifest, ApiError>;
2677+ }
2678 diff --git a/crates/api/src/error.rs b/crates/api/src/error.rs
2679new file mode 100644
2680index 0000000..15298d6
2681--- /dev/null
2682+++ b/crates/api/src/error.rs
2683 @@ -0,0 +1,10 @@
2684+ use serde::{Deserialize, Serialize};
2685+ use thiserror::Error;
2686+
2687+ #[derive(Error, Debug, Serialize, Deserialize)]
2688+ pub enum ApiError {
2689+ #[error("generic api error occurred")]
2690+ Message(String),
2691+ #[error("resource not found")]
2692+ NotFound(String),
2693+ }
2694 diff --git a/crates/api/src/jobs.rs b/crates/api/src/jobs.rs
2695new file mode 100644
2696index 0000000..00e07b9
2697--- /dev/null
2698+++ b/crates/api/src/jobs.rs
2699 @@ -0,0 +1,53 @@
2700+ use std::fmt::Display;
2701+
2702+ use serde::{Deserialize, Serialize};
2703+ use tarpc::service;
2704+
2705+ #[derive(Debug, Deserialize, Serialize, Clone)]
2706+ /// defines the type of job being ran
2707+ pub enum Kind {
2708+ /// store commit level contribution data
2709+ Contributors,
2710+ /// count lines of code
2711+ Cloc,
2712+ }
2713+
2714+ impl Display for Kind {
2715+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2716+ match self {
2717+ Kind::Contributors => f.write_str("Contributors"),
2718+ Kind::Cloc => f.write_str("Cloc"),
2719+ }
2720+ }
2721+ }
2722+ impl From<String> for Kind {
2723+ fn from(value: String) -> Self {
2724+ match value.as_str() {
2725+ "CONTRIBUTORS" | "Contributors" | "contributors" => Kind::Contributors,
2726+ "CLOC" | "Cloc" | "cloc" => Kind::Cloc,
2727+ _ => panic!("invalid kind"),
2728+ }
2729+ }
2730+ }
2731+
2732+ #[derive(Debug, Deserialize, Serialize, Clone)]
2733+ /// generic definition of a "job" with a related "kind"
2734+ pub struct Job {
2735+ pub id: i64,
2736+ pub created_at: i64,
2737+ pub kind: Kind,
2738+ pub repo_path: String,
2739+ pub runtime: i64,
2740+ pub success: bool,
2741+ pub commits: i64,
2742+ }
2743+
2744+ #[service]
2745+ pub trait Server {
2746+ /// Submit a new job to be run in the background
2747+ async fn submit(kind: Kind, repo_path: String);
2748+ /// List submitted and complete jobs
2749+ async fn list(repo_path: Option<String>) -> Vec<Job>;
2750+ /// Purge all jobs associated with the given repository
2751+ async fn purge(repo_path: String);
2752+ }
2753 diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs
2754index 3721061..1daadf3 100644
2755--- a/crates/api/src/lib.rs
2756+++ b/crates/api/src/lib.rs
2757 @@ -1,76 +1,5 @@
2758- pub mod models {
2759-
2760- use crate::jobs_capnp;
2761- use std::fmt;
2762-
2763- use serde::{Deserialize, Serialize};
2764-
2765- #[derive(Serialize, Deserialize, Clone, Debug)]
2766- pub enum Kind {
2767- Contributors,
2768- Cloc,
2769- }
2770-
2771- impl fmt::Display for Kind {
2772- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2773- match self {
2774- Kind::Contributors => write!(f, "CONTRIBUTORS"),
2775- Kind::Cloc => write!(f, "CLOC"),
2776- }
2777- }
2778- }
2779-
2780- impl From<String> for Kind {
2781- fn from(value: String) -> Self {
2782- match value.as_str() {
2783- "CONTRIBUTORS" | "Contributors" | "contributors" => Kind::Contributors,
2784- "CLOC" | "Cloc" | "cloc" => Kind::Cloc,
2785- _ => panic!("invalid kind"),
2786- }
2787- }
2788- }
2789-
2790- impl From<jobs_capnp::Kind> for Kind {
2791- fn from(value: jobs_capnp::Kind) -> Self {
2792- match value {
2793- jobs_capnp::Kind::Contributors => Kind::Contributors,
2794- jobs_capnp::Kind::Cloc => Kind::Cloc,
2795- }
2796- }
2797- }
2798-
2799- impl From<Kind> for jobs_capnp::Kind {
2800- fn from(val: Kind) -> Self {
2801- match val {
2802- Kind::Contributors => jobs_capnp::Kind::Contributors,
2803- Kind::Cloc => jobs_capnp::Kind::Cloc,
2804- }
2805- }
2806- }
2807-
2808- #[derive(Serialize, Deserialize, Clone, Debug)]
2809- pub struct Job {
2810- pub id: i64,
2811- pub created_at: i64,
2812- pub repo_path: String,
2813- pub runtime: i64,
2814- pub kind: Kind,
2815- pub commits: i64,
2816- }
2817- }
2818-
2819- pub mod jobs_capnp {
2820- include!(concat!(env!("OUT_DIR"), "/jobs_capnp.rs"));
2821- }
2822-
2823- pub mod mail_capnp {
2824- include!(concat!(env!("OUT_DIR"), "/mail_capnp.rs"));
2825- }
2826-
2827- pub mod xmpp_capnp {
2828- include!(concat!(env!("OUT_DIR"), "/xmpp_capnp.rs"));
2829- }
2830-
2831- pub mod build_capnp {
2832- include!(concat!(env!("OUT_DIR"), "/build_capnp.rs"));
2833- }
2834+ pub mod build;
2835+ pub mod error;
2836+ pub mod jobs;
2837+ pub mod mail;
2838+ pub mod xmpp;
2839 diff --git a/crates/api/src/mail.rs b/crates/api/src/mail.rs
2840new file mode 100644
2841index 0000000..963822c
2842--- /dev/null
2843+++ b/crates/api/src/mail.rs
2844 @@ -0,0 +1,66 @@
2845+ use serde::{Deserialize, Serialize};
2846+ use tarpc::service;
2847+
2848+ use crate::error::ApiError;
2849+
2850+ #[derive(Debug, Deserialize, Serialize)]
2851+ /// A single email message
2852+ pub struct Message {
2853+ pub id: i64,
2854+ pub message_id: String,
2855+ /// epoch time
2856+ pub timestamp: u64,
2857+ pub from: String,
2858+ pub address: String,
2859+ /// entire email including headers
2860+ pub body: String,
2861+ /// text of just the email message
2862+ pub text: String,
2863+ /// true if the message is a patch
2864+ pub is_patch: bool,
2865+ }
2866+
2867+ #[derive(Debug, Deserialize, Serialize)]
2868+ pub struct Thread {
2869+ /// initial message sent to the mailing list
2870+ pub first: Message,
2871+ /// number of follow up messages associated with thread
2872+ pub n_replies: i64,
2873+ /// true if the thread contains one or more patch
2874+ pub has_patch: bool,
2875+ }
2876+
2877+ #[derive(Debug, Deserialize, Serialize)]
2878+ pub struct MailingList {
2879+ /// unique list identifier
2880+ pub id: String,
2881+ /// friendly mailing list name
2882+ pub name: String,
2883+ /// address to send "commands" to
2884+ pub request_address: String,
2885+ /// RFC5322 email address
2886+ pub address: String,
2887+ /// description of the mailing list purpose
2888+ pub description: String,
2889+ /// Free form array of tags
2890+ pub topics: Vec<String>,
2891+ }
2892+
2893+ #[service]
2894+ pub trait Server {
2895+ /// return all managed mailing lists
2896+ async fn lists() -> Result<Vec<MailingList>, ApiError>;
2897+ /// list all of the threads associated with a mailing list
2898+ async fn list_threads(id: String, offset: i64, limit: i64) -> Result<Vec<Thread>, ApiError>;
2899+ /// list all of the responses associated with a post starting from messageId
2900+ async fn read_thread(
2901+ id: String,
2902+ message_id: String,
2903+ offset: i64,
2904+ limit: i64,
2905+ ) -> Result<Vec<Message>, ApiError>;
2906+ /// read a single post
2907+ async fn read_post(id: String, message_id: String) -> Result<Message, ApiError>;
2908+ /// export one or more messages in mbox format
2909+ async fn export(id: String, message_id: Option<String>, as_thread: bool) -> Result<Vec<u8>, ApiError>;
2910+ }
2911 diff --git a/crates/api/src/xmpp.rs b/crates/api/src/xmpp.rs
2912new file mode 100644
2913index 0000000..c827b23
2914--- /dev/null
2915+++ b/crates/api/src/xmpp.rs
2916 @@ -0,0 +1,39 @@
2917+ use serde::{Deserialize, Serialize};
2918+ use tarpc::service;
2919+
2920+ use crate::error::ApiError;
2921+
2922+ #[derive(Debug, Deserialize, Serialize)]
2923+ pub struct ChannelStat {
2924+ /// name of the channel
2925+ pub name: String,
2926+ /// number of users currently online
2927+ pub users_online: i64,
2928+ /// number of messages ever sent to the channel since we've benen recording
2929+ pub num_messages: i64,
2930+ }
2931+
2932+ #[derive(Debug, Deserialize, Serialize)]
2933+ /// chat message sent in the channel
2934+ pub struct Message {
2935+ /// unique message identifier
2936+ pub message_id: String,
2937+ /// tiemstamp the message was sent
2938+ pub timestamp: String,
2939+ /// user nickname as part of the channel
2940+ pub nickname: String,
2941+ /// body containing the message text
2942+ pub body: String,
2943+ }
2944+
2945+ #[service]
2946+ pub trait Server {
2947+ /// return stats for the given channel names
2948+ async fn stats(channels: Vec<String>) -> Result<Vec<ChannelStat>, ApiError>;
2949+ /// return public messages for the channel
2950+ async fn messages(
2951+ channel_name: String,
2952+ last_message: Option<String>,
2953+ limit: i64,
2954+ ) -> Result<Vec<Message>, ApiError>;
2955+ }
2956 diff --git a/crates/api/v1/build.capnp b/crates/api/v1/build.capnp
2957deleted file mode 100644
2958index 4124839..0000000
2959--- a/crates/api/v1/build.capnp
2960+++ /dev/null
2961 @@ -1,69 +0,0 @@
2962- @0xeb9468d06a22ac45;
2963-
2964- # defines some kind of event that occurs against a repository
2965- enum Kind {
2966- commit @0;
2967- tag @1;
2968- }
2969-
2970- struct Event {
2971- repositoryUrl @0 :Text;
2972- commitHash @1 :Text;
2973- kind @2 :Kind;
2974- }
2975-
2976- # array of samples taken to construct informative charts in the UI
2977- struct Samples {
2978- load1m @0 :List(Float32);
2979- load5m @1 :List(Float32);
2980- load15m @2 :List(Float32);
2981- diskTotalBytes @3 :List(Int64);
2982- diskFreeBytes @4 :List(Int64);
2983- netSentBytes @5 :List(Int64);
2984- netReceivedBytes @6 :List(Int64);
2985- netSentPackets @7 :List(Int64);
2986- netReceivedPackets @8 :List(Int64);
2987- memTotalBytes @9 :List(Int64);
2988- memFreeBytes @10 :List(Int64);
2989- memAvailableBytes @11 :List(Int64);
2990- }
2991-
2992- struct Step {
2993- id @0 :Int64;
2994- input @1 :Text;
2995- shell @2 :Text;
2996- }
2997-
2998- struct Job {
2999- id @0 :Int64;
3000- name @1 :Text;
3001- # ordered in the way they were evaluated via dependency graph
3002- steps @2 :List(Step);
3003- }
3004-
3005- struct Manifest {
3006- id @0 :Int64;
3007- repositoryUrl @1 :Text;
3008- gitHash @2 :Text;
3009- # DOT representation of DAG rendered via manifest
3010- dotChart @3 :Text;
3011- # Resource usage samples taken during build
3012- samples @4 :Samples;
3013- createdAt @5 :Int64;
3014- startedAt @6 :Int64;
3015- finishedAt @7 :Int64;
3016- success @8 :Bool;
3017- # not included on manfest list calls otherwise ordered in the way
3018- # that they are evaluated via dependency graph
3019- jobs @9 :List(Job);
3020- }
3021-
3022- # running service that will respond to events
3023- interface Worker {
3024- # submit a new event which may result in manifests being run
3025- submit @0 (event: Event);
3026- # list manifests available on the worker
3027- listManifests @1 (offset: Int64, limit: Int64) -> (manifests: List(Manifest));
3028- # read a manifest including it's status and all job output
3029- readManifest @2 (manifest_id: Text) -> (manifest: Manifest);
3030- }
3031 diff --git a/crates/api/v1/jobs.capnp b/crates/api/v1/jobs.capnp
3032deleted file mode 100644
3033index 50a41ae..0000000
3034--- a/crates/api/v1/jobs.capnp
3035+++ /dev/null
3036 @@ -1,30 +0,0 @@
3037- @0xaa252f70d9fb4cd5;
3038-
3039- # defines the type of job being ran
3040- enum Kind {
3041- # store commit level contribution data
3042- contributors @0;
3043- # count lines of code
3044- cloc @1;
3045- }
3046-
3047- # generic definition of a "job" with a related "kind"
3048- struct Job {
3049- id @0 :Int64;
3050- createdAt @1 :Int64;
3051- kind @2 :Kind;
3052- repoPath @3 :Text;
3053- runtime @4 :Int64;
3054- success @5 :Bool;
3055- commits @6 :Int64;
3056- }
3057-
3058- interface Server {
3059- # submit a new job of associated kind for the given repository to be ran in
3060- # the background.
3061- submit @0 (kind :Kind, repo_path :Text);
3062- # list submitted and complete jobs
3063- list @1 (repo_path :Text) -> (jobs :List(Job));
3064- # purge all jobs and computations associated with the given repository
3065- purge @2 (repo_path :Text);
3066- }
3067 diff --git a/crates/api/v1/mail.capnp b/crates/api/v1/mail.capnp
3068deleted file mode 100644
3069index 8e7c791..0000000
3070--- a/crates/api/v1/mail.capnp
3071+++ /dev/null
3072 @@ -1,55 +0,0 @@
3073- @0xe282ac1f72de3195;
3074-
3075- # a single email
3076- struct Message {
3077- # primary key of the message in the mailpot db
3078- id @0 :Int64;
3079- messageId @1 :Text;
3080- timestamp @2 :Int64;
3081- from @3 :Text;
3082- address @4 :Text;
3083- subject @5 :Text;
3084- # entire email including headers
3085- body @6 :Text;
3086- # text of just the email message
3087- text @7 :Text;
3088- # true if the message is a patch
3089- isPatch @8 :Bool;
3090- }
3091-
3092- struct Thread {
3093- # initial message sent to the mailing list
3094- first @0 :Message;
3095- # number of follow up messages associated with thread
3096- nReplies @1 :Int64;
3097- # true if the thread contains one or more patch
3098- hasPatch @2 :Bool;
3099- }
3100-
3101- struct MailingList {
3102- # unique list identifier
3103- id @0 :Text;
3104- # freindly mailing list name
3105- name @1 :Text;
3106- # address to send "commands" to
3107- requestAddress @2 :Text;
3108- # RFC 5322 email address
3109- address @3 :Text;
3110- # description of the mailing list purpose
3111- description @4 :Text;
3112- # free form array of tags
3113- topics @5 :List(Text);
3114- }
3115-
3116- interface Server {
3117- # return all managed mailing lists
3118- lists @0 () -> (lists: List(MailingList));
3119- # list all of the threads associated with a mailing list
3120- listThreads @1 (id: Text, offset: Int64, limit: Int64) -> (threads: List(Thread));
3121- # list all of the responses associated with a post starting from messageId
3122- readThread @2 (id: Text, messageId: Text, offset: Int64, limit: Int64) -> (thread: List(Message));
3123- # read a single post
3124- readPost @3 (id: Text, messageId: Text) -> Message;
3125- # export one or more messages in mbox format
3126- export @4 (id: Text, messageId: Text, asThread: Bool) -> (thread :Data);
3127- }
3128 diff --git a/crates/api/v1/xmpp.capnp b/crates/api/v1/xmpp.capnp
3129deleted file mode 100644
3130index bc938be..0000000
3131--- a/crates/api/v1/xmpp.capnp
3132+++ /dev/null
3133 @@ -1,29 +0,0 @@
3134- @0xe9c7e2def07a9ac5;
3135-
3136- struct ChannelStat {
3137- # name of the channel
3138- name @0 :Text;
3139- # number of users currently active in channel
3140- usersOnline @1 :Int64;
3141- # number of messages ever sent in the channel
3142- nMessages @2 :Int64;
3143- }
3144-
3145- # chat message sent in a chnanel
3146- struct Message {
3147- # unique message identifier
3148- messageId @0 :Text;
3149- # time the message was sent
3150- timestamp @1 :Text;
3151- # users nickname as part of the channel
3152- nickname @2 :Text;
3153- # body containing the message text
3154- body @3 :Text;
3155- }
3156-
3157- interface Server {
3158- # return stats for the given channel names
3159- stats @0 (channels: List(Text)) -> (stats: List(ChannelStat));
3160- # return public messages from the channel
3161- messages @1 (channel_name: Text, last_message: Text, limit: Int64) -> (messages: List(Message));
3162- }
3163 diff --git a/crates/git/src/config.rs b/crates/git/src/config.rs
3164index 17f2ee8..aa5794c 100644
3165--- a/crates/git/src/config.rs
3166+++ b/crates/git/src/config.rs
3167 @@ -1,3 +1,5 @@
3168+ use std::fmt::Display;
3169+
3170 use git2::Config as GitConfig;
3171 use serde::Serialize;
3172
3173 @@ -19,6 +21,15 @@ pub enum ChatKind {
3174 Irc,
3175 }
3176
3177+ impl Display for ChatKind {
3178+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3179+ match self {
3180+ ChatKind::Xmpp => f.write_str("Xmpp"),
3181+ ChatKind::Irc => f.write_str("Irc"),
3182+ }
3183+ }
3184+ }
3185+
3186 #[derive(Serialize, Clone, Debug)]
3187 pub struct ChatLink {
3188 pub url: String,
3189 diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml
3190index 3133248..810eab6 100644
3191--- a/crates/rpc/Cargo.toml
3192+++ b/crates/rpc/Cargo.toml
3193 @@ -7,13 +7,8 @@ edition = "2021"
3194
3195 [dependencies]
3196 async-trait = "0.1.74"
3197- capnp = "0.18.1"
3198- capnp-rpc = "0.18.0"
3199- capnpc = "0.18.0"
3200 futures = "0.3.28"
3201+ tarpc = { version = "0.34.0", features = ["full"] }
3202 tokio = { version = "1.33.0", features = ["full"] }
3203 tokio-util = { version = "0.7.9", features = ["compat"] }
3204 tracing = { version = "0.1.40", features = ["log"] }
3205-
3206- [build-dependencies]
3207- capnpc = "0.18.0"
3208 diff --git a/crates/rpc/README.md b/crates/rpc/README.md
3209index 88d593a..8fb3e29 100644
3210--- a/crates/rpc/README.md
3211+++ b/crates/rpc/README.md
3212 @@ -1,3 +1,3 @@
3213 # rpc
3214
3215- capnp helper code to reduce boilerplate
3216+ Helper code to reduce boilerplate
3217 diff --git a/crates/rpc/build.rs b/crates/rpc/build.rs
3218deleted file mode 100644
3219index 4c3beee..0000000
3220--- a/crates/rpc/build.rs
3221+++ /dev/null
3222 @@ -1,7 +0,0 @@
3223- fn main() {
3224- capnpc::CompilerCommand::new()
3225- .src_prefix("tests")
3226- .file("tests/hello.capnp")
3227- .run()
3228- .expect("compiling schema");
3229- }
3230 diff --git a/crates/rpc/src/lib.rs b/crates/rpc/src/lib.rs
3231index 618b648..752b26c 100644
3232--- a/crates/rpc/src/lib.rs
3233+++ b/crates/rpc/src/lib.rs
3234 @@ -1,218 +1,24 @@
3235- use std::error::Error as StdError;
3236- use std::fmt::Display;
3237- use std::fs::{metadata, remove_file};
3238+ use std::fs::remove_file;
3239 use std::future::Future;
3240 use std::io::Error as IoError;
3241- use std::marker::PhantomData;
3242 use std::path::Path;
3243
3244- use capnp::capability::Client as CapabilityClient;
3245- pub use capnp::capability::FromClientHook;
3246- use capnp::Error as CapnpError;
3247- use capnp_rpc::{rpc_twoparty_capnp::Side, twoparty::VatNetwork, RpcSystem};
3248- use futures::AsyncReadExt;
3249- use tokio::net::{UnixListener, UnixStream};
3250- use tokio::runtime::Handle;
3251- use tokio::task::{spawn_blocking, LocalSet};
3252- use tokio_util::compat::TokioAsyncReadCompatExt;
3253- use tracing::log;
3254+ // TODO: due to https://github.com/google/tarpc/issues/421 it's not currently
3255+ // possible to generalize serving into a helper function. For now I am only
3256+ // re-exporting tarpc for ease of use in downstream server implementations.
3257
3258- pub enum Kind {
3259- Xmpp,
3260- Mail,
3261- }
3262-
3263- #[derive(Debug)]
3264- pub enum Error {
3265- CapnpError(CapnpError),
3266- IoError(IoError),
3267- }
3268-
3269- impl StdError for Error {
3270- fn source(&self) -> Option<&(dyn StdError + 'static)> {
3271- match self {
3272- Error::CapnpError(e) => Some(e),
3273- Error::IoError(e) => Some(e),
3274- }
3275- }
3276- }
3277-
3278- impl Display for Error {
3279- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3280- match self {
3281- Error::CapnpError(e) => e.fmt(f),
3282- Error::IoError(e) => e.fmt(f),
3283- }
3284- }
3285- }
3286-
3287- impl From<IoError> for Error {
3288- fn from(value: IoError) -> Self {
3289- Error::IoError(value)
3290- }
3291- }
3292-
3293- impl From<CapnpError> for Error {
3294- fn from(value: CapnpError) -> Self {
3295- Error::CapnpError(value)
3296- }
3297- }
3298-
3299- /// reduce some boilerplate required to setup capnp servers and clients
3300- pub struct Server<C, S> {
3301- server: S,
3302- socket_path: String,
3303- client_type: PhantomData<C>,
3304- }
3305-
3306- impl<C, S> Server<C, S>
3307- where
3308- C: capnp::capability::FromServer<S> + Clone,
3309- {
3310- pub fn new(socket_path: &str, server: S) -> Self {
3311- Server {
3312- server,
3313- socket_path: socket_path.to_string(),
3314- client_type: PhantomData {},
3315- }
3316- }
3317-
3318- pub async fn serve(self) -> Result<(), Error> {
3319- let socket_path = Path::new(&self.socket_path);
3320- if metadata(socket_path).is_ok() {
3321- remove_file(socket_path)?;
3322- };
3323- // TODO: need to support both sockets and TCP connections for build
3324- // servers since they will cross network boundaries on QEMU, etc.
3325- log::info!("listening @ {}", self.socket_path);
3326- let listener = UnixListener::bind(socket_path)?;
3327- let client: C = capnp_rpc::new_client(self.server);
3328- loop {
3329- log::debug!("starting rpc loop");
3330- let (stream, _) = listener.accept().await?;
3331- let (reader, writer) = TokioAsyncReadCompatExt::compat(stream).split();
3332- let network = VatNetwork::new(reader, writer, Side::Server, Default::default());
3333- let next = CapabilityClient::new(client.clone().into_client_hook());
3334- let rpc_system = RpcSystem::new(Box::new(network), Some(next));
3335- tokio::task::spawn_local(rpc_system);
3336- log::debug!("rpc request finished");
3337- }
3338- }
3339- }
3340-
3341- #[derive(Clone)]
3342- pub struct Client {
3343- socket_path: String,
3344- }
3345-
3346- impl Client {
3347- pub fn new(socket_path: &str) -> Self {
3348- Client {
3349- socket_path: socket_path.to_string(),
3350- }
3351- }
3352- pub async fn invoke<C, T, F, Fut>(self, f: F) -> Result<T, Error>
3353- where
3354- C: capnp::capability::FromClientHook,
3355- T: Send + 'static,
3356- F: (FnOnce(C) -> Fut) + Send + 'static,
3357- Fut: Future<Output = Result<T, Error>>,
3358- {
3359- let result = spawn_blocking(move || {
3360- Handle::current().block_on(async move {
3361- LocalSet::new()
3362- .run_until(async move {
3363- log::info!("connecting to rpc service: @ {}", self.socket_path);
3364- let stream = UnixStream::connect(self.socket_path).await?;
3365- let (reader, writer) = TokioAsyncReadCompatExt::compat(stream).split();
3366- let rpc_network = Box::new(VatNetwork::new(
3367- reader,
3368- writer,
3369- Side::Client,
3370- Default::default(),
3371- ));
3372- let mut rpc_system = RpcSystem::new(rpc_network, None);
3373- let jobs_client: C = rpc_system.bootstrap(Side::Server);
3374- tokio::task::spawn_local(rpc_system);
3375- f(jobs_client).await
3376- })
3377- .await
3378- })
3379- })
3380- .await
3381- .unwrap()?;
3382- log::info!("finished rpc request");
3383- Ok(result)
3384- }
3385- }
3386+ pub use futures;
3387+ pub use tarpc;
3388
3389- pub mod helpers {
3390- pub fn string(result: capnp::Result<capnp::text::Reader>) -> String {
3391- result.map_or(String::new(), |result| result.to_string().unwrap())
3392- }
3393+ /// Spawn a future in a separate thread
3394+ pub async fn spawn(fut: impl Future<Output = ()> + Send + 'static) {
3395+ tokio::spawn(fut);
3396 }
3397
3398- /// service for testing only
3399- #[allow(dead_code)]
3400- mod hello_capnp {
3401- include!(concat!(env!("OUT_DIR"), "/hello_capnp.rs"));
3402- }
3403-
3404- #[cfg(test)]
3405- mod tests {
3406- use super::*;
3407-
3408- use std::thread::spawn;
3409- use tokio::runtime::Builder;
3410- use tokio::task::LocalSet;
3411-
3412- #[derive(Clone)]
3413- struct TestImpl;
3414-
3415- impl hello_capnp::server::Server for TestImpl {
3416- fn greet(
3417- &mut self,
3418- params: hello_capnp::server::GreetParams,
3419- mut results: hello_capnp::server::GreetResults,
3420- ) -> capnp::capability::Promise<(), capnp::Error> {
3421- let name = params.get().unwrap().get_name().unwrap().to_string();
3422- results
3423- .get()
3424- .set_greeting(format!("Hello: {}", name.unwrap()).as_str().into());
3425- capnp::capability::Promise::ok(())
3426- }
3427- }
3428-
3429- #[test]
3430- fn test_client_call() {
3431- spawn(move || {
3432- let server_runtime = Builder::new_current_thread().enable_all().build().unwrap();
3433- server_runtime.block_on(async {
3434- let server = Server::<hello_capnp::server::Client, TestImpl>::new(
3435- "/tmp/rpc_test.sock",
3436- TestImpl,
3437- );
3438- LocalSet::new().run_until(server.serve()).await.unwrap()
3439- })
3440- });
3441- let client_runtime = Builder::new_current_thread().enable_all().build().unwrap();
3442- client_runtime.block_on(async {
3443- let client = Client::new("/tmp/rpc_test.sock");
3444- LocalSet::new()
3445- .run_until(async {
3446- let greeting = client
3447- .invoke(move |c: hello_capnp::server::Client| async move {
3448- let mut req = c.greet_request();
3449- req.get().set_name("fuubar".into());
3450- let response = req.send().promise.await.unwrap();
3451- let greeting = response.get().unwrap().get_greeting().unwrap();
3452- Ok(greeting.to_string().unwrap())
3453- })
3454- .await
3455- .unwrap();
3456- assert!(greeting == "Hello: fuubar")
3457- })
3458- .await;
3459- });
3460+ /// Initialize the socket for listening
3461+ pub fn init_socket(path: &Path) -> Result<(), IoError> {
3462+ if path.exists() {
3463+ remove_file(path)?;
3464 }
3465+ Ok(())
3466 }
3467 diff --git a/crates/rpc/tests/hello.capnp b/crates/rpc/tests/hello.capnp
3468deleted file mode 100644
3469index cfccf8c..0000000
3470--- a/crates/rpc/tests/hello.capnp
3471+++ /dev/null
3472 @@ -1,7 +0,0 @@
3473- # test file for capnp rpc helpers
3474- @0x9168d956160fde90;
3475-
3476- interface Server {
3477- # return a friendly greeting
3478- greet @0 (name: Text) -> (greeting :Text);
3479- }
3480 diff --git a/scripts/check_build_dependencies.sh b/scripts/check_build_dependencies.sh
3481index 8cc6dfd..cfb4893 100755
3482--- a/scripts/check_build_dependencies.sh
3483+++ b/scripts/check_build_dependencies.sh
3484 @@ -30,7 +30,6 @@ check_cmd "pkg-config"
3485 check_cmd "sassc"
3486 check_cmd "sqlx"
3487 check_cmd "sqlite3"
3488- check_cmd "capnpc"
3489 check_cmd "npm"
3490
3491 # check for openssl which annoyingly is required to build sqlx-cli if that is
3492 diff --git a/www/content/_index.md b/www/content/_index.md
3493index ea236ea..f2cabc5 100644
3494--- a/www/content/_index.md
3495+++ b/www/content/_index.md
3496 @@ -57,8 +57,7 @@ and/or cron to extend forge functionality.
3497
3498 #### Extensible Plugin System
3499
3500- An extensible CapnProto based plugin system allows adding large external
3501- integrations via RPC calls.
3502+ An extensible RPC based plugin system allows adding large external integrations.
3503
3504 </article>
3505
3506 diff --git a/www/content/docs/architecture.md b/www/content/docs/architecture.md
3507index 5eb2bdf..8dd6597 100644
3508--- a/www/content/docs/architecture.md
3509+++ b/www/content/docs/architecture.md
3510 @@ -8,7 +8,7 @@ weight = 0
3511
3512 It's basic architecture is that of a single binary providing a web interface to
3513 browse and interact with Git repositories as well as an RPC server based on
3514- [Cap'n Proto](https://capnproto.org/) with an embedded client. The RPC server
3515+ [tarpc](https://github.com/google/tarpc) with an embedded client. The RPC server
3516 running in the core Ayllu binary is called the `job_server` and it provides an
3517 interface to interact with Ayllu for administrators. Much of the functionality
3518 of Ayllu is implemented via `RPC extensions` which are minimal binaries that
3519 diff --git a/www/design.nomnom b/www/design.nomnom
3520new file mode 100644
3521index 0000000..6f755b6
3522--- /dev/null
3523+++ b/www/design.nomnom
3524 @@ -0,0 +1,51 @@
3525+ [reverse_proxy]
3526+
3527+ [reverse_proxy]<-->[Rudolfs (LFS API)]
3528+
3529+ [reverse_proxy]<-->[ayllu-core |
3530+ [axum http interface]
3531+ [sites static hosting]
3532+ [tarpc-server (job_server)]
3533+ [git repositories via libgit2]
3534+ [<database> sqlite]
3535+ ]
3536+
3537+ [ayllu-core] <--> [
3538+ ayllu-mail|
3539+ [tarpc-server]
3540+ [mailpot]
3541+ [postfix]
3542+ [<database> sqlite]
3543+ ] <-- SMTP [<actor> contributor-2]
3544+
3545+ [ayllu-core] <--> [
3546+ ayllu-xmpp |
3547+ [tarpc-server]
3548+ [chat interface/bot]
3549+ [<database> sqlite]
3550+ ] <-- XMPP [<actor>; contributor-2]
3551+
3552+ [ayllu-core] <--> [
3553+ ayllu-build |
3554+ [tarpc-server]
3555+ [<database> sqlite]
3556+ ]
3557+
3558+ [ayllu-core] <--> [ayllu-shell] <-- SSH [<actor> contributor]
3559+ [ayllu-core] <--> [ayllu-cli] <-- [<actor> admin]
3560+
3561+
3562+ [ayllu-build]<-->[
3563+ ayllu-builder-1 |
3564+ [<package>qemu-x86_64| tarpc-server | executor]
3565+ ]
3566+
3567+ [ayllu-build]<-->[
3568+ ayllu-builder-2 |
3569+ [<package>qemu-aarch64| tarpc-server | exectuor]
3570+ ]
3571+
3572+ [ayllu-build]<-->[
3573+ ayllu-builder-n |
3574+ [<package>lxc| tarpc-server | executor]
3575+ ] --> [ayllu-core]
3576 diff --git a/www/public/assets/architecture.svg b/www/public/assets/architecture.svg
3577index 2de2a37..e05c148 100644
3578--- a/www/public/assets/architecture.svg
3579+++ b/www/public/assets/architecture.svg
3580 @@ -1,35 +1,34 @@
3581- <svg version="1.1" baseProfile="full" width="1938.0" height="655.0" viewbox="0 0 1938 655" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events">
3582- <desc >
3583- [reverse_proxy]
3584+ <svg version="1.1" baseProfile="full" width="1817.5" height="664.0" viewBox="0 0 1817.5 664" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events">
3585+ <desc >[reverse_proxy]
3586
3587 [reverse_proxy]&lt;--&gt;[Rudolfs (LFS API)]
3588
3589 [reverse_proxy]&lt;--&gt;[ayllu-core |
3590- [axum http interface]
3591+ [axum http interface]
3592 [sites static hosting]
3593- [capnp-rpc-server (job_server)]
3594+ [tarpc-server (job_server)]
3595 [git repositories via libgit2]
3596 [&lt;database&gt; sqlite]
3597 ]
3598
3599 [ayllu-core] &lt;--&gt; [
3600 ayllu-mail|
3601- [capnp-rpc-server]
3602+ [tarpc-server]
3603 [mailpot]
3604 [postfix]
3605 [&lt;database&gt; sqlite]
3606 ] &lt;-- SMTP [&lt;actor&gt; contributor-2]
3607
3608 [ayllu-core] &lt;--&gt; [
3609- ayllu-xmpp |
3610- [capnp-rpc-server]
3611- [chat interface/bot]
3612+ ayllu-xmpp |
3613+ [tarpc-server]
3614+ [chat interface/bot]
3615 [&lt;database&gt; sqlite]
3616- ] &lt;-- XMPP [&lt;actor&gt; contributor-2]
3617+ ] &lt;-- XMPP [&lt;actor&gt;; contributor-2]
3618
3619 [ayllu-core] &lt;--&gt; [
3620- ayllu-build |
3621- [capnp-rpc-server]
3622+ ayllu-build |
3623+ [tarpc-server]
3624 [&lt;database&gt; sqlite]
3625 ]
3626
3627 @@ -38,553 +37,553 @@
3628
3629
3630 [ayllu-build]&lt;--&gt;[
3631- ayllu-builder-1 |
3632- [&lt;package&gt;qemu-x86_64| capnp-rpc-server | executor]
3633+ ayllu-builder-1 |
3634+ [&lt;package&gt;qemu-x86_64| tarpc-server | executor]
3635 ]
3636
3637 [ayllu-build]&lt;--&gt;[
3638- ayllu-builder-2 |
3639- [&lt;package&gt;qemu-aarch64| capnp-rpc-server | exectuor]
3640+ ayllu-builder-2 |
3641+ [&lt;package&gt;qemu-aarch64| tarpc-server | exectuor]
3642 ]
3643
3644 [ayllu-build]&lt;--&gt;[
3645- ayllu-builder-n |
3646- [&lt;package&gt;lxc| capnp-rpc-server | executor]
3647- ] --&gt; [ayllu-core]</desc>
3648+ ayllu-builder-n |
3649+ [&lt;package&gt;lxc| tarpc-server | executor]
3650+ ] --&gt; [ayllu-core]
3651+ </desc>
3652 <g stroke-width="1.0" text-align="left" font="12pt Helvetica, Arial, sans-serif" font-size="12pt" font-family="Helvetica" font-weight="normal" font-style="normal">
3653- <g font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" stroke-width="3.0" stroke-linejoin="round" stroke-linecap="round" stroke="#33322E">
3654- <g stroke="transparent" fill="transparent">
3655- <rect x="0.0" y="0.0" height="655.0" width="1938.0" stroke="none"></rect>
3656- </g>
3657+ <g font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" stroke-width="3.0" stroke-linejoin="round" stroke-linecap="round" stroke="#33322E">
3658+ <g stroke="transparent" fill="transparent">
3659+ <rect x="0.0" y="0.0" height="664.0" width="1817.5" stroke="none"></rect>
3660+ </g>
3661 <g transform="translate(8, 8)" fill="#33322E">
3662- <g transform="translate(20, 20)" fill="#33322E" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal">
3663- <g stroke-dasharray="6 6">
3664- <path d="M965.1 23.1 L713 51 L713 115.83333333333333 L713.0 115.8 " fill="none"></path>
3665- </g>
3666- <path d="M707.7 109.2 L713.0 115.8 L718.3 109.2 L713.0 122.5 Z"></path>
3667- <path d="M959.1 29.2 L965.1 23.1 L957.9 18.6 L971.8 22.4 Z"></path>
3668+ <g transform="translate(20, 20)" fill="#33322E" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal">
3669 <g stroke-dasharray="6 6">
3670- <path d="M1103.4 23.1 L1355.5 51 L1355.5 64.33333333333333 L1355.5 64.3 " fill="none"></path>
3671- </g>
3672- <path d="M1350.2 57.7 L1355.5 64.3 L1360.8 57.7 L1355.5 71.0 Z"></path>
3673- <path d="M1110.6 18.6 L1103.4 23.1 L1109.4 29.2 L1096.8 22.4 Z"></path>
3674+ <path d="M873.9 24.0 L631.5 52 L631.5 117.33333333333333 L631.5 117.3 " fill="none"></path>
3675+ </g>
3676+ <path d="M626.2 110.7 L631.5 117.3 L636.8 110.7 L631.5 124.0 Z"></path>
3677+ <path d="M867.9 30.1 L873.9 24.0 L866.6 19.5 L880.5 23.2 Z"></path>
3678 <g stroke-dasharray="6 6">
3679- <path d="M822.4 180.4 L261 225 L261 238.33333333333334 L261.0 238.3 " fill="none"></path>
3680- </g>
3681- <path d="M255.7 231.7 L261.0 238.3 L266.3 231.7 L261.0 245.0 Z"></path>
3682- <path d="M816.1 186.2 L822.4 180.4 L815.3 175.6 L829.0 179.9 Z"></path>
3683- <text x="758.0" y="489.4" stroke="none">SMTP</text>
3684+ <path d="M1012.1 24.0 L1254.5 52 L1254.5 65.33333333333333 L1254.5 65.3 " fill="none"></path>
3685+ </g>
3686+ <path d="M1249.2 58.7 L1254.5 65.3 L1259.8 58.7 L1254.5 72.0 Z"></path>
3687+ <path d="M1019.4 19.5 L1012.1 24.0 L1018.1 30.1 L1005.5 23.2 Z"></path>
3688 <g stroke-dasharray="6 6">
3689- <path d="M261.0 385.7 L261 399 L750 497.4263494967978 L750.0 497.4 " fill="none"></path>
3690- </g>
3691- <path d="M266.3 392.3 L261.0 385.7 L255.7 392.3 L261.0 379.0 Z"></path>
3692+ <path d="M740.9 184.6 L242 228 L242 241.33333333333334 L242.0 241.3 " fill="none"></path>
3693+ </g>
3694+ <path d="M236.7 234.7 L242.0 241.3 L247.3 234.7 L242.0 248.0 Z"></path>
3695+ <path d="M734.7 190.5 L740.9 184.6 L733.8 179.9 L747.5 184.1 Z"></path>
3696+ <text x="698.3" y="495.3" stroke="none">SMTP</text>
3697 <g stroke-dasharray="6 6">
3698- <path d="M926.9 206.0 L807.5 225 L807.5 238.33333333333334 L807.5 238.3 " fill="none"></path>
3699- </g>
3700- <path d="M802.2 231.7 L807.5 238.3 L812.8 231.7 L807.5 245.0 Z"></path>
3701- <path d="M921.1 212.4 L926.9 206.0 L919.5 201.8 L933.5 205.0 Z"></path>
3702- <text x="754.7" y="473.5" stroke="none">XMPP</text>
3703+ <path d="M242.0 390.7 L242 404 L690.25 503.2664359861592 L690.3 503.3 " fill="none"></path>
3704+ </g>
3705+ <path d="M247.3 397.3 L242.0 390.7 L236.7 397.3 L242.0 384.0 Z"></path>
3706 <g stroke-dasharray="6 6">
3707- <path d="M807.5 385.7 L807.5 399 L807.5 481.5 L807.5 481.5 " fill="none"></path>
3708- </g>
3709- <path d="M812.8 392.3 L807.5 385.7 L802.2 392.3 L807.5 379.0 Z"></path>
3710+ <path d="M858.5 209.1 L750.5 228 L750.5 241.33333333333334 L750.5 241.3 " fill="none"></path>
3711+ </g>
3712+ <path d="M745.2 234.7 L750.5 241.3 L755.8 234.7 L750.5 248.0 Z"></path>
3713+ <path d="M852.8 215.5 L858.5 209.1 L851.0 205.0 L865.0 208.0 Z"></path>
3714+ <text x="695.6" y="480.0" stroke="none">XMPP</text>
3715 <g stroke-dasharray="6 6">
3716- <path d="M1276.2 209.5 L1259 225 L1259 238.33333333333334 L1259.0 238.3 " fill="none"></path>
3717- </g>
3718- <path d="M1253.7 231.7 L1259.0 238.3 L1264.3 231.7 L1259.0 245.0 Z"></path>
3719- <path d="M1274.9 217.9 L1276.2 209.5 L1267.7 210.0 L1281.2 205.0 Z"></path>
3720+ <path d="M750.5 390.7 L750.5 404 L748.4375 488 L748.4 488.0 " fill="none"></path>
3721+ </g>
3722+ <path d="M755.8 397.3 L750.5 390.7 L745.2 397.3 L750.5 384.0 Z"></path>
3723 <g stroke-dasharray="6 6">
3724- <path d="M1595.6 206.8 L1659 225 L1659 289.8333333333333 L1659.0 289.8 " fill="none"></path>
3725- </g>
3726- <path d="M1653.7 283.2 L1659.0 289.8 L1664.3 283.2 L1659.0 296.5 Z"></path>
3727- <path d="M1603.5 203.5 L1595.6 206.8 L1600.6 213.8 L1589.2 205.0 Z"></path>
3728- <text x="1618.3" y="473.5" stroke="none">SSH</text>
3729+ <path d="M1183.6 212.8 L1168.75 228 L1168.75 241.33333333333334 L1168.8 241.3 " fill="none"></path>
3730+ </g>
3731+ <path d="M1163.4 234.7 L1168.8 241.3 L1174.1 234.7 L1168.8 248.0 Z"></path>
3732+ <path d="M1182.8 221.3 L1183.6 212.8 L1175.1 213.8 L1188.2 208.0 Z"></path>
3733 <g stroke-dasharray="6 6">
3734- <path d="M1659.0 334.2 L1659 399 L1659 481.5 L1659.0 481.5 " fill="none"></path>
3735- </g>
3736- <path d="M1664.3 340.8 L1659.0 334.2 L1653.7 340.8 L1659.0 327.5 Z"></path>
3737+ <path d="M1472.2 210.0 L1528 228 L1528 293.3333333333333 L1528.0 293.3 " fill="none"></path>
3738+ </g>
3739+ <path d="M1522.7 286.7 L1528.0 293.3 L1533.3 286.7 L1528.0 300.0 Z"></path>
3740+ <path d="M1480.2 207.0 L1472.2 210.0 L1476.9 217.2 L1465.8 208.0 Z"></path>
3741+ <text x="1487.3" y="480.0" stroke="none">SSH</text>
3742 <g stroke-dasharray="6 6">
3743- <path d="M1691.6 206.3 L1783.5 225 L1783.5 289.8333333333333 L1783.5 289.8 " fill="none"></path>
3744- </g>
3745- <path d="M1778.2 283.2 L1783.5 289.8 L1788.8 283.2 L1783.5 296.5 Z"></path>
3746- <path d="M1699.2 202.4 L1691.6 206.3 L1697.1 212.9 L1685.1 205.0 Z"></path>
3747+ <path d="M1528.0 338.7 L1528 404 L1528 488 L1528.0 488.0 " fill="none"></path>
3748+ </g>
3749+ <path d="M1533.3 345.3 L1528.0 338.7 L1522.7 345.3 L1528.0 332.0 Z"></path>
3750 <g stroke-dasharray="6 6">
3751- <path d="M1783.5 334.2 L1783.5 399 L1783.5 481.5 L1783.5 481.5 " fill="none"></path>
3752- </g>
3753- <path d="M1788.8 340.8 L1783.5 334.2 L1778.2 340.8 L1783.5 327.5 Z"></path>
3754+ <path d="M1568.6 209.4 L1652.5 228 L1652.5 293.3333333333333 L1652.5 293.3 " fill="none"></path>
3755+ </g>
3756+ <path d="M1647.2 286.7 L1652.5 293.3 L1657.8 286.7 L1652.5 300.0 Z"></path>
3757+ <path d="M1576.2 205.7 L1568.6 209.4 L1573.9 216.1 L1562.0 208.0 Z"></path>
3758 <g stroke-dasharray="6 6">
3759- <path d="M1102.2 365.9 L1006 399 L1006 412.3333333333333 L1006.0 412.3 " fill="none"></path>
3760- </g>
3761- <path d="M1000.7 405.7 L1006.0 412.3 L1011.3 405.7 L1006.0 419.0 Z"></path>
3762- <path d="M1097.6 373.1 L1102.2 365.9 L1094.2 363.0 L1108.5 363.8 Z"></path>
3763+ <path d="M1652.5 338.7 L1652.5 404 L1652.5 488 L1652.5 488.0 " fill="none"></path>
3764+ </g>
3765+ <path d="M1657.8 345.3 L1652.5 338.7 L1647.2 345.3 L1652.5 332.0 Z"></path>
3766 <g stroke-dasharray="6 6">
3767- <path d="M1242.1 385.5 L1239 399 L1239 412.3333333333333 L1239.0 412.3 " fill="none"></path>
3768- </g>
3769- <path d="M1233.7 405.7 L1239.0 412.3 L1244.3 405.7 L1239.0 419.0 Z"></path>
3770- <path d="M1245.8 393.2 L1242.1 385.5 L1235.4 390.8 L1243.6 379.0 Z"></path>
3771+ <path d="M1031.0 367.4 L933 404 L933 417.3333333333333 L933.0 417.3 " fill="none"></path>
3772+ </g>
3773+ <path d="M927.7 410.7 L933.0 417.3 L938.3 410.7 L933.0 424.0 Z"></path>
3774+ <path d="M1026.6 374.7 L1031.0 367.4 L1022.9 364.8 L1037.3 365.1 Z"></path>
3775 <g stroke-dasharray="6 6">
3776- <path d="M1354.1 383.0 L1375.5 399 L1388.6489765726408 413.9884707045646 L1388.6 414.0 " fill="none"></path>
3777- </g>
3778- <path d="M1380.2 412.5 L1388.6 414.0 L1388.3 405.5 L1393.0 419.0 Z"></path>
3779- <path d="M1362.6 382.7 L1354.1 383.0 L1356.2 391.3 L1348.7 379.0 Z"></path>
3780+ <path d="M1152.9 390.5 L1150 404 L1150 417.3333333333333 L1150.0 417.3 " fill="none"></path>
3781+ </g>
3782+ <path d="M1144.7 410.7 L1150.0 417.3 L1155.3 410.7 L1150.0 424.0 Z"></path>
3783+ <path d="M1156.7 398.2 L1152.9 390.5 L1146.3 395.9 L1154.3 384.0 Z"></path>
3784 <g stroke-dasharray="6 6">
3785- <path d="M1512.9 419.0 L1522 399 L1522 312 L1522 312 L1522 225 L1522 225 L1489.6328041618644 208.08741118367695 L1489.6 208.1 " fill="none"></path>
3786- </g>
3787- <path d="M1498.0 206.4 L1489.6 208.1 L1493.1 215.9 L1483.7 205.0 Z"></path>
3788+ <path d="M1255.6 388.3 L1274.5 404 L1285.5740965207704 418.6780924298968 L1285.6 418.7 " fill="none"></path>
3789+ </g>
3790+ <path d="M1277.3 416.6 L1285.6 418.7 L1285.8 410.1 L1289.6 424.0 Z"></path>
3791+ <path d="M1264.1 388.4 L1255.6 388.3 L1257.3 396.6 L1250.5 384.0 Z"></path>
3792+ <g stroke-dasharray="6 6">
3793+ <path d="M1392.7 424.0 L1400 404 L1400 316 L1400 316 L1400 228 L1400 228 L1372.6362943277163 211.45012990267378 L1372.6 211.5 " fill="none"></path>
3794+ </g>
3795+ <path d="M1381.1 210.3 L1372.6 211.5 L1375.6 219.5 L1366.9 208.0 Z"></path>
3796 <g data-name="reverse_proxy">
3797- <g fill="#eee8d5" stroke="#33322E" data-name="reverse_proxy">
3798- <rect x="971.8" y="0.0" height="31.0" width="125.0" data-name="reverse_proxy"></rect>
3799- </g>
3800- <g transform="translate(971.75, 0)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="reverse_proxy">
3801- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="reverse_proxy">
3802- <text x="54.5" y="13.5" stroke="none" text-anchor="middle" data-name="reverse_proxy">reverse_proxy</text>
3803+ <g fill="#eee8d5" stroke="#33322E" data-name="reverse_proxy">
3804+ <rect x="880.5" y="0.0" height="32.0" width="125.0" data-name="reverse_proxy"></rect>
3805+ </g>
3806+ <g transform="translate(880.5, 0)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="reverse_proxy">
3807+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="reverse_proxy">
3808+ <text x="54.5" y="14.1" stroke="none" text-anchor="middle" data-name="reverse_proxy">reverse_proxy</text>
3809
3810- </g>
3811 </g>
3812 </g>
3813+ </g>
3814 <g data-name="Rudolfs (LFS API)">
3815- <g fill="#eee8d5" stroke="#33322E" data-name="Rudolfs (LFS API)">
3816- <rect x="637.0" y="122.5" height="31.0" width="152.0" data-name="Rudolfs (LFS API)"></rect>
3817- </g>
3818- <g transform="translate(637, 122.5)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="Rudolfs (LFS API)">
3819- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="Rudolfs (LFS API)">
3820- <text x="68.0" y="13.5" stroke="none" text-anchor="middle" data-name="Rudolfs (LFS API)">Rudolfs (LFS API)</text>
3821+ <g fill="#eee8d5" stroke="#33322E" data-name="Rudolfs (LFS API)">
3822+ <rect x="555.5" y="124.0" height="32.0" width="152.0" data-name="Rudolfs (LFS API)"></rect>
3823+ </g>
3824+ <g transform="translate(555.5, 124)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="Rudolfs (LFS API)">
3825+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="Rudolfs (LFS API)">
3826+ <text x="68.0" y="14.1" stroke="none" text-anchor="middle" data-name="Rudolfs (LFS API)">Rudolfs (LFS API)</text>
3827
3828- </g>
3829 </g>
3830 </g>
3831+ </g>
3832 <g data-name="ayllu-core">
3833- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-core">
3834- <rect x="829.0" y="71.0" height="134.0" width="1053.0" data-name="ayllu-core"></rect>
3835- <path d="M829.0 102.0 L1882.0 102.0" fill="none" data-name="ayllu-core"></path>
3836- </g>
3837- <g transform="translate(829, 71)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-core">
3838- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-core">
3839- <text x="518.5" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-core">ayllu-core</text>
3840+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-core">
3841+ <rect x="747.5" y="72.0" height="136.0" width="1014.0" data-name="ayllu-core"></rect>
3842+ <path d="M747.5 104.0 L1761.5 104.0" fill="none" data-name="ayllu-core"></path>
3843+ </g>
3844+ <g transform="translate(747.5, 72)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-core">
3845+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-core">
3846+ <text x="499.0" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-core">ayllu-core</text>
3847
3848- </g>
3849 </g>
3850- <g transform="translate(829, 102)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-core">
3851- <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-core">
3852- <g transform="translate(20, 20)" data-name="ayllu-core">
3853- <g data-name="axum http interface">
3854- <g fill="#fdf6e3" stroke="#33322E" data-name="axum http interface">
3855- <rect x="0.0" y="8.0" height="31.0" width="164.0" data-name="axum http interface"></rect>
3856- </g>
3857+ </g>
3858+ <g transform="translate(747.5, 104)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-core">
3859+ <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-core">
3860+ <g transform="translate(20, 20)" data-name="ayllu-core">
3861+ <g data-name="axum http interface">
3862+ <g fill="#fdf6e3" stroke="#33322E" data-name="axum http interface">
3863+ <rect x="0.0" y="8.0" height="32.0" width="164.0" data-name="axum http interface"></rect>
3864+ </g>
3865 <g transform="translate(0, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="axum http interface">
3866- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="axum http interface">
3867- <text x="74.0" y="13.5" stroke="none" text-anchor="middle" data-name="axum http interface">axum http interface</text>
3868+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="axum http interface">
3869+ <text x="74.0" y="14.1" stroke="none" text-anchor="middle" data-name="axum http interface">axum http interface</text>
3870
3871- </g>
3872 </g>
3873 </g>
3874+ </g>
3875 <g data-name="sites static hosting">
3876- <g fill="#fdf6e3" stroke="#33322E" data-name="sites static hosting">
3877- <rect x="204.0" y="8.0" height="31.0" width="161.0" data-name="sites static hosting"></rect>
3878- </g>
3879+ <g fill="#fdf6e3" stroke="#33322E" data-name="sites static hosting">
3880+ <rect x="204.0" y="8.0" height="32.0" width="161.0" data-name="sites static hosting"></rect>
3881+ </g>
3882 <g transform="translate(204, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sites static hosting">
3883- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sites static hosting">
3884- <text x="72.5" y="13.5" stroke="none" text-anchor="middle" data-name="sites static hosting">sites static hosting</text>
3885+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sites static hosting">
3886+ <text x="72.5" y="14.1" stroke="none" text-anchor="middle" data-name="sites static hosting">sites static hosting</text>
3887
3888- </g>
3889 </g>
3890 </g>
3891- <g data-name="capnp-rpc-server (job_server)">
3892- <g fill="#fdf6e3" stroke="#33322E" data-name="capnp-rpc-server (job_server)">
3893- <rect x="405.0" y="8.0" height="31.0" width="244.0" data-name="capnp-rpc-server (job_server)"></rect>
3894- </g>
3895- <g transform="translate(405, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="capnp-rpc-server (job_server)">
3896- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="capnp-rpc-server (job_server)">
3897- <text x="114.0" y="13.5" stroke="none" text-anchor="middle" data-name="capnp-rpc-server (job_server)">capnp-rpc-server (job_server)</text>
3898+ </g>
3899+ <g data-name="tarpc-server (job_server)">
3900+ <g fill="#fdf6e3" stroke="#33322E" data-name="tarpc-server (job_server)">
3901+ <rect x="405.0" y="8.0" height="32.0" width="205.0" data-name="tarpc-server (job_server)"></rect>
3902+ </g>
3903+ <g transform="translate(405, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="tarpc-server (job_server)">
3904+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="tarpc-server (job_server)">
3905+ <text x="94.5" y="14.1" stroke="none" text-anchor="middle" data-name="tarpc-server (job_server)">tarpc-server (job_server)</text>
3906
3907- </g>
3908 </g>
3909 </g>
3910+ </g>
3911 <g data-name="git repositories via libgit2">
3912- <g fill="#fdf6e3" stroke="#33322E" data-name="git repositories via libgit2">
3913- <rect x="689.0" y="8.0" height="31.0" width="210.0" data-name="git repositories via libgit2"></rect>
3914- </g>
3915- <g transform="translate(689, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="git repositories via libgit2">
3916- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="git repositories via libgit2">
3917- <text x="97.0" y="13.5" stroke="none" text-anchor="middle" data-name="git repositories via libgit2">git repositories via libgit2</text>
3918+ <g fill="#fdf6e3" stroke="#33322E" data-name="git repositories via libgit2">
3919+ <rect x="650.0" y="8.0" height="32.0" width="210.0" data-name="git repositories via libgit2"></rect>
3920+ </g>
3921+ <g transform="translate(650, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="git repositories via libgit2">
3922+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="git repositories via libgit2">
3923+ <text x="97.0" y="14.1" stroke="none" text-anchor="middle" data-name="git repositories via libgit2">git repositories via libgit2</text>
3924
3925- </g>
3926 </g>
3927 </g>
3928+ </g>
3929 <g data-name="sqlite">
3930- <g fill="#fdf6e3" stroke="#33322E" data-name="sqlite">
3931- <rect x="939.0" y="8.0" height="31.0" width="58.0" stroke="none" data-name="sqlite"></rect>
3932- <path d="M939.0 8.0 L939.0 39.0" fill="none" data-name="sqlite"></path>
3933- <path d="M997.0 8.0 L997.0 39.0" fill="none" data-name="sqlite"></path>
3934- <ellipse cx="968.0" cy="8.0" rx="29.0" ry="6.0" data-name="sqlite"></ellipse>
3935- <path d="M997.0 39.0 L997.0 39.3 L996.9 39.6 L996.7 39.9 L996.4 40.2 L996.1 40.5 L995.7 40.8 L995.3 41.1 L994.7 41.3 L994.1 41.6 L993.5 41.9 L992.7 42.1 L992.0 42.4 L991.1 42.6 L990.2 42.9 L989.3 43.1 L988.2 43.3 L987.2 43.5 L986.1 43.7 L984.9 43.9 L983.7 44.0 L982.5 44.2 L981.2 44.3 L979.9 44.5 L978.6 44.6 L977.2 44.7 L975.9 44.8 L974.5 44.8 L973.0 44.9 L971.6 45.0 L970.2 45.0 L968.7 45.0 L967.3 45.0 L965.8 45.0 L964.4 45.0 L963.0 44.9 L961.5 44.8 L960.1 44.8 L958.8 44.7 L957.4 44.6 L956.1 44.5 L954.8 44.3 L953.5 44.2 L952.3 44.0 L951.1 43.9 L949.9 43.7 L948.8 43.5 L947.8 43.3 L946.7 43.1 L945.8 42.9 L944.9 42.6 L944.0 42.4 L943.3 42.1 L942.5 41.9 L941.9 41.6 L941.3 41.3 L940.7 41.1 L940.3 40.8 L939.9 40.5 L939.6 40.2 L939.3 39.9 L939.1 39.6 L939.0 39.3 L939.0 39.0" data-name="sqlite"></path>
3936- </g>
3937- <g transform="translate(939, 12)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sqlite">
3938- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sqlite">
3939- <text x="21.0" y="13.5" stroke="none" text-anchor="middle" data-name="sqlite">sqlite</text>
3940+ <g fill="#fdf6e3" stroke="#33322E" data-name="sqlite">
3941+ <rect x="900.0" y="8.0" height="32.0" width="58.0" stroke="none" data-name="sqlite"></rect>
3942+ <path d="M900.0 8.0 L900.0 40.0" fill="none" data-name="sqlite"></path>
3943+ <path d="M958.0 8.0 L958.0 40.0" fill="none" data-name="sqlite"></path>
3944+ <ellipse cx="929.0" cy="8.0" rx="29.0" ry="6.0" data-name="sqlite"></ellipse>
3945+ <path d="M958.0 40.0 L958.0 40.3 L957.9 40.6 L957.7 40.9 L957.4 41.2 L957.1 41.5 L956.7 41.8 L956.3 42.1 L955.7 42.3 L955.1 42.6 L954.5 42.9 L953.7 43.1 L953.0 43.4 L952.1 43.6 L951.2 43.9 L950.3 44.1 L949.2 44.3 L948.2 44.5 L947.1 44.7 L945.9 44.9 L944.7 45.0 L943.5 45.2 L942.2 45.3 L940.9 45.5 L939.6 45.6 L938.2 45.7 L936.9 45.8 L935.5 45.8 L934.0 45.9 L932.6 46.0 L931.2 46.0 L929.7 46.0 L928.3 46.0 L926.8 46.0 L925.4 46.0 L924.0 45.9 L922.5 45.8 L921.1 45.8 L919.8 45.7 L918.4 45.6 L917.1 45.5 L915.8 45.3 L914.5 45.2 L913.3 45.0 L912.1 44.9 L910.9 44.7 L909.8 44.5 L908.8 44.3 L907.7 44.1 L906.8 43.9 L905.9 43.6 L905.0 43.4 L904.3 43.1 L903.5 42.9 L902.9 42.6 L902.3 42.3 L901.7 42.1 L901.3 41.8 L900.9 41.5 L900.6 41.2 L900.3 40.9 L900.1 40.6 L900.0 40.3 L900.0 40.0" data-name="sqlite"></path>
3946+ </g>
3947+ <g transform="translate(900, 12)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sqlite">
3948+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sqlite">
3949+ <text x="21.0" y="14.1" stroke="none" text-anchor="middle" data-name="sqlite">sqlite</text>
3950
3951- </g>
3952 </g>
3953 </g>
3954 </g>
3955 </g>
3956 </g>
3957 </g>
3958+ </g>
3959 <g data-name="ayllu-mail">
3960- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-mail">
3961- <rect x="0.0" y="245.0" height="134.0" width="522.0" data-name="ayllu-mail"></rect>
3962- <path d="M0.0 276.0 L522.0 276.0" fill="none" data-name="ayllu-mail"></path>
3963- </g>
3964- <g transform="translate(0, 245)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-mail">
3965- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-mail">
3966- <text x="253.0" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-mail">ayllu-mail</text>
3967+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-mail">
3968+ <rect x="0.0" y="248.0" height="136.0" width="484.0" data-name="ayllu-mail"></rect>
3969+ <path d="M0.0 280.0 L484.0 280.0" fill="none" data-name="ayllu-mail"></path>
3970+ </g>
3971+ <g transform="translate(0, 248)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-mail">
3972+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-mail">
3973+ <text x="234.0" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-mail">ayllu-mail</text>
3974
3975- </g>
3976 </g>
3977- <g transform="translate(0, 276)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-mail">
3978- <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-mail">
3979- <g transform="translate(20, 20)" data-name="ayllu-mail">
3980- <g data-name="capnp-rpc-server">
3981- <g fill="#fdf6e3" stroke="#33322E" data-name="capnp-rpc-server">
3982- <rect x="0.0" y="8.0" height="31.0" width="147.0" data-name="capnp-rpc-server"></rect>
3983- </g>
3984- <g transform="translate(0, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="capnp-rpc-server">
3985- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="capnp-rpc-server">
3986- <text x="65.5" y="13.5" stroke="none" text-anchor="middle" data-name="capnp-rpc-server">capnp-rpc-server</text>
3987+ </g>
3988+ <g transform="translate(0, 280)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-mail">
3989+ <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-mail">
3990+ <g transform="translate(20, 20)" data-name="ayllu-mail">
3991+ <g data-name="tarpc-server">
3992+ <g fill="#fdf6e3" stroke="#33322E" data-name="tarpc-server">
3993+ <rect x="0.0" y="8.0" height="32.0" width="109.0" data-name="tarpc-server"></rect>
3994+ </g>
3995+ <g transform="translate(0, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="tarpc-server">
3996+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="tarpc-server">
3997+ <text x="46.5" y="14.1" stroke="none" text-anchor="middle" data-name="tarpc-server">tarpc-server</text>
3998
3999- </g>
4000 </g>
4001 </g>
4002+ </g>
4003 <g data-name="mailpot">
4004- <g fill="#fdf6e3" stroke="#33322E" data-name="mailpot">
4005- <rect x="187.0" y="8.0" height="31.0" width="73.0" data-name="mailpot"></rect>
4006- </g>
4007- <g transform="translate(187, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="mailpot">
4008- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="mailpot">
4009- <text x="28.5" y="13.5" stroke="none" text-anchor="middle" data-name="mailpot">mailpot</text>
4010+ <g fill="#fdf6e3" stroke="#33322E" data-name="mailpot">
4011+ <rect x="149.0" y="8.0" height="32.0" width="73.0" data-name="mailpot"></rect>
4012+ </g>
4013+ <g transform="translate(149, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="mailpot">
4014+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="mailpot">
4015+ <text x="28.5" y="14.1" stroke="none" text-anchor="middle" data-name="mailpot">mailpot</text>
4016
4017- </g>
4018 </g>
4019 </g>
4020+ </g>
4021 <g data-name="postfix">
4022- <g fill="#fdf6e3" stroke="#33322E" data-name="postfix">
4023- <rect x="300.0" y="8.0" height="31.0" width="68.0" data-name="postfix"></rect>
4024- </g>
4025- <g transform="translate(300, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="postfix">
4026- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="postfix">
4027- <text x="26.0" y="13.5" stroke="none" text-anchor="middle" data-name="postfix">postfix</text>
4028+ <g fill="#fdf6e3" stroke="#33322E" data-name="postfix">
4029+ <rect x="262.0" y="8.0" height="32.0" width="68.0" data-name="postfix"></rect>
4030+ </g>
4031+ <g transform="translate(262, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="postfix">
4032+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="postfix">
4033+ <text x="26.0" y="14.1" stroke="none" text-anchor="middle" data-name="postfix">postfix</text>
4034
4035- </g>
4036 </g>
4037 </g>
4038+ </g>
4039 <g data-name="sqlite">
4040- <g fill="#fdf6e3" stroke="#33322E" data-name="sqlite">
4041- <rect x="408.0" y="8.0" height="31.0" width="58.0" stroke="none" data-name="sqlite"></rect>
4042- <path d="M408.0 8.0 L408.0 39.0" fill="none" data-name="sqlite"></path>
4043- <path d="M466.0 8.0 L466.0 39.0" fill="none" data-name="sqlite"></path>
4044- <ellipse cx="437.0" cy="8.0" rx="29.0" ry="6.0" data-name="sqlite"></ellipse>
4045- <path d="M466.0 39.0 L466.0 39.3 L465.9 39.6 L465.7 39.9 L465.4 40.2 L465.1 40.5 L464.7 40.8 L464.3 41.1 L463.7 41.3 L463.1 41.6 L462.5 41.9 L461.7 42.1 L461.0 42.4 L460.1 42.6 L459.2 42.9 L458.3 43.1 L457.2 43.3 L456.2 43.5 L455.1 43.7 L453.9 43.9 L452.7 44.0 L451.5 44.2 L450.2 44.3 L448.9 44.5 L447.6 44.6 L446.2 44.7 L444.9 44.8 L443.5 44.8 L442.0 44.9 L440.6 45.0 L439.2 45.0 L437.7 45.0 L436.3 45.0 L434.8 45.0 L433.4 45.0 L432.0 44.9 L430.5 44.8 L429.1 44.8 L427.8 44.7 L426.4 44.6 L425.1 44.5 L423.8 44.3 L422.5 44.2 L421.3 44.0 L420.1 43.9 L418.9 43.7 L417.8 43.5 L416.8 43.3 L415.7 43.1 L414.8 42.9 L413.9 42.6 L413.0 42.4 L412.3 42.1 L411.5 41.9 L410.9 41.6 L410.3 41.3 L409.7 41.1 L409.3 40.8 L408.9 40.5 L408.6 40.2 L408.3 39.9 L408.1 39.6 L408.0 39.3 L408.0 39.0" data-name="sqlite"></path>
4046- </g>
4047- <g transform="translate(408, 12)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sqlite">
4048- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sqlite">
4049- <text x="21.0" y="13.5" stroke="none" text-anchor="middle" data-name="sqlite">sqlite</text>
4050+ <g fill="#fdf6e3" stroke="#33322E" data-name="sqlite">
4051+ <rect x="370.0" y="8.0" height="32.0" width="58.0" stroke="none" data-name="sqlite"></rect>
4052+ <path d="M370.0 8.0 L370.0 40.0" fill="none" data-name="sqlite"></path>
4053+ <path d="M428.0 8.0 L428.0 40.0" fill="none" data-name="sqlite"></path>
4054+ <ellipse cx="399.0" cy="8.0" rx="29.0" ry="6.0" data-name="sqlite"></ellipse>
4055+ <path d="M428.0 40.0 L428.0 40.3 L427.9 40.6 L427.7 40.9 L427.4 41.2 L427.1 41.5 L426.7 41.8 L426.3 42.1 L425.7 42.3 L425.1 42.6 L424.5 42.9 L423.7 43.1 L423.0 43.4 L422.1 43.6 L421.2 43.9 L420.3 44.1 L419.2 44.3 L418.2 44.5 L417.1 44.7 L415.9 44.9 L414.7 45.0 L413.5 45.2 L412.2 45.3 L410.9 45.5 L409.6 45.6 L408.2 45.7 L406.9 45.8 L405.5 45.8 L404.0 45.9 L402.6 46.0 L401.2 46.0 L399.7 46.0 L398.3 46.0 L396.8 46.0 L395.4 46.0 L394.0 45.9 L392.5 45.8 L391.1 45.8 L389.8 45.7 L388.4 45.6 L387.1 45.5 L385.8 45.3 L384.5 45.2 L383.3 45.0 L382.1 44.9 L380.9 44.7 L379.8 44.5 L378.8 44.3 L377.7 44.1 L376.8 43.9 L375.9 43.6 L375.0 43.4 L374.3 43.1 L373.5 42.9 L372.9 42.6 L372.3 42.3 L371.7 42.1 L371.3 41.8 L370.9 41.5 L370.6 41.2 L370.3 40.9 L370.1 40.6 L370.0 40.3 L370.0 40.0" data-name="sqlite"></path>
4056+ </g>
4057+ <g transform="translate(370, 12)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sqlite">
4058+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sqlite">
4059+ <text x="21.0" y="14.1" stroke="none" text-anchor="middle" data-name="sqlite">sqlite</text>
4060
4061- </g>
4062 </g>
4063 </g>
4064 </g>
4065 </g>
4066 </g>
4067 </g>
4068+ </g>
4069 <g data-name="contributor-2">
4070- <g fill="#eee8d5" stroke="#33322E" data-name="contributor-2">
4071- <circle r="4.0" cx="807.5" cy="493.5" data-name="contributor-2"></circle>
4072- <path d="M807.5 497.5 L807.5 505.5" fill="none" data-name="contributor-2"></path>
4073- <path d="M803.5 501.5 L811.5 501.5" fill="none" data-name="contributor-2"></path>
4074- <path d="M803.5 509.5 L807.5 505.5 L811.5 509.5" fill="none" data-name="contributor-2"></path>
4075- </g>
4076- <g transform="translate(750, 505.5)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="contributor-2">
4077- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="contributor-2">
4078- <text x="49.5" y="13.5" stroke="none" text-anchor="middle" data-name="contributor-2">contributor-2</text>
4079+ <g fill="#eee8d5" stroke="#33322E" data-name="contributor-2">
4080+ <circle r="4.0" cx="747.8" cy="500.0" data-name="contributor-2"></circle>
4081+ <path d="M747.8 504.0 L747.8 512.0" fill="none" data-name="contributor-2"></path>
4082+ <path d="M743.8 508.0 L751.8 508.0" fill="none" data-name="contributor-2"></path>
4083+ <path d="M743.8 516.0 L747.8 512.0 L751.8 516.0" fill="none" data-name="contributor-2"></path>
4084+ </g>
4085+ <g transform="translate(690.25, 512)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="contributor-2">
4086+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="contributor-2">
4087+ <text x="49.5" y="14.1" stroke="none" text-anchor="middle" data-name="contributor-2">contributor-2</text>
4088
4089- </g>
4090 </g>
4091 </g>
4092+ </g>
4093 <g data-name="ayllu-xmpp">
4094- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-xmpp">
4095- <rect x="562.0" y="245.0" height="134.0" width="491.0" data-name="ayllu-xmpp"></rect>
4096- <path d="M562.0 276.0 L1053.0 276.0" fill="none" data-name="ayllu-xmpp"></path>
4097- </g>
4098- <g transform="translate(562, 245)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-xmpp">
4099- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-xmpp">
4100- <text x="237.5" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-xmpp">ayllu-xmpp</text>
4101+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-xmpp">
4102+ <rect x="524.0" y="248.0" height="136.0" width="453.0" data-name="ayllu-xmpp"></rect>
4103+ <path d="M524.0 280.0 L977.0 280.0" fill="none" data-name="ayllu-xmpp"></path>
4104+ </g>
4105+ <g transform="translate(524, 248)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-xmpp">
4106+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-xmpp">
4107+ <text x="218.5" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-xmpp">ayllu-xmpp</text>
4108
4109- </g>
4110 </g>
4111- <g transform="translate(562, 276)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-xmpp">
4112- <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-xmpp">
4113- <g transform="translate(20, 20)" data-name="ayllu-xmpp">
4114- <g data-name="capnp-rpc-server">
4115- <g fill="#fdf6e3" stroke="#33322E" data-name="capnp-rpc-server">
4116- <rect x="0.0" y="8.0" height="31.0" width="147.0" data-name="capnp-rpc-server"></rect>
4117- </g>
4118- <g transform="translate(0, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="capnp-rpc-server">
4119- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="capnp-rpc-server">
4120- <text x="65.5" y="13.5" stroke="none" text-anchor="middle" data-name="capnp-rpc-server">capnp-rpc-server</text>
4121+ </g>
4122+ <g transform="translate(524, 280)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-xmpp">
4123+ <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-xmpp">
4124+ <g transform="translate(20, 20)" data-name="ayllu-xmpp">
4125+ <g data-name="tarpc-server">
4126+ <g fill="#fdf6e3" stroke="#33322E" data-name="tarpc-server">
4127+ <rect x="0.0" y="8.0" height="32.0" width="109.0" data-name="tarpc-server"></rect>
4128+ </g>
4129+ <g transform="translate(0, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="tarpc-server">
4130+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="tarpc-server">
4131+ <text x="46.5" y="14.1" stroke="none" text-anchor="middle" data-name="tarpc-server">tarpc-server</text>
4132
4133- </g>
4134 </g>
4135 </g>
4136+ </g>
4137 <g data-name="chat interface/bot">
4138- <g fill="#fdf6e3" stroke="#33322E" data-name="chat interface/bot">
4139- <rect x="187.0" y="8.0" height="31.0" width="150.0" data-name="chat interface/bot"></rect>
4140- </g>
4141- <g transform="translate(187, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="chat interface/bot">
4142- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="chat interface/bot">
4143- <text x="67.0" y="13.5" stroke="none" text-anchor="middle" data-name="chat interface/bot">chat interface/bot</text>
4144+ <g fill="#fdf6e3" stroke="#33322E" data-name="chat interface/bot">
4145+ <rect x="149.0" y="8.0" height="32.0" width="150.0" data-name="chat interface/bot"></rect>
4146+ </g>
4147+ <g transform="translate(149, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="chat interface/bot">
4148+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="chat interface/bot">
4149+ <text x="67.0" y="14.1" stroke="none" text-anchor="middle" data-name="chat interface/bot">chat interface/bot</text>
4150
4151- </g>
4152 </g>
4153 </g>
4154+ </g>
4155 <g data-name="sqlite">
4156- <g fill="#fdf6e3" stroke="#33322E" data-name="sqlite">
4157- <rect x="377.0" y="8.0" height="31.0" width="58.0" stroke="none" data-name="sqlite"></rect>
4158- <path d="M377.0 8.0 L377.0 39.0" fill="none" data-name="sqlite"></path>
4159- <path d="M435.0 8.0 L435.0 39.0" fill="none" data-name="sqlite"></path>
4160- <ellipse cx="406.0" cy="8.0" rx="29.0" ry="6.0" data-name="sqlite"></ellipse>
4161- <path d="M435.0 39.0 L435.0 39.3 L434.9 39.6 L434.7 39.9 L434.4 40.2 L434.1 40.5 L433.7 40.8 L433.3 41.1 L432.7 41.3 L432.1 41.6 L431.5 41.9 L430.7 42.1 L430.0 42.4 L429.1 42.6 L428.2 42.9 L427.3 43.1 L426.2 43.3 L425.2 43.5 L424.1 43.7 L422.9 43.9 L421.7 44.0 L420.5 44.2 L419.2 44.3 L417.9 44.5 L416.6 44.6 L415.2 44.7 L413.9 44.8 L412.5 44.8 L411.0 44.9 L409.6 45.0 L408.2 45.0 L406.7 45.0 L405.3 45.0 L403.8 45.0 L402.4 45.0 L401.0 44.9 L399.5 44.8 L398.1 44.8 L396.8 44.7 L395.4 44.6 L394.1 44.5 L392.8 44.3 L391.5 44.2 L390.3 44.0 L389.1 43.9 L387.9 43.7 L386.8 43.5 L385.8 43.3 L384.7 43.1 L383.8 42.9 L382.9 42.6 L382.0 42.4 L381.3 42.1 L380.5 41.9 L379.9 41.6 L379.3 41.3 L378.7 41.1 L378.3 40.8 L377.9 40.5 L377.6 40.2 L377.3 39.9 L377.1 39.6 L377.0 39.3 L377.0 39.0" data-name="sqlite"></path>
4162- </g>
4163- <g transform="translate(377, 12)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sqlite">
4164- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sqlite">
4165- <text x="21.0" y="13.5" stroke="none" text-anchor="middle" data-name="sqlite">sqlite</text>
4166+ <g fill="#fdf6e3" stroke="#33322E" data-name="sqlite">
4167+ <rect x="339.0" y="8.0" height="32.0" width="58.0" stroke="none" data-name="sqlite"></rect>
4168+ <path d="M339.0 8.0 L339.0 40.0" fill="none" data-name="sqlite"></path>
4169+ <path d="M397.0 8.0 L397.0 40.0" fill="none" data-name="sqlite"></path>
4170+ <ellipse cx="368.0" cy="8.0" rx="29.0" ry="6.0" data-name="sqlite"></ellipse>
4171+ <path d="M397.0 40.0 L397.0 40.3 L396.9 40.6 L396.7 40.9 L396.4 41.2 L396.1 41.5 L395.7 41.8 L395.3 42.1 L394.7 42.3 L394.1 42.6 L393.5 42.9 L392.7 43.1 L392.0 43.4 L391.1 43.6 L390.2 43.9 L389.3 44.1 L388.2 44.3 L387.2 44.5 L386.1 44.7 L384.9 44.9 L383.7 45.0 L382.5 45.2 L381.2 45.3 L379.9 45.5 L378.6 45.6 L377.2 45.7 L375.9 45.8 L374.5 45.8 L373.0 45.9 L371.6 46.0 L370.2 46.0 L368.7 46.0 L367.3 46.0 L365.8 46.0 L364.4 46.0 L363.0 45.9 L361.5 45.8 L360.1 45.8 L358.8 45.7 L357.4 45.6 L356.1 45.5 L354.8 45.3 L353.5 45.2 L352.3 45.0 L351.1 44.9 L349.9 44.7 L348.8 44.5 L347.8 44.3 L346.7 44.1 L345.8 43.9 L344.9 43.6 L344.0 43.4 L343.3 43.1 L342.5 42.9 L341.9 42.6 L341.3 42.3 L340.7 42.1 L340.3 41.8 L339.9 41.5 L339.6 41.2 L339.3 40.9 L339.1 40.6 L339.0 40.3 L339.0 40.0" data-name="sqlite"></path>
4172+ </g>
4173+ <g transform="translate(339, 12)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sqlite">
4174+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sqlite">
4175+ <text x="21.0" y="14.1" stroke="none" text-anchor="middle" data-name="sqlite">sqlite</text>
4176
4177- </g>
4178 </g>
4179 </g>
4180 </g>
4181 </g>
4182 </g>
4183 </g>
4184+ </g>
4185 <g data-name="ayllu-build">
4186- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-build">
4187- <rect x="1108.5" y="245.0" height="134.0" width="301.0" data-name="ayllu-build"></rect>
4188- <path d="M1108.5 276.0 L1409.5 276.0" fill="none" data-name="ayllu-build"></path>
4189- </g>
4190- <g transform="translate(1108.5, 245)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-build">
4191- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-build">
4192- <text x="142.5" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-build">ayllu-build</text>
4193+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-build">
4194+ <rect x="1037.3" y="248.0" height="136.0" width="263.0" data-name="ayllu-build"></rect>
4195+ <path d="M1037.3 280.0 L1300.3 280.0" fill="none" data-name="ayllu-build"></path>
4196+ </g>
4197+ <g transform="translate(1037.25, 248)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-build">
4198+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-build">
4199+ <text x="123.5" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-build">ayllu-build</text>
4200
4201- </g>
4202 </g>
4203- <g transform="translate(1108.5, 276)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-build">
4204- <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-build">
4205- <g transform="translate(20, 20)" data-name="ayllu-build">
4206- <g data-name="capnp-rpc-server">
4207- <g fill="#fdf6e3" stroke="#33322E" data-name="capnp-rpc-server">
4208- <rect x="0.0" y="8.0" height="31.0" width="147.0" data-name="capnp-rpc-server"></rect>
4209- </g>
4210- <g transform="translate(0, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="capnp-rpc-server">
4211- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="capnp-rpc-server">
4212- <text x="65.5" y="13.5" stroke="none" text-anchor="middle" data-name="capnp-rpc-server">capnp-rpc-server</text>
4213+ </g>
4214+ <g transform="translate(1037.25, 280)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-build">
4215+ <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-build">
4216+ <g transform="translate(20, 20)" data-name="ayllu-build">
4217+ <g data-name="tarpc-server">
4218+ <g fill="#fdf6e3" stroke="#33322E" data-name="tarpc-server">
4219+ <rect x="0.0" y="8.0" height="32.0" width="109.0" data-name="tarpc-server"></rect>
4220+ </g>
4221+ <g transform="translate(0, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="tarpc-server">
4222+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="tarpc-server">
4223+ <text x="46.5" y="14.1" stroke="none" text-anchor="middle" data-name="tarpc-server">tarpc-server</text>
4224
4225- </g>
4226 </g>
4227 </g>
4228+ </g>
4229 <g data-name="sqlite">
4230- <g fill="#fdf6e3" stroke="#33322E" data-name="sqlite">
4231- <rect x="187.0" y="8.0" height="31.0" width="58.0" stroke="none" data-name="sqlite"></rect>
4232- <path d="M187.0 8.0 L187.0 39.0" fill="none" data-name="sqlite"></path>
4233- <path d="M245.0 8.0 L245.0 39.0" fill="none" data-name="sqlite"></path>
4234- <ellipse cx="216.0" cy="8.0" rx="29.0" ry="6.0" data-name="sqlite"></ellipse>
4235- <path d="M245.0 39.0 L245.0 39.3 L244.9 39.6 L244.7 39.9 L244.4 40.2 L244.1 40.5 L243.7 40.8 L243.3 41.1 L242.7 41.3 L242.1 41.6 L241.5 41.9 L240.7 42.1 L240.0 42.4 L239.1 42.6 L238.2 42.9 L237.3 43.1 L236.2 43.3 L235.2 43.5 L234.1 43.7 L232.9 43.9 L231.7 44.0 L230.5 44.2 L229.2 44.3 L227.9 44.5 L226.6 44.6 L225.2 44.7 L223.9 44.8 L222.5 44.8 L221.0 44.9 L219.6 45.0 L218.2 45.0 L216.7 45.0 L215.3 45.0 L213.8 45.0 L212.4 45.0 L211.0 44.9 L209.5 44.8 L208.1 44.8 L206.8 44.7 L205.4 44.6 L204.1 44.5 L202.8 44.3 L201.5 44.2 L200.3 44.0 L199.1 43.9 L197.9 43.7 L196.8 43.5 L195.8 43.3 L194.7 43.1 L193.8 42.9 L192.9 42.6 L192.0 42.4 L191.3 42.1 L190.5 41.9 L189.9 41.6 L189.3 41.3 L188.7 41.1 L188.3 40.8 L187.9 40.5 L187.6 40.2 L187.3 39.9 L187.1 39.6 L187.0 39.3 L187.0 39.0" data-name="sqlite"></path>
4236- </g>
4237- <g transform="translate(187, 12)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sqlite">
4238- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sqlite">
4239- <text x="21.0" y="13.5" stroke="none" text-anchor="middle" data-name="sqlite">sqlite</text>
4240+ <g fill="#fdf6e3" stroke="#33322E" data-name="sqlite">
4241+ <rect x="149.0" y="8.0" height="32.0" width="58.0" stroke="none" data-name="sqlite"></rect>
4242+ <path d="M149.0 8.0 L149.0 40.0" fill="none" data-name="sqlite"></path>
4243+ <path d="M207.0 8.0 L207.0 40.0" fill="none" data-name="sqlite"></path>
4244+ <ellipse cx="178.0" cy="8.0" rx="29.0" ry="6.0" data-name="sqlite"></ellipse>
4245+ <path d="M207.0 40.0 L207.0 40.3 L206.9 40.6 L206.7 40.9 L206.4 41.2 L206.1 41.5 L205.7 41.8 L205.3 42.1 L204.7 42.3 L204.1 42.6 L203.5 42.9 L202.7 43.1 L202.0 43.4 L201.1 43.6 L200.2 43.9 L199.3 44.1 L198.2 44.3 L197.2 44.5 L196.1 44.7 L194.9 44.9 L193.7 45.0 L192.5 45.2 L191.2 45.3 L189.9 45.5 L188.6 45.6 L187.2 45.7 L185.9 45.8 L184.5 45.8 L183.0 45.9 L181.6 46.0 L180.2 46.0 L178.7 46.0 L177.3 46.0 L175.8 46.0 L174.4 46.0 L173.0 45.9 L171.5 45.8 L170.1 45.8 L168.8 45.7 L167.4 45.6 L166.1 45.5 L164.8 45.3 L163.5 45.2 L162.3 45.0 L161.1 44.9 L159.9 44.7 L158.8 44.5 L157.8 44.3 L156.7 44.1 L155.8 43.9 L154.9 43.6 L154.0 43.4 L153.3 43.1 L152.5 42.9 L151.9 42.6 L151.3 42.3 L150.7 42.1 L150.3 41.8 L149.9 41.5 L149.6 41.2 L149.3 40.9 L149.1 40.6 L149.0 40.3 L149.0 40.0" data-name="sqlite"></path>
4246+ </g>
4247+ <g transform="translate(149, 12)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sqlite">
4248+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sqlite">
4249+ <text x="21.0" y="14.1" stroke="none" text-anchor="middle" data-name="sqlite">sqlite</text>
4250
4251- </g>
4252 </g>
4253 </g>
4254 </g>
4255 </g>
4256 </g>
4257 </g>
4258+ </g>
4259 <g data-name="ayllu-shell">
4260- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-shell">
4261- <rect x="1612.0" y="296.5" height="31.0" width="94.0" data-name="ayllu-shell"></rect>
4262- </g>
4263- <g transform="translate(1612, 296.5)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-shell">
4264- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-shell">
4265- <text x="39.0" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-shell">ayllu-shell</text>
4266+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-shell">
4267+ <rect x="1481.0" y="300.0" height="32.0" width="94.0" data-name="ayllu-shell"></rect>
4268+ </g>
4269+ <g transform="translate(1481, 300)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-shell">
4270+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-shell">
4271+ <text x="39.0" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-shell">ayllu-shell</text>
4272
4273- </g>
4274 </g>
4275 </g>
4276+ </g>
4277 <g data-name="contributor">
4278- <g fill="#eee8d5" stroke="#33322E" data-name="contributor">
4279- <circle r="4.0" cx="1659.0" cy="493.5" data-name="contributor"></circle>
4280- <path d="M1659.0 497.5 L1659.0 505.5" fill="none" data-name="contributor"></path>
4281- <path d="M1655.0 501.5 L1663.0 501.5" fill="none" data-name="contributor"></path>
4282- <path d="M1655.0 509.5 L1659.0 505.5 L1663.0 509.5" fill="none" data-name="contributor"></path>
4283- </g>
4284- <g transform="translate(1608.5, 505.5)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="contributor">
4285- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="contributor">
4286- <text x="42.5" y="13.5" stroke="none" text-anchor="middle" data-name="contributor">contributor</text>
4287+ <g fill="#eee8d5" stroke="#33322E" data-name="contributor">
4288+ <circle r="4.0" cx="1528.0" cy="500.0" data-name="contributor"></circle>
4289+ <path d="M1528.0 504.0 L1528.0 512.0" fill="none" data-name="contributor"></path>
4290+ <path d="M1524.0 508.0 L1532.0 508.0" fill="none" data-name="contributor"></path>
4291+ <path d="M1524.0 516.0 L1528.0 512.0 L1532.0 516.0" fill="none" data-name="contributor"></path>
4292+ </g>
4293+ <g transform="translate(1477.5, 512)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="contributor">
4294+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="contributor">
4295+ <text x="42.5" y="14.1" stroke="none" text-anchor="middle" data-name="contributor">contributor</text>
4296
4297- </g>
4298 </g>
4299 </g>
4300+ </g>
4301 <g data-name="ayllu-cli">
4302- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-cli">
4303- <rect x="1746.0" y="296.5" height="31.0" width="75.0" data-name="ayllu-cli"></rect>
4304- </g>
4305- <g transform="translate(1746, 296.5)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-cli">
4306- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-cli">
4307- <text x="29.5" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-cli">ayllu-cli</text>
4308+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-cli">
4309+ <rect x="1615.0" y="300.0" height="32.0" width="75.0" data-name="ayllu-cli"></rect>
4310+ </g>
4311+ <g transform="translate(1615, 300)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-cli">
4312+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-cli">
4313+ <text x="29.5" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-cli">ayllu-cli</text>
4314
4315- </g>
4316 </g>
4317 </g>
4318+ </g>
4319 <g data-name="admin">
4320- <g fill="#eee8d5" stroke="#33322E" data-name="admin">
4321- <circle r="4.0" cx="1783.5" cy="493.5" data-name="admin"></circle>
4322- <path d="M1783.5 497.5 L1783.5 505.5" fill="none" data-name="admin"></path>
4323- <path d="M1779.5 501.5 L1787.5 501.5" fill="none" data-name="admin"></path>
4324- <path d="M1779.5 509.5 L1783.5 505.5 L1787.5 509.5" fill="none" data-name="admin"></path>
4325- </g>
4326- <g transform="translate(1752, 505.5)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="admin">
4327- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="admin">
4328- <text x="23.5" y="13.5" stroke="none" text-anchor="middle" data-name="admin">admin</text>
4329+ <g fill="#eee8d5" stroke="#33322E" data-name="admin">
4330+ <circle r="4.0" cx="1652.5" cy="500.0" data-name="admin"></circle>
4331+ <path d="M1652.5 504.0 L1652.5 512.0" fill="none" data-name="admin"></path>
4332+ <path d="M1648.5 508.0 L1656.5 508.0" fill="none" data-name="admin"></path>
4333+ <path d="M1648.5 516.0 L1652.5 512.0 L1656.5 516.0" fill="none" data-name="admin"></path>
4334+ </g>
4335+ <g transform="translate(1621, 512)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="admin">
4336+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="admin">
4337+ <text x="23.5" y="14.1" stroke="none" text-anchor="middle" data-name="admin">admin</text>
4338
4339- </g>
4340 </g>
4341 </g>
4342+ </g>
4343 <g data-name="ayllu-builder-1">
4344- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-builder-1">
4345- <rect x="909.5" y="419.0" height="180.0" width="193.0" data-name="ayllu-builder-1"></rect>
4346- <path d="M909.5 450.0 L1102.5 450.0" fill="none" data-name="ayllu-builder-1"></path>
4347- </g>
4348- <g transform="translate(909.5, 419)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-builder-1">
4349- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-builder-1">
4350- <text x="88.5" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-builder-1">ayllu-builder-1</text>
4351+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-builder-1">
4352+ <rect x="846.5" y="424.0" height="184.0" width="173.0" data-name="ayllu-builder-1"></rect>
4353+ <path d="M846.5 456.0 L1019.5 456.0" fill="none" data-name="ayllu-builder-1"></path>
4354+ </g>
4355+ <g transform="translate(846.5, 424)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-builder-1">
4356+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-builder-1">
4357+ <text x="78.5" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-builder-1">ayllu-builder-1</text>
4358
4359- </g>
4360 </g>
4361- <g transform="translate(909.5, 450)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-builder-1">
4362- <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-builder-1">
4363- <g transform="translate(20, 20)" data-name="ayllu-builder-1">
4364- <g data-name="qemu-x86_64">
4365- <g fill="#fdf6e3" stroke="#33322E" data-name="qemu-x86_64">
4366- <rect x="0.0" y="31.0" height="62.0" width="137.0" data-name="qemu-x86_64"></rect>
4367- <path d="M0.0 31.0 L0.0 0.0 L113.1 0.0 L113.1 31.0 Z" data-name="qemu-x86_64"></path>
4368- <path d="M0.0 31.0 L137.0 31.0" fill="none" data-name="qemu-x86_64"></path>
4369- <path d="M0.0 62.0 L137.0 62.0" fill="none" data-name="qemu-x86_64"></path>
4370- </g>
4371+ </g>
4372+ <g transform="translate(846.5, 456)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-builder-1">
4373+ <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-builder-1">
4374+ <g transform="translate(20, 20)" data-name="ayllu-builder-1">
4375+ <g data-name="qemu-x86_64">
4376+ <g fill="#fdf6e3" stroke="#33322E" data-name="qemu-x86_64">
4377+ <rect x="0.0" y="32.0" height="64.0" width="117.0" data-name="qemu-x86_64"></rect>
4378+ <path d="M0.0 32.0 L0.0 0.0 L113.1 0.0 L113.1 32.0 Z" data-name="qemu-x86_64"></path>
4379+ <path d="M0.0 32.0 L117.0 32.0" fill="none" data-name="qemu-x86_64"></path>
4380+ <path d="M0.0 64.0 L117.0 64.0" fill="none" data-name="qemu-x86_64"></path>
4381+ </g>
4382 <g transform="translate(0, 0)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-x86_64">
4383- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-x86_64">
4384- <text x="0.0" y="13.5" stroke="none" data-name="qemu-x86_64">qemu-x86_64</text>
4385+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-x86_64">
4386+ <text x="0.0" y="14.1" stroke="none" data-name="qemu-x86_64">qemu-x86_64</text>
4387
4388- </g>
4389 </g>
4390- <g transform="translate(0, 31)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-x86_64">
4391- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-x86_64">
4392- <text x="0.0" y="13.5" stroke="none" data-name="qemu-x86_64">capnp-rpc-server</text>
4393+ </g>
4394+ <g transform="translate(0, 32)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-x86_64">
4395+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-x86_64">
4396+ <text x="0.0" y="14.1" stroke="none" data-name="qemu-x86_64">tarpc-server</text>
4397
4398- </g>
4399 </g>
4400- <g transform="translate(0, 62)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-x86_64">
4401- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-x86_64">
4402- <text x="0.0" y="13.5" stroke="none" data-name="qemu-x86_64">executor</text>
4403+ </g>
4404+ <g transform="translate(0, 64)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-x86_64">
4405+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-x86_64">
4406+ <text x="0.0" y="14.1" stroke="none" data-name="qemu-x86_64">executor</text>
4407
4408- </g>
4409 </g>
4410 </g>
4411 </g>
4412 </g>
4413 </g>
4414 </g>
4415+ </g>
4416 <g data-name="ayllu-builder-2">
4417- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-builder-2">
4418- <rect x="1142.5" y="419.0" height="180.0" width="193.0" data-name="ayllu-builder-2"></rect>
4419- <path d="M1142.5 450.0 L1335.5 450.0" fill="none" data-name="ayllu-builder-2"></path>
4420- </g>
4421- <g transform="translate(1142.5, 419)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-builder-2">
4422- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-builder-2">
4423- <text x="88.5" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-builder-2">ayllu-builder-2</text>
4424+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-builder-2">
4425+ <rect x="1059.5" y="424.0" height="184.0" width="181.0" data-name="ayllu-builder-2"></rect>
4426+ <path d="M1059.5 456.0 L1240.5 456.0" fill="none" data-name="ayllu-builder-2"></path>
4427+ </g>
4428+ <g transform="translate(1059.5, 424)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-builder-2">
4429+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-builder-2">
4430+ <text x="82.5" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-builder-2">ayllu-builder-2</text>
4431
4432- </g>
4433 </g>
4434- <g transform="translate(1142.5, 450)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-builder-2">
4435- <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-builder-2">
4436- <g transform="translate(20, 20)" data-name="ayllu-builder-2">
4437- <g data-name="qemu-aarch64">
4438- <g fill="#fdf6e3" stroke="#33322E" data-name="qemu-aarch64">
4439- <rect x="0.0" y="31.0" height="62.0" width="137.0" data-name="qemu-aarch64"></rect>
4440- <path d="M0.0 31.0 L0.0 0.0 L118.9 0.0 L118.9 31.0 Z" data-name="qemu-aarch64"></path>
4441- <path d="M0.0 31.0 L137.0 31.0" fill="none" data-name="qemu-aarch64"></path>
4442- <path d="M0.0 62.0 L137.0 62.0" fill="none" data-name="qemu-aarch64"></path>
4443- </g>
4444+ </g>
4445+ <g transform="translate(1059.5, 456)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-builder-2">
4446+ <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-builder-2">
4447+ <g transform="translate(20, 20)" data-name="ayllu-builder-2">
4448+ <g data-name="qemu-aarch64">
4449+ <g fill="#fdf6e3" stroke="#33322E" data-name="qemu-aarch64">
4450+ <rect x="0.0" y="32.0" height="64.0" width="125.0" data-name="qemu-aarch64"></rect>
4451+ <path d="M0.0 32.0 L0.0 0.0 L118.9 0.0 L118.9 32.0 Z" data-name="qemu-aarch64"></path>
4452+ <path d="M0.0 32.0 L125.0 32.0" fill="none" data-name="qemu-aarch64"></path>
4453+ <path d="M0.0 64.0 L125.0 64.0" fill="none" data-name="qemu-aarch64"></path>
4454+ </g>
4455 <g transform="translate(0, 0)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-aarch64">
4456- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-aarch64">
4457- <text x="0.0" y="13.5" stroke="none" data-name="qemu-aarch64">qemu-aarch64</text>
4458+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-aarch64">
4459+ <text x="0.0" y="14.1" stroke="none" data-name="qemu-aarch64">qemu-aarch64</text>
4460
4461- </g>
4462 </g>
4463- <g transform="translate(0, 31)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-aarch64">
4464- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-aarch64">
4465- <text x="0.0" y="13.5" stroke="none" data-name="qemu-aarch64">capnp-rpc-server</text>
4466+ </g>
4467+ <g transform="translate(0, 32)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-aarch64">
4468+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-aarch64">
4469+ <text x="0.0" y="14.1" stroke="none" data-name="qemu-aarch64">tarpc-server</text>
4470
4471- </g>
4472 </g>
4473- <g transform="translate(0, 62)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-aarch64">
4474- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-aarch64">
4475- <text x="0.0" y="13.5" stroke="none" data-name="qemu-aarch64">exectuor</text>
4476+ </g>
4477+ <g transform="translate(0, 64)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-aarch64">
4478+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-aarch64">
4479+ <text x="0.0" y="14.1" stroke="none" data-name="qemu-aarch64">exectuor</text>
4480
4481- </g>
4482 </g>
4483 </g>
4484 </g>
4485 </g>
4486 </g>
4487 </g>
4488+ </g>
4489 <g data-name="ayllu-builder-n">
4490- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-builder-n">
4491- <rect x="1375.5" y="419.0" height="180.0" width="193.0" data-name="ayllu-builder-n"></rect>
4492- <path d="M1375.5 450.0 L1568.5 450.0" fill="none" data-name="ayllu-builder-n"></path>
4493- </g>
4494- <g transform="translate(1375.5, 419)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-builder-n">
4495- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-builder-n">
4496- <text x="88.5" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-builder-n">ayllu-builder-n</text>
4497+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-builder-n">
4498+ <rect x="1280.5" y="424.0" height="184.0" width="157.0" data-name="ayllu-builder-n"></rect>
4499+ <path d="M1280.5 456.0 L1437.5 456.0" fill="none" data-name="ayllu-builder-n"></path>
4500+ </g>
4501+ <g transform="translate(1280.5, 424)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-builder-n">
4502+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-builder-n">
4503+ <text x="70.5" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-builder-n">ayllu-builder-n</text>
4504
4505- </g>
4506 </g>
4507- <g transform="translate(1375.5, 450)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-builder-n">
4508- <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-builder-n">
4509- <g transform="translate(20, 20)" data-name="ayllu-builder-n">
4510- <g data-name="lxc">
4511- <g fill="#fdf6e3" stroke="#33322E" data-name="lxc">
4512- <rect x="0.0" y="31.0" height="62.0" width="137.0" data-name="lxc"></rect>
4513- <path d="M0.0 31.0 L0.0 0.0 L35.2 0.0 L35.2 31.0 Z" data-name="lxc"></path>
4514- <path d="M0.0 31.0 L137.0 31.0" fill="none" data-name="lxc"></path>
4515- <path d="M0.0 62.0 L137.0 62.0" fill="none" data-name="lxc"></path>
4516- </g>
4517+ </g>
4518+ <g transform="translate(1280.5, 456)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-builder-n">
4519+ <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-builder-n">
4520+ <g transform="translate(20, 20)" data-name="ayllu-builder-n">
4521+ <g data-name="lxc">
4522+ <g fill="#fdf6e3" stroke="#33322E" data-name="lxc">
4523+ <rect x="0.0" y="32.0" height="64.0" width="101.0" data-name="lxc"></rect>
4524+ <path d="M0.0 32.0 L0.0 0.0 L35.2 0.0 L35.2 32.0 Z" data-name="lxc"></path>
4525+ <path d="M0.0 32.0 L101.0 32.0" fill="none" data-name="lxc"></path>
4526+ <path d="M0.0 64.0 L101.0 64.0" fill="none" data-name="lxc"></path>
4527+ </g>
4528 <g transform="translate(0, 0)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="lxc">
4529- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="lxc">
4530- <text x="0.0" y="13.5" stroke="none" data-name="lxc">lxc</text>
4531+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="lxc">
4532+ <text x="0.0" y="14.1" stroke="none" data-name="lxc">lxc</text>
4533
4534- </g>
4535 </g>
4536- <g transform="translate(0, 31)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="lxc">
4537- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="lxc">
4538- <text x="0.0" y="13.5" stroke="none" data-name="lxc">capnp-rpc-server</text>
4539+ </g>
4540+ <g transform="translate(0, 32)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="lxc">
4541+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="lxc">
4542+ <text x="0.0" y="14.1" stroke="none" data-name="lxc">tarpc-server</text>
4543
4544- </g>
4545 </g>
4546- <g transform="translate(0, 62)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="lxc">
4547- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="lxc">
4548- <text x="0.0" y="13.5" stroke="none" data-name="lxc">executor</text>
4549+ </g>
4550+ <g transform="translate(0, 64)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="lxc">
4551+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="lxc">
4552+ <text x="0.0" y="14.1" stroke="none" data-name="lxc">executor</text>
4553
4554- </g>
4555 </g>
4556 </g>
4557 </g>
4558 @@ -595,4 +594,5 @@
4559 </g>
4560 </g>
4561 </g>
4562- </svg>
4563\ No newline at end of file
4564+ </g>
4565+ </svg>
4566 diff --git a/www/public/docs/architecture/index.html b/www/public/docs/architecture/index.html
4567index 25dbdfb..bfeb01a 100644
4568--- a/www/public/docs/architecture/index.html
4569+++ b/www/public/docs/architecture/index.html
4570 @@ -1 +1 @@
4571- <!doctype html><html lang=en><head><meta charset=utf-8><link href=/main.css rel=stylesheet><link href=/assets/ayllu_logo.svg rel=icon type=image/svg+xml><link href=/assets/ayllu_logo.png rel=icon sizes=any type=image/png><title>Ayllu</title></head><nav><ul><li><a href=/> <img class=logo src=/assets/ayllu_logo.png> </a></ul><ul><li><a href=https://ayllu-forge.org/browse>browse</a><li><a href=/docs>docs</a></ul></nav><body class=container><div class=wrapper><main class=page-body><div class=documentation><div class=side-panel><ul><li class=active><a href=https://ayllu-forge.org/docs/architecture/>Architecture</a><li><a href=https://ayllu-forge.org/docs/builds/>Builds</a><li><a href=https://ayllu-forge.org/docs/configuration/>Configuration</a><li><a href=https://ayllu-forge.org/docs/developers/>Developers</a><li><a href=https://ayllu-forge.org/docs/installation/>Installation</a><li><a href=https://ayllu-forge.org/docs/faq/>FAQ</a><li><a href=https://ayllu-forge.org/docs/mail/>Mail</a></ul></div><div class=doc-content><h1>Architecture</h1><p><strong>Ayllu is a new software project and it's design is evolving.</strong><p>It's basic architecture is that of a single binary providing a web interface to browse and interact with Git repositories as well as an RPC server based on <a href=https://capnproto.org/>Cap'n Proto</a> with an embedded client. The RPC server running in the core Ayllu binary is called the <code>job_server</code> and it provides an interface to interact with Ayllu for administrators. Much of the functionality of Ayllu is implemented via <code>RPC extensions</code> which are minimal binaries that are used to extend the core functionality of Ayllu.<p>The current architecture of Ayllu is depicted below:<p><a href=/assets/architecture.svg><img class=diagram src=/assets/architecture.svg></a></div></div></main><footer class=page-footer>2024, <a href=/projects/ayllu/blob/main/ATTRIBUTIONS.md>attributions</a></footer></div>
4572\ No newline at end of file
4573+ <!doctype html><html lang=en><head><meta charset=utf-8><link href=/main.css rel=stylesheet><link href=/assets/ayllu_logo.svg rel=icon type=image/svg+xml><link href=/assets/ayllu_logo.png rel=icon sizes=any type=image/png><title>Ayllu</title></head><nav><ul><li><a href=/> <img class=logo src=/assets/ayllu_logo.png> </a></ul><ul><li><a href=https://ayllu-forge.org/browse>browse</a><li><a href=/docs>docs</a></ul></nav><body class=container><div class=wrapper><main class=page-body><div class=documentation><div class=side-panel><ul><li class=active><a href=https://ayllu-forge.org/docs/architecture/>Architecture</a><li><a href=https://ayllu-forge.org/docs/builds/>Builds</a><li><a href=https://ayllu-forge.org/docs/configuration/>Configuration</a><li><a href=https://ayllu-forge.org/docs/developers/>Developers</a><li><a href=https://ayllu-forge.org/docs/installation/>Installation</a><li><a href=https://ayllu-forge.org/docs/faq/>FAQ</a><li><a href=https://ayllu-forge.org/docs/mail/>Mail</a></ul></div><div class=doc-content><h1>Architecture</h1><p><strong>Ayllu is a new software project and it's design is evolving.</strong><p>It's basic architecture is that of a single binary providing a web interface to browse and interact with Git repositories as well as an RPC server based on <a href=https://github.com/google/tarpc>tarpc</a> with an embedded client. The RPC server running in the core Ayllu binary is called the <code>job_server</code> and it provides an interface to interact with Ayllu for administrators. Much of the functionality of Ayllu is implemented via <code>RPC extensions</code> which are minimal binaries that are used to extend the core functionality of Ayllu.<p>The current architecture of Ayllu is depicted below:<p><a href=/assets/architecture.svg><img class=diagram src=/assets/architecture.svg></a></div></div></main><footer class=page-footer>2024, <a href=/projects/ayllu/blob/main/ATTRIBUTIONS.md>attributions</a></footer></div>
4574\ No newline at end of file
4575 diff --git a/www/public/index.html b/www/public/index.html
4576index 1ab90ef..0188f0a 100644
4577--- a/www/public/index.html
4578+++ b/www/public/index.html
4579 @@ -1 +1 @@
4580- <!doctype html><html lang=en><head><meta charset=utf-8><link href=/main.css rel=stylesheet><link href=/assets/ayllu_logo.svg rel=icon type=image/svg+xml><link href=/assets/ayllu_logo.png rel=icon sizes=any type=image/png><title>Ayllu</title></head><nav><ul><li><a href=/> <img class=logo src=/assets/ayllu_logo.png> </a></ul><ul><li><a href=https://ayllu-forge.org/browse>browse</a><li><a href=/docs>docs</a></ul></nav><body class=container><div class=wrapper><main class=page-body><section class=home-header><div class=blurb><h1 id=hyper-performant-hackable-code-forge-built-on-open-standards>Hyper Performant & Hackable Code Forge Built on Open Standards</h1><p>Ayllu is a lightweight code forge designed to enable individuals and community projects develop software in collaboration across open internet standards.</p><a href=https://ayllu-forge.org/projects/ayllu> <button>source</button> </a></div><div class=screenshot><a href=https://ayllu-forge.org/projects/ayllu> <img class="screenshot night" src=/assets/images/ui.png></a><img class="screenshot day" src=/assets/images/ui-day.png></div></section><div class=home-wrapper><article><h4 id=general-purpose-git-ui>General Purpose Git UI</h4><p>General purpose web based UI for git. Browse hosted projects, view contributors, source code, blame, releases, etc.</article><article><h4 id=source-code-analysis>Source Code Analysis</h4><p>Super accurate syntax highlighting and code composition insight across project history.</article><article><h4 id=incremental-jobs>Incremental Jobs</h4><p>A built-in configurable incremental jobs system controlled via Git hooks and/or cron to extend forge functionality.</article><article><h4 id=extensible-plugin-system>Extensible Plugin System</h4><p>An extensible CapnProto based plugin system allows adding large external integrations via RPC calls.</article><article><h4 id=completely-themable>Completely Themable</h4><p>The web interface is fully customizable, a few themes <a href=https://ayllu-forge.org/config>already</a> exist and more are on the way.</article><article><h4 id=web-1-5>Web 1.5</h4><p>Super fast web frontend with first class support for RSS based project subscriptions. Ayllu loads faster than any other code forge in existence. <em>If it doesn't, it's a bug!</em></article><article><h4 id=static-hosting>Static Hosting</h4><p>Serve static websites directly from a git repository making project sites, documentation, or other assets simple to self-host.</article><article><h4 id=mailing-list-management>Mailing List Management</h4><p>Full featured email based software development workflows and mailing list management based on <a href=https://git.meli-email.org/meli/mailpot>Mailpot 🍯</a>.</article><article><h4 id=much-more>Much More</h4><p>New features planned such as continuous integration, mailing list support, external API, federation, and more.</article></div></main><footer class=page-footer>2024, <a href=/projects/ayllu/blob/main/ATTRIBUTIONS.md>attributions</a></footer></div>
4581\ No newline at end of file
4582+ <!doctype html><html lang=en><head><meta charset=utf-8><link href=/main.css rel=stylesheet><link href=/assets/ayllu_logo.svg rel=icon type=image/svg+xml><link href=/assets/ayllu_logo.png rel=icon sizes=any type=image/png><title>Ayllu</title></head><nav><ul><li><a href=/> <img class=logo src=/assets/ayllu_logo.png> </a></ul><ul><li><a href=https://ayllu-forge.org/browse>browse</a><li><a href=/docs>docs</a></ul></nav><body class=container><div class=wrapper><main class=page-body><section class=home-header><div class=blurb><h1 id=hyper-performant-hackable-code-forge-built-on-open-standards>Hyper Performant & Hackable Code Forge Built on Open Standards</h1><p>Ayllu is a lightweight code forge designed to enable individuals and community projects develop software in collaboration across open internet standards.</p><a href=https://ayllu-forge.org/projects/ayllu> <button>source</button> </a></div><div class=screenshot><a href=https://ayllu-forge.org/projects/ayllu> <img class="screenshot night" src=/assets/images/ui.png></a><img class="screenshot day" src=/assets/images/ui-day.png></div></section><div class=home-wrapper><article><h4 id=general-purpose-git-ui>General Purpose Git UI</h4><p>General purpose web based UI for git. Browse hosted projects, view contributors, source code, blame, releases, etc.</article><article><h4 id=source-code-analysis>Source Code Analysis</h4><p>Super accurate syntax highlighting and code composition insight across project history.</article><article><h4 id=incremental-jobs>Incremental Jobs</h4><p>A built-in configurable incremental jobs system controlled via Git hooks and/or cron to extend forge functionality.</article><article><h4 id=extensible-plugin-system>Extensible Plugin System</h4><p>An extensible RPC based plugin system allows adding large external integrations.</article><article><h4 id=completely-themable>Completely Themable</h4><p>The web interface is fully customizable, a few themes <a href=https://ayllu-forge.org/config>already</a> exist and more are on the way.</article><article><h4 id=web-1-5>Web 1.5</h4><p>Super fast web frontend with first class support for RSS based project subscriptions. Ayllu loads faster than any other code forge in existence. <em>If it doesn't, it's a bug!</em></article><article><h4 id=static-hosting>Static Hosting</h4><p>Serve static websites directly from a git repository making project sites, documentation, or other assets simple to self-host.</article><article><h4 id=mailing-list-management>Mailing List Management</h4><p>Full featured email based software development workflows and mailing list management based on <a href=https://git.meli-email.org/meli/mailpot>Mailpot 🍯</a>.</article><article><h4 id=much-more>Much More</h4><p>New features planned such as continuous integration, mailing list support, external API, federation, and more.</article></div></main><footer class=page-footer>2024, <a href=/projects/ayllu/blob/main/ATTRIBUTIONS.md>attributions</a></footer></div>
4583\ No newline at end of file
4584 diff --git a/www/static/assets/architecture.svg b/www/static/assets/architecture.svg
4585index 2de2a37..e05c148 100644
4586--- a/www/static/assets/architecture.svg
4587+++ b/www/static/assets/architecture.svg
4588 @@ -1,35 +1,34 @@
4589- <svg version="1.1" baseProfile="full" width="1938.0" height="655.0" viewbox="0 0 1938 655" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events">
4590- <desc >
4591- [reverse_proxy]
4592+ <svg version="1.1" baseProfile="full" width="1817.5" height="664.0" viewBox="0 0 1817.5 664" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events">
4593+ <desc >[reverse_proxy]
4594
4595 [reverse_proxy]&lt;--&gt;[Rudolfs (LFS API)]
4596
4597 [reverse_proxy]&lt;--&gt;[ayllu-core |
4598- [axum http interface]
4599+ [axum http interface]
4600 [sites static hosting]
4601- [capnp-rpc-server (job_server)]
4602+ [tarpc-server (job_server)]
4603 [git repositories via libgit2]
4604 [&lt;database&gt; sqlite]
4605 ]
4606
4607 [ayllu-core] &lt;--&gt; [
4608 ayllu-mail|
4609- [capnp-rpc-server]
4610+ [tarpc-server]
4611 [mailpot]
4612 [postfix]
4613 [&lt;database&gt; sqlite]
4614 ] &lt;-- SMTP [&lt;actor&gt; contributor-2]
4615
4616 [ayllu-core] &lt;--&gt; [
4617- ayllu-xmpp |
4618- [capnp-rpc-server]
4619- [chat interface/bot]
4620+ ayllu-xmpp |
4621+ [tarpc-server]
4622+ [chat interface/bot]
4623 [&lt;database&gt; sqlite]
4624- ] &lt;-- XMPP [&lt;actor&gt; contributor-2]
4625+ ] &lt;-- XMPP [&lt;actor&gt;; contributor-2]
4626
4627 [ayllu-core] &lt;--&gt; [
4628- ayllu-build |
4629- [capnp-rpc-server]
4630+ ayllu-build |
4631+ [tarpc-server]
4632 [&lt;database&gt; sqlite]
4633 ]
4634
4635 @@ -38,553 +37,553 @@
4636
4637
4638 [ayllu-build]&lt;--&gt;[
4639- ayllu-builder-1 |
4640- [&lt;package&gt;qemu-x86_64| capnp-rpc-server | executor]
4641+ ayllu-builder-1 |
4642+ [&lt;package&gt;qemu-x86_64| tarpc-server | executor]
4643 ]
4644
4645 [ayllu-build]&lt;--&gt;[
4646- ayllu-builder-2 |
4647- [&lt;package&gt;qemu-aarch64| capnp-rpc-server | exectuor]
4648+ ayllu-builder-2 |
4649+ [&lt;package&gt;qemu-aarch64| tarpc-server | exectuor]
4650 ]
4651
4652 [ayllu-build]&lt;--&gt;[
4653- ayllu-builder-n |
4654- [&lt;package&gt;lxc| capnp-rpc-server | executor]
4655- ] --&gt; [ayllu-core]</desc>
4656+ ayllu-builder-n |
4657+ [&lt;package&gt;lxc| tarpc-server | executor]
4658+ ] --&gt; [ayllu-core]
4659+ </desc>
4660 <g stroke-width="1.0" text-align="left" font="12pt Helvetica, Arial, sans-serif" font-size="12pt" font-family="Helvetica" font-weight="normal" font-style="normal">
4661- <g font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" stroke-width="3.0" stroke-linejoin="round" stroke-linecap="round" stroke="#33322E">
4662- <g stroke="transparent" fill="transparent">
4663- <rect x="0.0" y="0.0" height="655.0" width="1938.0" stroke="none"></rect>
4664- </g>
4665+ <g font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" stroke-width="3.0" stroke-linejoin="round" stroke-linecap="round" stroke="#33322E">
4666+ <g stroke="transparent" fill="transparent">
4667+ <rect x="0.0" y="0.0" height="664.0" width="1817.5" stroke="none"></rect>
4668+ </g>
4669 <g transform="translate(8, 8)" fill="#33322E">
4670- <g transform="translate(20, 20)" fill="#33322E" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal">
4671- <g stroke-dasharray="6 6">
4672- <path d="M965.1 23.1 L713 51 L713 115.83333333333333 L713.0 115.8 " fill="none"></path>
4673- </g>
4674- <path d="M707.7 109.2 L713.0 115.8 L718.3 109.2 L713.0 122.5 Z"></path>
4675- <path d="M959.1 29.2 L965.1 23.1 L957.9 18.6 L971.8 22.4 Z"></path>
4676+ <g transform="translate(20, 20)" fill="#33322E" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal">
4677 <g stroke-dasharray="6 6">
4678- <path d="M1103.4 23.1 L1355.5 51 L1355.5 64.33333333333333 L1355.5 64.3 " fill="none"></path>
4679- </g>
4680- <path d="M1350.2 57.7 L1355.5 64.3 L1360.8 57.7 L1355.5 71.0 Z"></path>
4681- <path d="M1110.6 18.6 L1103.4 23.1 L1109.4 29.2 L1096.8 22.4 Z"></path>
4682+ <path d="M873.9 24.0 L631.5 52 L631.5 117.33333333333333 L631.5 117.3 " fill="none"></path>
4683+ </g>
4684+ <path d="M626.2 110.7 L631.5 117.3 L636.8 110.7 L631.5 124.0 Z"></path>
4685+ <path d="M867.9 30.1 L873.9 24.0 L866.6 19.5 L880.5 23.2 Z"></path>
4686 <g stroke-dasharray="6 6">
4687- <path d="M822.4 180.4 L261 225 L261 238.33333333333334 L261.0 238.3 " fill="none"></path>
4688- </g>
4689- <path d="M255.7 231.7 L261.0 238.3 L266.3 231.7 L261.0 245.0 Z"></path>
4690- <path d="M816.1 186.2 L822.4 180.4 L815.3 175.6 L829.0 179.9 Z"></path>
4691- <text x="758.0" y="489.4" stroke="none">SMTP</text>
4692+ <path d="M1012.1 24.0 L1254.5 52 L1254.5 65.33333333333333 L1254.5 65.3 " fill="none"></path>
4693+ </g>
4694+ <path d="M1249.2 58.7 L1254.5 65.3 L1259.8 58.7 L1254.5 72.0 Z"></path>
4695+ <path d="M1019.4 19.5 L1012.1 24.0 L1018.1 30.1 L1005.5 23.2 Z"></path>
4696 <g stroke-dasharray="6 6">
4697- <path d="M261.0 385.7 L261 399 L750 497.4263494967978 L750.0 497.4 " fill="none"></path>
4698- </g>
4699- <path d="M266.3 392.3 L261.0 385.7 L255.7 392.3 L261.0 379.0 Z"></path>
4700+ <path d="M740.9 184.6 L242 228 L242 241.33333333333334 L242.0 241.3 " fill="none"></path>
4701+ </g>
4702+ <path d="M236.7 234.7 L242.0 241.3 L247.3 234.7 L242.0 248.0 Z"></path>
4703+ <path d="M734.7 190.5 L740.9 184.6 L733.8 179.9 L747.5 184.1 Z"></path>
4704+ <text x="698.3" y="495.3" stroke="none">SMTP</text>
4705 <g stroke-dasharray="6 6">
4706- <path d="M926.9 206.0 L807.5 225 L807.5 238.33333333333334 L807.5 238.3 " fill="none"></path>
4707- </g>
4708- <path d="M802.2 231.7 L807.5 238.3 L812.8 231.7 L807.5 245.0 Z"></path>
4709- <path d="M921.1 212.4 L926.9 206.0 L919.5 201.8 L933.5 205.0 Z"></path>
4710- <text x="754.7" y="473.5" stroke="none">XMPP</text>
4711+ <path d="M242.0 390.7 L242 404 L690.25 503.2664359861592 L690.3 503.3 " fill="none"></path>
4712+ </g>
4713+ <path d="M247.3 397.3 L242.0 390.7 L236.7 397.3 L242.0 384.0 Z"></path>
4714 <g stroke-dasharray="6 6">
4715- <path d="M807.5 385.7 L807.5 399 L807.5 481.5 L807.5 481.5 " fill="none"></path>
4716- </g>
4717- <path d="M812.8 392.3 L807.5 385.7 L802.2 392.3 L807.5 379.0 Z"></path>
4718+ <path d="M858.5 209.1 L750.5 228 L750.5 241.33333333333334 L750.5 241.3 " fill="none"></path>
4719+ </g>
4720+ <path d="M745.2 234.7 L750.5 241.3 L755.8 234.7 L750.5 248.0 Z"></path>
4721+ <path d="M852.8 215.5 L858.5 209.1 L851.0 205.0 L865.0 208.0 Z"></path>
4722+ <text x="695.6" y="480.0" stroke="none">XMPP</text>
4723 <g stroke-dasharray="6 6">
4724- <path d="M1276.2 209.5 L1259 225 L1259 238.33333333333334 L1259.0 238.3 " fill="none"></path>
4725- </g>
4726- <path d="M1253.7 231.7 L1259.0 238.3 L1264.3 231.7 L1259.0 245.0 Z"></path>
4727- <path d="M1274.9 217.9 L1276.2 209.5 L1267.7 210.0 L1281.2 205.0 Z"></path>
4728+ <path d="M750.5 390.7 L750.5 404 L748.4375 488 L748.4 488.0 " fill="none"></path>
4729+ </g>
4730+ <path d="M755.8 397.3 L750.5 390.7 L745.2 397.3 L750.5 384.0 Z"></path>
4731 <g stroke-dasharray="6 6">
4732- <path d="M1595.6 206.8 L1659 225 L1659 289.8333333333333 L1659.0 289.8 " fill="none"></path>
4733- </g>
4734- <path d="M1653.7 283.2 L1659.0 289.8 L1664.3 283.2 L1659.0 296.5 Z"></path>
4735- <path d="M1603.5 203.5 L1595.6 206.8 L1600.6 213.8 L1589.2 205.0 Z"></path>
4736- <text x="1618.3" y="473.5" stroke="none">SSH</text>
4737+ <path d="M1183.6 212.8 L1168.75 228 L1168.75 241.33333333333334 L1168.8 241.3 " fill="none"></path>
4738+ </g>
4739+ <path d="M1163.4 234.7 L1168.8 241.3 L1174.1 234.7 L1168.8 248.0 Z"></path>
4740+ <path d="M1182.8 221.3 L1183.6 212.8 L1175.1 213.8 L1188.2 208.0 Z"></path>
4741 <g stroke-dasharray="6 6">
4742- <path d="M1659.0 334.2 L1659 399 L1659 481.5 L1659.0 481.5 " fill="none"></path>
4743- </g>
4744- <path d="M1664.3 340.8 L1659.0 334.2 L1653.7 340.8 L1659.0 327.5 Z"></path>
4745+ <path d="M1472.2 210.0 L1528 228 L1528 293.3333333333333 L1528.0 293.3 " fill="none"></path>
4746+ </g>
4747+ <path d="M1522.7 286.7 L1528.0 293.3 L1533.3 286.7 L1528.0 300.0 Z"></path>
4748+ <path d="M1480.2 207.0 L1472.2 210.0 L1476.9 217.2 L1465.8 208.0 Z"></path>
4749+ <text x="1487.3" y="480.0" stroke="none">SSH</text>
4750 <g stroke-dasharray="6 6">
4751- <path d="M1691.6 206.3 L1783.5 225 L1783.5 289.8333333333333 L1783.5 289.8 " fill="none"></path>
4752- </g>
4753- <path d="M1778.2 283.2 L1783.5 289.8 L1788.8 283.2 L1783.5 296.5 Z"></path>
4754- <path d="M1699.2 202.4 L1691.6 206.3 L1697.1 212.9 L1685.1 205.0 Z"></path>
4755+ <path d="M1528.0 338.7 L1528 404 L1528 488 L1528.0 488.0 " fill="none"></path>
4756+ </g>
4757+ <path d="M1533.3 345.3 L1528.0 338.7 L1522.7 345.3 L1528.0 332.0 Z"></path>
4758 <g stroke-dasharray="6 6">
4759- <path d="M1783.5 334.2 L1783.5 399 L1783.5 481.5 L1783.5 481.5 " fill="none"></path>
4760- </g>
4761- <path d="M1788.8 340.8 L1783.5 334.2 L1778.2 340.8 L1783.5 327.5 Z"></path>
4762+ <path d="M1568.6 209.4 L1652.5 228 L1652.5 293.3333333333333 L1652.5 293.3 " fill="none"></path>
4763+ </g>
4764+ <path d="M1647.2 286.7 L1652.5 293.3 L1657.8 286.7 L1652.5 300.0 Z"></path>
4765+ <path d="M1576.2 205.7 L1568.6 209.4 L1573.9 216.1 L1562.0 208.0 Z"></path>
4766 <g stroke-dasharray="6 6">
4767- <path d="M1102.2 365.9 L1006 399 L1006 412.3333333333333 L1006.0 412.3 " fill="none"></path>
4768- </g>
4769- <path d="M1000.7 405.7 L1006.0 412.3 L1011.3 405.7 L1006.0 419.0 Z"></path>
4770- <path d="M1097.6 373.1 L1102.2 365.9 L1094.2 363.0 L1108.5 363.8 Z"></path>
4771+ <path d="M1652.5 338.7 L1652.5 404 L1652.5 488 L1652.5 488.0 " fill="none"></path>
4772+ </g>
4773+ <path d="M1657.8 345.3 L1652.5 338.7 L1647.2 345.3 L1652.5 332.0 Z"></path>
4774 <g stroke-dasharray="6 6">
4775- <path d="M1242.1 385.5 L1239 399 L1239 412.3333333333333 L1239.0 412.3 " fill="none"></path>
4776- </g>
4777- <path d="M1233.7 405.7 L1239.0 412.3 L1244.3 405.7 L1239.0 419.0 Z"></path>
4778- <path d="M1245.8 393.2 L1242.1 385.5 L1235.4 390.8 L1243.6 379.0 Z"></path>
4779+ <path d="M1031.0 367.4 L933 404 L933 417.3333333333333 L933.0 417.3 " fill="none"></path>
4780+ </g>
4781+ <path d="M927.7 410.7 L933.0 417.3 L938.3 410.7 L933.0 424.0 Z"></path>
4782+ <path d="M1026.6 374.7 L1031.0 367.4 L1022.9 364.8 L1037.3 365.1 Z"></path>
4783 <g stroke-dasharray="6 6">
4784- <path d="M1354.1 383.0 L1375.5 399 L1388.6489765726408 413.9884707045646 L1388.6 414.0 " fill="none"></path>
4785- </g>
4786- <path d="M1380.2 412.5 L1388.6 414.0 L1388.3 405.5 L1393.0 419.0 Z"></path>
4787- <path d="M1362.6 382.7 L1354.1 383.0 L1356.2 391.3 L1348.7 379.0 Z"></path>
4788+ <path d="M1152.9 390.5 L1150 404 L1150 417.3333333333333 L1150.0 417.3 " fill="none"></path>
4789+ </g>
4790+ <path d="M1144.7 410.7 L1150.0 417.3 L1155.3 410.7 L1150.0 424.0 Z"></path>
4791+ <path d="M1156.7 398.2 L1152.9 390.5 L1146.3 395.9 L1154.3 384.0 Z"></path>
4792 <g stroke-dasharray="6 6">
4793- <path d="M1512.9 419.0 L1522 399 L1522 312 L1522 312 L1522 225 L1522 225 L1489.6328041618644 208.08741118367695 L1489.6 208.1 " fill="none"></path>
4794- </g>
4795- <path d="M1498.0 206.4 L1489.6 208.1 L1493.1 215.9 L1483.7 205.0 Z"></path>
4796+ <path d="M1255.6 388.3 L1274.5 404 L1285.5740965207704 418.6780924298968 L1285.6 418.7 " fill="none"></path>
4797+ </g>
4798+ <path d="M1277.3 416.6 L1285.6 418.7 L1285.8 410.1 L1289.6 424.0 Z"></path>
4799+ <path d="M1264.1 388.4 L1255.6 388.3 L1257.3 396.6 L1250.5 384.0 Z"></path>
4800+ <g stroke-dasharray="6 6">
4801+ <path d="M1392.7 424.0 L1400 404 L1400 316 L1400 316 L1400 228 L1400 228 L1372.6362943277163 211.45012990267378 L1372.6 211.5 " fill="none"></path>
4802+ </g>
4803+ <path d="M1381.1 210.3 L1372.6 211.5 L1375.6 219.5 L1366.9 208.0 Z"></path>
4804 <g data-name="reverse_proxy">
4805- <g fill="#eee8d5" stroke="#33322E" data-name="reverse_proxy">
4806- <rect x="971.8" y="0.0" height="31.0" width="125.0" data-name="reverse_proxy"></rect>
4807- </g>
4808- <g transform="translate(971.75, 0)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="reverse_proxy">
4809- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="reverse_proxy">
4810- <text x="54.5" y="13.5" stroke="none" text-anchor="middle" data-name="reverse_proxy">reverse_proxy</text>
4811+ <g fill="#eee8d5" stroke="#33322E" data-name="reverse_proxy">
4812+ <rect x="880.5" y="0.0" height="32.0" width="125.0" data-name="reverse_proxy"></rect>
4813+ </g>
4814+ <g transform="translate(880.5, 0)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="reverse_proxy">
4815+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="reverse_proxy">
4816+ <text x="54.5" y="14.1" stroke="none" text-anchor="middle" data-name="reverse_proxy">reverse_proxy</text>
4817
4818- </g>
4819 </g>
4820 </g>
4821+ </g>
4822 <g data-name="Rudolfs (LFS API)">
4823- <g fill="#eee8d5" stroke="#33322E" data-name="Rudolfs (LFS API)">
4824- <rect x="637.0" y="122.5" height="31.0" width="152.0" data-name="Rudolfs (LFS API)"></rect>
4825- </g>
4826- <g transform="translate(637, 122.5)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="Rudolfs (LFS API)">
4827- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="Rudolfs (LFS API)">
4828- <text x="68.0" y="13.5" stroke="none" text-anchor="middle" data-name="Rudolfs (LFS API)">Rudolfs (LFS API)</text>
4829+ <g fill="#eee8d5" stroke="#33322E" data-name="Rudolfs (LFS API)">
4830+ <rect x="555.5" y="124.0" height="32.0" width="152.0" data-name="Rudolfs (LFS API)"></rect>
4831+ </g>
4832+ <g transform="translate(555.5, 124)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="Rudolfs (LFS API)">
4833+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="Rudolfs (LFS API)">
4834+ <text x="68.0" y="14.1" stroke="none" text-anchor="middle" data-name="Rudolfs (LFS API)">Rudolfs (LFS API)</text>
4835
4836- </g>
4837 </g>
4838 </g>
4839+ </g>
4840 <g data-name="ayllu-core">
4841- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-core">
4842- <rect x="829.0" y="71.0" height="134.0" width="1053.0" data-name="ayllu-core"></rect>
4843- <path d="M829.0 102.0 L1882.0 102.0" fill="none" data-name="ayllu-core"></path>
4844- </g>
4845- <g transform="translate(829, 71)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-core">
4846- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-core">
4847- <text x="518.5" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-core">ayllu-core</text>
4848+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-core">
4849+ <rect x="747.5" y="72.0" height="136.0" width="1014.0" data-name="ayllu-core"></rect>
4850+ <path d="M747.5 104.0 L1761.5 104.0" fill="none" data-name="ayllu-core"></path>
4851+ </g>
4852+ <g transform="translate(747.5, 72)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-core">
4853+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-core">
4854+ <text x="499.0" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-core">ayllu-core</text>
4855
4856- </g>
4857 </g>
4858- <g transform="translate(829, 102)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-core">
4859- <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-core">
4860- <g transform="translate(20, 20)" data-name="ayllu-core">
4861- <g data-name="axum http interface">
4862- <g fill="#fdf6e3" stroke="#33322E" data-name="axum http interface">
4863- <rect x="0.0" y="8.0" height="31.0" width="164.0" data-name="axum http interface"></rect>
4864- </g>
4865+ </g>
4866+ <g transform="translate(747.5, 104)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-core">
4867+ <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-core">
4868+ <g transform="translate(20, 20)" data-name="ayllu-core">
4869+ <g data-name="axum http interface">
4870+ <g fill="#fdf6e3" stroke="#33322E" data-name="axum http interface">
4871+ <rect x="0.0" y="8.0" height="32.0" width="164.0" data-name="axum http interface"></rect>
4872+ </g>
4873 <g transform="translate(0, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="axum http interface">
4874- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="axum http interface">
4875- <text x="74.0" y="13.5" stroke="none" text-anchor="middle" data-name="axum http interface">axum http interface</text>
4876+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="axum http interface">
4877+ <text x="74.0" y="14.1" stroke="none" text-anchor="middle" data-name="axum http interface">axum http interface</text>
4878
4879- </g>
4880 </g>
4881 </g>
4882+ </g>
4883 <g data-name="sites static hosting">
4884- <g fill="#fdf6e3" stroke="#33322E" data-name="sites static hosting">
4885- <rect x="204.0" y="8.0" height="31.0" width="161.0" data-name="sites static hosting"></rect>
4886- </g>
4887+ <g fill="#fdf6e3" stroke="#33322E" data-name="sites static hosting">
4888+ <rect x="204.0" y="8.0" height="32.0" width="161.0" data-name="sites static hosting"></rect>
4889+ </g>
4890 <g transform="translate(204, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sites static hosting">
4891- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sites static hosting">
4892- <text x="72.5" y="13.5" stroke="none" text-anchor="middle" data-name="sites static hosting">sites static hosting</text>
4893+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sites static hosting">
4894+ <text x="72.5" y="14.1" stroke="none" text-anchor="middle" data-name="sites static hosting">sites static hosting</text>
4895
4896- </g>
4897 </g>
4898 </g>
4899- <g data-name="capnp-rpc-server (job_server)">
4900- <g fill="#fdf6e3" stroke="#33322E" data-name="capnp-rpc-server (job_server)">
4901- <rect x="405.0" y="8.0" height="31.0" width="244.0" data-name="capnp-rpc-server (job_server)"></rect>
4902- </g>
4903- <g transform="translate(405, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="capnp-rpc-server (job_server)">
4904- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="capnp-rpc-server (job_server)">
4905- <text x="114.0" y="13.5" stroke="none" text-anchor="middle" data-name="capnp-rpc-server (job_server)">capnp-rpc-server (job_server)</text>
4906+ </g>
4907+ <g data-name="tarpc-server (job_server)">
4908+ <g fill="#fdf6e3" stroke="#33322E" data-name="tarpc-server (job_server)">
4909+ <rect x="405.0" y="8.0" height="32.0" width="205.0" data-name="tarpc-server (job_server)"></rect>
4910+ </g>
4911+ <g transform="translate(405, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="tarpc-server (job_server)">
4912+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="tarpc-server (job_server)">
4913+ <text x="94.5" y="14.1" stroke="none" text-anchor="middle" data-name="tarpc-server (job_server)">tarpc-server (job_server)</text>
4914
4915- </g>
4916 </g>
4917 </g>
4918+ </g>
4919 <g data-name="git repositories via libgit2">
4920- <g fill="#fdf6e3" stroke="#33322E" data-name="git repositories via libgit2">
4921- <rect x="689.0" y="8.0" height="31.0" width="210.0" data-name="git repositories via libgit2"></rect>
4922- </g>
4923- <g transform="translate(689, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="git repositories via libgit2">
4924- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="git repositories via libgit2">
4925- <text x="97.0" y="13.5" stroke="none" text-anchor="middle" data-name="git repositories via libgit2">git repositories via libgit2</text>
4926+ <g fill="#fdf6e3" stroke="#33322E" data-name="git repositories via libgit2">
4927+ <rect x="650.0" y="8.0" height="32.0" width="210.0" data-name="git repositories via libgit2"></rect>
4928+ </g>
4929+ <g transform="translate(650, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="git repositories via libgit2">
4930+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="git repositories via libgit2">
4931+ <text x="97.0" y="14.1" stroke="none" text-anchor="middle" data-name="git repositories via libgit2">git repositories via libgit2</text>
4932
4933- </g>
4934 </g>
4935 </g>
4936+ </g>
4937 <g data-name="sqlite">
4938- <g fill="#fdf6e3" stroke="#33322E" data-name="sqlite">
4939- <rect x="939.0" y="8.0" height="31.0" width="58.0" stroke="none" data-name="sqlite"></rect>
4940- <path d="M939.0 8.0 L939.0 39.0" fill="none" data-name="sqlite"></path>
4941- <path d="M997.0 8.0 L997.0 39.0" fill="none" data-name="sqlite"></path>
4942- <ellipse cx="968.0" cy="8.0" rx="29.0" ry="6.0" data-name="sqlite"></ellipse>
4943- <path d="M997.0 39.0 L997.0 39.3 L996.9 39.6 L996.7 39.9 L996.4 40.2 L996.1 40.5 L995.7 40.8 L995.3 41.1 L994.7 41.3 L994.1 41.6 L993.5 41.9 L992.7 42.1 L992.0 42.4 L991.1 42.6 L990.2 42.9 L989.3 43.1 L988.2 43.3 L987.2 43.5 L986.1 43.7 L984.9 43.9 L983.7 44.0 L982.5 44.2 L981.2 44.3 L979.9 44.5 L978.6 44.6 L977.2 44.7 L975.9 44.8 L974.5 44.8 L973.0 44.9 L971.6 45.0 L970.2 45.0 L968.7 45.0 L967.3 45.0 L965.8 45.0 L964.4 45.0 L963.0 44.9 L961.5 44.8 L960.1 44.8 L958.8 44.7 L957.4 44.6 L956.1 44.5 L954.8 44.3 L953.5 44.2 L952.3 44.0 L951.1 43.9 L949.9 43.7 L948.8 43.5 L947.8 43.3 L946.7 43.1 L945.8 42.9 L944.9 42.6 L944.0 42.4 L943.3 42.1 L942.5 41.9 L941.9 41.6 L941.3 41.3 L940.7 41.1 L940.3 40.8 L939.9 40.5 L939.6 40.2 L939.3 39.9 L939.1 39.6 L939.0 39.3 L939.0 39.0" data-name="sqlite"></path>
4944- </g>
4945- <g transform="translate(939, 12)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sqlite">
4946- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sqlite">
4947- <text x="21.0" y="13.5" stroke="none" text-anchor="middle" data-name="sqlite">sqlite</text>
4948+ <g fill="#fdf6e3" stroke="#33322E" data-name="sqlite">
4949+ <rect x="900.0" y="8.0" height="32.0" width="58.0" stroke="none" data-name="sqlite"></rect>
4950+ <path d="M900.0 8.0 L900.0 40.0" fill="none" data-name="sqlite"></path>
4951+ <path d="M958.0 8.0 L958.0 40.0" fill="none" data-name="sqlite"></path>
4952+ <ellipse cx="929.0" cy="8.0" rx="29.0" ry="6.0" data-name="sqlite"></ellipse>
4953+ <path d="M958.0 40.0 L958.0 40.3 L957.9 40.6 L957.7 40.9 L957.4 41.2 L957.1 41.5 L956.7 41.8 L956.3 42.1 L955.7 42.3 L955.1 42.6 L954.5 42.9 L953.7 43.1 L953.0 43.4 L952.1 43.6 L951.2 43.9 L950.3 44.1 L949.2 44.3 L948.2 44.5 L947.1 44.7 L945.9 44.9 L944.7 45.0 L943.5 45.2 L942.2 45.3 L940.9 45.5 L939.6 45.6 L938.2 45.7 L936.9 45.8 L935.5 45.8 L934.0 45.9 L932.6 46.0 L931.2 46.0 L929.7 46.0 L928.3 46.0 L926.8 46.0 L925.4 46.0 L924.0 45.9 L922.5 45.8 L921.1 45.8 L919.8 45.7 L918.4 45.6 L917.1 45.5 L915.8 45.3 L914.5 45.2 L913.3 45.0 L912.1 44.9 L910.9 44.7 L909.8 44.5 L908.8 44.3 L907.7 44.1 L906.8 43.9 L905.9 43.6 L905.0 43.4 L904.3 43.1 L903.5 42.9 L902.9 42.6 L902.3 42.3 L901.7 42.1 L901.3 41.8 L900.9 41.5 L900.6 41.2 L900.3 40.9 L900.1 40.6 L900.0 40.3 L900.0 40.0" data-name="sqlite"></path>
4954+ </g>
4955+ <g transform="translate(900, 12)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sqlite">
4956+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sqlite">
4957+ <text x="21.0" y="14.1" stroke="none" text-anchor="middle" data-name="sqlite">sqlite</text>
4958
4959- </g>
4960 </g>
4961 </g>
4962 </g>
4963 </g>
4964 </g>
4965 </g>
4966+ </g>
4967 <g data-name="ayllu-mail">
4968- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-mail">
4969- <rect x="0.0" y="245.0" height="134.0" width="522.0" data-name="ayllu-mail"></rect>
4970- <path d="M0.0 276.0 L522.0 276.0" fill="none" data-name="ayllu-mail"></path>
4971- </g>
4972- <g transform="translate(0, 245)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-mail">
4973- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-mail">
4974- <text x="253.0" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-mail">ayllu-mail</text>
4975+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-mail">
4976+ <rect x="0.0" y="248.0" height="136.0" width="484.0" data-name="ayllu-mail"></rect>
4977+ <path d="M0.0 280.0 L484.0 280.0" fill="none" data-name="ayllu-mail"></path>
4978+ </g>
4979+ <g transform="translate(0, 248)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-mail">
4980+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-mail">
4981+ <text x="234.0" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-mail">ayllu-mail</text>
4982
4983- </g>
4984 </g>
4985- <g transform="translate(0, 276)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-mail">
4986- <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-mail">
4987- <g transform="translate(20, 20)" data-name="ayllu-mail">
4988- <g data-name="capnp-rpc-server">
4989- <g fill="#fdf6e3" stroke="#33322E" data-name="capnp-rpc-server">
4990- <rect x="0.0" y="8.0" height="31.0" width="147.0" data-name="capnp-rpc-server"></rect>
4991- </g>
4992- <g transform="translate(0, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="capnp-rpc-server">
4993- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="capnp-rpc-server">
4994- <text x="65.5" y="13.5" stroke="none" text-anchor="middle" data-name="capnp-rpc-server">capnp-rpc-server</text>
4995+ </g>
4996+ <g transform="translate(0, 280)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-mail">
4997+ <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-mail">
4998+ <g transform="translate(20, 20)" data-name="ayllu-mail">
4999+ <g data-name="tarpc-server">
5000+ <g fill="#fdf6e3" stroke="#33322E" data-name="tarpc-server">
5001+ <rect x="0.0" y="8.0" height="32.0" width="109.0" data-name="tarpc-server"></rect>
5002+ </g>
5003+ <g transform="translate(0, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="tarpc-server">
5004+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="tarpc-server">
5005+ <text x="46.5" y="14.1" stroke="none" text-anchor="middle" data-name="tarpc-server">tarpc-server</text>
5006
5007- </g>
5008 </g>
5009 </g>
5010+ </g>
5011 <g data-name="mailpot">
5012- <g fill="#fdf6e3" stroke="#33322E" data-name="mailpot">
5013- <rect x="187.0" y="8.0" height="31.0" width="73.0" data-name="mailpot"></rect>
5014- </g>
5015- <g transform="translate(187, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="mailpot">
5016- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="mailpot">
5017- <text x="28.5" y="13.5" stroke="none" text-anchor="middle" data-name="mailpot">mailpot</text>
5018+ <g fill="#fdf6e3" stroke="#33322E" data-name="mailpot">
5019+ <rect x="149.0" y="8.0" height="32.0" width="73.0" data-name="mailpot"></rect>
5020+ </g>
5021+ <g transform="translate(149, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="mailpot">
5022+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="mailpot">
5023+ <text x="28.5" y="14.1" stroke="none" text-anchor="middle" data-name="mailpot">mailpot</text>
5024
5025- </g>
5026 </g>
5027 </g>
5028+ </g>
5029 <g data-name="postfix">
5030- <g fill="#fdf6e3" stroke="#33322E" data-name="postfix">
5031- <rect x="300.0" y="8.0" height="31.0" width="68.0" data-name="postfix"></rect>
5032- </g>
5033- <g transform="translate(300, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="postfix">
5034- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="postfix">
5035- <text x="26.0" y="13.5" stroke="none" text-anchor="middle" data-name="postfix">postfix</text>
5036+ <g fill="#fdf6e3" stroke="#33322E" data-name="postfix">
5037+ <rect x="262.0" y="8.0" height="32.0" width="68.0" data-name="postfix"></rect>
5038+ </g>
5039+ <g transform="translate(262, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="postfix">
5040+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="postfix">
5041+ <text x="26.0" y="14.1" stroke="none" text-anchor="middle" data-name="postfix">postfix</text>
5042
5043- </g>
5044 </g>
5045 </g>
5046+ </g>
5047 <g data-name="sqlite">
5048- <g fill="#fdf6e3" stroke="#33322E" data-name="sqlite">
5049- <rect x="408.0" y="8.0" height="31.0" width="58.0" stroke="none" data-name="sqlite"></rect>
5050- <path d="M408.0 8.0 L408.0 39.0" fill="none" data-name="sqlite"></path>
5051- <path d="M466.0 8.0 L466.0 39.0" fill="none" data-name="sqlite"></path>
5052- <ellipse cx="437.0" cy="8.0" rx="29.0" ry="6.0" data-name="sqlite"></ellipse>
5053- <path d="M466.0 39.0 L466.0 39.3 L465.9 39.6 L465.7 39.9 L465.4 40.2 L465.1 40.5 L464.7 40.8 L464.3 41.1 L463.7 41.3 L463.1 41.6 L462.5 41.9 L461.7 42.1 L461.0 42.4 L460.1 42.6 L459.2 42.9 L458.3 43.1 L457.2 43.3 L456.2 43.5 L455.1 43.7 L453.9 43.9 L452.7 44.0 L451.5 44.2 L450.2 44.3 L448.9 44.5 L447.6 44.6 L446.2 44.7 L444.9 44.8 L443.5 44.8 L442.0 44.9 L440.6 45.0 L439.2 45.0 L437.7 45.0 L436.3 45.0 L434.8 45.0 L433.4 45.0 L432.0 44.9 L430.5 44.8 L429.1 44.8 L427.8 44.7 L426.4 44.6 L425.1 44.5 L423.8 44.3 L422.5 44.2 L421.3 44.0 L420.1 43.9 L418.9 43.7 L417.8 43.5 L416.8 43.3 L415.7 43.1 L414.8 42.9 L413.9 42.6 L413.0 42.4 L412.3 42.1 L411.5 41.9 L410.9 41.6 L410.3 41.3 L409.7 41.1 L409.3 40.8 L408.9 40.5 L408.6 40.2 L408.3 39.9 L408.1 39.6 L408.0 39.3 L408.0 39.0" data-name="sqlite"></path>
5054- </g>
5055- <g transform="translate(408, 12)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sqlite">
5056- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sqlite">
5057- <text x="21.0" y="13.5" stroke="none" text-anchor="middle" data-name="sqlite">sqlite</text>
5058+ <g fill="#fdf6e3" stroke="#33322E" data-name="sqlite">
5059+ <rect x="370.0" y="8.0" height="32.0" width="58.0" stroke="none" data-name="sqlite"></rect>
5060+ <path d="M370.0 8.0 L370.0 40.0" fill="none" data-name="sqlite"></path>
5061+ <path d="M428.0 8.0 L428.0 40.0" fill="none" data-name="sqlite"></path>
5062+ <ellipse cx="399.0" cy="8.0" rx="29.0" ry="6.0" data-name="sqlite"></ellipse>
5063+ <path d="M428.0 40.0 L428.0 40.3 L427.9 40.6 L427.7 40.9 L427.4 41.2 L427.1 41.5 L426.7 41.8 L426.3 42.1 L425.7 42.3 L425.1 42.6 L424.5 42.9 L423.7 43.1 L423.0 43.4 L422.1 43.6 L421.2 43.9 L420.3 44.1 L419.2 44.3 L418.2 44.5 L417.1 44.7 L415.9 44.9 L414.7 45.0 L413.5 45.2 L412.2 45.3 L410.9 45.5 L409.6 45.6 L408.2 45.7 L406.9 45.8 L405.5 45.8 L404.0 45.9 L402.6 46.0 L401.2 46.0 L399.7 46.0 L398.3 46.0 L396.8 46.0 L395.4 46.0 L394.0 45.9 L392.5 45.8 L391.1 45.8 L389.8 45.7 L388.4 45.6 L387.1 45.5 L385.8 45.3 L384.5 45.2 L383.3 45.0 L382.1 44.9 L380.9 44.7 L379.8 44.5 L378.8 44.3 L377.7 44.1 L376.8 43.9 L375.9 43.6 L375.0 43.4 L374.3 43.1 L373.5 42.9 L372.9 42.6 L372.3 42.3 L371.7 42.1 L371.3 41.8 L370.9 41.5 L370.6 41.2 L370.3 40.9 L370.1 40.6 L370.0 40.3 L370.0 40.0" data-name="sqlite"></path>
5064+ </g>
5065+ <g transform="translate(370, 12)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sqlite">
5066+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sqlite">
5067+ <text x="21.0" y="14.1" stroke="none" text-anchor="middle" data-name="sqlite">sqlite</text>
5068
5069- </g>
5070 </g>
5071 </g>
5072 </g>
5073 </g>
5074 </g>
5075 </g>
5076+ </g>
5077 <g data-name="contributor-2">
5078- <g fill="#eee8d5" stroke="#33322E" data-name="contributor-2">
5079- <circle r="4.0" cx="807.5" cy="493.5" data-name="contributor-2"></circle>
5080- <path d="M807.5 497.5 L807.5 505.5" fill="none" data-name="contributor-2"></path>
5081- <path d="M803.5 501.5 L811.5 501.5" fill="none" data-name="contributor-2"></path>
5082- <path d="M803.5 509.5 L807.5 505.5 L811.5 509.5" fill="none" data-name="contributor-2"></path>
5083- </g>
5084- <g transform="translate(750, 505.5)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="contributor-2">
5085- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="contributor-2">
5086- <text x="49.5" y="13.5" stroke="none" text-anchor="middle" data-name="contributor-2">contributor-2</text>
5087+ <g fill="#eee8d5" stroke="#33322E" data-name="contributor-2">
5088+ <circle r="4.0" cx="747.8" cy="500.0" data-name="contributor-2"></circle>
5089+ <path d="M747.8 504.0 L747.8 512.0" fill="none" data-name="contributor-2"></path>
5090+ <path d="M743.8 508.0 L751.8 508.0" fill="none" data-name="contributor-2"></path>
5091+ <path d="M743.8 516.0 L747.8 512.0 L751.8 516.0" fill="none" data-name="contributor-2"></path>
5092+ </g>
5093+ <g transform="translate(690.25, 512)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="contributor-2">
5094+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="contributor-2">
5095+ <text x="49.5" y="14.1" stroke="none" text-anchor="middle" data-name="contributor-2">contributor-2</text>
5096
5097- </g>
5098 </g>
5099 </g>
5100+ </g>
5101 <g data-name="ayllu-xmpp">
5102- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-xmpp">
5103- <rect x="562.0" y="245.0" height="134.0" width="491.0" data-name="ayllu-xmpp"></rect>
5104- <path d="M562.0 276.0 L1053.0 276.0" fill="none" data-name="ayllu-xmpp"></path>
5105- </g>
5106- <g transform="translate(562, 245)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-xmpp">
5107- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-xmpp">
5108- <text x="237.5" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-xmpp">ayllu-xmpp</text>
5109+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-xmpp">
5110+ <rect x="524.0" y="248.0" height="136.0" width="453.0" data-name="ayllu-xmpp"></rect>
5111+ <path d="M524.0 280.0 L977.0 280.0" fill="none" data-name="ayllu-xmpp"></path>
5112+ </g>
5113+ <g transform="translate(524, 248)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-xmpp">
5114+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-xmpp">
5115+ <text x="218.5" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-xmpp">ayllu-xmpp</text>
5116
5117- </g>
5118 </g>
5119- <g transform="translate(562, 276)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-xmpp">
5120- <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-xmpp">
5121- <g transform="translate(20, 20)" data-name="ayllu-xmpp">
5122- <g data-name="capnp-rpc-server">
5123- <g fill="#fdf6e3" stroke="#33322E" data-name="capnp-rpc-server">
5124- <rect x="0.0" y="8.0" height="31.0" width="147.0" data-name="capnp-rpc-server"></rect>
5125- </g>
5126- <g transform="translate(0, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="capnp-rpc-server">
5127- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="capnp-rpc-server">
5128- <text x="65.5" y="13.5" stroke="none" text-anchor="middle" data-name="capnp-rpc-server">capnp-rpc-server</text>
5129+ </g>
5130+ <g transform="translate(524, 280)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-xmpp">
5131+ <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-xmpp">
5132+ <g transform="translate(20, 20)" data-name="ayllu-xmpp">
5133+ <g data-name="tarpc-server">
5134+ <g fill="#fdf6e3" stroke="#33322E" data-name="tarpc-server">
5135+ <rect x="0.0" y="8.0" height="32.0" width="109.0" data-name="tarpc-server"></rect>
5136+ </g>
5137+ <g transform="translate(0, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="tarpc-server">
5138+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="tarpc-server">
5139+ <text x="46.5" y="14.1" stroke="none" text-anchor="middle" data-name="tarpc-server">tarpc-server</text>
5140
5141- </g>
5142 </g>
5143 </g>
5144+ </g>
5145 <g data-name="chat interface/bot">
5146- <g fill="#fdf6e3" stroke="#33322E" data-name="chat interface/bot">
5147- <rect x="187.0" y="8.0" height="31.0" width="150.0" data-name="chat interface/bot"></rect>
5148- </g>
5149- <g transform="translate(187, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="chat interface/bot">
5150- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="chat interface/bot">
5151- <text x="67.0" y="13.5" stroke="none" text-anchor="middle" data-name="chat interface/bot">chat interface/bot</text>
5152+ <g fill="#fdf6e3" stroke="#33322E" data-name="chat interface/bot">
5153+ <rect x="149.0" y="8.0" height="32.0" width="150.0" data-name="chat interface/bot"></rect>
5154+ </g>
5155+ <g transform="translate(149, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="chat interface/bot">
5156+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="chat interface/bot">
5157+ <text x="67.0" y="14.1" stroke="none" text-anchor="middle" data-name="chat interface/bot">chat interface/bot</text>
5158
5159- </g>
5160 </g>
5161 </g>
5162+ </g>
5163 <g data-name="sqlite">
5164- <g fill="#fdf6e3" stroke="#33322E" data-name="sqlite">
5165- <rect x="377.0" y="8.0" height="31.0" width="58.0" stroke="none" data-name="sqlite"></rect>
5166- <path d="M377.0 8.0 L377.0 39.0" fill="none" data-name="sqlite"></path>
5167- <path d="M435.0 8.0 L435.0 39.0" fill="none" data-name="sqlite"></path>
5168- <ellipse cx="406.0" cy="8.0" rx="29.0" ry="6.0" data-name="sqlite"></ellipse>
5169- <path d="M435.0 39.0 L435.0 39.3 L434.9 39.6 L434.7 39.9 L434.4 40.2 L434.1 40.5 L433.7 40.8 L433.3 41.1 L432.7 41.3 L432.1 41.6 L431.5 41.9 L430.7 42.1 L430.0 42.4 L429.1 42.6 L428.2 42.9 L427.3 43.1 L426.2 43.3 L425.2 43.5 L424.1 43.7 L422.9 43.9 L421.7 44.0 L420.5 44.2 L419.2 44.3 L417.9 44.5 L416.6 44.6 L415.2 44.7 L413.9 44.8 L412.5 44.8 L411.0 44.9 L409.6 45.0 L408.2 45.0 L406.7 45.0 L405.3 45.0 L403.8 45.0 L402.4 45.0 L401.0 44.9 L399.5 44.8 L398.1 44.8 L396.8 44.7 L395.4 44.6 L394.1 44.5 L392.8 44.3 L391.5 44.2 L390.3 44.0 L389.1 43.9 L387.9 43.7 L386.8 43.5 L385.8 43.3 L384.7 43.1 L383.8 42.9 L382.9 42.6 L382.0 42.4 L381.3 42.1 L380.5 41.9 L379.9 41.6 L379.3 41.3 L378.7 41.1 L378.3 40.8 L377.9 40.5 L377.6 40.2 L377.3 39.9 L377.1 39.6 L377.0 39.3 L377.0 39.0" data-name="sqlite"></path>
5170- </g>
5171- <g transform="translate(377, 12)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sqlite">
5172- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sqlite">
5173- <text x="21.0" y="13.5" stroke="none" text-anchor="middle" data-name="sqlite">sqlite</text>
5174+ <g fill="#fdf6e3" stroke="#33322E" data-name="sqlite">
5175+ <rect x="339.0" y="8.0" height="32.0" width="58.0" stroke="none" data-name="sqlite"></rect>
5176+ <path d="M339.0 8.0 L339.0 40.0" fill="none" data-name="sqlite"></path>
5177+ <path d="M397.0 8.0 L397.0 40.0" fill="none" data-name="sqlite"></path>
5178+ <ellipse cx="368.0" cy="8.0" rx="29.0" ry="6.0" data-name="sqlite"></ellipse>
5179+ <path d="M397.0 40.0 L397.0 40.3 L396.9 40.6 L396.7 40.9 L396.4 41.2 L396.1 41.5 L395.7 41.8 L395.3 42.1 L394.7 42.3 L394.1 42.6 L393.5 42.9 L392.7 43.1 L392.0 43.4 L391.1 43.6 L390.2 43.9 L389.3 44.1 L388.2 44.3 L387.2 44.5 L386.1 44.7 L384.9 44.9 L383.7 45.0 L382.5 45.2 L381.2 45.3 L379.9 45.5 L378.6 45.6 L377.2 45.7 L375.9 45.8 L374.5 45.8 L373.0 45.9 L371.6 46.0 L370.2 46.0 L368.7 46.0 L367.3 46.0 L365.8 46.0 L364.4 46.0 L363.0 45.9 L361.5 45.8 L360.1 45.8 L358.8 45.7 L357.4 45.6 L356.1 45.5 L354.8 45.3 L353.5 45.2 L352.3 45.0 L351.1 44.9 L349.9 44.7 L348.8 44.5 L347.8 44.3 L346.7 44.1 L345.8 43.9 L344.9 43.6 L344.0 43.4 L343.3 43.1 L342.5 42.9 L341.9 42.6 L341.3 42.3 L340.7 42.1 L340.3 41.8 L339.9 41.5 L339.6 41.2 L339.3 40.9 L339.1 40.6 L339.0 40.3 L339.0 40.0" data-name="sqlite"></path>
5180+ </g>
5181+ <g transform="translate(339, 12)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sqlite">
5182+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sqlite">
5183+ <text x="21.0" y="14.1" stroke="none" text-anchor="middle" data-name="sqlite">sqlite</text>
5184
5185- </g>
5186 </g>
5187 </g>
5188 </g>
5189 </g>
5190 </g>
5191 </g>
5192+ </g>
5193 <g data-name="ayllu-build">
5194- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-build">
5195- <rect x="1108.5" y="245.0" height="134.0" width="301.0" data-name="ayllu-build"></rect>
5196- <path d="M1108.5 276.0 L1409.5 276.0" fill="none" data-name="ayllu-build"></path>
5197- </g>
5198- <g transform="translate(1108.5, 245)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-build">
5199- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-build">
5200- <text x="142.5" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-build">ayllu-build</text>
5201+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-build">
5202+ <rect x="1037.3" y="248.0" height="136.0" width="263.0" data-name="ayllu-build"></rect>
5203+ <path d="M1037.3 280.0 L1300.3 280.0" fill="none" data-name="ayllu-build"></path>
5204+ </g>
5205+ <g transform="translate(1037.25, 248)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-build">
5206+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-build">
5207+ <text x="123.5" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-build">ayllu-build</text>
5208
5209- </g>
5210 </g>
5211- <g transform="translate(1108.5, 276)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-build">
5212- <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-build">
5213- <g transform="translate(20, 20)" data-name="ayllu-build">
5214- <g data-name="capnp-rpc-server">
5215- <g fill="#fdf6e3" stroke="#33322E" data-name="capnp-rpc-server">
5216- <rect x="0.0" y="8.0" height="31.0" width="147.0" data-name="capnp-rpc-server"></rect>
5217- </g>
5218- <g transform="translate(0, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="capnp-rpc-server">
5219- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="capnp-rpc-server">
5220- <text x="65.5" y="13.5" stroke="none" text-anchor="middle" data-name="capnp-rpc-server">capnp-rpc-server</text>
5221+ </g>
5222+ <g transform="translate(1037.25, 280)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-build">
5223+ <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-build">
5224+ <g transform="translate(20, 20)" data-name="ayllu-build">
5225+ <g data-name="tarpc-server">
5226+ <g fill="#fdf6e3" stroke="#33322E" data-name="tarpc-server">
5227+ <rect x="0.0" y="8.0" height="32.0" width="109.0" data-name="tarpc-server"></rect>
5228+ </g>
5229+ <g transform="translate(0, 8)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="tarpc-server">
5230+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="tarpc-server">
5231+ <text x="46.5" y="14.1" stroke="none" text-anchor="middle" data-name="tarpc-server">tarpc-server</text>
5232
5233- </g>
5234 </g>
5235 </g>
5236+ </g>
5237 <g data-name="sqlite">
5238- <g fill="#fdf6e3" stroke="#33322E" data-name="sqlite">
5239- <rect x="187.0" y="8.0" height="31.0" width="58.0" stroke="none" data-name="sqlite"></rect>
5240- <path d="M187.0 8.0 L187.0 39.0" fill="none" data-name="sqlite"></path>
5241- <path d="M245.0 8.0 L245.0 39.0" fill="none" data-name="sqlite"></path>
5242- <ellipse cx="216.0" cy="8.0" rx="29.0" ry="6.0" data-name="sqlite"></ellipse>
5243- <path d="M245.0 39.0 L245.0 39.3 L244.9 39.6 L244.7 39.9 L244.4 40.2 L244.1 40.5 L243.7 40.8 L243.3 41.1 L242.7 41.3 L242.1 41.6 L241.5 41.9 L240.7 42.1 L240.0 42.4 L239.1 42.6 L238.2 42.9 L237.3 43.1 L236.2 43.3 L235.2 43.5 L234.1 43.7 L232.9 43.9 L231.7 44.0 L230.5 44.2 L229.2 44.3 L227.9 44.5 L226.6 44.6 L225.2 44.7 L223.9 44.8 L222.5 44.8 L221.0 44.9 L219.6 45.0 L218.2 45.0 L216.7 45.0 L215.3 45.0 L213.8 45.0 L212.4 45.0 L211.0 44.9 L209.5 44.8 L208.1 44.8 L206.8 44.7 L205.4 44.6 L204.1 44.5 L202.8 44.3 L201.5 44.2 L200.3 44.0 L199.1 43.9 L197.9 43.7 L196.8 43.5 L195.8 43.3 L194.7 43.1 L193.8 42.9 L192.9 42.6 L192.0 42.4 L191.3 42.1 L190.5 41.9 L189.9 41.6 L189.3 41.3 L188.7 41.1 L188.3 40.8 L187.9 40.5 L187.6 40.2 L187.3 39.9 L187.1 39.6 L187.0 39.3 L187.0 39.0" data-name="sqlite"></path>
5244- </g>
5245- <g transform="translate(187, 12)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sqlite">
5246- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sqlite">
5247- <text x="21.0" y="13.5" stroke="none" text-anchor="middle" data-name="sqlite">sqlite</text>
5248+ <g fill="#fdf6e3" stroke="#33322E" data-name="sqlite">
5249+ <rect x="149.0" y="8.0" height="32.0" width="58.0" stroke="none" data-name="sqlite"></rect>
5250+ <path d="M149.0 8.0 L149.0 40.0" fill="none" data-name="sqlite"></path>
5251+ <path d="M207.0 8.0 L207.0 40.0" fill="none" data-name="sqlite"></path>
5252+ <ellipse cx="178.0" cy="8.0" rx="29.0" ry="6.0" data-name="sqlite"></ellipse>
5253+ <path d="M207.0 40.0 L207.0 40.3 L206.9 40.6 L206.7 40.9 L206.4 41.2 L206.1 41.5 L205.7 41.8 L205.3 42.1 L204.7 42.3 L204.1 42.6 L203.5 42.9 L202.7 43.1 L202.0 43.4 L201.1 43.6 L200.2 43.9 L199.3 44.1 L198.2 44.3 L197.2 44.5 L196.1 44.7 L194.9 44.9 L193.7 45.0 L192.5 45.2 L191.2 45.3 L189.9 45.5 L188.6 45.6 L187.2 45.7 L185.9 45.8 L184.5 45.8 L183.0 45.9 L181.6 46.0 L180.2 46.0 L178.7 46.0 L177.3 46.0 L175.8 46.0 L174.4 46.0 L173.0 45.9 L171.5 45.8 L170.1 45.8 L168.8 45.7 L167.4 45.6 L166.1 45.5 L164.8 45.3 L163.5 45.2 L162.3 45.0 L161.1 44.9 L159.9 44.7 L158.8 44.5 L157.8 44.3 L156.7 44.1 L155.8 43.9 L154.9 43.6 L154.0 43.4 L153.3 43.1 L152.5 42.9 L151.9 42.6 L151.3 42.3 L150.7 42.1 L150.3 41.8 L149.9 41.5 L149.6 41.2 L149.3 40.9 L149.1 40.6 L149.0 40.3 L149.0 40.0" data-name="sqlite"></path>
5254+ </g>
5255+ <g transform="translate(149, 12)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="sqlite">
5256+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="sqlite">
5257+ <text x="21.0" y="14.1" stroke="none" text-anchor="middle" data-name="sqlite">sqlite</text>
5258
5259- </g>
5260 </g>
5261 </g>
5262 </g>
5263 </g>
5264 </g>
5265 </g>
5266+ </g>
5267 <g data-name="ayllu-shell">
5268- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-shell">
5269- <rect x="1612.0" y="296.5" height="31.0" width="94.0" data-name="ayllu-shell"></rect>
5270- </g>
5271- <g transform="translate(1612, 296.5)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-shell">
5272- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-shell">
5273- <text x="39.0" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-shell">ayllu-shell</text>
5274+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-shell">
5275+ <rect x="1481.0" y="300.0" height="32.0" width="94.0" data-name="ayllu-shell"></rect>
5276+ </g>
5277+ <g transform="translate(1481, 300)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-shell">
5278+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-shell">
5279+ <text x="39.0" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-shell">ayllu-shell</text>
5280
5281- </g>
5282 </g>
5283 </g>
5284+ </g>
5285 <g data-name="contributor">
5286- <g fill="#eee8d5" stroke="#33322E" data-name="contributor">
5287- <circle r="4.0" cx="1659.0" cy="493.5" data-name="contributor"></circle>
5288- <path d="M1659.0 497.5 L1659.0 505.5" fill="none" data-name="contributor"></path>
5289- <path d="M1655.0 501.5 L1663.0 501.5" fill="none" data-name="contributor"></path>
5290- <path d="M1655.0 509.5 L1659.0 505.5 L1663.0 509.5" fill="none" data-name="contributor"></path>
5291- </g>
5292- <g transform="translate(1608.5, 505.5)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="contributor">
5293- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="contributor">
5294- <text x="42.5" y="13.5" stroke="none" text-anchor="middle" data-name="contributor">contributor</text>
5295+ <g fill="#eee8d5" stroke="#33322E" data-name="contributor">
5296+ <circle r="4.0" cx="1528.0" cy="500.0" data-name="contributor"></circle>
5297+ <path d="M1528.0 504.0 L1528.0 512.0" fill="none" data-name="contributor"></path>
5298+ <path d="M1524.0 508.0 L1532.0 508.0" fill="none" data-name="contributor"></path>
5299+ <path d="M1524.0 516.0 L1528.0 512.0 L1532.0 516.0" fill="none" data-name="contributor"></path>
5300+ </g>
5301+ <g transform="translate(1477.5, 512)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="contributor">
5302+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="contributor">
5303+ <text x="42.5" y="14.1" stroke="none" text-anchor="middle" data-name="contributor">contributor</text>
5304
5305- </g>
5306 </g>
5307 </g>
5308+ </g>
5309 <g data-name="ayllu-cli">
5310- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-cli">
5311- <rect x="1746.0" y="296.5" height="31.0" width="75.0" data-name="ayllu-cli"></rect>
5312- </g>
5313- <g transform="translate(1746, 296.5)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-cli">
5314- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-cli">
5315- <text x="29.5" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-cli">ayllu-cli</text>
5316+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-cli">
5317+ <rect x="1615.0" y="300.0" height="32.0" width="75.0" data-name="ayllu-cli"></rect>
5318+ </g>
5319+ <g transform="translate(1615, 300)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-cli">
5320+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-cli">
5321+ <text x="29.5" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-cli">ayllu-cli</text>
5322
5323- </g>
5324 </g>
5325 </g>
5326+ </g>
5327 <g data-name="admin">
5328- <g fill="#eee8d5" stroke="#33322E" data-name="admin">
5329- <circle r="4.0" cx="1783.5" cy="493.5" data-name="admin"></circle>
5330- <path d="M1783.5 497.5 L1783.5 505.5" fill="none" data-name="admin"></path>
5331- <path d="M1779.5 501.5 L1787.5 501.5" fill="none" data-name="admin"></path>
5332- <path d="M1779.5 509.5 L1783.5 505.5 L1787.5 509.5" fill="none" data-name="admin"></path>
5333- </g>
5334- <g transform="translate(1752, 505.5)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="admin">
5335- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="admin">
5336- <text x="23.5" y="13.5" stroke="none" text-anchor="middle" data-name="admin">admin</text>
5337+ <g fill="#eee8d5" stroke="#33322E" data-name="admin">
5338+ <circle r="4.0" cx="1652.5" cy="500.0" data-name="admin"></circle>
5339+ <path d="M1652.5 504.0 L1652.5 512.0" fill="none" data-name="admin"></path>
5340+ <path d="M1648.5 508.0 L1656.5 508.0" fill="none" data-name="admin"></path>
5341+ <path d="M1648.5 516.0 L1652.5 512.0 L1656.5 516.0" fill="none" data-name="admin"></path>
5342+ </g>
5343+ <g transform="translate(1621, 512)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="admin">
5344+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="admin">
5345+ <text x="23.5" y="14.1" stroke="none" text-anchor="middle" data-name="admin">admin</text>
5346
5347- </g>
5348 </g>
5349 </g>
5350+ </g>
5351 <g data-name="ayllu-builder-1">
5352- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-builder-1">
5353- <rect x="909.5" y="419.0" height="180.0" width="193.0" data-name="ayllu-builder-1"></rect>
5354- <path d="M909.5 450.0 L1102.5 450.0" fill="none" data-name="ayllu-builder-1"></path>
5355- </g>
5356- <g transform="translate(909.5, 419)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-builder-1">
5357- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-builder-1">
5358- <text x="88.5" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-builder-1">ayllu-builder-1</text>
5359+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-builder-1">
5360+ <rect x="846.5" y="424.0" height="184.0" width="173.0" data-name="ayllu-builder-1"></rect>
5361+ <path d="M846.5 456.0 L1019.5 456.0" fill="none" data-name="ayllu-builder-1"></path>
5362+ </g>
5363+ <g transform="translate(846.5, 424)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-builder-1">
5364+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-builder-1">
5365+ <text x="78.5" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-builder-1">ayllu-builder-1</text>
5366
5367- </g>
5368 </g>
5369- <g transform="translate(909.5, 450)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-builder-1">
5370- <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-builder-1">
5371- <g transform="translate(20, 20)" data-name="ayllu-builder-1">
5372- <g data-name="qemu-x86_64">
5373- <g fill="#fdf6e3" stroke="#33322E" data-name="qemu-x86_64">
5374- <rect x="0.0" y="31.0" height="62.0" width="137.0" data-name="qemu-x86_64"></rect>
5375- <path d="M0.0 31.0 L0.0 0.0 L113.1 0.0 L113.1 31.0 Z" data-name="qemu-x86_64"></path>
5376- <path d="M0.0 31.0 L137.0 31.0" fill="none" data-name="qemu-x86_64"></path>
5377- <path d="M0.0 62.0 L137.0 62.0" fill="none" data-name="qemu-x86_64"></path>
5378- </g>
5379+ </g>
5380+ <g transform="translate(846.5, 456)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-builder-1">
5381+ <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-builder-1">
5382+ <g transform="translate(20, 20)" data-name="ayllu-builder-1">
5383+ <g data-name="qemu-x86_64">
5384+ <g fill="#fdf6e3" stroke="#33322E" data-name="qemu-x86_64">
5385+ <rect x="0.0" y="32.0" height="64.0" width="117.0" data-name="qemu-x86_64"></rect>
5386+ <path d="M0.0 32.0 L0.0 0.0 L113.1 0.0 L113.1 32.0 Z" data-name="qemu-x86_64"></path>
5387+ <path d="M0.0 32.0 L117.0 32.0" fill="none" data-name="qemu-x86_64"></path>
5388+ <path d="M0.0 64.0 L117.0 64.0" fill="none" data-name="qemu-x86_64"></path>
5389+ </g>
5390 <g transform="translate(0, 0)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-x86_64">
5391- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-x86_64">
5392- <text x="0.0" y="13.5" stroke="none" data-name="qemu-x86_64">qemu-x86_64</text>
5393+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-x86_64">
5394+ <text x="0.0" y="14.1" stroke="none" data-name="qemu-x86_64">qemu-x86_64</text>
5395
5396- </g>
5397 </g>
5398- <g transform="translate(0, 31)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-x86_64">
5399- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-x86_64">
5400- <text x="0.0" y="13.5" stroke="none" data-name="qemu-x86_64">capnp-rpc-server</text>
5401+ </g>
5402+ <g transform="translate(0, 32)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-x86_64">
5403+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-x86_64">
5404+ <text x="0.0" y="14.1" stroke="none" data-name="qemu-x86_64">tarpc-server</text>
5405
5406- </g>
5407 </g>
5408- <g transform="translate(0, 62)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-x86_64">
5409- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-x86_64">
5410- <text x="0.0" y="13.5" stroke="none" data-name="qemu-x86_64">executor</text>
5411+ </g>
5412+ <g transform="translate(0, 64)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-x86_64">
5413+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-x86_64">
5414+ <text x="0.0" y="14.1" stroke="none" data-name="qemu-x86_64">executor</text>
5415
5416- </g>
5417 </g>
5418 </g>
5419 </g>
5420 </g>
5421 </g>
5422 </g>
5423+ </g>
5424 <g data-name="ayllu-builder-2">
5425- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-builder-2">
5426- <rect x="1142.5" y="419.0" height="180.0" width="193.0" data-name="ayllu-builder-2"></rect>
5427- <path d="M1142.5 450.0 L1335.5 450.0" fill="none" data-name="ayllu-builder-2"></path>
5428- </g>
5429- <g transform="translate(1142.5, 419)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-builder-2">
5430- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-builder-2">
5431- <text x="88.5" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-builder-2">ayllu-builder-2</text>
5432+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-builder-2">
5433+ <rect x="1059.5" y="424.0" height="184.0" width="181.0" data-name="ayllu-builder-2"></rect>
5434+ <path d="M1059.5 456.0 L1240.5 456.0" fill="none" data-name="ayllu-builder-2"></path>
5435+ </g>
5436+ <g transform="translate(1059.5, 424)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-builder-2">
5437+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-builder-2">
5438+ <text x="82.5" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-builder-2">ayllu-builder-2</text>
5439
5440- </g>
5441 </g>
5442- <g transform="translate(1142.5, 450)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-builder-2">
5443- <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-builder-2">
5444- <g transform="translate(20, 20)" data-name="ayllu-builder-2">
5445- <g data-name="qemu-aarch64">
5446- <g fill="#fdf6e3" stroke="#33322E" data-name="qemu-aarch64">
5447- <rect x="0.0" y="31.0" height="62.0" width="137.0" data-name="qemu-aarch64"></rect>
5448- <path d="M0.0 31.0 L0.0 0.0 L118.9 0.0 L118.9 31.0 Z" data-name="qemu-aarch64"></path>
5449- <path d="M0.0 31.0 L137.0 31.0" fill="none" data-name="qemu-aarch64"></path>
5450- <path d="M0.0 62.0 L137.0 62.0" fill="none" data-name="qemu-aarch64"></path>
5451- </g>
5452+ </g>
5453+ <g transform="translate(1059.5, 456)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-builder-2">
5454+ <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-builder-2">
5455+ <g transform="translate(20, 20)" data-name="ayllu-builder-2">
5456+ <g data-name="qemu-aarch64">
5457+ <g fill="#fdf6e3" stroke="#33322E" data-name="qemu-aarch64">
5458+ <rect x="0.0" y="32.0" height="64.0" width="125.0" data-name="qemu-aarch64"></rect>
5459+ <path d="M0.0 32.0 L0.0 0.0 L118.9 0.0 L118.9 32.0 Z" data-name="qemu-aarch64"></path>
5460+ <path d="M0.0 32.0 L125.0 32.0" fill="none" data-name="qemu-aarch64"></path>
5461+ <path d="M0.0 64.0 L125.0 64.0" fill="none" data-name="qemu-aarch64"></path>
5462+ </g>
5463 <g transform="translate(0, 0)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-aarch64">
5464- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-aarch64">
5465- <text x="0.0" y="13.5" stroke="none" data-name="qemu-aarch64">qemu-aarch64</text>
5466+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-aarch64">
5467+ <text x="0.0" y="14.1" stroke="none" data-name="qemu-aarch64">qemu-aarch64</text>
5468
5469- </g>
5470 </g>
5471- <g transform="translate(0, 31)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-aarch64">
5472- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-aarch64">
5473- <text x="0.0" y="13.5" stroke="none" data-name="qemu-aarch64">capnp-rpc-server</text>
5474+ </g>
5475+ <g transform="translate(0, 32)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-aarch64">
5476+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-aarch64">
5477+ <text x="0.0" y="14.1" stroke="none" data-name="qemu-aarch64">tarpc-server</text>
5478
5479- </g>
5480 </g>
5481- <g transform="translate(0, 62)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-aarch64">
5482- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-aarch64">
5483- <text x="0.0" y="13.5" stroke="none" data-name="qemu-aarch64">exectuor</text>
5484+ </g>
5485+ <g transform="translate(0, 64)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="qemu-aarch64">
5486+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="qemu-aarch64">
5487+ <text x="0.0" y="14.1" stroke="none" data-name="qemu-aarch64">exectuor</text>
5488
5489- </g>
5490 </g>
5491 </g>
5492 </g>
5493 </g>
5494 </g>
5495 </g>
5496+ </g>
5497 <g data-name="ayllu-builder-n">
5498- <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-builder-n">
5499- <rect x="1375.5" y="419.0" height="180.0" width="193.0" data-name="ayllu-builder-n"></rect>
5500- <path d="M1375.5 450.0 L1568.5 450.0" fill="none" data-name="ayllu-builder-n"></path>
5501- </g>
5502- <g transform="translate(1375.5, 419)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-builder-n">
5503- <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-builder-n">
5504- <text x="88.5" y="13.5" stroke="none" text-anchor="middle" data-name="ayllu-builder-n">ayllu-builder-n</text>
5505+ <g fill="#eee8d5" stroke="#33322E" data-name="ayllu-builder-n">
5506+ <rect x="1280.5" y="424.0" height="184.0" width="157.0" data-name="ayllu-builder-n"></rect>
5507+ <path d="M1280.5 456.0 L1437.5 456.0" fill="none" data-name="ayllu-builder-n"></path>
5508+ </g>
5509+ <g transform="translate(1280.5, 424)" font-family="Helvetica" font-size="12pt" font-weight="bold" font-style="normal" data-name="ayllu-builder-n">
5510+ <g transform="translate(8, 8)" fill="#33322E" text-align="center" data-name="ayllu-builder-n">
5511+ <text x="70.5" y="14.1" stroke="none" text-anchor="middle" data-name="ayllu-builder-n">ayllu-builder-n</text>
5512
5513- </g>
5514 </g>
5515- <g transform="translate(1375.5, 450)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-builder-n">
5516- <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-builder-n">
5517- <g transform="translate(20, 20)" data-name="ayllu-builder-n">
5518- <g data-name="lxc">
5519- <g fill="#fdf6e3" stroke="#33322E" data-name="lxc">
5520- <rect x="0.0" y="31.0" height="62.0" width="137.0" data-name="lxc"></rect>
5521- <path d="M0.0 31.0 L0.0 0.0 L35.2 0.0 L35.2 31.0 Z" data-name="lxc"></path>
5522- <path d="M0.0 31.0 L137.0 31.0" fill="none" data-name="lxc"></path>
5523- <path d="M0.0 62.0 L137.0 62.0" fill="none" data-name="lxc"></path>
5524- </g>
5525+ </g>
5526+ <g transform="translate(1280.5, 456)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="ayllu-builder-n">
5527+ <g transform="translate(8, 8)" fill="#33322E" data-name="ayllu-builder-n">
5528+ <g transform="translate(20, 20)" data-name="ayllu-builder-n">
5529+ <g data-name="lxc">
5530+ <g fill="#fdf6e3" stroke="#33322E" data-name="lxc">
5531+ <rect x="0.0" y="32.0" height="64.0" width="101.0" data-name="lxc"></rect>
5532+ <path d="M0.0 32.0 L0.0 0.0 L35.2 0.0 L35.2 32.0 Z" data-name="lxc"></path>
5533+ <path d="M0.0 32.0 L101.0 32.0" fill="none" data-name="lxc"></path>
5534+ <path d="M0.0 64.0 L101.0 64.0" fill="none" data-name="lxc"></path>
5535+ </g>
5536 <g transform="translate(0, 0)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="lxc">
5537- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="lxc">
5538- <text x="0.0" y="13.5" stroke="none" data-name="lxc">lxc</text>
5539+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="lxc">
5540+ <text x="0.0" y="14.1" stroke="none" data-name="lxc">lxc</text>
5541
5542- </g>
5543 </g>
5544- <g transform="translate(0, 31)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="lxc">
5545- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="lxc">
5546- <text x="0.0" y="13.5" stroke="none" data-name="lxc">capnp-rpc-server</text>
5547+ </g>
5548+ <g transform="translate(0, 32)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="lxc">
5549+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="lxc">
5550+ <text x="0.0" y="14.1" stroke="none" data-name="lxc">tarpc-server</text>
5551
5552- </g>
5553 </g>
5554- <g transform="translate(0, 62)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="lxc">
5555- <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="lxc">
5556- <text x="0.0" y="13.5" stroke="none" data-name="lxc">executor</text>
5557+ </g>
5558+ <g transform="translate(0, 64)" font-family="Helvetica" font-size="12pt" font-weight="normal" font-style="normal" data-name="lxc">
5559+ <g transform="translate(8, 8)" fill="#33322E" text-align="left" data-name="lxc">
5560+ <text x="0.0" y="14.1" stroke="none" data-name="lxc">executor</text>
5561
5562- </g>
5563 </g>
5564 </g>
5565 </g>
5566 @@ -595,4 +594,5 @@
5567 </g>
5568 </g>
5569 </g>
5570- </svg>
5571\ No newline at end of file
5572+ </g>
5573+ </svg>