mirror of
https://github.com/harivansh-afk/betterNAS.git
synced 2026-04-15 17:00:58 +00:00
init
This commit is contained in:
parent
4f174ec3a8
commit
db1dea097f
81 changed files with 6263 additions and 545 deletions
30
apps/nextcloud-app/appinfo/info.xml
Normal file
30
apps/nextcloud-app/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>betternascontrolplane</id>
|
||||
<name>betterNAS Control Plane</name>
|
||||
<summary>Thin betterNAS shell app for Nextcloud integration</summary>
|
||||
<description>Provides betterNAS-branded entry points inside Nextcloud while delegating business logic to the betterNAS control plane.</description>
|
||||
<version>0.1.0</version>
|
||||
<licence>AGPL-3.0-or-later</licence>
|
||||
<author homepage="https://betternas.local">betterNAS</author>
|
||||
<namespace>BetterNasControlplane</namespace>
|
||||
<category>integration</category>
|
||||
<dependencies>
|
||||
<nextcloud min-version="31" max-version="33"/>
|
||||
</dependencies>
|
||||
<navigations>
|
||||
<navigation>
|
||||
<id>betternascontrolplane</id>
|
||||
<name>betterNAS</name>
|
||||
<route>betternascontrolplane.page.index</route>
|
||||
<icon>app.svg</icon>
|
||||
<type>link</type>
|
||||
</navigation>
|
||||
</navigations>
|
||||
<settings>
|
||||
<admin>OCA\BetterNasControlplane\Settings\Admin</admin>
|
||||
<admin-section>OCA\BetterNasControlplane\Settings\AdminSection</admin-section>
|
||||
</settings>
|
||||
</info>
|
||||
|
||||
17
apps/nextcloud-app/composer.json
Normal file
17
apps/nextcloud-app/composer.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "betternas/betternascontrolplane",
|
||||
"description": "betterNAS Nextcloud shell app",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"OCA\\BetterNasControlplane\\": "lib/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"nextcloud/ocp": "dev-stable31"
|
||||
}
|
||||
}
|
||||
|
||||
85
apps/nextcloud-app/css/betternascontrolplane.css
Normal file
85
apps/nextcloud-app/css/betternascontrolplane.css
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
.betternas-shell {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.betternas-shell__hero {
|
||||
margin-bottom: 28px;
|
||||
padding: 28px;
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(135deg, #10212d 0%, #184152 100%);
|
||||
color: #f6fafc;
|
||||
}
|
||||
|
||||
.betternas-shell__eyebrow {
|
||||
margin: 0 0 12px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.betternas-shell__title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 32px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.betternas-shell__copy {
|
||||
margin: 0;
|
||||
max-width: 70ch;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.betternas-shell__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.betternas-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);
|
||||
}
|
||||
|
||||
.betternas-shell__card h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.betternas-shell__card dl {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 160px) 1fr;
|
||||
gap: 8px 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.betternas-shell__card dt {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.betternas-shell__card dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.betternas-shell__card code {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
background: #eef4f7;
|
||||
}
|
||||
|
||||
.betternas-shell__card ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.betternas-shell__error {
|
||||
margin-top: 16px;
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
6
apps/nextcloud-app/img/app-dark.svg
Normal file
6
apps/nextcloud-app/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/nextcloud-app/img/app.svg
Normal file
6
apps/nextcloud-app/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/nextcloud-app/lib/AppInfo/Application.php
Normal file
25
apps/nextcloud-app/lib/AppInfo/Application.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\BetterNasControlplane\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 = 'betternascontrolplane';
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct(self::APP_ID);
|
||||
}
|
||||
|
||||
public function register(IRegistrationContext $context): void {
|
||||
}
|
||||
|
||||
public function boot(IBootContext $context): void {
|
||||
}
|
||||
}
|
||||
|
||||
29
apps/nextcloud-app/lib/Controller/ApiController.php
Normal file
29
apps/nextcloud-app/lib/Controller/ApiController.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\BetterNasControlplane\Controller;
|
||||
|
||||
use OCA\BetterNasControlplane\AppInfo\Application;
|
||||
use OCA\BetterNasControlplane\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/nextcloud-app/lib/Controller/PageController.php
Normal file
43
apps/nextcloud-app/lib/Controller/PageController.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\BetterNasControlplane\Controller;
|
||||
|
||||
use OCA\BetterNasControlplane\AppInfo\Application;
|
||||
use OCA\BetterNasControlplane\Service\ControlPlaneClient;
|
||||
use OCA\BetterNasControlplane\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' => 'betterNAS Control Plane',
|
||||
'controlPlaneUrl' => $this->controlPlaneConfig->getBaseUrl(),
|
||||
'snapshot' => $this->controlPlaneClient->fetchSnapshot(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
88
apps/nextcloud-app/lib/Service/ControlPlaneClient.php
Normal file
88
apps/nextcloud-app/lib/Service/ControlPlaneClient.php
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\BetterNasControlplane\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 betterNAS 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/nextcloud-app/lib/Service/ControlPlaneConfig.php
Normal file
31
apps/nextcloud-app/lib/Service/ControlPlaneConfig.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\BetterNasControlplane\Service;
|
||||
|
||||
use OCA\BetterNasControlplane\AppInfo\Application;
|
||||
use OCP\IAppConfig;
|
||||
|
||||
class ControlPlaneConfig {
|
||||
public function __construct(
|
||||
private readonly IAppConfig $appConfig,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getBaseUrl(): string {
|
||||
$environmentUrl = getenv('BETTERNAS_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/nextcloud-app/lib/Settings/Admin.php
Normal file
39
apps/nextcloud-app/lib/Settings/Admin.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\BetterNasControlplane\Settings;
|
||||
|
||||
use OCA\BetterNasControlplane\AppInfo\Application;
|
||||
use OCA\BetterNasControlplane\Service\ControlPlaneClient;
|
||||
use OCA\BetterNasControlplane\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/nextcloud-app/lib/Settings/AdminSection.php
Normal file
35
apps/nextcloud-app/lib/Settings/AdminSection.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\BetterNasControlplane\Settings;
|
||||
|
||||
use OCA\BetterNasControlplane\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('betterNAS');
|
||||
}
|
||||
|
||||
public function getPriority(): int {
|
||||
return 50;
|
||||
}
|
||||
|
||||
public function getIcon(): ?string {
|
||||
return $this->urlGenerator->imagePath(Application::APP_ID, 'app-dark.svg');
|
||||
}
|
||||
}
|
||||
|
||||
43
apps/nextcloud-app/templates/admin.php
Normal file
43
apps/nextcloud-app/templates/admin.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use OCA\BetterNasControlplane\AppInfo\Application;
|
||||
use OCP\Util;
|
||||
|
||||
Util::addStyle(Application::APP_ID, 'betternascontrolplane');
|
||||
|
||||
$snapshot = $_['snapshot'];
|
||||
$reachable = !empty($snapshot['available']) ? 'yes' : 'no';
|
||||
$version = $snapshot['version']['version'] ?? 'unreachable';
|
||||
?>
|
||||
|
||||
<div class="betternas-shell betternas-shell--admin">
|
||||
<div class="betternas-shell__hero">
|
||||
<p class="betternas-shell__eyebrow">Admin settings</p>
|
||||
<h1 class="betternas-shell__title">betterNAS control-plane wiring</h1>
|
||||
<p class="betternas-shell__copy">
|
||||
The local scaffold wires this app to the control plane through the <code>BETTERNAS_CONTROL_PLANE_URL</code> environment variable in the Nextcloud container.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="betternas-shell__grid">
|
||||
<section class="betternas-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="betternas-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/nextcloud-app/templates/index.php
Normal file
51
apps/nextcloud-app/templates/index.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use OCA\BetterNasControlplane\AppInfo\Application;
|
||||
use OCP\Util;
|
||||
|
||||
Util::addStyle(Application::APP_ID, 'betternascontrolplane');
|
||||
|
||||
$snapshot = $_['snapshot'];
|
||||
$version = $snapshot['version']['version'] ?? 'unreachable';
|
||||
$status = !empty($snapshot['available']) ? 'Connected' : 'Unavailable';
|
||||
$error = $snapshot['error'] ?? null;
|
||||
?>
|
||||
|
||||
<div class="betternas-shell">
|
||||
<div class="betternas-shell__hero">
|
||||
<p class="betternas-shell__eyebrow">betterNAS inside Nextcloud</p>
|
||||
<h1 class="betternas-shell__title"><?php p($_['appName']); ?></h1>
|
||||
<p class="betternas-shell__copy">
|
||||
This shell app stays intentionally thin. It exposes betterNAS entry points inside Nextcloud and delegates business logic to the external control-plane service.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="betternas-shell__grid">
|
||||
<section class="betternas-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="betternas-shell__error"><?php p($error); ?></p>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="betternas-shell__card">
|
||||
<h2>Boundary</h2>
|
||||
<ul>
|
||||
<li>Nextcloud provides file and client primitives.</li>
|
||||
<li>betterNAS owns control-plane policy and orchestration.</li>
|
||||
<li>The shell app only adapts between the two.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue