mirror of
https://github.com/harivansh-afk/betterNAS.git
synced 2026-04-15 03:00:44 +00:00
init (#5)
This commit is contained in:
parent
4f174ec3a8
commit
b68151035a
81 changed files with 6263 additions and 545 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -3,3 +3,6 @@ dist/
|
|||
.DS_Store
|
||||
npm-debug.log*
|
||||
.codex/
|
||||
.turbo/
|
||||
coverage/
|
||||
apps/web/.next/
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -22,3 +22,16 @@ Read these in order:
|
|||
- the control plane is the system of record.
|
||||
- the NAS node serves bytes directly whenever possible.
|
||||
- Nextcloud is an optional cloud/web adapter, not the product center.
|
||||
|
||||
## Monorepo
|
||||
|
||||
- `apps/web`: Next.js control-plane UI
|
||||
- `apps/control-plane`: Go control-plane service
|
||||
- `apps/node-agent`: Go NAS runtime / WebDAV node
|
||||
- `apps/nextcloud-app`: optional Nextcloud adapter
|
||||
- `packages/contracts`: canonical shared contracts
|
||||
- `packages/sdk-ts`: TypeScript SDK surface for the web app
|
||||
- `packages/ui`: shared React UI
|
||||
- `infra/docker`: local Docker runtime
|
||||
|
||||
The root planning and delegation guide lives in [skeleton.md](/home/rathi/Documents/GitHub/betterNAS/skeleton.md).
|
||||
|
|
|
|||
15
apps/control-plane/Dockerfile
Normal file
15
apps/control-plane/Dockerfile
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
FROM golang:1.26-alpine AS build
|
||||
|
||||
WORKDIR /src
|
||||
COPY apps/control-plane ./apps/control-plane
|
||||
|
||||
WORKDIR /src/apps/control-plane
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/control-plane ./cmd/control-plane
|
||||
|
||||
FROM alpine:3.21
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /out/control-plane /usr/local/bin/control-plane
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["control-plane"]
|
||||
15
apps/control-plane/README.md
Normal file
15
apps/control-plane/README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# betterNAS Control Plane
|
||||
|
||||
Go service that owns the product control plane.
|
||||
|
||||
It is intentionally small for now:
|
||||
|
||||
- `GET /health`
|
||||
- `GET /version`
|
||||
- `POST /api/v1/nodes/register`
|
||||
- `GET /api/v1/exports`
|
||||
- `POST /api/v1/mount-profiles/issue`
|
||||
- `POST /api/v1/cloud-profiles/issue`
|
||||
|
||||
The request and response shapes must follow the contracts in
|
||||
[`packages/contracts`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts).
|
||||
103
apps/control-plane/cmd/control-plane/main.go
Normal file
103
apps/control-plane/cmd/control-plane/main.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type jsonObject map[string]any
|
||||
|
||||
func main() {
|
||||
port := env("PORT", "8081")
|
||||
startedAt := time.Now()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, jsonObject{
|
||||
"service": "control-plane",
|
||||
"status": "ok",
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
"uptimeSeconds": int(time.Since(startedAt).Seconds()),
|
||||
"nextcloud": jsonObject{
|
||||
"configured": false,
|
||||
"baseUrl": env("NEXTCLOUD_BASE_URL", ""),
|
||||
"provider": "nextcloud",
|
||||
},
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, jsonObject{
|
||||
"service": "control-plane",
|
||||
"version": env("BETTERNAS_VERSION", "0.1.0-dev"),
|
||||
"apiVersion": "v1",
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/api/v1/exports", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, []jsonObject{})
|
||||
})
|
||||
mux.HandleFunc("/api/v1/mount-profiles/issue", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, jsonObject{
|
||||
"id": "dev-profile",
|
||||
"exportId": "dev-export",
|
||||
"protocol": "webdav",
|
||||
"displayName": "Example export",
|
||||
"mountUrl": env("BETTERNAS_EXAMPLE_MOUNT_URL", "http://localhost:8090/dav"),
|
||||
"readonly": false,
|
||||
"credentialMode": "session-token",
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/api/v1/cloud-profiles/issue", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, jsonObject{
|
||||
"id": "dev-cloud",
|
||||
"exportId": "dev-export",
|
||||
"provider": "nextcloud",
|
||||
"baseUrl": env("NEXTCLOUD_BASE_URL", "http://localhost:8080"),
|
||||
"path": "/apps/files/files",
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/api/v1/nodes/register", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, jsonObject{
|
||||
"id": "dev-node",
|
||||
"machineId": "dev-machine",
|
||||
"displayName": "Development NAS",
|
||||
"agentVersion": "0.1.0-dev",
|
||||
"status": "online",
|
||||
"lastSeenAt": time.Now().UTC().Format(time.RFC3339),
|
||||
"directAddress": env("BETTERNAS_NODE_DIRECT_ADDRESS", "http://localhost:8090"),
|
||||
"relayAddress": nil,
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
log.Printf("betterNAS control plane listening on :%s", port)
|
||||
log.Fatal(server.ListenAndServe())
|
||||
}
|
||||
|
||||
func env(key, fallback string) string {
|
||||
value, ok := os.LookupEnv(key)
|
||||
if !ok || value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, statusCode int, payload any) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(statusCode)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
3
apps/control-plane/go.mod
Normal file
3
apps/control-plane/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/rathi/betternas/apps/control-plane
|
||||
|
||||
go 1.26.0
|
||||
12
apps/control-plane/package.json
Normal file
12
apps/control-plane/package.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "@betternas/control-plane",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "CGO_ENABLED=0 go run ./cmd/control-plane",
|
||||
"build": "mkdir -p dist && CGO_ENABLED=0 go build -o dist/control-plane ./cmd/control-plane",
|
||||
"lint": "CGO_ENABLED=0 go vet ./...",
|
||||
"check-types": "CGO_ENABLED=0 go test ./...",
|
||||
"test": "CGO_ENABLED=0 go test ./..."
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 292 B |
|
Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 292 B |
10
apps/node-agent/README.md
Normal file
10
apps/node-agent/README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# betterNAS Node Agent
|
||||
|
||||
Go service that runs on the NAS machine.
|
||||
|
||||
For the scaffold it does two things:
|
||||
|
||||
- serves `GET /health`
|
||||
- serves a WebDAV export at `/dav/`
|
||||
|
||||
This is the first real storage-facing surface in the monorepo.
|
||||
46
apps/node-agent/cmd/node-agent/main.go
Normal file
46
apps/node-agent/cmd/node-agent/main.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
func main() {
|
||||
port := env("PORT", "8090")
|
||||
exportPath := env("BETTERNAS_EXPORT_PATH", ".")
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, _ = w.Write([]byte("ok\n"))
|
||||
})
|
||||
|
||||
dav := &webdav.Handler{
|
||||
Prefix: "/dav",
|
||||
FileSystem: webdav.Dir(exportPath),
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
}
|
||||
mux.Handle("/dav/", dav)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
log.Printf("betterNAS node agent serving %s on :%s", exportPath, port)
|
||||
log.Fatal(server.ListenAndServe())
|
||||
}
|
||||
|
||||
func env(key, fallback string) string {
|
||||
value, ok := os.LookupEnv(key)
|
||||
if !ok || value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
5
apps/node-agent/go.mod
Normal file
5
apps/node-agent/go.mod
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module github.com/rathi/betternas/apps/node-agent
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require golang.org/x/net v0.46.0
|
||||
2
apps/node-agent/go.sum
Normal file
2
apps/node-agent/go.sum
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
12
apps/node-agent/package.json
Normal file
12
apps/node-agent/package.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "@betternas/node-agent",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "CGO_ENABLED=0 go run ./cmd/node-agent",
|
||||
"build": "mkdir -p dist && CGO_ENABLED=0 go build -o dist/node-agent ./cmd/node-agent",
|
||||
"lint": "CGO_ENABLED=0 go vet ./...",
|
||||
"check-types": "CGO_ENABLED=0 go test ./...",
|
||||
"test": "CGO_ENABLED=0 go test ./..."
|
||||
}
|
||||
}
|
||||
36
apps/web/.gitignore
vendored
Normal file
36
apps/web/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# env files (can opt-in for commiting if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
13
apps/web/README.md
Normal file
13
apps/web/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# betterNAS Web
|
||||
|
||||
Next.js control-plane UI for betterNAS.
|
||||
|
||||
Use this app for:
|
||||
|
||||
- admin and operator workflows
|
||||
- node and export visibility
|
||||
- issuing mount profiles
|
||||
- later cloud-mode management
|
||||
|
||||
Do not move the product system of record into this app. It should stay a UI and
|
||||
thin BFF layer over the Go control plane.
|
||||
BIN
apps/web/app/favicon.ico
Normal file
BIN
apps/web/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
apps/web/app/fonts/GeistMonoVF.woff
Normal file
BIN
apps/web/app/fonts/GeistMonoVF.woff
Normal file
Binary file not shown.
BIN
apps/web/app/fonts/GeistVF.woff
Normal file
BIN
apps/web/app/fonts/GeistVF.woff
Normal file
Binary file not shown.
50
apps/web/app/globals.css
Normal file
50
apps/web/app/globals.css
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.imgDark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.imgLight {
|
||||
display: none;
|
||||
}
|
||||
.imgDark {
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
31
apps/web/app/layout.tsx
Normal file
31
apps/web/app/layout.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { Metadata } from "next";
|
||||
import localFont from "next/font/local";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = localFont({
|
||||
src: "./fonts/GeistVF.woff",
|
||||
variable: "--font-geist-sans",
|
||||
});
|
||||
const geistMono = localFont({
|
||||
src: "./fonts/GeistMonoVF.woff",
|
||||
variable: "--font-geist-mono",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "betterNAS",
|
||||
description: "Contract-first monorepo for NAS mounts and optional cloud access",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${geistSans.variable} ${geistMono.variable}`}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
77
apps/web/app/page.module.css
Normal file
77
apps/web/app/page.module.css
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
.page {
|
||||
min-height: 100svh;
|
||||
padding: 48px 24px 80px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(91, 186, 166, 0.18), transparent 28%),
|
||||
linear-gradient(180deg, #f5fbfa 0%, #edf5f3 100%);
|
||||
color: #10212d;
|
||||
}
|
||||
|
||||
.hero {
|
||||
max-width: 860px;
|
||||
margin: 0 auto 32px;
|
||||
padding: 32px;
|
||||
border-radius: 28px;
|
||||
background: linear-gradient(135deg, #123043 0%, #1d5466 100%);
|
||||
color: #f7fbfc;
|
||||
box-shadow: 0 24px 80px rgba(16, 33, 45, 0.14);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 12px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 12px;
|
||||
font-size: clamp(2.25rem, 5vw, 4rem);
|
||||
line-height: 0.98;
|
||||
}
|
||||
|
||||
.copy {
|
||||
margin: 0;
|
||||
max-width: 64ch;
|
||||
font-size: 1rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.grid {
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: block;
|
||||
padding: 22px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 1px solid rgba(18, 48, 67, 0.08);
|
||||
box-shadow: 0 12px 30px rgba(18, 48, 67, 0.08);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.page {
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
45
apps/web/app/page.tsx
Normal file
45
apps/web/app/page.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { Card } from "@betternas/ui/card";
|
||||
import styles from "./page.module.css";
|
||||
|
||||
const lanes = [
|
||||
{
|
||||
title: "NAS node",
|
||||
body: "Runs on the storage machine. Exposes WebDAV, reports exports, and stays close to the bytes.",
|
||||
},
|
||||
{
|
||||
title: "Control plane",
|
||||
body: "Owns users, devices, nodes, grants, mount profiles, and cloud profiles.",
|
||||
},
|
||||
{
|
||||
title: "Local device",
|
||||
body: "Consumes mount profiles and uses Finder WebDAV flows before we ship a helper app.",
|
||||
},
|
||||
{
|
||||
title: "Cloud layer",
|
||||
body: "Keeps Nextcloud optional and thin for browser, mobile, and sharing flows.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className={styles.page}>
|
||||
<section className={styles.hero}>
|
||||
<p className={styles.eyebrow}>betterNAS monorepo</p>
|
||||
<h1 className={styles.title}>Contract-first scaffold for NAS mounts and cloud mode.</h1>
|
||||
<p className={styles.copy}>
|
||||
The repo is organized so each system part can be built in parallel
|
||||
without inventing new interfaces. The source of truth is the root
|
||||
contract plus the shared contracts package.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className={styles.grid}>
|
||||
{lanes.map((lane) => (
|
||||
<Card key={lane.title} className={styles.card} title={lane.title} href="/#">
|
||||
{lane.body}
|
||||
</Card>
|
||||
))}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
4
apps/web/eslint.config.js
Normal file
4
apps/web/eslint.config.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { nextJsConfig } from "@betternas/eslint-config/next-js";
|
||||
|
||||
/** @type {import("eslint").Linter.Config[]} */
|
||||
export default nextJsConfig;
|
||||
6
apps/web/next.config.js
Normal file
6
apps/web/next.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
29
apps/web/package.json
Normal file
29
apps/web/package.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "@betternas/web",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --port 3000",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint --max-warnings 0",
|
||||
"check-types": "next typegen && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@betternas/sdk-ts": "*",
|
||||
"@betternas/ui": "*",
|
||||
"next": "16.2.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@betternas/eslint-config": "*",
|
||||
"@betternas/typescript-config": "*",
|
||||
"@types/node": "^22.15.3",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "5.9.2"
|
||||
}
|
||||
}
|
||||
18
apps/web/tsconfig.json
Normal file
18
apps/web/tsconfig.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": "@betternas/typescript-config/nextjs.json",
|
||||
"compilerOptions": {
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"next-env.d.ts",
|
||||
"next.config.js"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ The other three parts are execution surfaces:
|
|||
| Part | Steal first | Write ourselves |
|
||||
|---|---|---|
|
||||
| NAS node | NixOS/Nix module patterns, existing WebDAV servers | node agent, export model, node registration flow |
|
||||
| Control plane | existing repo scaffold, TS stack, Postgres/Redis patterns | product domain model, policy engine, mount/cloud APIs, registry |
|
||||
| Control plane | Go stdlib routing, pgx/sqlc, go-redis/asynq, OpenAPI codegen | product domain model, policy engine, mount/cloud APIs, registry |
|
||||
| Local device | Finder WebDAV mount, macOS Keychain, later maybe launch agent patterns | helper app, mount profile handling, auto-mount UX |
|
||||
| Cloud/web layer | Nextcloud server, Nextcloud shell app, Nextcloud share/file UI, Nextcloud mobile references | betterNAS integration layer, mapping between product model and Nextcloud, later branded UI |
|
||||
|
||||
|
|
@ -72,10 +72,11 @@ The NAS node should be boring and reproducible.
|
|||
## 2. Control plane
|
||||
|
||||
Start from:
|
||||
- the current `exapps/control-plane` scaffold
|
||||
- TypeScript
|
||||
- Postgres
|
||||
- Redis
|
||||
- Go
|
||||
- standard library routing first
|
||||
- Postgres via `pgx` and `sqlc`
|
||||
- Redis via `go-redis`
|
||||
- OpenAPI-driven contracts
|
||||
- standalone API mindset
|
||||
|
||||
Do not start by writing:
|
||||
|
|
@ -151,7 +152,7 @@ This is high leverage, but should not block Phase A.
|
|||
- Nix module patterns
|
||||
|
||||
### Control plane
|
||||
- current TypeScript repo scaffold
|
||||
- Go API service scaffold
|
||||
- Postgres
|
||||
- Redis
|
||||
|
||||
|
|
@ -193,7 +194,7 @@ This is high leverage, but should not block Phase A.
|
|||
| Part | First scaffold |
|
||||
|---|---|
|
||||
| NAS node | Nix flake/module + WebDAV server service config |
|
||||
| Control plane | current TypeScript control-plane service scaffold |
|
||||
| Control plane | Go service + OpenAPI contract + Postgres/Redis adapters later |
|
||||
| Local device | documented Finder mount flow, then lightweight helper app |
|
||||
| Cloud/web layer | current Nextcloud scaffold and shell app |
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,19 @@ Use these in this order:
|
|||
- [`docs/04-cloud-web-layer.md`](/home/rathi/Documents/GitHub/betterNAS/docs/04-cloud-web-layer.md)
|
||||
- [`docs/05-build-plan.md`](/home/rathi/Documents/GitHub/betterNAS/docs/05-build-plan.md)
|
||||
|
||||
## Repo lanes
|
||||
|
||||
The monorepo is split into these primary implementation lanes:
|
||||
|
||||
- [`apps/node-agent`](/home/rathi/Documents/GitHub/betterNAS/apps/node-agent)
|
||||
- [`apps/control-plane`](/home/rathi/Documents/GitHub/betterNAS/apps/control-plane)
|
||||
- [`apps/web`](/home/rathi/Documents/GitHub/betterNAS/apps/web)
|
||||
- [`apps/nextcloud-app`](/home/rathi/Documents/GitHub/betterNAS/apps/nextcloud-app)
|
||||
- [`packages/contracts`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts)
|
||||
|
||||
Every parallel task should primarily stay inside one of those lanes unless it is
|
||||
an explicit contract task.
|
||||
|
||||
## The contract surface we need first
|
||||
|
||||
The first shared contract set should cover only the seams that let all four
|
||||
|
|
|
|||
|
|
@ -32,27 +32,57 @@ The goal is simple: do not lose the external pieces that give us leverage.
|
|||
|
||||
## Control plane
|
||||
|
||||
### Current scaffold
|
||||
|
||||
- current control-plane seed
|
||||
- path: [exapps/control-plane](/home/rathi/Documents/GitHub/betterNAS/exapps/control-plane)
|
||||
- why: existing TypeScript seed for the first real backend
|
||||
|
||||
### Backend and infra references
|
||||
|
||||
- Fastify
|
||||
- repo: https://github.com/fastify/fastify
|
||||
- why: likely good fit for a TypeScript API backend
|
||||
- Go routing enhancements
|
||||
- docs: https://go.dev/blog/routing-enhancements
|
||||
- why: best low-dependency baseline if we stay with the standard library
|
||||
|
||||
- `chi`
|
||||
- repo: https://github.com/go-chi/chi
|
||||
- why: thin stdlib-friendly router if we want middleware and route groups
|
||||
|
||||
- PostgreSQL
|
||||
- docs: https://www.postgresql.org/docs/
|
||||
- why: source of truth for product metadata
|
||||
|
||||
- `pgx`
|
||||
- repo: https://github.com/jackc/pgx
|
||||
- why: Postgres-first Go driver and toolkit
|
||||
|
||||
- `sqlc`
|
||||
- repo: https://github.com/sqlc-dev/sqlc
|
||||
- why: typed query generation for Go
|
||||
|
||||
- Redis
|
||||
- docs: https://redis.io/docs/latest/
|
||||
- why: cache, jobs, ephemeral coordination
|
||||
|
||||
### SSH access / gateway references
|
||||
- `go-redis`
|
||||
- repo: https://github.com/redis/go-redis
|
||||
- why: primary Redis client for Go
|
||||
|
||||
- `asynq`
|
||||
- repo: https://github.com/hibiken/asynq
|
||||
- why: practical Redis-backed background jobs
|
||||
|
||||
- `koanf`
|
||||
- repo: https://github.com/knadh/koanf
|
||||
- why: layered config if the control plane grows beyond env-only config
|
||||
|
||||
- `envconfig`
|
||||
- repo: https://github.com/kelseyhightower/envconfig
|
||||
- why: small env-only config loader
|
||||
|
||||
- `log/slog`
|
||||
- docs: https://pkg.go.dev/log/slog
|
||||
- why: structured logging without extra dependencies
|
||||
|
||||
- `oapi-codegen`
|
||||
- repo: https://github.com/oapi-codegen/oapi-codegen
|
||||
- why: generate Go and TS surfaces from OpenAPI with less drift
|
||||
|
||||
### SSH access / gateway reference
|
||||
|
||||
- `sshpiper`
|
||||
- repo: https://github.com/tg123/sshpiper
|
||||
|
|
@ -76,6 +106,18 @@ The goal is simple: do not lose the external pieces that give us leverage.
|
|||
- docs: https://developer.apple.com/documentation/
|
||||
- why: Keychain, launch agents, desktop helpers, future native integration
|
||||
|
||||
- Keychain data protection
|
||||
- docs: https://support.apple.com/guide/security/keychain-data-protection-secb0694df1a/web
|
||||
- why: baseline secret-storage model for device credentials
|
||||
|
||||
- Finder Sync extensions
|
||||
- docs: https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/Finder.html
|
||||
- why: future helper-app integration pattern if Finder UX grows
|
||||
|
||||
- WebDAV RFC 4918
|
||||
- docs: https://www.rfc-editor.org/rfc/rfc4918
|
||||
- why: protocol semantics and caveats
|
||||
|
||||
## Cloud / web layer
|
||||
|
||||
### Nextcloud server and app references
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json tsconfig.base.json ./
|
||||
COPY packages/contracts ./packages/contracts
|
||||
COPY exapps/control-plane ./exapps/control-plane
|
||||
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
WORKDIR /app/exapps/control-plane
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"name": "@betternas/control-plane",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/index.js",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@betternas/contracts": "file:../../packages/contracts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.18.6",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import type { NextcloudBackendStatus } from "@betternas/contracts";
|
||||
|
||||
export class NextcloudBackendAdapter {
|
||||
constructor(private readonly baseUrl: string) {}
|
||||
|
||||
describe(): NextcloudBackendStatus {
|
||||
return {
|
||||
configured: this.baseUrl.length > 0,
|
||||
baseUrl: this.baseUrl,
|
||||
provider: "nextcloud"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||
import {
|
||||
CONTROL_PLANE_ROUTES,
|
||||
type ControlPlaneHealthResponse,
|
||||
type ControlPlaneVersionResponse
|
||||
} from "@betternas/contracts";
|
||||
|
||||
import type { ControlPlaneConfig } from "./config.js";
|
||||
import { NextcloudBackendAdapter } from "./adapters/nextcloud-backend.js";
|
||||
|
||||
export function createApp(config: ControlPlaneConfig) {
|
||||
const startedAt = Date.now();
|
||||
const nextcloudBackend = new NextcloudBackendAdapter(config.nextcloudBaseUrl);
|
||||
|
||||
return createServer((request, response) => {
|
||||
if (request.method !== "GET" || !request.url) {
|
||||
writeJson(response, 405, { error: "Method not allowed" });
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(request.url, "http://localhost");
|
||||
|
||||
if (url.pathname === CONTROL_PLANE_ROUTES.health) {
|
||||
const payload: ControlPlaneHealthResponse = {
|
||||
service: "control-plane",
|
||||
status: "ok",
|
||||
timestamp: new Date().toISOString(),
|
||||
uptimeSeconds: Math.floor((Date.now() - startedAt) / 1000),
|
||||
nextcloud: nextcloudBackend.describe()
|
||||
};
|
||||
|
||||
writeJson(response, 200, payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === CONTROL_PLANE_ROUTES.version) {
|
||||
const payload: ControlPlaneVersionResponse = {
|
||||
service: "control-plane",
|
||||
version: config.version,
|
||||
apiVersion: "v1"
|
||||
};
|
||||
|
||||
writeJson(response, 200, payload);
|
||||
return;
|
||||
}
|
||||
|
||||
writeJson(response, 404, {
|
||||
error: "Not found"
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function writeJson(response: ServerResponse<IncomingMessage>, statusCode: number, payload: unknown) {
|
||||
response.writeHead(statusCode, {
|
||||
"content-type": "application/json; charset=utf-8"
|
||||
});
|
||||
response.end(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
export interface ControlPlaneConfig {
|
||||
port: number;
|
||||
version: string;
|
||||
nextcloudBaseUrl: string;
|
||||
}
|
||||
|
||||
export function loadConfig(env: NodeJS.ProcessEnv = process.env): ControlPlaneConfig {
|
||||
const portValue = env.PORT ?? "3000";
|
||||
const port = Number.parseInt(portValue, 10);
|
||||
|
||||
if (Number.isNaN(port)) {
|
||||
throw new Error(`Invalid PORT value: ${portValue}`);
|
||||
}
|
||||
|
||||
return {
|
||||
port,
|
||||
version: env.BETTERNAS_VERSION ?? "0.1.0-dev",
|
||||
nextcloudBaseUrl: normalizeBaseUrl(env.NEXTCLOUD_BASE_URL ?? "http://nextcloud")
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(url: string): string {
|
||||
return url.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { createApp } from "./app.js";
|
||||
import { loadConfig } from "./config.js";
|
||||
|
||||
const config = loadConfig();
|
||||
const app = createApp(config);
|
||||
|
||||
app.listen(config.port, "0.0.0.0", () => {
|
||||
console.log(`betterNAS control plane listening on port ${config.port}`);
|
||||
});
|
||||
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
6
go.work
Normal file
6
go.work
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
go 1.26.0
|
||||
|
||||
use (
|
||||
./apps/control-plane
|
||||
./apps/node-agent
|
||||
)
|
||||
1
go.work.sum
Normal file
1
go.work.sum
Normal file
|
|
@ -0,0 +1 @@
|
|||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
|
|
@ -26,8 +26,8 @@ services:
|
|||
|
||||
control-plane:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: exapps/control-plane/Dockerfile
|
||||
context: ../..
|
||||
dockerfile: apps/control-plane/Dockerfile
|
||||
environment:
|
||||
PORT: 3000
|
||||
BETTERNAS_VERSION: local-dev
|
||||
5033
package-lock.json
generated
5033
package-lock.json
generated
File diff suppressed because it is too large
Load diff
28
package.json
28
package.json
|
|
@ -1,15 +1,25 @@
|
|||
{
|
||||
"name": "betternas",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"lint": "turbo run lint",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"check-types": "turbo run check-types",
|
||||
"test": "turbo run test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.7.4",
|
||||
"turbo": "^2.9.3",
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"packageManager": "npm@10.9.7",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"exapps/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm run build --workspace @betternas/contracts && npm run build --workspace @betternas/control-plane",
|
||||
"typecheck": "npm run typecheck --workspace @betternas/contracts && npm run typecheck --workspace @betternas/control-plane",
|
||||
"dev:control-plane": "npm run dev --workspace @betternas/control-plane"
|
||||
}
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ Use it to keep the four product parts aligned:
|
|||
|
||||
## What belongs here
|
||||
|
||||
- OpenAPI source documents
|
||||
- shared TypeScript types
|
||||
- route constants
|
||||
- JSON schemas for payloads we want to validate outside TypeScript
|
||||
|
|
@ -28,6 +29,8 @@ Use it to keep the four product parts aligned:
|
|||
- current runtime scaffold for health and version
|
||||
- [`src/foundation.ts`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/src/foundation.ts)
|
||||
- first product-level entities and route constants for node, mount, and cloud flows
|
||||
- [`openapi/`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/openapi)
|
||||
- language-neutral source documents for future SDK generation
|
||||
- [`schemas/`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/schemas)
|
||||
- JSON schema mirrors for the first shared entities
|
||||
|
||||
|
|
|
|||
307
packages/contracts/openapi/betternas.v1.yaml
Normal file
307
packages/contracts/openapi/betternas.v1.yaml
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
openapi: 3.1.0
|
||||
info:
|
||||
title: betterNAS Foundation API
|
||||
version: 0.1.0
|
||||
summary: Foundation contract for node registration, export metadata, and profile issuance.
|
||||
servers:
|
||||
- url: http://localhost:8081
|
||||
paths:
|
||||
/health:
|
||||
get:
|
||||
operationId: getControlPlaneHealth
|
||||
responses:
|
||||
"200":
|
||||
description: Control-plane health
|
||||
/version:
|
||||
get:
|
||||
operationId: getControlPlaneVersion
|
||||
responses:
|
||||
"200":
|
||||
description: Control-plane version
|
||||
/api/v1/nodes/register:
|
||||
post:
|
||||
operationId: registerNode
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/NodeRegistrationRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Registered node
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/NasNode"
|
||||
/api/v1/nodes/{nodeId}/heartbeat:
|
||||
post:
|
||||
operationId: recordNodeHeartbeat
|
||||
parameters:
|
||||
- in: path
|
||||
name: nodeId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/NodeHeartbeatRequest"
|
||||
responses:
|
||||
"204":
|
||||
description: Heartbeat accepted
|
||||
/api/v1/exports:
|
||||
get:
|
||||
operationId: listExports
|
||||
responses:
|
||||
"200":
|
||||
description: Export list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/StorageExport"
|
||||
/api/v1/mount-profiles/issue:
|
||||
post:
|
||||
operationId: issueMountProfile
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MountProfileRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Mount profile
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MountProfile"
|
||||
/api/v1/cloud-profiles/issue:
|
||||
post:
|
||||
operationId: issueCloudProfile
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CloudProfileRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Cloud profile
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CloudProfile"
|
||||
components:
|
||||
schemas:
|
||||
NasNode:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- machineId
|
||||
- displayName
|
||||
- agentVersion
|
||||
- status
|
||||
- lastSeenAt
|
||||
- directAddress
|
||||
- relayAddress
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
machineId:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
agentVersion:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
enum: [online, offline, degraded]
|
||||
lastSeenAt:
|
||||
type: string
|
||||
directAddress:
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
relayAddress:
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
StorageExport:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- nasNodeId
|
||||
- label
|
||||
- path
|
||||
- protocols
|
||||
- capacityBytes
|
||||
- tags
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
nasNodeId:
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
protocols:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum: [webdav]
|
||||
capacityBytes:
|
||||
type:
|
||||
- integer
|
||||
- "null"
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
MountProfile:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- exportId
|
||||
- protocol
|
||||
- displayName
|
||||
- mountUrl
|
||||
- readonly
|
||||
- credentialMode
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
exportId:
|
||||
type: string
|
||||
protocol:
|
||||
type: string
|
||||
enum: [webdav]
|
||||
displayName:
|
||||
type: string
|
||||
mountUrl:
|
||||
type: string
|
||||
readonly:
|
||||
type: boolean
|
||||
credentialMode:
|
||||
type: string
|
||||
enum: [session-token, app-password]
|
||||
CloudProfile:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- exportId
|
||||
- provider
|
||||
- baseUrl
|
||||
- path
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
exportId:
|
||||
type: string
|
||||
provider:
|
||||
type: string
|
||||
enum: [nextcloud]
|
||||
baseUrl:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
StorageExportInput:
|
||||
type: object
|
||||
required:
|
||||
- label
|
||||
- path
|
||||
- protocols
|
||||
- capacityBytes
|
||||
- tags
|
||||
properties:
|
||||
label:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
protocols:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum: [webdav]
|
||||
capacityBytes:
|
||||
type:
|
||||
- integer
|
||||
- "null"
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
NodeRegistrationRequest:
|
||||
type: object
|
||||
required:
|
||||
- machineId
|
||||
- displayName
|
||||
- agentVersion
|
||||
- directAddress
|
||||
- relayAddress
|
||||
- exports
|
||||
properties:
|
||||
machineId:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
agentVersion:
|
||||
type: string
|
||||
directAddress:
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
relayAddress:
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
exports:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/StorageExportInput"
|
||||
NodeHeartbeatRequest:
|
||||
type: object
|
||||
required:
|
||||
- nodeId
|
||||
- status
|
||||
- lastSeenAt
|
||||
properties:
|
||||
nodeId:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
enum: [online, offline, degraded]
|
||||
lastSeenAt:
|
||||
type: string
|
||||
MountProfileRequest:
|
||||
type: object
|
||||
required:
|
||||
- userId
|
||||
- deviceId
|
||||
- exportId
|
||||
properties:
|
||||
userId:
|
||||
type: string
|
||||
deviceId:
|
||||
type: string
|
||||
exportId:
|
||||
type: string
|
||||
CloudProfileRequest:
|
||||
type: object
|
||||
required:
|
||||
- userId
|
||||
- exportId
|
||||
- provider
|
||||
properties:
|
||||
userId:
|
||||
type: string
|
||||
exportId:
|
||||
type: string
|
||||
provider:
|
||||
type: string
|
||||
enum: [nextcloud]
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"openapi",
|
||||
"schemas"
|
||||
],
|
||||
"scripts": {
|
||||
|
|
@ -14,4 +15,3 @@
|
|||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
3
packages/eslint-config/README.md
Normal file
3
packages/eslint-config/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# `@betternas/eslint-config`
|
||||
|
||||
Collection of internal eslint configurations.
|
||||
32
packages/eslint-config/base.js
Normal file
32
packages/eslint-config/base.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import js from "@eslint/js";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import turboPlugin from "eslint-plugin-turbo";
|
||||
import tseslint from "typescript-eslint";
|
||||
import onlyWarn from "eslint-plugin-only-warn";
|
||||
|
||||
/**
|
||||
* A shared ESLint configuration for the repository.
|
||||
*
|
||||
* @type {import("eslint").Linter.Config[]}
|
||||
* */
|
||||
export const config = [
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
plugins: {
|
||||
turbo: turboPlugin,
|
||||
},
|
||||
rules: {
|
||||
"turbo/no-undeclared-env-vars": "warn",
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
onlyWarn,
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ["dist/**"],
|
||||
},
|
||||
];
|
||||
57
packages/eslint-config/next.js
Normal file
57
packages/eslint-config/next.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import js from "@eslint/js";
|
||||
import { globalIgnores } from "eslint/config";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import tseslint from "typescript-eslint";
|
||||
import pluginReactHooks from "eslint-plugin-react-hooks";
|
||||
import pluginReact from "eslint-plugin-react";
|
||||
import globals from "globals";
|
||||
import pluginNext from "@next/eslint-plugin-next";
|
||||
import { config as baseConfig } from "./base.js";
|
||||
|
||||
/**
|
||||
* A custom ESLint configuration for libraries that use Next.js.
|
||||
*
|
||||
* @type {import("eslint").Linter.Config[]}
|
||||
* */
|
||||
export const nextJsConfig = [
|
||||
...baseConfig,
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
...tseslint.configs.recommended,
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
{
|
||||
...pluginReact.configs.flat.recommended,
|
||||
languageOptions: {
|
||||
...pluginReact.configs.flat.recommended.languageOptions,
|
||||
globals: {
|
||||
...globals.serviceworker,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
"@next/next": pluginNext,
|
||||
},
|
||||
rules: {
|
||||
...pluginNext.configs.recommended.rules,
|
||||
...pluginNext.configs["core-web-vitals"].rules,
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
"react-hooks": pluginReactHooks,
|
||||
},
|
||||
settings: { react: { version: "detect" } },
|
||||
rules: {
|
||||
...pluginReactHooks.configs.recommended.rules,
|
||||
// React scope no longer necessary with new JSX transform.
|
||||
"react/react-in-jsx-scope": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
24
packages/eslint-config/package.json
Normal file
24
packages/eslint-config/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "@betternas/eslint-config",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"exports": {
|
||||
"./base": "./base.js",
|
||||
"./next-js": "./next.js",
|
||||
"./react-internal": "./react-internal.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@next/eslint-plugin-next": "^16.2.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-plugin-only-warn": "^1.1.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-turbo": "^2.7.1",
|
||||
"globals": "^16.5.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.50.0"
|
||||
}
|
||||
}
|
||||
39
packages/eslint-config/react-internal.js
vendored
Normal file
39
packages/eslint-config/react-internal.js
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import js from "@eslint/js";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import tseslint from "typescript-eslint";
|
||||
import pluginReactHooks from "eslint-plugin-react-hooks";
|
||||
import pluginReact from "eslint-plugin-react";
|
||||
import globals from "globals";
|
||||
import { config as baseConfig } from "./base.js";
|
||||
|
||||
/**
|
||||
* A custom ESLint configuration for libraries that use React.
|
||||
*
|
||||
* @type {import("eslint").Linter.Config[]} */
|
||||
export const config = [
|
||||
...baseConfig,
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
...tseslint.configs.recommended,
|
||||
pluginReact.configs.flat.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
...pluginReact.configs.flat.recommended.languageOptions,
|
||||
globals: {
|
||||
...globals.serviceworker,
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
"react-hooks": pluginReactHooks,
|
||||
},
|
||||
settings: { react: { version: "detect" } },
|
||||
rules: {
|
||||
...pluginReactHooks.configs.recommended.rules,
|
||||
// React scope no longer necessary with new JSX transform.
|
||||
"react/react-in-jsx-scope": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
10
packages/sdk-ts/README.md
Normal file
10
packages/sdk-ts/README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# `@betternas/sdk-ts`
|
||||
|
||||
Temporary TypeScript-facing SDK surface for the Next.js app.
|
||||
|
||||
The source of truth remains:
|
||||
|
||||
- [`packages/contracts/openapi`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/openapi)
|
||||
- [`packages/contracts/src`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/src)
|
||||
|
||||
Later this package should become generated code from the OpenAPI contracts.
|
||||
3
packages/sdk-ts/eslint.config.mjs
Normal file
3
packages/sdk-ts/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { config } from "@betternas/eslint-config/base";
|
||||
|
||||
export default config;
|
||||
24
packages/sdk-ts/package.json
Normal file
24
packages/sdk-ts/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "@betternas/sdk-ts",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"lint": "eslint . --max-warnings 0",
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@betternas/contracts": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@betternas/eslint-config": "*",
|
||||
"@betternas/typescript-config": "*",
|
||||
"@types/node": "^22.15.3",
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "5.9.2"
|
||||
}
|
||||
}
|
||||
1
packages/sdk-ts/src/index.ts
Normal file
1
packages/sdk-ts/src/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "@betternas/contracts";
|
||||
8
packages/sdk-ts/tsconfig.json
Normal file
8
packages/sdk-ts/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "@betternas/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
19
packages/typescript-config/base.json
Normal file
19
packages/typescript-config/base.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"esModuleInterop": true,
|
||||
"incremental": false,
|
||||
"isolatedModules": true,
|
||||
"lib": ["es2022", "DOM", "DOM.Iterable"],
|
||||
"module": "NodeNext",
|
||||
"moduleDetection": "force",
|
||||
"moduleResolution": "NodeNext",
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"target": "ES2022"
|
||||
}
|
||||
}
|
||||
12
packages/typescript-config/nextjs.json
Normal file
12
packages/typescript-config/nextjs.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"plugins": [{ "name": "next" }],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowJs": true,
|
||||
"jsx": "preserve",
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
9
packages/typescript-config/package.json
Normal file
9
packages/typescript-config/package.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@betternas/typescript-config",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
7
packages/typescript-config/react-library.json
Normal file
7
packages/typescript-config/react-library.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
4
packages/ui/eslint.config.mjs
Normal file
4
packages/ui/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { config } from "@betternas/eslint-config/react-internal";
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default config;
|
||||
27
packages/ui/package.json
Normal file
27
packages/ui/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "@betternas/ui",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"exports": {
|
||||
"./*": "./src/*.tsx"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"lint": "eslint . --max-warnings 0",
|
||||
"generate:component": "turbo gen react-component",
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@betternas/eslint-config": "*",
|
||||
"@betternas/typescript-config": "*",
|
||||
"@types/node": "^22.15.3",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
}
|
||||
}
|
||||
17
packages/ui/src/button.tsx
Normal file
17
packages/ui/src/button.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface ButtonProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const Button = ({ children, className, onClick }: ButtonProps) => {
|
||||
return (
|
||||
<button className={className} onClick={onClick} type="button">
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
22
packages/ui/src/card.tsx
Normal file
22
packages/ui/src/card.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { type JSX } from "react";
|
||||
|
||||
export function Card({
|
||||
className,
|
||||
title,
|
||||
children,
|
||||
href,
|
||||
}: {
|
||||
className?: string;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<a className={className} href={href}>
|
||||
<h2>
|
||||
{title} <span>-></span>
|
||||
</h2>
|
||||
<p>{children}</p>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
11
packages/ui/src/code.tsx
Normal file
11
packages/ui/src/code.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { type JSX } from "react";
|
||||
|
||||
export function Code({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}): JSX.Element {
|
||||
return <code className={className}>{children}</code>;
|
||||
}
|
||||
9
packages/ui/tsconfig.json
Normal file
9
packages/ui/tsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "@betternas/typescript-config/react-library.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
compose_file="$repo_root/docker/compose.dev.yml"
|
||||
compose_file="$repo_root/infra/docker/compose.dev.yml"
|
||||
|
||||
args=(down --remove-orphans)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
compose_file="$repo_root/docker/compose.dev.yml"
|
||||
app_source_dir="$repo_root/apps/betternascontrolplane"
|
||||
compose_file="$repo_root/infra/docker/compose.dev.yml"
|
||||
app_source_dir="$repo_root/apps/nextcloud-app"
|
||||
|
||||
nextcloud_occ() {
|
||||
docker compose -f "$compose_file" exec -T --user www-data --workdir /var/www/html nextcloud php occ "$@"
|
||||
|
|
|
|||
263
skeleton.md
Normal file
263
skeleton.md
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
# betterNAS Skeleton
|
||||
|
||||
This is the root build skeleton for the monorepo.
|
||||
|
||||
Its job is simple:
|
||||
|
||||
- lock the repo shape
|
||||
- lock the language per runtime
|
||||
- lock the first shared contract surface
|
||||
- give agents a safe place to work in parallel
|
||||
- keep the list of upstream references we are stealing from
|
||||
|
||||
## Repo shape
|
||||
|
||||
```text
|
||||
betterNAS/
|
||||
├── apps/
|
||||
│ ├── web/ # Next.js control-plane UI
|
||||
│ ├── control-plane/ # Go control-plane API
|
||||
│ ├── node-agent/ # Go NAS runtime + WebDAV surface
|
||||
│ └── nextcloud-app/ # optional Nextcloud adapter
|
||||
├── packages/
|
||||
│ ├── contracts/ # canonical OpenAPI, schemas, TS types
|
||||
│ ├── sdk-ts/ # TS SDK facade for the web app
|
||||
│ ├── ui/ # shared React UI
|
||||
│ ├── eslint-config/ # shared lint config
|
||||
│ └── typescript-config/ # shared TS config
|
||||
├── infra/
|
||||
│ └── docker/ # local runtime stack
|
||||
├── docs/ # architecture and part docs
|
||||
├── scripts/ # local helper scripts
|
||||
├── go.work # Go workspace
|
||||
├── turbo.json # Turborepo task graph
|
||||
└── skeleton.md # this file
|
||||
```
|
||||
|
||||
## Runtime and language choices
|
||||
|
||||
| Part | Language | Why |
|
||||
|---|---|---|
|
||||
| `apps/web` | TypeScript + Next.js | best UI velocity, best admin/control-plane UX |
|
||||
| `apps/control-plane` | Go | strong concurrency, static binaries, operationally simple |
|
||||
| `apps/node-agent` | Go | best fit for host runtime, WebDAV service, and future Nix deployment |
|
||||
| `apps/nextcloud-app` | PHP | native language for the Nextcloud adapter surface |
|
||||
| `packages/contracts` | OpenAPI + JSON Schema + TypeScript | language-neutral source of truth with practical TS ergonomics |
|
||||
| `packages/sdk-ts` | TypeScript | ergonomic consumption from the Next.js app |
|
||||
|
||||
## Canonical contract rule
|
||||
|
||||
The source of truth for shared interfaces is:
|
||||
|
||||
1. [`docs/architecture.md`](/home/rathi/Documents/GitHub/betterNAS/docs/architecture.md)
|
||||
2. [`packages/contracts/openapi/betternas.v1.yaml`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/openapi/betternas.v1.yaml)
|
||||
3. [`packages/contracts/schemas`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/schemas)
|
||||
4. [`packages/contracts/src`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/src)
|
||||
|
||||
Agents must not invent private shared request or response shapes outside those
|
||||
locations.
|
||||
|
||||
## Parallel lanes
|
||||
|
||||
```text
|
||||
shared write surface
|
||||
+-------------------------------------------+
|
||||
| docs/architecture.md |
|
||||
| packages/contracts/ |
|
||||
+----------------+--------------------------+
|
||||
|
|
||||
+-----------------+-----------------+-----------------+
|
||||
| | | |
|
||||
v v v v
|
||||
NAS node control plane local device cloud layer
|
||||
lane lane lane lane
|
||||
```
|
||||
|
||||
Allowed ownership:
|
||||
|
||||
- NAS node lane
|
||||
- `apps/node-agent`
|
||||
- future `infra/nix/node-*`
|
||||
- control-plane lane
|
||||
- `apps/control-plane`
|
||||
- DB and queue integration code later
|
||||
- local-device lane
|
||||
- mount docs first
|
||||
- future helper app
|
||||
- cloud layer lane
|
||||
- `apps/nextcloud-app`
|
||||
- Nextcloud mapping logic
|
||||
- shared contract lane
|
||||
- `packages/contracts`
|
||||
- `packages/sdk-ts`
|
||||
- `docs/architecture.md`
|
||||
|
||||
## The first verification loop
|
||||
|
||||
```text
|
||||
[node-agent]
|
||||
serves WebDAV export
|
||||
|
|
||||
v
|
||||
[control-plane]
|
||||
registers node + export
|
||||
issues mount profile
|
||||
|
|
||||
v
|
||||
[local device]
|
||||
mounts WebDAV in Finder
|
||||
|
|
||||
v
|
||||
[cloud layer]
|
||||
optionally exposes same export in Nextcloud
|
||||
```
|
||||
|
||||
If a task does not make one of those steps more real, it is probably too early.
|
||||
|
||||
## Upstream references to steal from
|
||||
|
||||
### Monorepo and web
|
||||
|
||||
- Turborepo `create-turbo`
|
||||
- https://turborepo.dev/repo/docs/reference/create-turbo
|
||||
- why: monorepo base scaffold
|
||||
|
||||
- Turborepo structuring guide
|
||||
- https://turborepo.dev/repo/docs/crafting-your-repository/structuring-a-repository
|
||||
- why: package boundaries and task graph rules
|
||||
|
||||
- Next.js backend-for-frontend guide
|
||||
- https://nextjs.org/docs/app/guides/backend-for-frontend
|
||||
- why: keep Next.js as UI/BFF, not the system-of-record backend
|
||||
|
||||
### Go control plane
|
||||
|
||||
- Go routing enhancements
|
||||
- https://go.dev/blog/routing-enhancements
|
||||
- why: stdlib-first routing baseline
|
||||
|
||||
- `chi`
|
||||
- https://github.com/go-chi/chi
|
||||
- why: minimal router if stdlib patterns become too bare
|
||||
|
||||
- `pgx`
|
||||
- https://github.com/jackc/pgx
|
||||
- why: Postgres-first Go driver
|
||||
|
||||
- `sqlc`
|
||||
- https://github.com/sqlc-dev/sqlc
|
||||
- why: typed query generation
|
||||
|
||||
- `go-redis`
|
||||
- https://github.com/redis/go-redis
|
||||
- why: primary Redis client
|
||||
|
||||
- `asynq`
|
||||
- https://github.com/hibiken/asynq
|
||||
- why: practical Redis-backed job system
|
||||
|
||||
- `koanf`
|
||||
- https://github.com/knadh/koanf
|
||||
- why: layered config if env-only config becomes too small
|
||||
|
||||
- `envconfig`
|
||||
- https://github.com/kelseyhightower/envconfig
|
||||
- why: tiny env-only config option
|
||||
|
||||
- `log/slog`
|
||||
- https://pkg.go.dev/log/slog
|
||||
- why: structured logging without adding a logging framework first
|
||||
|
||||
- `oapi-codegen`
|
||||
- https://github.com/oapi-codegen/oapi-codegen
|
||||
- why: generate Go and TS surfaces from OpenAPI
|
||||
|
||||
### NAS node and WebDAV
|
||||
|
||||
- Go WebDAV package
|
||||
- https://pkg.go.dev/golang.org/x/net/webdav
|
||||
- why: embeddable WebDAV server implementation
|
||||
|
||||
- `hacdias/webdav`
|
||||
- https://github.com/hacdias/webdav
|
||||
- why: small standalone WebDAV reference
|
||||
|
||||
- NixOS manual
|
||||
- https://nixos.org/manual/nixos/stable/
|
||||
- why: declarative host setup and service wiring
|
||||
|
||||
- Nixpkgs
|
||||
- https://github.com/NixOS/nixpkgs
|
||||
- why: service module and packaging reference
|
||||
|
||||
### Local device and mount UX
|
||||
|
||||
- Finder `Connect to Server`
|
||||
- https://support.apple.com/en-lamr/guide/mac-help/mchlp3015/mac
|
||||
- why: native mount UX baseline
|
||||
|
||||
- Finder WebDAV mounting
|
||||
- https://support.apple.com/is-is/guide/mac-help/mchlp1546/mac
|
||||
- why: exact v1 mount path
|
||||
|
||||
- Keychain data protection
|
||||
- https://support.apple.com/guide/security/keychain-data-protection-secb0694df1a/web
|
||||
- why: local credential storage model
|
||||
|
||||
- Finder Sync extensions
|
||||
- https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/Finder.html
|
||||
- why: future helper app / Finder integration reference
|
||||
|
||||
- WebDAV RFC 4918
|
||||
- https://www.rfc-editor.org/rfc/rfc4918
|
||||
- why: protocol semantics and edge cases
|
||||
|
||||
### Cloud and adapter layer
|
||||
|
||||
- Nextcloud app template
|
||||
- https://github.com/nextcloud/app_template
|
||||
- why: thin adapter app reference
|
||||
|
||||
- AppAPI / External Apps
|
||||
- https://docs.nextcloud.com/server/latest/admin_manual/exapps_management/AppAPIAndExternalApps.html
|
||||
- why: official external-app integration path
|
||||
|
||||
- Nextcloud WebDAV docs
|
||||
- https://docs.nextcloud.com/server/latest/user_manual/en/files/access_webdav.html
|
||||
- why: protocol/client behavior reference
|
||||
|
||||
- Nextcloud external storage
|
||||
- https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/external_storage_configuration_gui.html
|
||||
- why: storage aggregation behavior
|
||||
|
||||
- Nextcloud file sharing config
|
||||
- https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/file_sharing_configuration.html
|
||||
- why: share semantics reference
|
||||
|
||||
## What we steal vs what we own
|
||||
|
||||
### Steal
|
||||
|
||||
- Turborepo repo shape and task graph
|
||||
- Next.js web-app conventions
|
||||
- Go stdlib and proven Go infra libraries
|
||||
- Go WebDAV implementation
|
||||
- Finder native WebDAV mount UX
|
||||
- Nextcloud shell-app and cloud/web primitives
|
||||
|
||||
### Own
|
||||
|
||||
- the betterNAS domain model
|
||||
- the control-plane API
|
||||
- the node registration and export model
|
||||
- the mount profile model
|
||||
- the mapping between cloud mode and mount mode
|
||||
- the repo contract and shared schemas
|
||||
|
||||
## The first implementation slices after this scaffold
|
||||
|
||||
1. make `apps/node-agent` serve a real configurable WebDAV export
|
||||
2. make `apps/control-plane` store real node/export records
|
||||
3. issue real mount profiles from the control plane
|
||||
4. make `apps/web` let a user pick an export and request a profile
|
||||
5. keep `apps/nextcloud-app` thin and optional
|
||||
24
turbo.json
Normal file
24
turbo.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"$schema": "https://turborepo.dev/schema.json",
|
||||
"ui": "tui",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^lint"]
|
||||
},
|
||||
"check-types": {
|
||||
"dependsOn": ["^check-types"]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["^test"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue