mirror of
https://github.com/harivansh-afk/betterNAS.git
synced 2026-04-15 08:03:42 +00:00
Merge pull request #1 from harivansh-afk/phase1
Scaffold control plane foundation
This commit is contained in:
commit
753f3df197
43 changed files with 1914 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
npm-debug.log*
|
||||
|
||||
51
README.md
Normal file
51
README.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# aiNAS
|
||||
|
||||
aiNAS is a storage control-plane project that uses vanilla Nextcloud as an upstream backend instead of forking the core server. This repository starts with the foundational pieces we need to build our own product surfaces while keeping file primitives, sync compatibility, and existing client integrations delegated to Nextcloud.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
- `docker/`: local development runtime for Nextcloud and aiNAS services
|
||||
- `apps/ainas-controlplane/`: thin Nextcloud shell app
|
||||
- `exapps/control-plane/`: aiNAS-owned control-plane service
|
||||
- `packages/contracts/`: shared API contracts used by aiNAS services and adapters
|
||||
- `docs/`: architecture and development notes
|
||||
- `scripts/`: repeatable developer workflows
|
||||
|
||||
## Local Development
|
||||
|
||||
Requirements:
|
||||
- Docker with Compose support
|
||||
- Node.js 22+
|
||||
- npm 10+
|
||||
|
||||
Bootstrap the JavaScript workspace:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Start the local stack:
|
||||
|
||||
```bash
|
||||
./scripts/dev-up
|
||||
```
|
||||
|
||||
Stop the local stack:
|
||||
|
||||
```bash
|
||||
./scripts/dev-down
|
||||
```
|
||||
|
||||
Once the stack is up:
|
||||
- Nextcloud: `http://localhost:8080`
|
||||
- aiNAS control plane: `http://localhost:3001`
|
||||
|
||||
The `dev-up` script waits for Nextcloud installation to finish and then enables the `ainascontrolplane` custom app inside the container.
|
||||
|
||||
## Architecture
|
||||
|
||||
The intended boundary is documented in `docs/architecture.md`. The short version is:
|
||||
|
||||
- Nextcloud remains an upstream storage and client-compatibility backend.
|
||||
- The custom Nextcloud app is a shell and adapter layer.
|
||||
- aiNAS business logic lives in the control-plane service.
|
||||
30
apps/ainas-controlplane/appinfo/info.xml
Normal file
30
apps/ainas-controlplane/appinfo/info.xml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0"?>
|
||||
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
|
||||
<id>ainascontrolplane</id>
|
||||
<name>aiNAS Control Plane</name>
|
||||
<summary>Thin aiNAS shell app for Nextcloud integration</summary>
|
||||
<description>Provides aiNAS-branded entry points inside Nextcloud while delegating business logic to the aiNAS control plane.</description>
|
||||
<version>0.1.0</version>
|
||||
<licence>AGPL-3.0-or-later</licence>
|
||||
<author homepage="https://ainas.local">aiNAS</author>
|
||||
<namespace>AinasControlplane</namespace>
|
||||
<category>integration</category>
|
||||
<dependencies>
|
||||
<nextcloud min-version="31" max-version="33"/>
|
||||
</dependencies>
|
||||
<navigations>
|
||||
<navigation>
|
||||
<id>ainascontrolplane</id>
|
||||
<name>aiNAS</name>
|
||||
<route>ainascontrolplane.page.index</route>
|
||||
<icon>app.svg</icon>
|
||||
<type>link</type>
|
||||
</navigation>
|
||||
</navigations>
|
||||
<settings>
|
||||
<admin>OCA\AinasControlplane\Settings\Admin</admin>
|
||||
<admin-section>OCA\AinasControlplane\Settings\AdminSection</admin-section>
|
||||
</settings>
|
||||
</info>
|
||||
|
||||
17
apps/ainas-controlplane/composer.json
Normal file
17
apps/ainas-controlplane/composer.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "ainas/ainascontrolplane",
|
||||
"description": "aiNAS Nextcloud shell app",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"OCA\\AinasControlplane\\": "lib/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"nextcloud/ocp": "dev-stable31"
|
||||
}
|
||||
}
|
||||
|
||||
85
apps/ainas-controlplane/css/ainascontrolplane.css
Normal file
85
apps/ainas-controlplane/css/ainascontrolplane.css
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
.ainas-shell {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.ainas-shell__hero {
|
||||
margin-bottom: 28px;
|
||||
padding: 28px;
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(135deg, #10212d 0%, #184152 100%);
|
||||
color: #f6fafc;
|
||||
}
|
||||
|
||||
.ainas-shell__eyebrow {
|
||||
margin: 0 0 12px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.ainas-shell__title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 32px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.ainas-shell__copy {
|
||||
margin: 0;
|
||||
max-width: 70ch;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.ainas-shell__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.ainas-shell__card {
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(16, 33, 45, 0.12);
|
||||
border-radius: 20px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 20px 40px rgba(16, 33, 45, 0.06);
|
||||
}
|
||||
|
||||
.ainas-shell__card h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.ainas-shell__card dl {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 160px) 1fr;
|
||||
gap: 8px 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ainas-shell__card dt {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ainas-shell__card dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ainas-shell__card code {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
background: #eef4f7;
|
||||
}
|
||||
|
||||
.ainas-shell__card ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.ainas-shell__error {
|
||||
margin-top: 16px;
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
6
apps/ainas-controlplane/img/app-dark.svg
Normal file
6
apps/ainas-controlplane/img/app-dark.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" fill="none">
|
||||
<rect width="128" height="128" rx="28" fill="#0D1720"/>
|
||||
<path d="M26 88L47 42H59L80 88H69L64 75H42L37 88H26ZM46 66H60L53 49L46 66Z" fill="#F6FAFC"/>
|
||||
<path d="M78 88V42H90V78H110V88H78Z" fill="#4EC2B2"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 292 B |
6
apps/ainas-controlplane/img/app.svg
Normal file
6
apps/ainas-controlplane/img/app.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" fill="none">
|
||||
<rect width="128" height="128" rx="28" fill="#184152"/>
|
||||
<path d="M26 88L47 42H59L80 88H69L64 75H42L37 88H26ZM46 66H60L53 49L46 66Z" fill="#F6FAFC"/>
|
||||
<path d="M78 88V42H90V78H110V88H78Z" fill="#7CE0D2"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 292 B |
25
apps/ainas-controlplane/lib/AppInfo/Application.php
Normal file
25
apps/ainas-controlplane/lib/AppInfo/Application.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\AinasControlplane\AppInfo;
|
||||
|
||||
use OCP\AppFramework\App;
|
||||
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||
|
||||
class Application extends App implements IBootstrap {
|
||||
public const APP_ID = 'ainascontrolplane';
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct(self::APP_ID);
|
||||
}
|
||||
|
||||
public function register(IRegistrationContext $context): void {
|
||||
}
|
||||
|
||||
public function boot(IBootContext $context): void {
|
||||
}
|
||||
}
|
||||
|
||||
29
apps/ainas-controlplane/lib/Controller/ApiController.php
Normal file
29
apps/ainas-controlplane/lib/Controller/ApiController.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\AinasControlplane\Controller;
|
||||
|
||||
use OCA\AinasControlplane\AppInfo\Application;
|
||||
use OCA\AinasControlplane\Service\ControlPlaneClient;
|
||||
use OCP\AppFramework\Http\Attribute\ApiRoute;
|
||||
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCSController;
|
||||
use OCP\IRequest;
|
||||
|
||||
class ApiController extends OCSController {
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
private readonly ControlPlaneClient $controlPlaneClient,
|
||||
) {
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
}
|
||||
|
||||
#[NoAdminRequired]
|
||||
#[ApiRoute(verb: 'GET', url: '/api/status')]
|
||||
public function status(): DataResponse {
|
||||
return new DataResponse($this->controlPlaneClient->fetchSnapshot());
|
||||
}
|
||||
}
|
||||
|
||||
43
apps/ainas-controlplane/lib/Controller/PageController.php
Normal file
43
apps/ainas-controlplane/lib/Controller/PageController.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\AinasControlplane\Controller;
|
||||
|
||||
use OCA\AinasControlplane\AppInfo\Application;
|
||||
use OCA\AinasControlplane\Service\ControlPlaneClient;
|
||||
use OCA\AinasControlplane\Service\ControlPlaneConfig;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
|
||||
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
|
||||
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
|
||||
use OCP\AppFramework\Http\Attribute\OpenAPI;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\IRequest;
|
||||
|
||||
class PageController extends Controller {
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
private readonly ControlPlaneClient $controlPlaneClient,
|
||||
private readonly ControlPlaneConfig $controlPlaneConfig,
|
||||
) {
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
}
|
||||
|
||||
#[NoCSRFRequired]
|
||||
#[NoAdminRequired]
|
||||
#[OpenAPI(OpenAPI::SCOPE_IGNORE)]
|
||||
#[FrontpageRoute(verb: 'GET', url: '/')]
|
||||
public function index(): TemplateResponse {
|
||||
return new TemplateResponse(
|
||||
Application::APP_ID,
|
||||
'index',
|
||||
[
|
||||
'appName' => 'aiNAS Control Plane',
|
||||
'controlPlaneUrl' => $this->controlPlaneConfig->getBaseUrl(),
|
||||
'snapshot' => $this->controlPlaneClient->fetchSnapshot(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
88
apps/ainas-controlplane/lib/Service/ControlPlaneClient.php
Normal file
88
apps/ainas-controlplane/lib/Service/ControlPlaneClient.php
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\AinasControlplane\Service;
|
||||
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\Http\Client\IResponse;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class ControlPlaneClient {
|
||||
public function __construct(
|
||||
private readonly IClientService $clientService,
|
||||
private readonly ControlPlaneConfig $controlPlaneConfig,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function fetchSnapshot(): array {
|
||||
$baseUrl = $this->controlPlaneConfig->getBaseUrl();
|
||||
|
||||
try {
|
||||
$healthResponse = $this->request($baseUrl . '/health');
|
||||
$versionResponse = $this->request($baseUrl . '/version');
|
||||
|
||||
return [
|
||||
'available' => $healthResponse['statusCode'] === 200,
|
||||
'url' => $baseUrl,
|
||||
'health' => $healthResponse['body'],
|
||||
'version' => $versionResponse['body'],
|
||||
];
|
||||
} catch (\Throwable $exception) {
|
||||
$this->logger->warning('Failed to reach aiNAS control plane', [
|
||||
'exception' => $exception,
|
||||
'url' => $baseUrl,
|
||||
]);
|
||||
|
||||
return [
|
||||
'available' => false,
|
||||
'url' => $baseUrl,
|
||||
'error' => $exception->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{statusCode: int, body: array<string, mixed>}
|
||||
*/
|
||||
private function request(string $url): array {
|
||||
$client = $this->clientService->newClient();
|
||||
$response = $client->get($url, [
|
||||
'headers' => [
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
'http_errors' => false,
|
||||
'timeout' => 2,
|
||||
'nextcloud' => [
|
||||
'allow_local_address' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
return [
|
||||
'statusCode' => $response->getStatusCode(),
|
||||
'body' => $this->decodeBody($response),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function decodeBody(IResponse $response): array {
|
||||
$body = $response->getBody();
|
||||
if ($body === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($decoded)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
31
apps/ainas-controlplane/lib/Service/ControlPlaneConfig.php
Normal file
31
apps/ainas-controlplane/lib/Service/ControlPlaneConfig.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\AinasControlplane\Service;
|
||||
|
||||
use OCA\AinasControlplane\AppInfo\Application;
|
||||
use OCP\IAppConfig;
|
||||
|
||||
class ControlPlaneConfig {
|
||||
public function __construct(
|
||||
private readonly IAppConfig $appConfig,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getBaseUrl(): string {
|
||||
$environmentUrl = getenv('AINAS_CONTROL_PLANE_URL');
|
||||
if (is_string($environmentUrl) && $environmentUrl !== '') {
|
||||
return rtrim($environmentUrl, '/');
|
||||
}
|
||||
|
||||
$configuredUrl = $this->appConfig->getValueString(
|
||||
Application::APP_ID,
|
||||
'control_plane_url',
|
||||
'http://control-plane:3000',
|
||||
);
|
||||
|
||||
return rtrim($configuredUrl, '/');
|
||||
}
|
||||
}
|
||||
|
||||
39
apps/ainas-controlplane/lib/Settings/Admin.php
Normal file
39
apps/ainas-controlplane/lib/Settings/Admin.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\AinasControlplane\Settings;
|
||||
|
||||
use OCA\AinasControlplane\AppInfo\Application;
|
||||
use OCA\AinasControlplane\Service\ControlPlaneClient;
|
||||
use OCA\AinasControlplane\Service\ControlPlaneConfig;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\Settings\ISettings;
|
||||
|
||||
class Admin implements ISettings {
|
||||
public function __construct(
|
||||
private readonly ControlPlaneConfig $controlPlaneConfig,
|
||||
private readonly ControlPlaneClient $controlPlaneClient,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getForm(): TemplateResponse {
|
||||
return new TemplateResponse(
|
||||
Application::APP_ID,
|
||||
'admin',
|
||||
[
|
||||
'controlPlaneUrl' => $this->controlPlaneConfig->getBaseUrl(),
|
||||
'snapshot' => $this->controlPlaneClient->fetchSnapshot(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function getSection(): string {
|
||||
return Application::APP_ID;
|
||||
}
|
||||
|
||||
public function getPriority(): int {
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
||||
35
apps/ainas-controlplane/lib/Settings/AdminSection.php
Normal file
35
apps/ainas-controlplane/lib/Settings/AdminSection.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\AinasControlplane\Settings;
|
||||
|
||||
use OCA\AinasControlplane\AppInfo\Application;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Settings\IIconSection;
|
||||
|
||||
class AdminSection implements IIconSection {
|
||||
public function __construct(
|
||||
private readonly IURLGenerator $urlGenerator,
|
||||
private readonly IL10N $l,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getID(): string {
|
||||
return Application::APP_ID;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->l->t('aiNAS');
|
||||
}
|
||||
|
||||
public function getPriority(): int {
|
||||
return 50;
|
||||
}
|
||||
|
||||
public function getIcon(): ?string {
|
||||
return $this->urlGenerator->imagePath(Application::APP_ID, 'app-dark.svg');
|
||||
}
|
||||
}
|
||||
|
||||
43
apps/ainas-controlplane/templates/admin.php
Normal file
43
apps/ainas-controlplane/templates/admin.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use OCA\AinasControlplane\AppInfo\Application;
|
||||
use OCP\Util;
|
||||
|
||||
Util::addStyle(Application::APP_ID, 'ainascontrolplane');
|
||||
|
||||
$snapshot = $_['snapshot'];
|
||||
$reachable = !empty($snapshot['available']) ? 'yes' : 'no';
|
||||
$version = $snapshot['version']['version'] ?? 'unreachable';
|
||||
?>
|
||||
|
||||
<div class="ainas-shell ainas-shell--admin">
|
||||
<div class="ainas-shell__hero">
|
||||
<p class="ainas-shell__eyebrow">Admin settings</p>
|
||||
<h1 class="ainas-shell__title">aiNAS control-plane wiring</h1>
|
||||
<p class="ainas-shell__copy">
|
||||
The local scaffold wires this app to the control plane through the <code>AINAS_CONTROL_PLANE_URL</code> environment variable in the Nextcloud container.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="ainas-shell__grid">
|
||||
<section class="ainas-shell__card">
|
||||
<h2>Current wiring</h2>
|
||||
<dl>
|
||||
<dt>Control-plane URL</dt>
|
||||
<dd><code><?php p($_['controlPlaneUrl']); ?></code></dd>
|
||||
<dt>Reachable</dt>
|
||||
<dd><?php p($reachable); ?></dd>
|
||||
<dt>Reported version</dt>
|
||||
<dd><?php p($version); ?></dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="ainas-shell__card">
|
||||
<h2>Next step</h2>
|
||||
<p>Keep storage policy, sharing logic, and orchestration in the control-plane service. This page should remain a thin integration surface.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
51
apps/ainas-controlplane/templates/index.php
Normal file
51
apps/ainas-controlplane/templates/index.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use OCA\AinasControlplane\AppInfo\Application;
|
||||
use OCP\Util;
|
||||
|
||||
Util::addStyle(Application::APP_ID, 'ainascontrolplane');
|
||||
|
||||
$snapshot = $_['snapshot'];
|
||||
$version = $snapshot['version']['version'] ?? 'unreachable';
|
||||
$status = !empty($snapshot['available']) ? 'Connected' : 'Unavailable';
|
||||
$error = $snapshot['error'] ?? null;
|
||||
?>
|
||||
|
||||
<div class="ainas-shell">
|
||||
<div class="ainas-shell__hero">
|
||||
<p class="ainas-shell__eyebrow">aiNAS inside Nextcloud</p>
|
||||
<h1 class="ainas-shell__title"><?php p($_['appName']); ?></h1>
|
||||
<p class="ainas-shell__copy">
|
||||
This shell app stays intentionally thin. It exposes aiNAS entry points inside Nextcloud and delegates business logic to the external control-plane service.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="ainas-shell__grid">
|
||||
<section class="ainas-shell__card">
|
||||
<h2>Control plane</h2>
|
||||
<dl>
|
||||
<dt>Configured URL</dt>
|
||||
<dd><code><?php p($_['controlPlaneUrl']); ?></code></dd>
|
||||
<dt>Status</dt>
|
||||
<dd><?php p($status); ?></dd>
|
||||
<dt>Version</dt>
|
||||
<dd><?php p($version); ?></dd>
|
||||
</dl>
|
||||
<?php if ($error !== null): ?>
|
||||
<p class="ainas-shell__error"><?php p($error); ?></p>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="ainas-shell__card">
|
||||
<h2>Boundary</h2>
|
||||
<ul>
|
||||
<li>Nextcloud provides file and client primitives.</li>
|
||||
<li>aiNAS owns control-plane policy and orchestration.</li>
|
||||
<li>The shell app only adapts between the two.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
75
docker/compose.dev.yml
Normal file
75
docker/compose.dev.yml
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: nextcloud
|
||||
POSTGRES_USER: nextcloud
|
||||
POSTGRES_PASSWORD: nextcloud
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U nextcloud -d nextcloud"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
control-plane:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: exapps/control-plane/Dockerfile
|
||||
environment:
|
||||
PORT: 3000
|
||||
AINAS_VERSION: local-dev
|
||||
NEXTCLOUD_BASE_URL: http://nextcloud
|
||||
ports:
|
||||
- "3001:3000"
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"node -e \"fetch('http://127.0.0.1:3000/health').then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))\""
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
nextcloud:
|
||||
image: nextcloud:31-apache
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
control-plane:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
POSTGRES_HOST: db
|
||||
POSTGRES_DB: nextcloud
|
||||
POSTGRES_USER: nextcloud
|
||||
POSTGRES_PASSWORD: nextcloud
|
||||
REDIS_HOST: redis
|
||||
NEXTCLOUD_ADMIN_USER: admin
|
||||
NEXTCLOUD_ADMIN_PASSWORD: admin
|
||||
AINAS_CONTROL_PLANE_URL: http://control-plane:3000
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- nextcloud-data:/var/www/html
|
||||
- ../apps/ainas-controlplane:/var/www/html/custom_apps/ainascontrolplane
|
||||
|
||||
volumes:
|
||||
nextcloud-data:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
|
||||
54
docs/architecture.md
Normal file
54
docs/architecture.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# aiNAS Architecture Boundary
|
||||
|
||||
## Core Decision
|
||||
|
||||
aiNAS treats Nextcloud as an upstream backend, not as the place where aiNAS product logic should accumulate.
|
||||
|
||||
That leads to three explicit boundaries:
|
||||
|
||||
1. `apps/ainas-controlplane/` is a thin shell inside Nextcloud.
|
||||
2. `exapps/control-plane/` owns aiNAS business logic and internal APIs.
|
||||
3. `packages/contracts/` defines the interface between the shell app and the control plane.
|
||||
|
||||
## Why This Boundary Exists
|
||||
|
||||
Forking `nextcloud/server` would force aiNAS to own upstream patching and compatibility work too early. Pushing aiNAS logic into a traditional Nextcloud app would make the product harder to evolve outside the PHP monolith. The scaffold in this repository is designed to avoid both traps.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
### Nextcloud shell app
|
||||
|
||||
The shell app is responsible for:
|
||||
- navigation entries
|
||||
- branded entry pages inside Nextcloud
|
||||
- admin-facing integration surfaces
|
||||
- adapter calls into the aiNAS control plane
|
||||
|
||||
The shell app is not responsible for:
|
||||
- storage policy rules
|
||||
- orchestration logic
|
||||
- aiNAS-native RBAC decisions
|
||||
- product workflows that may later be reused by desktop, iOS, or standalone web clients
|
||||
|
||||
### Control-plane service
|
||||
|
||||
The control plane is responsible for:
|
||||
- domain logic
|
||||
- policy decisions
|
||||
- internal APIs consumed by aiNAS surfaces
|
||||
- Nextcloud integration adapters kept at the service boundary
|
||||
|
||||
### Shared contracts
|
||||
|
||||
Contracts live in `packages/contracts/` so request and response shapes do not get duplicated between PHP and TypeScript codebases.
|
||||
|
||||
## Local Runtime
|
||||
|
||||
The local development stack uses Docker Compose so developers can bring up:
|
||||
- Nextcloud
|
||||
- PostgreSQL
|
||||
- Redis
|
||||
- the aiNAS control-plane service
|
||||
|
||||
The Nextcloud shell app is mounted as a custom app and enabled through `./scripts/dev-up`.
|
||||
|
||||
15
exapps/control-plane/Dockerfile
Normal file
15
exapps/control-plane/Dockerfile
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
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"]
|
||||
|
||||
21
exapps/control-plane/package.json
Normal file
21
exapps/control-plane/package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "@ainas/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": {
|
||||
"@ainas/contracts": "file:../../packages/contracts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.18.6",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
14
exapps/control-plane/src/adapters/nextcloud-backend.ts
Normal file
14
exapps/control-plane/src/adapters/nextcloud-backend.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import type { NextcloudBackendStatus } from "@ainas/contracts";
|
||||
|
||||
export class NextcloudBackendAdapter {
|
||||
constructor(private readonly baseUrl: string) {}
|
||||
|
||||
describe(): NextcloudBackendStatus {
|
||||
return {
|
||||
configured: this.baseUrl.length > 0,
|
||||
baseUrl: this.baseUrl,
|
||||
provider: "nextcloud"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
59
exapps/control-plane/src/app.ts
Normal file
59
exapps/control-plane/src/app.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||
import {
|
||||
CONTROL_PLANE_ROUTES,
|
||||
type ControlPlaneHealthResponse,
|
||||
type ControlPlaneVersionResponse
|
||||
} from "@ainas/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));
|
||||
}
|
||||
|
||||
25
exapps/control-plane/src/config.ts
Normal file
25
exapps/control-plane/src/config.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
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.AINAS_VERSION ?? "0.1.0-dev",
|
||||
nextcloudBaseUrl: normalizeBaseUrl(env.NEXTCLOUD_BASE_URL ?? "http://nextcloud")
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(url: string): string {
|
||||
return url.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
10
exapps/control-plane/src/index.ts
Normal file
10
exapps/control-plane/src/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
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(`aiNAS control plane listening on port ${config.port}`);
|
||||
});
|
||||
|
||||
11
exapps/control-plane/tsconfig.json
Normal file
11
exapps/control-plane/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-03-31
|
||||
120
openspec/changes/scaffold-nextcloud-control-plane/design.md
Normal file
120
openspec/changes/scaffold-nextcloud-control-plane/design.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
## Context
|
||||
|
||||
aiNAS is starting as a greenfield project with a clear product boundary: we want our own storage control plane, product UX, and business logic while using Nextcloud as an upstream backend for file storage, sync primitives, sharing primitives, and existing client compatibility. The repository is effectively empty today, so this change needs to establish both the architectural stance and the initial developer scaffold.
|
||||
|
||||
The main constraint is maintenance ownership. Forking `nextcloud/server` would move security patches, upstream upgrade churn, and internal compatibility risk onto aiNAS too early. At the same time, pushing all product logic into a traditional Nextcloud app would make our business rules hard to evolve and tightly couple the product to the PHP monolith. The design therefore needs to leave us with a thin in-Nextcloud surface and a separate aiNAS-owned service layer.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Create a repository scaffold that supports local development with vanilla Nextcloud and aiNAS-owned services.
|
||||
- Define a thin Nextcloud shell app that handles navigation, branded entry points, and backend integration hooks.
|
||||
- Define an aiNAS control-plane service boundary for business logic, policy, and orchestration.
|
||||
- Keep interfaces typed and explicit so future web, desktop, and iOS clients can target aiNAS services rather than Nextcloud internals.
|
||||
- Make the initial architecture easy to extend without forcing a Nextcloud core fork.
|
||||
|
||||
**Non-Goals:**
|
||||
- Implement end-user storage features such as mounts, sync semantics, or sharing workflows in this change.
|
||||
- Build custom desktop or iOS clients in this change.
|
||||
- Replace Nextcloud's file storage, sync engine, or existing client stack.
|
||||
- Finalize long-term production deployment topology or multi-node scaling.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Use vanilla Nextcloud as an upstream backend, not a fork
|
||||
|
||||
aiNAS will run a stock Nextcloud instance in local development and future environments. We will extend it through a dedicated aiNAS app and service integrations instead of modifying core server code.
|
||||
|
||||
Rationale:
|
||||
- Keeps upstream upgrades and security patches tractable.
|
||||
- Lets us reuse mature file storage and client compatibility immediately.
|
||||
- Preserves an exit ramp if we later replace parts of the backend.
|
||||
|
||||
Alternatives considered:
|
||||
- Fork `nextcloud/server`: rejected due to long-term maintenance cost.
|
||||
- Build a custom storage platform first: rejected because it delays product iteration on higher-value workflows.
|
||||
|
||||
### 2. Keep the Nextcloud app thin and treat it as an adapter shell
|
||||
|
||||
The generated Nextcloud app will own aiNAS-specific UI entry points inside Nextcloud, settings pages, and integration hooks, but SHALL not become the home of core business logic. It will call aiNAS-owned APIs/services for control-plane decisions.
|
||||
|
||||
Rationale:
|
||||
- Keeps PHP app code small and replaceable.
|
||||
- Makes future non-Nextcloud clients first-class instead of afterthoughts.
|
||||
- Allows us to rewrite business logic without continually reshaping the shell app.
|
||||
|
||||
Alternatives considered:
|
||||
- Put most logic directly in the app: rejected because it couples product evolution to the monolith.
|
||||
|
||||
### 3. Scaffold an aiNAS control-plane service from the start
|
||||
|
||||
The repo will include a control-plane service that exposes internal HTTP APIs, owns domain models, and encapsulates policy and orchestration logic. In the first scaffold, this service may be packaged in an ExApp-compatible container, but the code structure SHALL keep Nextcloud-specific integration at the boundary rather than in domain logic.
|
||||
|
||||
Rationale:
|
||||
- Matches the product direction of aiNAS owning the control plane.
|
||||
- Gives one place for RBAC, storage policy, and orchestration logic to live.
|
||||
- Supports future desktop, iOS, and standalone web surfaces without coupling them to Nextcloud-rendered pages.
|
||||
|
||||
Alternatives considered:
|
||||
- Delay service creation and start with only a Nextcloud app: rejected because it encourages logic to accumulate in the wrong place.
|
||||
- Build multiple services immediately: rejected because one control-plane service is enough to establish the boundary.
|
||||
|
||||
### 4. Use a monorepo with explicit top-level boundaries
|
||||
|
||||
The initial scaffold will create clear top-level directories for infrastructure, app code, service code, shared contracts, docs, and scripts. The exact framework choices inside those directories can evolve, but the boundary layout should exist from day one.
|
||||
|
||||
Initial structure:
|
||||
- `docker/`: local orchestration and container assets
|
||||
- `apps/ainas-controlplane/`: generated Nextcloud shell app
|
||||
- `exapps/control-plane/`: aiNAS control-plane service, packaged for Nextcloud-compatible dev flows
|
||||
- `packages/contracts/`: shared schemas and API contracts
|
||||
- `docs/`: architecture and product model notes
|
||||
- `scripts/`: repeatable developer entry points
|
||||
|
||||
Rationale:
|
||||
- Makes ownership and coupling visible in the filesystem.
|
||||
- Supports gradual expansion into more services or clients without a repo rewrite.
|
||||
- Keeps the local developer story coherent.
|
||||
|
||||
Alternatives considered:
|
||||
- Single-app repo only: rejected because it hides important boundaries.
|
||||
- Many services on day one: rejected because it adds overhead before we know the cut lines.
|
||||
|
||||
### 5. Standardize on a Docker-based local platform first
|
||||
|
||||
The first scaffold will target a Docker Compose development environment that starts Nextcloud, its required backing services, and the aiNAS control-plane service. This gives a repeatable local runtime before we decide on production deployment.
|
||||
|
||||
Rationale:
|
||||
- Aligns with Nextcloud's easiest local development path.
|
||||
- Lowers friction for bootstrapping the first app and service.
|
||||
- Keeps infrastructure complexity proportional to the stage of the project.
|
||||
|
||||
Alternatives considered:
|
||||
- Nix-only local orchestration: rejected for now because the project needs a portable first runtime.
|
||||
- Production-like Kubernetes dev environment: rejected as premature.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Nextcloud coupling leaks into aiNAS service design] → Keep all Nextcloud-specific API calls and payload translation in adapter modules at the edge of the control-plane service.
|
||||
- [The shell app grows into a second control plane] → Enforce a rule that product decisions and persistent domain logic live in the control-plane service, not the Nextcloud app.
|
||||
- [ExApp packaging constrains future independence] → Structure the service so container packaging is a deployment concern rather than the application architecture.
|
||||
- [Initial repo layout may be wrong in details] → Optimize for a small number of strong boundaries now; revisit internal package names later without collapsing ownership boundaries.
|
||||
- [Docker dev environment differs from production NAS setups] → Treat the first environment as a development harness and keep storage/network assumptions explicit in docs.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Add the proposal artifacts that establish the architecture and scaffold requirements.
|
||||
2. Create the top-level repository layout and a Docker Compose development environment.
|
||||
3. Generate the Nextcloud shell app into `apps/ainas-controlplane/`.
|
||||
4. Scaffold the control-plane service and shared contracts package.
|
||||
5. Verify local startup, service discovery, and basic health paths before implementing product features.
|
||||
|
||||
Rollback strategy:
|
||||
- Because this is a greenfield scaffold, rollback is simply removing the generated directories and Compose wiring if the architectural choice changes early.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should the first control-plane service be implemented in Go, Python, or Node/TypeScript?
|
||||
- What authentication boundary should exist between the Nextcloud shell app and the control-plane service in local development?
|
||||
- Which parts of future sharing and RBAC behavior should remain delegated to Nextcloud, and which should be modeled natively in aiNAS?
|
||||
- Do we want the first web product surface to live inside Nextcloud pages, outside Nextcloud as a separate frontend, or both?
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
## Why
|
||||
|
||||
aiNAS needs an initial architecture and repository scaffold that lets us build our own storage control plane without inheriting the maintenance cost of a Nextcloud core fork. We want to move quickly on product-specific business logic, but still stand on top of a mature backend for files, sync, sharing primitives, and existing clients.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Create an initial aiNAS platform scaffold centered on vanilla Nextcloud running in Docker for local development.
|
||||
- Define a thin Nextcloud app shell that owns aiNAS-specific integration points, branded surfaces, and adapters into the Nextcloud backend.
|
||||
- Define a control-plane service boundary where aiNAS business logic, policy, and future orchestration will live outside the Nextcloud monolith.
|
||||
- Establish a repository layout for Docker infrastructure, Nextcloud app code, ExApp/service code, and shared API contracts.
|
||||
- Document the decision to treat Nextcloud as an upstream backend dependency rather than a forked application baseline.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `workspace-scaffold`: Repository structure and local development platform for running aiNAS with Nextcloud, service containers, and shared packages.
|
||||
- `nextcloud-shell-app`: Thin aiNAS app inside Nextcloud for navigation, settings, branded entry points, and backend integration hooks.
|
||||
- `control-plane-service`: External aiNAS service layer that owns business logic and exposes internal APIs used by the Nextcloud shell and future clients.
|
||||
|
||||
### Modified Capabilities
|
||||
- None.
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected code: new repository layout under `docker/`, `apps/`, `exapps/`, `packages/`, `docs/`, and `scripts/`
|
||||
- Affected systems: local developer workflow, Docker-based service orchestration, Nextcloud runtime, AppAPI/ExApp integration path
|
||||
- Dependencies: Nextcloud, Docker Compose, AppAPI/ExApps, shared contract definitions
|
||||
- APIs: new internal control-plane APIs and service boundaries for future desktop, iOS, and web clients
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Dedicated control-plane service
|
||||
The system SHALL provide an aiNAS-owned control-plane service that is separate from the Nextcloud shell app and owns product domain logic.
|
||||
|
||||
#### Scenario: aiNAS adds a new control-plane rule
|
||||
- **WHEN** a new business rule for storage policy, RBAC, orchestration, or future client behavior is introduced
|
||||
- **THEN** the rule MUST be implemented in the control-plane service rather than as primary logic inside the Nextcloud app
|
||||
|
||||
### Requirement: Client-agnostic internal API
|
||||
The control-plane service SHALL expose internal APIs that can be consumed by the Nextcloud shell app and future aiNAS clients without requiring direct coupling to Nextcloud internals.
|
||||
|
||||
#### Scenario: New aiNAS client consumes control-plane behavior
|
||||
- **WHEN** aiNAS adds a web, desktop, or iOS surface outside Nextcloud
|
||||
- **THEN** that surface MUST be able to consume control-plane behavior through documented aiNAS service interfaces
|
||||
|
||||
### Requirement: Nextcloud backend adapter boundary
|
||||
The control-plane service SHALL isolate Nextcloud-specific integration at its boundary so that storage and sharing backends remain replaceable over time.
|
||||
|
||||
#### Scenario: Service calls the Nextcloud backend
|
||||
- **WHEN** the control-plane service needs to interact with file or sharing primitives provided by Nextcloud
|
||||
- **THEN** the interaction MUST pass through a dedicated adapter boundary instead of spreading Nextcloud-specific calls across unrelated domain code
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: aiNAS shell app inside Nextcloud
|
||||
The system SHALL provide a dedicated aiNAS shell app inside Nextcloud that establishes branded entry points for aiNAS-owned product surfaces.
|
||||
|
||||
#### Scenario: aiNAS surface is visible in Nextcloud
|
||||
- **WHEN** the aiNAS app is installed in a local development environment
|
||||
- **THEN** Nextcloud MUST expose an aiNAS-branded application surface that can be used as the integration shell for future product flows
|
||||
|
||||
### Requirement: Thin adapter responsibility
|
||||
The aiNAS shell app SHALL act as an adapter layer and MUST keep core business logic outside the Nextcloud monolith.
|
||||
|
||||
#### Scenario: Product decision requires domain logic
|
||||
- **WHEN** the shell app needs information about policy, orchestration, or future product rules
|
||||
- **THEN** it MUST obtain that information through aiNAS-owned service boundaries instead of embedding the decision logic directly in the app
|
||||
|
||||
### Requirement: Nextcloud integration hooks
|
||||
The aiNAS shell app SHALL provide the minimal integration hooks required to connect aiNAS-owned services to Nextcloud runtime surfaces such as navigation, settings, and backend access points.
|
||||
|
||||
#### Scenario: aiNAS needs a Nextcloud-native entry point
|
||||
- **WHEN** aiNAS introduces a new product flow that starts from a Nextcloud-rendered page
|
||||
- **THEN** the shell app MUST provide a supported hook or page boundary where the flow can enter aiNAS-controlled logic
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Repository boundary scaffold
|
||||
The repository SHALL provide a top-level scaffold that separates infrastructure, Nextcloud app code, aiNAS-owned service code, shared contracts, documentation, and automation scripts.
|
||||
|
||||
#### Scenario: Fresh clone exposes expected boundaries
|
||||
- **WHEN** a developer inspects the repository after applying this change
|
||||
- **THEN** the repository MUST include dedicated locations for Docker runtime assets, the Nextcloud shell app, the control-plane service, shared contracts, documentation, and scripts
|
||||
|
||||
### Requirement: Local development platform
|
||||
The repository SHALL provide a local development runtime that starts a vanilla Nextcloud instance together with its required backing services and the aiNAS control-plane service.
|
||||
|
||||
#### Scenario: Developer boots the local stack
|
||||
- **WHEN** a developer runs the documented local startup flow
|
||||
- **THEN** the system MUST start Nextcloud and the aiNAS service dependencies without requiring a forked Nextcloud build
|
||||
|
||||
### Requirement: Shared contract package
|
||||
The repository SHALL include a shared contract location for schemas and service interfaces used between the Nextcloud shell app and aiNAS-owned services.
|
||||
|
||||
#### Scenario: Interface changes are modeled centrally
|
||||
- **WHEN** aiNAS defines an internal API or payload exchanged between the shell app and the control-plane service
|
||||
- **THEN** the schema MUST be represented in the shared contracts location rather than duplicated ad hoc across codebases
|
||||
27
openspec/changes/scaffold-nextcloud-control-plane/tasks.md
Normal file
27
openspec/changes/scaffold-nextcloud-control-plane/tasks.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
## 1. Repository and local platform scaffold
|
||||
|
||||
- [x] 1.1 Create the top-level repository structure for `docker/`, `apps/`, `exapps/`, `packages/`, `docs/`, and `scripts/`
|
||||
- [x] 1.2 Add a Docker Compose development stack for vanilla Nextcloud and its required backing services
|
||||
- [x] 1.3 Add the aiNAS control-plane service container to the local development stack
|
||||
- [x] 1.4 Add repeatable developer scripts and documentation for booting and stopping the local stack
|
||||
|
||||
## 2. Nextcloud shell app scaffold
|
||||
|
||||
- [x] 2.1 Generate the aiNAS Nextcloud app scaffold into `apps/ainas-controlplane/`
|
||||
- [x] 2.2 Configure the shell app with aiNAS branding, navigation entry points, and basic settings surface
|
||||
- [x] 2.3 Add an adapter layer in the shell app for calling aiNAS-owned service endpoints
|
||||
- [ ] 2.4 Verify the shell app installs and loads in the local Nextcloud runtime
|
||||
|
||||
## 3. Control-plane service scaffold
|
||||
|
||||
- [x] 3.1 Scaffold the aiNAS control-plane service in `exapps/control-plane/`
|
||||
- [x] 3.2 Add a minimal internal HTTP API surface with health and version endpoints
|
||||
- [x] 3.3 Create a dedicated Nextcloud adapter boundary inside the service for backend integrations
|
||||
- [x] 3.4 Wire local service configuration so the shell app can discover and call the control-plane service
|
||||
|
||||
## 4. Shared contracts and verification
|
||||
|
||||
- [x] 4.1 Create the shared contracts package for internal API schemas and payload definitions
|
||||
- [x] 4.2 Define the initial contracts used between the shell app and the control-plane service
|
||||
- [x] 4.3 Document the architectural boundary that keeps business logic out of the Nextcloud app
|
||||
- [ ] 4.4 Verify end-to-end local startup with Nextcloud, the shell app, and the control-plane service all reachable
|
||||
611
package-lock.json
generated
Normal file
611
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,611 @@
|
|||
{
|
||||
"name": "ainas",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ainas",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"exapps/*"
|
||||
]
|
||||
},
|
||||
"exapps/control-plane": {
|
||||
"name": "@ainas/control-plane",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@ainas/contracts": "file:../../packages/contracts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.18.6",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@ainas/contracts": {
|
||||
"resolved": "packages/contracts",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@ainas/control-plane": {
|
||||
"resolved": "exapps/control-plane",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
|
||||
"integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
|
||||
"integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
|
||||
"integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
|
||||
"integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
|
||||
"integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
|
||||
"integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
|
||||
"integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
|
||||
"integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
|
||||
"integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
|
||||
"integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
||||
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
|
||||
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.4",
|
||||
"@esbuild/android-arm": "0.27.4",
|
||||
"@esbuild/android-arm64": "0.27.4",
|
||||
"@esbuild/android-x64": "0.27.4",
|
||||
"@esbuild/darwin-arm64": "0.27.4",
|
||||
"@esbuild/darwin-x64": "0.27.4",
|
||||
"@esbuild/freebsd-arm64": "0.27.4",
|
||||
"@esbuild/freebsd-x64": "0.27.4",
|
||||
"@esbuild/linux-arm": "0.27.4",
|
||||
"@esbuild/linux-arm64": "0.27.4",
|
||||
"@esbuild/linux-ia32": "0.27.4",
|
||||
"@esbuild/linux-loong64": "0.27.4",
|
||||
"@esbuild/linux-mips64el": "0.27.4",
|
||||
"@esbuild/linux-ppc64": "0.27.4",
|
||||
"@esbuild/linux-riscv64": "0.27.4",
|
||||
"@esbuild/linux-s390x": "0.27.4",
|
||||
"@esbuild/linux-x64": "0.27.4",
|
||||
"@esbuild/netbsd-arm64": "0.27.4",
|
||||
"@esbuild/netbsd-x64": "0.27.4",
|
||||
"@esbuild/openbsd-arm64": "0.27.4",
|
||||
"@esbuild/openbsd-x64": "0.27.4",
|
||||
"@esbuild/openharmony-arm64": "0.27.4",
|
||||
"@esbuild/sunos-x64": "0.27.4",
|
||||
"@esbuild/win32-arm64": "0.27.4",
|
||||
"@esbuild/win32-ia32": "0.27.4",
|
||||
"@esbuild/win32-x64": "0.27.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.13.7",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz",
|
||||
"integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/contracts": {
|
||||
"name": "@ainas/contracts",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
package.json
Normal file
15
package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "ainas",
|
||||
"private": true,
|
||||
"packageManager": "npm@10.9.7",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"exapps/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm run build --workspace @ainas/contracts && npm run build --workspace @ainas/control-plane",
|
||||
"typecheck": "npm run typecheck --workspace @ainas/contracts && npm run typecheck --workspace @ainas/control-plane",
|
||||
"dev:control-plane": "npm run dev --workspace @ainas/control-plane"
|
||||
}
|
||||
}
|
||||
|
||||
17
packages/contracts/package.json
Normal file
17
packages/contracts/package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "@ainas/contracts",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"schemas"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||
}
|
||||
}
|
||||
|
||||
48
packages/contracts/schemas/control-plane-health.schema.json
Normal file
48
packages/contracts/schemas/control-plane-health.schema.json
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://ainas.local/schemas/control-plane-health.schema.json",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"service",
|
||||
"status",
|
||||
"timestamp",
|
||||
"uptimeSeconds",
|
||||
"nextcloud"
|
||||
],
|
||||
"properties": {
|
||||
"service": {
|
||||
"const": "control-plane"
|
||||
},
|
||||
"status": {
|
||||
"const": "ok"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
},
|
||||
"uptimeSeconds": {
|
||||
"type": "number"
|
||||
},
|
||||
"nextcloud": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"configured",
|
||||
"baseUrl",
|
||||
"provider"
|
||||
],
|
||||
"properties": {
|
||||
"configured": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"const": "nextcloud"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
packages/contracts/schemas/control-plane-version.schema.json
Normal file
23
packages/contracts/schemas/control-plane-version.schema.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://ainas.local/schemas/control-plane-version.schema.json",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"service",
|
||||
"version",
|
||||
"apiVersion"
|
||||
],
|
||||
"properties": {
|
||||
"service": {
|
||||
"const": "control-plane"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"apiVersion": {
|
||||
"const": "v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
packages/contracts/src/control-plane.ts
Normal file
25
packages/contracts/src/control-plane.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
export const CONTROL_PLANE_ROUTES = {
|
||||
health: "/health",
|
||||
version: "/version"
|
||||
} as const;
|
||||
|
||||
export interface NextcloudBackendStatus {
|
||||
configured: boolean;
|
||||
baseUrl: string;
|
||||
provider: "nextcloud";
|
||||
}
|
||||
|
||||
export interface ControlPlaneHealthResponse {
|
||||
service: "control-plane";
|
||||
status: "ok";
|
||||
timestamp: string;
|
||||
uptimeSeconds: number;
|
||||
nextcloud: NextcloudBackendStatus;
|
||||
}
|
||||
|
||||
export interface ControlPlaneVersionResponse {
|
||||
service: "control-plane";
|
||||
version: string;
|
||||
apiVersion: "v1";
|
||||
}
|
||||
|
||||
2
packages/contracts/src/index.ts
Normal file
2
packages/contracts/src/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./control-plane.js";
|
||||
|
||||
11
packages/contracts/tsconfig.json
Normal file
11
packages/contracts/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
9
scripts/dev-down
Executable file
9
scripts/dev-down
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
compose_file="$repo_root/docker/compose.dev.yml"
|
||||
|
||||
docker compose -f "$compose_file" down --remove-orphans
|
||||
|
||||
28
scripts/dev-up
Executable file
28
scripts/dev-up
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
compose_file="$repo_root/docker/compose.dev.yml"
|
||||
|
||||
docker compose -f "$compose_file" up -d --build
|
||||
|
||||
echo "Waiting for Nextcloud to finish installing..."
|
||||
ready=0
|
||||
for _ in {1..60}; do
|
||||
if docker compose -f "$compose_file" exec -T --user www-data nextcloud php occ status >/dev/null 2>&1; then
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [[ "$ready" -ne 1 ]]; then
|
||||
echo "Nextcloud did not become ready in time." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker compose -f "$compose_file" exec -T --user www-data nextcloud php occ app:enable --force ainascontrolplane >/dev/null
|
||||
|
||||
echo "Nextcloud: http://localhost:8080"
|
||||
echo "aiNAS control plane: http://localhost:3001"
|
||||
14
tsconfig.base.json
Normal file
14
tsconfig.base.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue