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
|
.DS_Store
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
.codex/
|
.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 control plane is the system of record.
|
||||||
- the NAS node serves bytes directly whenever possible.
|
- the NAS node serves bytes directly whenever possible.
|
||||||
- Nextcloud is an optional cloud/web adapter, not the product center.
|
- 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 |
|
| Part | Steal first | Write ourselves |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| NAS node | NixOS/Nix module patterns, existing WebDAV servers | node agent, export model, node registration flow |
|
| 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 |
|
| 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 |
|
| 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
|
## 2. Control plane
|
||||||
|
|
||||||
Start from:
|
Start from:
|
||||||
- the current `exapps/control-plane` scaffold
|
- Go
|
||||||
- TypeScript
|
- standard library routing first
|
||||||
- Postgres
|
- Postgres via `pgx` and `sqlc`
|
||||||
- Redis
|
- Redis via `go-redis`
|
||||||
|
- OpenAPI-driven contracts
|
||||||
- standalone API mindset
|
- standalone API mindset
|
||||||
|
|
||||||
Do not start by writing:
|
Do not start by writing:
|
||||||
|
|
@ -151,7 +152,7 @@ This is high leverage, but should not block Phase A.
|
||||||
- Nix module patterns
|
- Nix module patterns
|
||||||
|
|
||||||
### Control plane
|
### Control plane
|
||||||
- current TypeScript repo scaffold
|
- Go API service scaffold
|
||||||
- Postgres
|
- Postgres
|
||||||
- Redis
|
- Redis
|
||||||
|
|
||||||
|
|
@ -193,7 +194,7 @@ This is high leverage, but should not block Phase A.
|
||||||
| Part | First scaffold |
|
| Part | First scaffold |
|
||||||
|---|---|
|
|---|---|
|
||||||
| NAS node | Nix flake/module + WebDAV server service config |
|
| 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 |
|
| Local device | documented Finder mount flow, then lightweight helper app |
|
||||||
| Cloud/web layer | current Nextcloud scaffold and shell 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/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)
|
- [`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 contract surface we need first
|
||||||
|
|
||||||
The first shared contract set should cover only the seams that let all four
|
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
|
## 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
|
### Backend and infra references
|
||||||
|
|
||||||
- Fastify
|
- Go routing enhancements
|
||||||
- repo: https://github.com/fastify/fastify
|
- docs: https://go.dev/blog/routing-enhancements
|
||||||
- why: likely good fit for a TypeScript API backend
|
- 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
|
- PostgreSQL
|
||||||
- docs: https://www.postgresql.org/docs/
|
- docs: https://www.postgresql.org/docs/
|
||||||
- why: source of truth for product metadata
|
- 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
|
- Redis
|
||||||
- docs: https://redis.io/docs/latest/
|
- docs: https://redis.io/docs/latest/
|
||||||
- why: cache, jobs, ephemeral coordination
|
- 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`
|
- `sshpiper`
|
||||||
- repo: https://github.com/tg123/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/
|
- docs: https://developer.apple.com/documentation/
|
||||||
- why: Keychain, launch agents, desktop helpers, future native integration
|
- 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
|
## Cloud / web layer
|
||||||
|
|
||||||
### Nextcloud server and app references
|
### 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:
|
control-plane:
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ../..
|
||||||
dockerfile: exapps/control-plane/Dockerfile
|
dockerfile: apps/control-plane/Dockerfile
|
||||||
environment:
|
environment:
|
||||||
PORT: 3000
|
PORT: 3000
|
||||||
BETTERNAS_VERSION: local-dev
|
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",
|
"name": "betternas",
|
||||||
"private": true,
|
"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",
|
"packageManager": "npm@10.9.7",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"apps/*",
|
||||||
"exapps/*"
|
"packages/*"
|
||||||
],
|
]
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ Use it to keep the four product parts aligned:
|
||||||
|
|
||||||
## What belongs here
|
## What belongs here
|
||||||
|
|
||||||
|
- OpenAPI source documents
|
||||||
- shared TypeScript types
|
- shared TypeScript types
|
||||||
- route constants
|
- route constants
|
||||||
- JSON schemas for payloads we want to validate outside TypeScript
|
- 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
|
- current runtime scaffold for health and version
|
||||||
- [`src/foundation.ts`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/src/foundation.ts)
|
- [`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
|
- 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)
|
- [`schemas/`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/schemas)
|
||||||
- JSON schema mirrors for the first shared entities
|
- 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",
|
"types": "./dist/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
"openapi",
|
||||||
"schemas"
|
"schemas"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -14,4 +15,3 @@
|
||||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
"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
|
set -euo pipefail
|
||||||
|
|
||||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
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)
|
args=(down --remove-orphans)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
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"
|
||||||
app_source_dir="$repo_root/apps/betternascontrolplane"
|
app_source_dir="$repo_root/apps/nextcloud-app"
|
||||||
|
|
||||||
nextcloud_occ() {
|
nextcloud_occ() {
|
||||||
docker compose -f "$compose_file" exec -T --user www-data --workdir /var/www/html nextcloud php 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