Compare commits
No commits in common. "old-master" and "master" have entirely different histories.
old-master
...
master
7
.editorconfig
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[*]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = tab
|
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export DATABASE_URL=sqlite://$(pwd)/test.db
|
17
.gitignore
vendored
|
@ -1,8 +1,9 @@
|
||||||
/target
|
node_modules
|
||||||
/.env
|
dist
|
||||||
/output.log
|
target
|
||||||
/config.toml
|
.DS_Store
|
||||||
/public
|
**/export/export.json
|
||||||
/hellosu
|
test.db*
|
||||||
/hellosu.db*
|
.env
|
||||||
|
.direnv
|
||||||
|
/proto/generated
|
1
.tokeignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pnpm-lock.yaml
|
3
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
|
||||||
|
}
|
5367
Cargo.lock
generated
17
Cargo.toml
|
@ -1,4 +1,13 @@
|
||||||
[workspace]
|
workspace.resolver = "2"
|
||||||
members = [
|
workspace.members = ["ui/src-tauri"]
|
||||||
"daemon",
|
|
||||||
]
|
[profile.wasm-debug]
|
||||||
|
inherits = "dev"
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
|
[profile.wasm-release]
|
||||||
|
inherits = "release"
|
||||||
|
lto = true
|
||||||
|
opt-level = 's'
|
||||||
|
strip = true
|
||||||
|
panic = "abort"
|
||||||
|
|
13
Makefile
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
deploy-docs:
|
||||||
|
(cd docs; BASE_URL=/panorama bun run build) || true
|
||||||
|
rsync -azrP docs/dist/ root@veil:/home/blogDeploy/public/panorama
|
||||||
|
|
||||||
|
JOURNAL_SOURCES := $(shell find . apps/journal -name "*.rs" -not -path "./target/*")
|
||||||
|
journal: $(JOURNAL_SOURCES)
|
||||||
|
cargo build \
|
||||||
|
--profile=wasm-debug \
|
||||||
|
-p panorama-journal \
|
||||||
|
--target=wasm32-unknown-unknown
|
||||||
|
|
||||||
|
test-install-apps: journal
|
||||||
|
cargo test -p panorama-core -- tests::test_install_apps
|
11
README.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
panorama
|
||||||
|
========
|
||||||
|
|
||||||
|
Personal information manager.
|
||||||
|
|
||||||
|
Contact
|
||||||
|
-------
|
||||||
|
|
||||||
|
Author: Michael Zhang
|
||||||
|
|
||||||
|
License: GPL-3.0-only
|
6
apps/calendar/manifest.yml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
name: panorama/calendar
|
||||||
|
|
||||||
|
depends:
|
||||||
|
- name: panorama
|
||||||
|
|
||||||
|
# code: dist/index.js
|
175
apps/codetrack/.gitignore
vendored
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
npm-debug.log_
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Caches
|
||||||
|
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
|
||||||
|
pids
|
||||||
|
_.pid
|
||||||
|
_.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
15
apps/codetrack/README.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# codetrack
|
||||||
|
|
||||||
|
To install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
To run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
This project was created using `bun init` in bun v1.0.25. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
BIN
apps/codetrack/bun.lockb
Executable file
27
apps/codetrack/index.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import type { Context } from "koa";
|
||||||
|
import type {} from "@koa/bodyparser";
|
||||||
|
|
||||||
|
export async function createHeartbeats(ctx: Context) {
|
||||||
|
const results = [];
|
||||||
|
for (const heartbeat of ctx.request.body) {
|
||||||
|
console.log("heartbeat", heartbeat);
|
||||||
|
const time = new Date(heartbeat.time * 1000.0);
|
||||||
|
const resp = await fetch("http://localhost:3000/node", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
attributes: [
|
||||||
|
["panorama::time/start", time.toISOString()],
|
||||||
|
["panorama/codetrack::project", heartbeat.project],
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
results.push({
|
||||||
|
id: data.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ctx.status = 400;
|
||||||
|
// console.log("results", results);
|
||||||
|
ctx.body = {};
|
||||||
|
}
|
19
apps/codetrack/manifest.yml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
name: panorama/codetrack
|
||||||
|
|
||||||
|
depends:
|
||||||
|
- name: panorama
|
||||||
|
|
||||||
|
code: dist/index.js
|
||||||
|
|
||||||
|
attributes:
|
||||||
|
- name: heartbeat
|
||||||
|
type: interface
|
||||||
|
requires:
|
||||||
|
- panorama::time/start
|
||||||
|
|
||||||
|
- name: project
|
||||||
|
type: string
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
- route: /api/v1/users/current/heartbeats.bulk
|
||||||
|
handler: createHeartbeats
|
15
apps/codetrack/package.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "codetrack",
|
||||||
|
"module": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"@types/koa": "^2.15.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@koa/bodyparser": "^5.1.1"
|
||||||
|
}
|
||||||
|
}
|
22
apps/codetrack/tsconfig.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
}
|
||||||
|
}
|
1
apps/journal/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
index.js
|
BIN
apps/journal/bun.lockb
Executable file
41
apps/journal/index.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import type { Context } from "koa";
|
||||||
|
import { formatDate } from "date-fns";
|
||||||
|
import { uuidv7 } from "uuidv7";
|
||||||
|
|
||||||
|
export async function today(ctx: Context) {
|
||||||
|
const date = new Date();
|
||||||
|
const day = formatDate(date, "P");
|
||||||
|
|
||||||
|
const resp = await fetch("http://localhost:3000/node/sql", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: `
|
||||||
|
select * from node_has_attribute as na
|
||||||
|
join attribute as a on na.attrName = a.name
|
||||||
|
where a.name = 'day' and na.string = '${day}';
|
||||||
|
`,
|
||||||
|
parameters: [],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { rows } = await resp.json();
|
||||||
|
if (rows.length === 0) {
|
||||||
|
const id = uuidv7();
|
||||||
|
const resp = await fetch("http://localhost:3000/node/sql", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: `
|
||||||
|
begin transaction;
|
||||||
|
insert into node (id) values (?);
|
||||||
|
end transaction;
|
||||||
|
`,
|
||||||
|
parameters: [id],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
console.log("Result", data);
|
||||||
|
}
|
||||||
|
ctx.body = {};
|
||||||
|
}
|
10
apps/journal/manifest.yml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
name: panorama/journal
|
||||||
|
code: index.js
|
||||||
|
|
||||||
|
attributes:
|
||||||
|
- name: day
|
||||||
|
type: Option<String>
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
- route: /today
|
||||||
|
handler: today
|
9
apps/journal/package.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"date-fns": "^3.6.0",
|
||||||
|
"koa": "^2.15.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/koa": "^2.15.0"
|
||||||
|
}
|
||||||
|
}
|
7
apps/std/manifest.yml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
name: panorama
|
||||||
|
|
||||||
|
attributes:
|
||||||
|
- name: time/start
|
||||||
|
type: datetime
|
||||||
|
- name: time/end
|
||||||
|
type: datetime
|
18
biome.json
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/1.4.1/schema.json",
|
||||||
|
"organizeImports": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentWidth": 2,
|
||||||
|
"indentStyle": "space",
|
||||||
|
"lineWidth": 80
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
bun.lockb
Executable file
|
@ -1,16 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "panorama-daemon"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = "1.0.42"
|
|
||||||
serde = { version = "1.0.126", features = ["derive"] }
|
|
||||||
tokio = { version = "1.9.0", features = ["full"] }
|
|
||||||
clap = "3.0.0-beta.2"
|
|
||||||
futures = "0.3.16"
|
|
||||||
inotify = { version = "0.9.3", features = ["stream"] }
|
|
||||||
xdg = "2.2.0"
|
|
||||||
log = "0.4.14"
|
|
||||||
toml = "0.5.8"
|
|
||||||
stderrlog = "0.5.1"
|
|
|
@ -1,84 +0,0 @@
|
||||||
mod watcher;
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Read;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
pub use self::watcher::spawn_config_watcher_system;
|
|
||||||
|
|
||||||
/// Configuration
|
|
||||||
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
|
|
||||||
pub struct Config {
|
|
||||||
/// Version of the config to use
|
|
||||||
/// (potentially for migration later?)
|
|
||||||
pub version: String,
|
|
||||||
|
|
||||||
/// Directory to store panorama-related data in
|
|
||||||
pub data_dir: PathBuf,
|
|
||||||
|
|
||||||
/// Mail accounts
|
|
||||||
#[serde(rename = "mail")]
|
|
||||||
pub mail_accounts: HashMap<String, MailAccountConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub async fn from_file(path: impl AsRef<Path>) -> Result<Self> {
|
|
||||||
let mut file = File::open(path.as_ref())?;
|
|
||||||
let mut contents = Vec::new();
|
|
||||||
file.read_to_end(&mut contents)?;
|
|
||||||
let config = toml::from_slice(&contents)?;
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration for a single mail account
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
||||||
pub struct MailAccountConfig {
|
|
||||||
/// Imap
|
|
||||||
pub imap: ImapConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuring an IMAP server
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
||||||
pub struct ImapConfig {
|
|
||||||
/// Host of the IMAP server (needs to be hostname for TLS)
|
|
||||||
pub server: String,
|
|
||||||
|
|
||||||
/// Port of the IMAP server
|
|
||||||
pub port: u16,
|
|
||||||
|
|
||||||
/// TLS
|
|
||||||
pub tls: TlsMethod,
|
|
||||||
|
|
||||||
/// Auth
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub auth: ImapAuth,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Method of authentication for the IMAP server
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
||||||
#[serde(tag = "auth")]
|
|
||||||
pub enum ImapAuth {
|
|
||||||
/// Use plain username/password authentication
|
|
||||||
#[serde(rename = "plain")]
|
|
||||||
Plain { username: String, password: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Describes when to perform the TLS handshake
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
||||||
pub enum TlsMethod {
|
|
||||||
/// Perform TLS handshake immediately upon connection
|
|
||||||
#[serde(rename = "on")]
|
|
||||||
On,
|
|
||||||
|
|
||||||
/// Perform TLS handshake after issuing the STARTTLS command
|
|
||||||
#[serde(rename = "starttls")]
|
|
||||||
Starttls,
|
|
||||||
|
|
||||||
/// Don't perform TLS handshake at all (unsecured)
|
|
||||||
#[serde(rename = "off")]
|
|
||||||
Off,
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use futures::{future::TryFutureExt, stream::StreamExt};
|
|
||||||
use inotify::{Inotify, WatchMask};
|
|
||||||
use tokio::{sync::watch, task::JoinHandle};
|
|
||||||
use xdg::BaseDirectories;
|
|
||||||
|
|
||||||
use super::Config;
|
|
||||||
|
|
||||||
pub type ConfigWatcher = watch::Receiver<Config>;
|
|
||||||
|
|
||||||
/// Start the entire config watcher system, and return a [ConfigWatcher][self::ConfigWatcher],
|
|
||||||
/// which is a cloneable receiver of config update events.
|
|
||||||
pub fn spawn_config_watcher_system() -> Result<(JoinHandle<()>, ConfigWatcher)> {
|
|
||||||
let mut inotify = Inotify::init()?;
|
|
||||||
let xdg = BaseDirectories::new()?;
|
|
||||||
let config_home = xdg.get_config_home().join("panorama");
|
|
||||||
if !config_home.exists() {
|
|
||||||
fs::create_dir_all(&config_home)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
inotify
|
|
||||||
.add_watch(&config_home, WatchMask::CLOSE_WRITE)
|
|
||||||
.context("adding watch for config home")?;
|
|
||||||
|
|
||||||
debug!("watching {:?}", config_home);
|
|
||||||
let (config_tx, config_update) = watch::channel(Config::default());
|
|
||||||
let handle = tokio::spawn(
|
|
||||||
start_inotify_stream(inotify, config_home, config_tx).unwrap_or_else(|_err| todo!()),
|
|
||||||
);
|
|
||||||
Ok((handle, config_update))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_inotify_stream(
|
|
||||||
mut inotify: Inotify,
|
|
||||||
config_home: impl AsRef<Path>,
|
|
||||||
config_tx: watch::Sender<Config>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut buffer = vec![0u8; 1024];
|
|
||||||
let mut event_stream = inotify.event_stream(&mut buffer)?;
|
|
||||||
let config_home = config_home.as_ref().to_path_buf();
|
|
||||||
let config_path = config_home.join("panorama.toml");
|
|
||||||
|
|
||||||
// first shot
|
|
||||||
{
|
|
||||||
let config = Config::from_file(&config_path).await?;
|
|
||||||
config_tx.send(config)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("listening for inotify events");
|
|
||||||
while let Some(v) = event_stream.next().await {
|
|
||||||
let event = v.context("event")?;
|
|
||||||
debug!("inotify event: {:?}", event);
|
|
||||||
if let Some(name) = event.name {
|
|
||||||
let path = PathBuf::from(name);
|
|
||||||
let path_c = config_home
|
|
||||||
.clone()
|
|
||||||
.join(path.clone())
|
|
||||||
.canonicalize()
|
|
||||||
.context("osu")?;
|
|
||||||
if !path_c.exists() {
|
|
||||||
debug!("path {:?} doesn't exist", path_c);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// TODO: any better way to do this?
|
|
||||||
let config_path_c = config_path.canonicalize().context("cfg_path")?;
|
|
||||||
if config_path_c != path_c {
|
|
||||||
debug!("did not match {:?} {:?}", config_path_c, path_c);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("reading config from {:?}", path_c);
|
|
||||||
let config = Config::from_file(path_c).await.context("read")?;
|
|
||||||
// debug!("sending config {:?}", config);
|
|
||||||
config_tx.send(config)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
pub struct ImapClient {}
|
|
|
@ -1,66 +0,0 @@
|
||||||
#[macro_use]
|
|
||||||
extern crate serde;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate log;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate futures;
|
|
||||||
|
|
||||||
mod config;
|
|
||||||
mod imap;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use clap::Clap;
|
|
||||||
use futures::future::FutureExt;
|
|
||||||
use tokio::sync::oneshot;
|
|
||||||
|
|
||||||
use crate::config::Config;
|
|
||||||
|
|
||||||
type ExitListener = oneshot::Receiver<()>;
|
|
||||||
|
|
||||||
/// The panorama daemon runs in the background and communicates with other panorama components over Unix sockets.
|
|
||||||
#[derive(Debug, Clap)]
|
|
||||||
struct Options {
|
|
||||||
// /// Config file path (defaults to XDG)
|
|
||||||
// #[clap(long = "config", short = 'c')]
|
|
||||||
// config_file: Option<PathBuf>,
|
|
||||||
/// Verbose mode (-v, -vv, -vvv, etc)
|
|
||||||
#[clap(short = 'v', long = "verbose", parse(from_occurrences))]
|
|
||||||
verbose: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<()> {
|
|
||||||
let opt = Options::parse();
|
|
||||||
println!("{:?}", opt);
|
|
||||||
|
|
||||||
stderrlog::new()
|
|
||||||
.module(module_path!())
|
|
||||||
.verbosity(opt.verbose)
|
|
||||||
.init()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let (_, mut config_watcher) = config::spawn_config_watcher_system()?;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let (exit_tx, exit_rx) = oneshot::channel();
|
|
||||||
let new_config = config_watcher.borrow().clone();
|
|
||||||
tokio::spawn(run_with_config(new_config, exit_rx));
|
|
||||||
|
|
||||||
// wait till the config has changed, then tell the current thread to stop
|
|
||||||
config_watcher.changed().await?;
|
|
||||||
let _ = exit_tx.send(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_with_config(config: Config, exit: ExitListener) -> Result<()> {
|
|
||||||
println!("run with config: {:?}", config);
|
|
||||||
|
|
||||||
let mut exit = exit.fuse();
|
|
||||||
loop {
|
|
||||||
select! {
|
|
||||||
_ = exit => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
21
docs/.gitignore
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
4
docs/.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
11
docs/.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
37
docs/astro.config.mjs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { defineConfig } from "astro/config";
|
||||||
|
import starlight from "@astrojs/starlight";
|
||||||
|
import rehypeKatex from "rehype-katex";
|
||||||
|
import remarkMath from "remark-math";
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
base: process.env.BASE_URL ?? "/",
|
||||||
|
integrations: [
|
||||||
|
starlight({
|
||||||
|
title: "Panorama",
|
||||||
|
social: {
|
||||||
|
github: "https://git.mzhang.io/michael/panorama",
|
||||||
|
},
|
||||||
|
sidebar: [
|
||||||
|
{ label: "The panorama dream", link: "/dream" },
|
||||||
|
{
|
||||||
|
label: "High Level Design",
|
||||||
|
autogenerate: { directory: "high-level-design" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Technical Docs",
|
||||||
|
autogenerate: { directory: "technical-docs" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Protocols",
|
||||||
|
autogenerate: { directory: "protocols" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
customCss: ["./node_modules/katex/dist/katex.min.css"],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
markdown: {
|
||||||
|
remarkPlugins: [remarkMath],
|
||||||
|
rehypePlugins: [rehypeKatex],
|
||||||
|
},
|
||||||
|
});
|
BIN
docs/bun.lockb
Executable file
22
docs/package.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "docs",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro check && astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/check": "^0.7.0",
|
||||||
|
"@astrojs/starlight": "^0.24.5",
|
||||||
|
"astro": "^4.10.2",
|
||||||
|
"katex": "^0.16.10",
|
||||||
|
"rehype-katex": "^7.0.0",
|
||||||
|
"remark-math": "^6.0.0",
|
||||||
|
"sharp": "^0.32.5",
|
||||||
|
"typescript": "^5.5.2"
|
||||||
|
}
|
||||||
|
}
|
1
docs/public/favicon.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill-rule="evenodd" d="M81 36 64 0 47 36l-1 2-9-10a6 6 0 0 0-9 9l10 10h-2L0 64l36 17h2L28 91a6 6 0 1 0 9 9l9-10 1 2 17 36 17-36v-2l9 10a6 6 0 1 0 9-9l-9-9 2-1 36-17-36-17-2-1 9-9a6 6 0 1 0-9-9l-9 10v-2Zm-17 2-2 5c-4 8-11 15-19 19l-5 2 5 2c8 4 15 11 19 19l2 5 2-5c4-8 11-15 19-19l5-2-5-2c-8-4-15-11-19-19l-2-5Z" clip-rule="evenodd"/><path d="M118 19a6 6 0 0 0-9-9l-3 3a6 6 0 1 0 9 9l3-3Zm-96 4c-2 2-6 2-9 0l-3-3a6 6 0 1 1 9-9l3 3c3 2 3 6 0 9Zm0 82c-2-2-6-2-9 0l-3 3a6 6 0 1 0 9 9l3-3c3-2 3-6 0-9Zm96 4a6 6 0 0 1-9 9l-3-3a6 6 0 1 1 9-9l3 3Z"/><style>path{fill:#000}@media (prefers-color-scheme:dark){path{fill:#fff}}</style></svg>
|
After Width: | Height: | Size: 696 B |
BIN
docs/src/assets/houston.webp
Normal file
After Width: | Height: | Size: 96 KiB |
6
docs/src/content/config.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { defineCollection } from "astro:content";
|
||||||
|
import { docsSchema } from "@astrojs/starlight/schema";
|
||||||
|
|
||||||
|
export const collections = {
|
||||||
|
docs: defineCollection({ schema: docsSchema() }),
|
||||||
|
};
|
71
docs/src/content/docs/dream.md
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
---
|
||||||
|
title: The panorama dream
|
||||||
|
---
|
||||||
|
|
||||||
|
In the ideal world, you're reading this via panorama right now.
|
||||||
|
|
||||||
|
The panorama dream is to have an "everything" app that is fully managed by the user.
|
||||||
|
This page describes the vision for the app.
|
||||||
|
|
||||||
|
Almost everything on this list is something that I self host, or want to self
|
||||||
|
host, but hosts its own database separately. I want to unify the data source in
|
||||||
|
a very flexible way so that it can be shared among apps.
|
||||||
|
|
||||||
|
This app takes inspiration from many similar apps, such as Anytype, Logseq, Notion, etc.
|
||||||
|
|
||||||
|
## Features I want
|
||||||
|
|
||||||
|
- Graph view
|
||||||
|
- Instantly share/publish anything
|
||||||
|
- Full text+OCR search
|
||||||
|
- IFTTT workflows
|
||||||
|
- Notifications
|
||||||
|
- Multiuser
|
||||||
|
- Google docs like interface for docs / typst
|
||||||
|
|
||||||
|
## Development Principles
|
||||||
|
|
||||||
|
These are the goals for panorama development.
|
||||||
|
|
||||||
|
- **Local first.** Everything is first committed to a local database.
|
||||||
|
- **Keyboard friendly.**
|
||||||
|
- **Gradual adoption.**
|
||||||
|
|
||||||
|
## Custom Apps List
|
||||||
|
|
||||||
|
- File Backup
|
||||||
|
- Object storage
|
||||||
|
- Archivebox like system, bookmarking
|
||||||
|
- Journal
|
||||||
|
- Block-based editor
|
||||||
|
- Embed any node type into journal
|
||||||
|
- Food
|
||||||
|
- Recipe tracker
|
||||||
|
- Grocery list (adds to my todo list)
|
||||||
|
- Meal planner
|
||||||
|
- Food blogging
|
||||||
|
- Health+Fitness
|
||||||
|
- Running progress (incl. saving GPS waypoints)
|
||||||
|
- Workout log for various workouts
|
||||||
|
- Weight tracking
|
||||||
|
- Connect to smartwatch?
|
||||||
|
- Pictures
|
||||||
|
- Face recognition
|
||||||
|
- Map view
|
||||||
|
- Coding
|
||||||
|
- Code tracking like Wakatime
|
||||||
|
- Git forge???
|
||||||
|
- Calendar
|
||||||
|
- Calendly-like appointment booking system
|
||||||
|
- Social
|
||||||
|
- Store people into people app
|
||||||
|
- Email+matrix chat
|
||||||
|
- Video conferencing?
|
||||||
|
- Feed readers / RSS
|
||||||
|
- Media
|
||||||
|
- Music and video hosting / streaming i.e Navidrome
|
||||||
|
- Money tracking
|
||||||
|
- Education
|
||||||
|
- Anki flashcards
|
||||||
|
- Canvas???
|
||||||
|
- Dashboards
|
11
docs/src/content/docs/high-level-design/attributes.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
title: Attributes
|
||||||
|
---
|
||||||
|
|
||||||
|
The core idea behind panorama is that apps can choose to define attributes, which you can think of as slots.
|
||||||
|
|
||||||
|
The slots have some particular type, which can be filled with some node.
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
The absence of an attribute is different from the existence of the $\textsf{None}$ value.
|
||||||
|
:::
|
5
docs/src/content/docs/high-level-design/cryptography.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Cryptography
|
||||||
|
---
|
||||||
|
|
||||||
|
lol
|
30
docs/src/content/docs/high-level-design/device.md
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
title: Device
|
||||||
|
---
|
||||||
|
|
||||||
|
The panorama network keeps track of what devices join and leave the network.
|
||||||
|
|
||||||
|
Each device has certain attributes:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface DeviceConfig {
|
||||||
|
// Not used for anything important, just for displaying an icon if needed
|
||||||
|
formFactor: "desktop" | "server" | "laptop" | "phone" | "tablet" | string;
|
||||||
|
|
||||||
|
// A string that represents a duration of time. If it has been longer than
|
||||||
|
// this amount of time since last contacting this device, consider it to have
|
||||||
|
// gone offline
|
||||||
|
heartbeatDuration: string;
|
||||||
|
|
||||||
|
// Whether or not to schedule services to this device
|
||||||
|
canRunServices: boolean;
|
||||||
|
|
||||||
|
// Whether or not this device should be treated as a file store
|
||||||
|
// (recommended to be off for phones)
|
||||||
|
canStoreFiles: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each device keeps track of each other device, with a merkle tree of signatures.
|
||||||
|
|
||||||
|
Devices have their own keypairs. TODO: See how matrix does cross-signing
|
18
docs/src/content/docs/high-level-design/indexing.md
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
title: Indexing
|
||||||
|
---
|
||||||
|
|
||||||
|
There are several types of indexes in panorama.
|
||||||
|
Some are the database kind that updates immediately.
|
||||||
|
Others are the search kind that updates asynchronously.
|
||||||
|
|
||||||
|
Custom app authors can specify how their attributes should be indexed.
|
||||||
|
Then, whenever any node has that particular attribute touched, a hook is run.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
In the initial version of panorama, the daemon is thought of as having exclusive
|
||||||
|
control over the database. It should not be run as multiple copies of itself either.
|
||||||
|
|
||||||
|
This way, the daemon can separately control indexes if it wishes, allowing it to
|
||||||
|
call custom functions for indexing.
|
21
docs/src/content/docs/high-level-design/nodes.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
title: Nodes
|
||||||
|
---
|
||||||
|
|
||||||
|
Everything is organized into nodes.
|
||||||
|
|
||||||
|
Each app (journal, mail, etc.) creates nodes to represent their information.
|
||||||
|
These nodes are linked to each other through attributes.
|
||||||
|
|
||||||
|
When retrieving its contents, a closure-like query is conducted and all the
|
||||||
|
nodes reachable through its attributes are returned.
|
||||||
|
|
||||||
|
Think of a node as being represented like this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Node {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
attributes: string[];
|
||||||
|
}
|
||||||
|
```
|
13
docs/src/content/docs/high-level-design/onboarding.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
title: Onboarding
|
||||||
|
---
|
||||||
|
|
||||||
|
## Creating a new database
|
||||||
|
|
||||||
|
1. Download the software
|
||||||
|
2. It should automatically boot into a new database
|
||||||
|
- Automatically connect to the hosted panorama bridge service
|
||||||
|
3. Give the user the option to log into an existing database, and then allow them to merge
|
||||||
|
|
||||||
|
## Adding another device
|
||||||
|
|
10
docs/src/content/docs/high-level-design/permissions.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
title: Permissions
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Apps should probably not be allowed to read attributes they didn't explicitly request access to
|
||||||
|
- (there should be an option "Unless they created the node")
|
||||||
|
|
||||||
|
## Design
|
14
docs/src/content/docs/high-level-design/principles.md
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
title: Design Principles
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **Never use fully-qualified names starting from domain (i.e `com.example.package`).**
|
||||||
|
This makes it so migrating domains / package names becomes very hard.
|
||||||
|
|
||||||
|
## Data governance
|
||||||
|
|
||||||
|
- **Offline first, full control to the user.**
|
||||||
|
Synchronization is an important feature but must be built as a separate thing.
|
||||||
|
This also means that it should be possible for some devices to stay offline for long periods of time.
|
15
docs/src/content/docs/high-level-design/sync.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
title: Sync
|
||||||
|
---
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
This is documentation for a feature that is in development.
|
||||||
|
|
||||||
|
Almost none of this is implemented and most of it will probably change in the future.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Node-level sync
|
||||||
|
|
||||||
|
## Attribute-level sync
|
||||||
|
|
||||||
|
## Index-level sync
|
72
docs/src/content/docs/high-level-design/types.md
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
---
|
||||||
|
title: Types
|
||||||
|
---
|
||||||
|
|
||||||
|
Types exist to ensure that apps are treating data properly.
|
||||||
|
|
||||||
|
## Formal definition
|
||||||
|
|
||||||
|
An attribute's type can be one of the following:
|
||||||
|
|
||||||
|
$\tau :\equiv$
|
||||||
|
|
||||||
|
- $c$ (constant)
|
||||||
|
- $\alpha$ (type variable)
|
||||||
|
- $\mu \alpha . \tau$ (inductive type)
|
||||||
|
- $( \ell_k : \tau_k )_k$ (record type)
|
||||||
|
- $\{ \ell_k : \tau_k \}_k$ (sum type)
|
||||||
|
- $\#n$ (singleton type)
|
||||||
|
|
||||||
|
Constants may be node references, unit, unsigned/signed integers, decimal,
|
||||||
|
strings, booleans, instant (or timezone-aware timestamp), or URL
|
||||||
|
|
||||||
|
It is possible in the future that node references are also made using URLs, but
|
||||||
|
the URL format will need to be decided upon by then.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Nodes don't have types; only attributes do.
|
||||||
|
- All attributes must belong to _closed_ types.
|
||||||
|
This means type variables cannot exist at the top-level.
|
||||||
|
- When shown in the panorama UI, the constant type will not be shown as a separate type.
|
||||||
|
Instead the actual type itself will be inlined.
|
||||||
|
- The type registry doesn't canonically exist in the database (it may exist in the form of system logs).
|
||||||
|
Instead, apps register their types on boot.
|
||||||
|
Everything is known to the panorama daemon after app initialization.
|
||||||
|
- The following constant types have their fields embedded directly into the node table:
|
||||||
|
- Number (integer, bigdecimal), string, boolean: `value`
|
||||||
|
- Sum: `label` (which variant is used?)
|
||||||
|
- Record types are essentially a collection of forced attributes.
|
||||||
|
A node with a record type _must_ contain every field listed in the labels of the record type.
|
||||||
|
- The panorama type system is _structurally_ typed.
|
||||||
|
#TODO Maybe add some convenient way of introducing ways to distinguish types apart?
|
||||||
|
|
||||||
|
### Convenient types
|
||||||
|
|
||||||
|
- $\textsf{Optional}(\tau) :\equiv \{ \texttt{'none} : () , \texttt{'some} : \tau \}$ \
|
||||||
|
The optional type.
|
||||||
|
|
||||||
|
### What is the point of a singleton type?
|
||||||
|
|
||||||
|
Singleton types only consist of a node ID.
|
||||||
|
The point of this is so apps can create types that are forced to have exactly a single node.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
Apps with dashboards (mail) may create a type that represents the "entrypoint" into their application.
|
||||||
|
The process of creating it would look like this:
|
||||||
|
|
||||||
|
+ Upon app registration, I declare that I want a singleton type to be registered as `panorama-mail/entry`.
|
||||||
|
+ A node id will be assigned, if it doesn't already exist.
|
||||||
|
+ The application is returned the node ID.
|
||||||
|
+ The application can then register links to that node ID, and it can register a handler.
|
||||||
|
:::
|
||||||
|
|
||||||
|
When an app is registered, its types are parsed and registered into the database.
|
||||||
|
At the time of writing, if the node ID it refers to has already been found in the database, the type of the node will be checked against the given type.
|
||||||
|
If it doesn't match #TODO
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
Nodes contain attributes.
|
||||||
|
An attribute is a link to another node.
|
||||||
|
Attributes are typed, and the node it's linked to must have that type.
|
14
docs/src/content/docs/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
title: Welcome to Panorama
|
||||||
|
description: Get started building your docs site with Starlight.
|
||||||
|
template: splash
|
||||||
|
hero:
|
||||||
|
tagline: I love scope creep...
|
||||||
|
image:
|
||||||
|
file: ../../assets/houston.webp
|
||||||
|
actions:
|
||||||
|
- text: Read the docs
|
||||||
|
link: ./dream
|
||||||
|
icon: right-arrow
|
||||||
|
variant: primary
|
||||||
|
---
|
5
docs/src/content/docs/protocols/client_bridge.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Client-Bridge Protocols
|
||||||
|
---
|
||||||
|
|
||||||
|
A **bridge** is just a way of connecting two devices.
|
4
docs/src/content/docs/protocols/client_client.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Client-Client Protocols
|
||||||
|
---
|
||||||
|
|
25
docs/src/content/docs/technical-docs/custom_app_hooks.md
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
title: Custom app API
|
||||||
|
---
|
||||||
|
|
||||||
|
## Registration
|
||||||
|
|
||||||
|
The following types of things can be registered by the app:
|
||||||
|
|
||||||
|
- Named types
|
||||||
|
- Hooks (described below)
|
||||||
|
- Background services
|
||||||
|
- Frontend
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
|
||||||
|
Custom apps are allowed to hook into the following events:
|
||||||
|
|
||||||
|
- `install`: When the app is first being installed.
|
||||||
|
|
||||||
|
- `insert`, `update`, `delete`: CRUD hooks for nodes with a type that the app manages
|
||||||
|
|
||||||
|
- `attr-new`, `attr-update`, `attr-remove`: CRUD hooks for attributes with types that the app manages
|
||||||
|
|
||||||
|
Each hook is handled by a function, which must return with a success. If this
|
||||||
|
doesn't happen, the daemon will re-call the function with exponential backoff for a specific number of retries.
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
title: Custom app sandboxing
|
||||||
|
---
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
For the initial releases of panorama, I am not planning on including _any_
|
||||||
|
sandboxing whatsoever. The development overhead will be far too great to warrant supporting it.
|
||||||
|
|
||||||
|
The entire app _will_ be rewritten before the public alpha release, which will
|
||||||
|
include proper custom app sandboxing. This page lists some ideas.
|
||||||
|
:::
|
||||||
|
|
||||||
|
Custom apps are made up of two parts:
|
||||||
|
|
||||||
|
- The backend, which talks to the database
|
||||||
|
- The frontend, which talks to the user
|
||||||
|
|
||||||
|
I say "the" frontend, but there could possibly be multiple frontends. (TUI, headless, etc.)
|
||||||
|
Each part needs to be sandboxed individually.
|
||||||
|
|
||||||
|
## Backend sandboxing
|
||||||
|
|
||||||
|
This will be done via a WASM runtime. The custom app's backend software will
|
||||||
|
|
||||||
|
## Frontend sandboxing
|
||||||
|
|
||||||
|
lmao not sure if this is possible with a web-based host at all, looking into flutter...
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Formal verification
|
||||||
|
---
|
||||||
|
|
||||||
|
lol
|
6
docs/src/content/docs/technical-docs/loading_process.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
title: Loading process
|
||||||
|
---
|
||||||
|
|
||||||
|
The goal of panorama is to start up as quickly as possible.
|
||||||
|
The following tasks need to be performed on start:
|
5
docs/src/content/docs/technical-docs/notifications.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Notifications
|
||||||
|
---
|
||||||
|
|
||||||
|
https://unifiedpush.org/
|
10
docs/src/content/docs/technical-docs/protected_namespace.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
title: Protected namespaces
|
||||||
|
---
|
||||||
|
|
||||||
|
There's some protected namespace of nodes that's used to keep track of the
|
||||||
|
actual database functionality. For example:
|
||||||
|
|
||||||
|
- List of installed apps
|
||||||
|
- List of currently registered types (maybe not keep this?)
|
||||||
|
- System log
|
2
docs/src/env.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
|
/// <reference types="astro/client" />
|
5
docs/tsconfig.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"compilerOptions": { "skipLibCheck": true },
|
||||||
|
"exclude": ["dist"]
|
||||||
|
}
|
98
flake.lock
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"fenix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1719469637,
|
||||||
|
"narHash": "sha256-cOA40mIqjIIf+mCdtuglxdP/0to1LDL1Lkef7vqVykc=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"rev": "3374c72204714eb979719e77a1856009584ba4d7",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1710146030,
|
||||||
|
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "flake-utils",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1719554759,
|
||||||
|
"narHash": "sha256-B64IsJMis4A9dePPOKi2T5EEs9AJWfsvkMKSh9/NANs=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "44677ecde6c8a7a7e32f9a2709c316975bf89a60",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"fenix": "fenix",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-analyzer-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1719378198,
|
||||||
|
"narHash": "sha256-c1jWpdPlZyL6/a0pWa30680ivP7nMLNBPuz5hMGoifg=",
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"rev": "b33a0cae335b85e11a700df2d9a7c0006a3b80ec",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"ref": "nightly",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
42
flake.nix
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs";
|
||||||
|
fenix = {
|
||||||
|
url = "github:nix-community/fenix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils, fenix }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [ fenix.overlays.default ];
|
||||||
|
};
|
||||||
|
|
||||||
|
toolchain = pkgs.fenix.stable;
|
||||||
|
|
||||||
|
flakePkgs = {
|
||||||
|
#markout = pkgs.callPackage ./. { inherit toolchain; };
|
||||||
|
};
|
||||||
|
in rec {
|
||||||
|
packages = flake-utils.lib.flattenTree flakePkgs;
|
||||||
|
|
||||||
|
devShell = pkgs.mkShell {
|
||||||
|
inputsFrom = with packages;
|
||||||
|
[
|
||||||
|
#markout
|
||||||
|
];
|
||||||
|
packages = (with pkgs; [
|
||||||
|
cargo-watch
|
||||||
|
cargo-deny
|
||||||
|
cargo-edit
|
||||||
|
corepack
|
||||||
|
nodejs_20
|
||||||
|
sqlx-cli
|
||||||
|
go
|
||||||
|
]) ++ (with toolchain; [ cargo rustc rustfmt clippy ]);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
13
package.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"name": "panorama",
|
||||||
|
"private": true,
|
||||||
|
"workspaces": [
|
||||||
|
"packages/*",
|
||||||
|
"apps/*"
|
||||||
|
],
|
||||||
|
"trustedDependencies": [
|
||||||
|
"electron",
|
||||||
|
"esbuild",
|
||||||
|
"sqlite3"
|
||||||
|
]
|
||||||
|
}
|
175
packages/panorama-daemon/.gitignore
vendored
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
npm-debug.log_
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Caches
|
||||||
|
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
|
||||||
|
pids
|
||||||
|
_.pid
|
||||||
|
_.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
15
packages/panorama-daemon/README.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# panorama-daemon
|
||||||
|
|
||||||
|
To install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
To run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
This project was created using `bun init` in bun v1.0.25. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
BIN
packages/panorama-daemon/bun.lockb
Executable file
31
packages/panorama-daemon/package.json
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "panorama-daemon",
|
||||||
|
"module": "src/index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^1.8.3",
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"@types/koa-json": "^2.0.23",
|
||||||
|
"@types/koa__cors": "^5.0.0",
|
||||||
|
"@types/koa__router": "^12.0.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@koa/bodyparser": "^5.1.1",
|
||||||
|
"@koa/cors": "^5.0.0",
|
||||||
|
"@koa/router": "^12.0.1",
|
||||||
|
"koa": "^2.15.3",
|
||||||
|
"koa-json": "^2.0.2",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"sqlite3": "^5.1.7",
|
||||||
|
"typeorm": "^0.3.20",
|
||||||
|
"uuidv7": "^1.0.1",
|
||||||
|
"yaml": "^2.4.5",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"trustedDependencies": [
|
||||||
|
"@biomejs/biome"
|
||||||
|
]
|
||||||
|
}
|
101
packages/panorama-daemon/src/apps/index.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import { join, dirname } from "node:path";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
import { readdir, readFile } from "node:fs/promises";
|
||||||
|
import Router from "@koa/router";
|
||||||
|
import { manifestSchema, type Manifest } from "./manifest";
|
||||||
|
import { dataSource } from "../db";
|
||||||
|
import { App, Attribute } from "../models";
|
||||||
|
|
||||||
|
export interface CustomApp extends Manifest {
|
||||||
|
sanitizedName: string;
|
||||||
|
router: Router;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeName(name: string): string {
|
||||||
|
return name.replaceAll("/", "__");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadApps(): Promise<Map<string, CustomApp>> {
|
||||||
|
const apps = new Map();
|
||||||
|
const paths = [
|
||||||
|
join(dirname(dirname(dirname(dirname(dirname(import.meta.path))))), "apps"),
|
||||||
|
"/Users/michael/Projects/panorama/apps",
|
||||||
|
];
|
||||||
|
|
||||||
|
async function getChildren(dir: string): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
return await readdir(dir);
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const basePath of paths) {
|
||||||
|
const children = await getChildren(basePath);
|
||||||
|
for (const name of children) {
|
||||||
|
const child = join(basePath, name);
|
||||||
|
try {
|
||||||
|
const app = await loadApp(child);
|
||||||
|
apps.set(app.name, app);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error setting up ${child}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadApp(path: string): Promise<CustomApp> {
|
||||||
|
console.log("Loading app from", path);
|
||||||
|
const manifestPath = join(path, "manifest.yml");
|
||||||
|
const manifestRaw = parse(await readFile(manifestPath, "utf-8"));
|
||||||
|
const manifest = manifestSchema.parse(manifestRaw);
|
||||||
|
const sanitizedName = sanitizeName(manifest.name);
|
||||||
|
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
// load code
|
||||||
|
if (manifest.code) {
|
||||||
|
const codePath = join(path, manifest.code);
|
||||||
|
const codeModule = await import(codePath);
|
||||||
|
|
||||||
|
// wire up routes
|
||||||
|
for (const endpoint of manifest.endpoints || []) {
|
||||||
|
const func = codeModule[endpoint.handler];
|
||||||
|
router.all(endpoint.route, func);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await dataSource.transaction(async (em) => {
|
||||||
|
const app = await em
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select("app")
|
||||||
|
.from(App, "app")
|
||||||
|
.where("app.id = :id", { id: sanitizedName })
|
||||||
|
.getOne();
|
||||||
|
let appId = app?.id;
|
||||||
|
|
||||||
|
if (!appId) {
|
||||||
|
const result = await em.getRepository(App).insert({
|
||||||
|
id: sanitizedName,
|
||||||
|
name: manifest.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
appId = result.identifiers[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!appId) throw new Error("could not initialize");
|
||||||
|
|
||||||
|
// register all the attributes
|
||||||
|
for (const attribute of manifest.attributes || []) {
|
||||||
|
await em
|
||||||
|
.getRepository(Attribute)
|
||||||
|
.upsert({ appId, name: attribute.name, type: attribute.type }, [
|
||||||
|
"appId",
|
||||||
|
"name",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...manifest, sanitizedName, router };
|
||||||
|
}
|
26
packages/panorama-daemon/src/apps/manifest.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const manifestSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
code: z.string().optional(),
|
||||||
|
|
||||||
|
attributes: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
|
||||||
|
endpoints: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
route: z.string(),
|
||||||
|
handler: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Manifest = z.infer<typeof manifestSchema>;
|
16
packages/panorama-daemon/src/db.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { DataSource } from "typeorm";
|
||||||
|
import { App, Attribute, PNode, NodeHasAttribute } from "./models";
|
||||||
|
|
||||||
|
const AppDataSource = new DataSource({
|
||||||
|
type: "sqlite",
|
||||||
|
database: "test.db",
|
||||||
|
|
||||||
|
entities: [PNode, App, Attribute, NodeHasAttribute],
|
||||||
|
synchronize: true,
|
||||||
|
logging: true,
|
||||||
|
|
||||||
|
migrationsTableName: "migrations",
|
||||||
|
migrations: ["migrations/*"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const dataSource = await AppDataSource.initialize();
|
50
packages/panorama-daemon/src/index.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import Koa, { type Context } from "koa";
|
||||||
|
import cors from "@koa/cors";
|
||||||
|
import json from "koa-json";
|
||||||
|
import Router from "@koa/router";
|
||||||
|
import { dataSource } from "./db";
|
||||||
|
import { PNode } from "./models";
|
||||||
|
import { nodeRouter } from "./routes/node";
|
||||||
|
import { loadApps, sanitizeName } from "./apps";
|
||||||
|
import { bodyParser } from "@koa/bodyparser";
|
||||||
|
|
||||||
|
const app = new Koa();
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(json());
|
||||||
|
app.use(bodyParser());
|
||||||
|
|
||||||
|
app.use(async (ctx, next) => {
|
||||||
|
console.log(
|
||||||
|
"Got a request from %s for %s",
|
||||||
|
ctx.request.ip,
|
||||||
|
ctx.method,
|
||||||
|
ctx.path,
|
||||||
|
);
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
const apps = await loadApps();
|
||||||
|
for (const [name, customApp] of apps.entries()) {
|
||||||
|
console.log("name", name);
|
||||||
|
const sanitizedName = sanitizeName(name);
|
||||||
|
router.use(`/apps/${sanitizedName}`, customApp.router.routes());
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get("/", async (ctx: Context) => {
|
||||||
|
const nodeRepo = dataSource.getRepository(PNode);
|
||||||
|
const numNodes = await nodeRepo.count();
|
||||||
|
ctx.body = { nodes: numNodes };
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use("/node", nodeRouter.routes());
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"routes",
|
||||||
|
router.stack.map((i) => i.path),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use(router.routes()).use(router.allowedMethods());
|
||||||
|
|
||||||
|
app.listen(3000);
|
118
packages/panorama-daemon/src/models.ts
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
Index,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
PrimaryColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from "typeorm";
|
||||||
|
|
||||||
|
@Entity({ name: "node" })
|
||||||
|
export class PNode {
|
||||||
|
@PrimaryColumn()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
lastUpdated!: Date;
|
||||||
|
|
||||||
|
@OneToMany(
|
||||||
|
() => NodeHasAttribute,
|
||||||
|
(hasAttr) => hasAttr.node,
|
||||||
|
)
|
||||||
|
attributes!: NodeHasAttribute[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class App {
|
||||||
|
@PrimaryColumn()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
@Index({})
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@OneToMany(
|
||||||
|
() => Attribute,
|
||||||
|
(attr) => attr.app,
|
||||||
|
)
|
||||||
|
attributes!: Attribute[];
|
||||||
|
|
||||||
|
@OneToMany(
|
||||||
|
() => NodeHasAttribute,
|
||||||
|
(attr) => attr.app,
|
||||||
|
)
|
||||||
|
attributeInstances!: NodeHasAttribute[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Attribute {
|
||||||
|
@PrimaryColumn()
|
||||||
|
appId!: string;
|
||||||
|
|
||||||
|
@PrimaryColumn()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
@Index({})
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
@ManyToOne(
|
||||||
|
() => App,
|
||||||
|
(app) => app.attributes,
|
||||||
|
)
|
||||||
|
@JoinColumn({ name: "appId", referencedColumnName: "id" })
|
||||||
|
app!: App;
|
||||||
|
|
||||||
|
@OneToMany(
|
||||||
|
() => NodeHasAttribute,
|
||||||
|
(attr) => attr.attr,
|
||||||
|
)
|
||||||
|
instances!: NodeHasAttribute[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class NodeHasAttribute {
|
||||||
|
@ManyToOne(
|
||||||
|
() => PNode,
|
||||||
|
(node) => node.attributes,
|
||||||
|
)
|
||||||
|
@JoinColumn({ name: "nodeId" })
|
||||||
|
node!: PNode;
|
||||||
|
|
||||||
|
@PrimaryColumn()
|
||||||
|
nodeId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(
|
||||||
|
() => App,
|
||||||
|
(app) => app.attributeInstances,
|
||||||
|
)
|
||||||
|
@JoinColumn({ name: "appId", referencedColumnName: "id" })
|
||||||
|
app!: App;
|
||||||
|
|
||||||
|
@PrimaryColumn()
|
||||||
|
appId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(
|
||||||
|
() => Attribute,
|
||||||
|
(attr) => attr.instances,
|
||||||
|
)
|
||||||
|
@JoinColumn({ name: "appId", referencedColumnName: "appId" })
|
||||||
|
@JoinColumn({ name: "attrName", referencedColumnName: "name" })
|
||||||
|
attr!: Attribute;
|
||||||
|
|
||||||
|
@PrimaryColumn()
|
||||||
|
attrName!: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
nodeRef: number | undefined;
|
||||||
|
@Column({ nullable: true })
|
||||||
|
number: number | undefined;
|
||||||
|
@Column({ nullable: true })
|
||||||
|
string: string | undefined;
|
||||||
|
@Column({ nullable: true })
|
||||||
|
boolean: boolean | undefined;
|
||||||
|
@Column({ nullable: true })
|
||||||
|
instant: Date | undefined;
|
||||||
|
}
|
168
packages/panorama-daemon/src/routes/node.ts
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
import Router from "@koa/router";
|
||||||
|
import { App, Attribute, NodeHasAttribute, PNode } from "../models";
|
||||||
|
import { uuidv7 } from "uuidv7";
|
||||||
|
import { dataSource } from "../db";
|
||||||
|
|
||||||
|
export const nodeRouter = new Router();
|
||||||
|
|
||||||
|
nodeRouter.put("/", async (ctx) => {
|
||||||
|
const id = uuidv7();
|
||||||
|
const body = ctx.request.body;
|
||||||
|
await dataSource.transaction(async (em) => {
|
||||||
|
await em.getRepository(PNode).insert({ id });
|
||||||
|
|
||||||
|
const attributes: [string, any][] = body?.attributes ?? [];
|
||||||
|
const attributeNames = attributes
|
||||||
|
.map(([name, _]) => name)
|
||||||
|
.map((name) => name.split("::"));
|
||||||
|
const appNames = new Set(attributeNames.map(([app, _]) => app));
|
||||||
|
const attrNames = new Set(attributeNames.map(([_, attr]) => attr));
|
||||||
|
console.log("attribute names", appNames);
|
||||||
|
|
||||||
|
const result = await em
|
||||||
|
.createQueryBuilder(App, "app")
|
||||||
|
.where("app.name IN (:...appNames)", { appNames: [...appNames] })
|
||||||
|
.getMany();
|
||||||
|
const appIdMappingList: [string, string][] = result.map((app) => [
|
||||||
|
app.name,
|
||||||
|
app.id,
|
||||||
|
]);
|
||||||
|
const appIds = new Set([...appIdMappingList.map(([_, id]) => id)]);
|
||||||
|
const appIdMapping = new Map([...appIdMappingList]);
|
||||||
|
|
||||||
|
const result2 = await em
|
||||||
|
.createQueryBuilder(Attribute, "attr")
|
||||||
|
.where("attr.appId IN (:...appIds)", { appIds: [...appIds] })
|
||||||
|
.andWhere("attr.name IN (:...attrNames)", { attrNames: [...attrNames] })
|
||||||
|
.getMany();
|
||||||
|
const attributeTypesList: [string, string][] = result2.map((attr) => [
|
||||||
|
`${attr.appId}::${attr.name}`,
|
||||||
|
attr.type,
|
||||||
|
]);
|
||||||
|
const attributeTypes = new Map(attributeTypesList);
|
||||||
|
console.log("attribute types", attributeTypes);
|
||||||
|
|
||||||
|
const convertValue = (
|
||||||
|
type: string,
|
||||||
|
value: string,
|
||||||
|
): Partial<NodeHasAttribute> => {
|
||||||
|
switch (type) {
|
||||||
|
case "string":
|
||||||
|
return { string: value };
|
||||||
|
case "datetime":
|
||||||
|
return { instant: new Date(value) };
|
||||||
|
default:
|
||||||
|
console.error(`fuck, unknown type ${type}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const attributesMapped: Partial<NodeHasAttribute>[] = attributes.map(
|
||||||
|
([name, value]) => {
|
||||||
|
const [app, attrName] = name.split("::");
|
||||||
|
const appId = appIdMapping.get(app)!;
|
||||||
|
const type = attributeTypes.get(`${appId}::${attrName}`)!;
|
||||||
|
console.log("converting type", type, value);
|
||||||
|
return { nodeId: id, appId, attrName, ...convertValue(type, value) };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("mappped", attributesMapped);
|
||||||
|
|
||||||
|
const result3 = await em.insert(NodeHasAttribute, attributesMapped);
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.body = { id };
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: WILL BE REMOVED BEFORE ALPHA RELEASE
|
||||||
|
nodeRouter.post("/sql", async (ctx) => {
|
||||||
|
const body = ctx.request.body;
|
||||||
|
const { query, parameters } = body;
|
||||||
|
const rows = await dataSource.query(query, parameters ?? []);
|
||||||
|
ctx.body = { rows };
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeRouter.get("/recent", async (ctx) => {
|
||||||
|
const result = await dataSource.query<NodeHasAttribute[]>(`
|
||||||
|
SELECT
|
||||||
|
node20.id as nodeId,
|
||||||
|
hasAttr.attrName,
|
||||||
|
app.name AS appName,
|
||||||
|
attr.type AS attrType,
|
||||||
|
hasAttr.nodeRef,
|
||||||
|
hasAttr.number,
|
||||||
|
hasAttr.string,
|
||||||
|
hasAttr.boolean,
|
||||||
|
hasAttr.instant
|
||||||
|
FROM node_has_attribute hasAttr
|
||||||
|
INNER JOIN (SELECT * FROM node ORDER BY lastUpdated DESC LIMIT 20) node20 ON node20.id = hasAttr.nodeId
|
||||||
|
INNER JOIN app ON app.id = hasAttr.appId
|
||||||
|
INNER JOIN attribute attr ON attr.appId = hasAttr.appId AND attr.name = hasAttr.attrName
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log("result", result);
|
||||||
|
|
||||||
|
const convertValue = (type: string, row: Partial<NodeHasAttribute>): any => {
|
||||||
|
switch (type) {
|
||||||
|
case "string":
|
||||||
|
return row.string;
|
||||||
|
case "datetime":
|
||||||
|
return new Date(row.instant);
|
||||||
|
default:
|
||||||
|
console.error(`unknown type ${type}`);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const idAttrs = new Map<string, Map<string, any>>();
|
||||||
|
|
||||||
|
for (const hasAttr of result) {
|
||||||
|
if (!idAttrs.has(hasAttr.nodeId)) idAttrs.set(hasAttr.nodeId, new Map());
|
||||||
|
|
||||||
|
idAttrs
|
||||||
|
.get(hasAttr.nodeId)!
|
||||||
|
.set(
|
||||||
|
`${hasAttr.appName}::${hasAttr.attrName}`,
|
||||||
|
convertValue(hasAttr.attrType, hasAttr),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result2 = [...idAttrs.entries()].map(([id, attrs]) => ({
|
||||||
|
id,
|
||||||
|
attributes: Object.fromEntries(attrs.entries()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// result = result.map((v) => ({
|
||||||
|
// id: v.id,
|
||||||
|
// attributes: new Map(
|
||||||
|
// v.attr.map((attr) => [
|
||||||
|
// `${attr.app.name}::${attr.attrName}`,
|
||||||
|
// convertValue(attr.attr.type, attr),
|
||||||
|
// ]),
|
||||||
|
// ),
|
||||||
|
// }));
|
||||||
|
|
||||||
|
ctx.body = result2;
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeRouter.get("/:id", async (ctx) => {
|
||||||
|
const result: false | undefined = await dataSource.transaction(async (em) => {
|
||||||
|
const query = dataSource
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select("node")
|
||||||
|
.from(PNode, "node")
|
||||||
|
.where("node.id = :id", { id: ctx.params.id });
|
||||||
|
|
||||||
|
const node = await query.getOne();
|
||||||
|
if (node === null) return false;
|
||||||
|
ctx.body = { node };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result === false) {
|
||||||
|
ctx.status = 404;
|
||||||
|
ctx.body = { error: "Not found" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeRouter.post("/:id", async (ctx) => {});
|
26
packages/panorama-daemon/tsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true
|
||||||
|
}
|
||||||
|
}
|
5427
pnpm-lock.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
packages:
|
||||||
|
- 'app'
|
||||||
|
- 'docs'
|
||||||
|
- 'packages/*'
|
2
rustfmt.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
tab_spaces = 2
|
||||||
|
max_width = 80
|
3
tsconfig.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": { "jsx": "react-jsx" }
|
||||||
|
}
|
24
ui/.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
3
ui/.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
|
||||||
|
}
|
7
ui/README.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# Tauri + React + Typescript
|
||||||
|
|
||||||
|
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
BIN
ui/bun.lockb
Executable file
14
ui/index.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Tauri + React + Typescript</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
59
ui/package.json
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"name": "panorama",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"tauri": "tauri"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.11.4",
|
||||||
|
"@emotion/styled": "^11.11.5",
|
||||||
|
"@floating-ui/react": "^0.26.16",
|
||||||
|
"@fontsource/inter": "^5.0.18",
|
||||||
|
"@mui/icons-material": "^5.15.18",
|
||||||
|
"@mui/material": "^5.15.18",
|
||||||
|
"@react-spring/web": "^9.7.3",
|
||||||
|
"@remark-embedder/core": "^3.0.3",
|
||||||
|
"@tanstack/react-query": "^5.37.1",
|
||||||
|
"@tauri-apps/api": "^1",
|
||||||
|
"@tauri-apps/plugin-window-state": "2.0.0-beta.6",
|
||||||
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
|
"@uiw/react-md-editor": "^4.0.4",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
|
"date-fns": "^3.6.0",
|
||||||
|
"formik": "^2.4.6",
|
||||||
|
"hast-util-to-jsx-runtime": "^2.3.0",
|
||||||
|
"hast-util-to-mdast": "^10.1.0",
|
||||||
|
"immutable": "^4.3.6",
|
||||||
|
"javascript-time-ago": "^2.5.10",
|
||||||
|
"jotai": "^2.8.1",
|
||||||
|
"katex": "^0.16.10",
|
||||||
|
"mdast-util-from-markdown": "^2.0.0",
|
||||||
|
"mdast-util-to-markdown": "^2.1.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-markdown": "^9.0.1",
|
||||||
|
"react-time-ago": "^7.3.3",
|
||||||
|
"rehype-katex": "^7.0.0",
|
||||||
|
"remark": "^15.0.1",
|
||||||
|
"remark-math": "^6.0.0",
|
||||||
|
"remark-rehype": "^11.1.0",
|
||||||
|
"use-debounce": "^10.0.1",
|
||||||
|
"uuidv7": "^1.0.0",
|
||||||
|
"vfile": "^6.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "2.0.0-beta.20",
|
||||||
|
"@types/mdast": "^4.0.4",
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
|
"sass": "^1.77.2",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
6
ui/public/tauri.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||||
|
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
1
ui/public/vite.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
7
ui/src-tauri/.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# Generated by Tauri
|
||||||
|
# will have schema files for capabilities auto-completion
|
||||||
|
/gen/schemas
|
35
ui/src-tauri/Cargo.toml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
[package]
|
||||||
|
name = "panorama"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A Tauri App"
|
||||||
|
authors = ["you"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "app_lib"
|
||||||
|
crate-type = [
|
||||||
|
"staticlib",
|
||||||
|
"cdylib",
|
||||||
|
# "rlib",
|
||||||
|
"lib",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2.0.0-beta", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.5.7", features = ["derive"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
tauri = { version = "2.0.0-beta", features = [] }
|
||||||
|
tauri-build = { version = "2.0.0-beta.17", features = ["config-toml"] }
|
||||||
|
tauri-plugin-http = "2.0.0-beta.9"
|
||||||
|
tauri-plugin-shell = "2.0.0-beta.7"
|
||||||
|
tauri-plugin-single-instance = "2.0.0-beta.9"
|
||||||
|
tauri-plugin-window-state = "2.0.0-beta"
|
||||||
|
tokio = { version = "1.38.0", features = ["full"] }
|
||||||
|
tracing-subscriber = "0.3.18"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
||||||
|
custom-protocol = ["tauri/custom-protocol"]
|
3
ui/src-tauri/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
31
ui/src-tauri/capabilities/migrated.json
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"identifier": "migrated",
|
||||||
|
"description": "permissions that were migrated from v1",
|
||||||
|
"local": true,
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"path:default",
|
||||||
|
"event:default",
|
||||||
|
"window:default",
|
||||||
|
"app:default",
|
||||||
|
"resources:default",
|
||||||
|
"menu:default",
|
||||||
|
"tray:default",
|
||||||
|
"shell:allow-open",
|
||||||
|
{
|
||||||
|
"identifier": "http:default",
|
||||||
|
"allow": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:5195/*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://localhost:3000/*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"app:allow-app-show",
|
||||||
|
"app:allow-app-hide",
|
||||||
|
"shell:default",
|
||||||
|
"http:default"
|
||||||
|
]
|
||||||
|
}
|
BIN
ui/src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
ui/src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
ui/src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
ui/src-tauri/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
ui/src-tauri/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
ui/src-tauri/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
ui/src-tauri/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
ui/src-tauri/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 965 B |
BIN
ui/src-tauri/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 22 KiB |