Compare commits

...

No commits in common. "master" and "old-master" have entirely different histories.

137 changed files with 590 additions and 13816 deletions

View file

@ -1,7 +0,0 @@
[*]
indent_size = 2
indent_style = space
[Makefile]
indent_size = 4
indent_style = tab

1
.envrc
View file

@ -1 +0,0 @@
export DATABASE_URL=sqlite://$(pwd)/test.db

17
.gitignore vendored
View file

@ -1,9 +1,8 @@
node_modules /target
dist /.env
target /output.log
.DS_Store /config.toml
**/export/export.json /public
test.db* /hellosu
.env /hellosu.db*
.direnv
/proto/generated

View file

@ -1 +0,0 @@
pnpm-lock.yaml

View file

@ -1,3 +0,0 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

5361
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,4 @@
workspace.resolver = "2" [workspace]
workspace.members = ["ui/src-tauri"] members = [
"daemon",
[profile.wasm-debug] ]
inherits = "dev"
panic = "abort"
[profile.wasm-release]
inherits = "release"
lto = true
opt-level = 's'
strip = true
panic = "abort"

View file

@ -1,13 +0,0 @@
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

View file

@ -1,11 +0,0 @@
panorama
========
Personal information manager.
Contact
-------
Author: Michael Zhang
License: GPL-3.0-only

View file

@ -1,6 +0,0 @@
name: panorama/calendar
depends:
- name: panorama
# code: dist/index.js

View file

@ -1,175 +0,0 @@
# 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

View file

@ -1,15 +0,0 @@
# 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.

Binary file not shown.

View file

@ -1,27 +0,0 @@
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 = {};
}

View file

@ -1,19 +0,0 @@
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

View file

@ -1,15 +0,0 @@
{
"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"
}
}

View file

@ -1,22 +0,0 @@
{
"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
}
}

View file

@ -1 +0,0 @@
index.js

Binary file not shown.

View file

@ -1,41 +0,0 @@
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 = {};
}

View file

@ -1,10 +0,0 @@
name: panorama/journal
code: index.js
attributes:
- name: day
type: Option<String>
endpoints:
- route: /today
handler: today

View file

@ -1,9 +0,0 @@
{
"dependencies": {
"date-fns": "^3.6.0",
"koa": "^2.15.3"
},
"devDependencies": {
"@types/koa": "^2.15.0"
}
}

View file

@ -1,7 +0,0 @@
name: panorama
attributes:
- name: time/start
type: datetime
- name: time/end
type: datetime

View file

@ -1,18 +0,0 @@
{
"$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

Binary file not shown.

16
daemon/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[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"

84
daemon/src/config/mod.rs Normal file
View file

@ -0,0 +1,84 @@
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,
}

View file

@ -0,0 +1,81 @@
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
daemon/src/imap.rs Normal file
View file

@ -0,0 +1 @@
pub struct ImapClient {}

66
daemon/src/main.rs Normal file
View file

@ -0,0 +1,66 @@
#[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
View file

@ -1,21 +0,0 @@
# 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

View file

@ -1,4 +0,0 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

View file

@ -1,11 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

View file

@ -1,37 +0,0 @@
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],
},
});

Binary file not shown.

View file

@ -1,22 +0,0 @@
{
"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"
}
}

View file

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

View file

@ -1,6 +0,0 @@
import { defineCollection } from "astro:content";
import { docsSchema } from "@astrojs/starlight/schema";
export const collections = {
docs: defineCollection({ schema: docsSchema() }),
};

View file

@ -1,71 +0,0 @@
---
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

View file

@ -1,11 +0,0 @@
---
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.
:::

View file

@ -1,5 +0,0 @@
---
title: Cryptography
---
lol

View file

@ -1,30 +0,0 @@
---
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

View file

@ -1,18 +0,0 @@
---
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.

View file

@ -1,21 +0,0 @@
---
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[];
}
```

View file

@ -1,13 +0,0 @@
---
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

View file

@ -1,10 +0,0 @@
---
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

View file

@ -1,14 +0,0 @@
---
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.

View file

@ -1,15 +0,0 @@
---
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

View file

@ -1,72 +0,0 @@
---
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.

View file

@ -1,14 +0,0 @@
---
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
---

View file

@ -1,5 +0,0 @@
---
title: Client-Bridge Protocols
---
A **bridge** is just a way of connecting two devices.

View file

@ -1,4 +0,0 @@
---
title: Client-Client Protocols
---

View file

@ -1,25 +0,0 @@
---
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.

View file

@ -1,27 +0,0 @@
---
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...

View file

@ -1,5 +0,0 @@
---
title: Formal verification
---
lol

View file

@ -1,6 +0,0 @@
---
title: Loading process
---
The goal of panorama is to start up as quickly as possible.
The following tasks need to be performed on start:

View file

@ -1,5 +0,0 @@
---
title: Notifications
---
https://unifiedpush.org/

View file

@ -1,10 +0,0 @@
---
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
View file

@ -1,2 +0,0 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />

View file

@ -1,5 +0,0 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": { "skipLibCheck": true },
"exclude": ["dist"]
}

View file

@ -1,98 +0,0 @@
{
"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
}

View file

@ -1,42 +0,0 @@
{
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 ]);
};
});
}

View file

@ -1,13 +0,0 @@
{
"name": "panorama",
"private": true,
"workspaces": [
"packages/*",
"apps/*"
],
"trustedDependencies": [
"electron",
"esbuild",
"sqlite3"
]
}

View file

@ -1,175 +0,0 @@
# 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

View file

@ -1,15 +0,0 @@
# 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.

Binary file not shown.

View file

@ -1,31 +0,0 @@
{
"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"
]
}

View file

@ -1,101 +0,0 @@
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 };
}

View file

@ -1,26 +0,0 @@
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>;

View file

@ -1,16 +0,0 @@
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();

View file

@ -1,50 +0,0 @@
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);

View file

@ -1,118 +0,0 @@
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;
}

View file

@ -1,168 +0,0 @@
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) => {});

View file

@ -1,26 +0,0 @@
{
"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
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +0,0 @@
packages:
- 'app'
- 'docs'
- 'packages/*'

View file

@ -1,2 +0,0 @@
tab_spaces = 2
max_width = 80

View file

@ -1,3 +0,0 @@
{
"compilerOptions": { "jsx": "react-jsx" }
}

24
ui/.gitignore vendored
View file

@ -1,24 +0,0 @@
# 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?

View file

@ -1,3 +0,0 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

View file

@ -1,7 +0,0 @@
# 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)

Binary file not shown.

View file

@ -1,14 +0,0 @@
<!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>

View file

@ -1,59 +0,0 @@
{
"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"
}
}

View file

@ -1,6 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,7 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

View file

@ -1,35 +0,0 @@
[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"]

View file

@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

View file

@ -1,31 +0,0 @@
{
"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"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 965 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Some files were not shown because too many files have changed in this diff Show more