Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5598fa5156 | ||
|
|
3b94358162 | ||
|
|
e8e1658b3a | ||
|
|
ac9ce68fbe | ||
|
|
1ad16fb35b | ||
|
|
2b6a9f5433 | ||
|
|
8541e3b42e |
@@ -0,0 +1,27 @@
|
||||
# Git / IDE
|
||||
.git
|
||||
.gitignore
|
||||
.vs
|
||||
.idea
|
||||
.vscode
|
||||
*.user
|
||||
|
||||
# .NET build artifacts
|
||||
**/bin
|
||||
**/obj
|
||||
|
||||
# Node
|
||||
**/node_modules
|
||||
|
||||
# Build output (budowany wewnątrz obrazu)
|
||||
client/dist
|
||||
backend/wwwroot
|
||||
|
||||
# Local SurrealDB data — dostarczane przez wolumen ./data
|
||||
data
|
||||
backend/data
|
||||
|
||||
# Env / docs
|
||||
.env
|
||||
.env.*
|
||||
backend/local-docs
|
||||
+10
@@ -13,6 +13,9 @@ Thumbs.db
|
||||
bin/
|
||||
obj/
|
||||
|
||||
# Local SurrealDB data (embedded RocksDB) — persisted next to the running app
|
||||
data/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
dist/
|
||||
@@ -21,7 +24,14 @@ dist/
|
||||
# Client build output copied into backend
|
||||
backend/wwwroot/
|
||||
|
||||
# Local documentation (not tracked)
|
||||
backend/local-docs/
|
||||
|
||||
# Env / secrets
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Docker — lokalne nadpisania
|
||||
docker-compose.override.yaml
|
||||
docker-compose.override.yml
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# --- Stage 1: build klienta (Vue 3 + Vite) ---
|
||||
FROM node:22-alpine AS client
|
||||
WORKDIR /src/client
|
||||
COPY client/package.json client/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY client/ ./
|
||||
RUN npm run build
|
||||
|
||||
# --- Stage 2: build i publish backendu (ASP.NET Core, .NET 10) ---
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY backend/TarninaHub.csproj ./backend/
|
||||
RUN dotnet restore ./backend/TarninaHub.csproj
|
||||
COPY backend/ ./backend/
|
||||
COPY --from=client /src/client/dist ./backend/wwwroot/
|
||||
RUN dotnet publish ./backend/TarninaHub.csproj \
|
||||
--configuration Release \
|
||||
--output /app/publish \
|
||||
/p:UseAppHost=false
|
||||
|
||||
# --- Stage 3: runtime ---
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
ARG APP_VERSION=0.0.1
|
||||
LABEL org.opencontainers.image.title="TarninaHub" \
|
||||
org.opencontainers.image.version="${APP_VERSION}"
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV ASPNETCORE_ENVIRONMENT=Production \
|
||||
ASPNETCORE_URLS=http://+:8080 \
|
||||
DOTNET_EnableDiagnostics=0
|
||||
|
||||
COPY --from=build /app/publish ./
|
||||
|
||||
# Jedyny wolumen aplikacji: trwałe dane SurrealDB (embedded RocksDB).
|
||||
RUN mkdir -p /app/data
|
||||
VOLUME ["/app/data"]
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["dotnet", "TarninaHub.dll"]
|
||||
@@ -1,11 +1,14 @@
|
||||
# hubmi
|
||||
# TarninaHub
|
||||
|
||||
**Wersja: 0.0.1**
|
||||
|
||||
Platforma Małopolskiego Hubu Innowacji Społecznych (MVP hackathonu).
|
||||
|
||||
- **backend/** — aplikacja backendowa w C# (ASP.NET Core, `HubMi.Api`).
|
||||
- **backend/** — aplikacja backendowa w C# (ASP.NET Core, `TarninaHub`).
|
||||
- **client/** — aplikacja frontendowa Vue 3 + TypeScript (Vite).
|
||||
- **scripts/** — skrypty budujące całość.
|
||||
- `HubMi.slnx` — solution .NET.
|
||||
- `TarninaHub.slnx` — solution .NET.
|
||||
- `Dockerfile` + `docker-compose.yaml` — uruchomienie kontenerowe.
|
||||
|
||||
## Wymagania
|
||||
|
||||
@@ -34,15 +37,33 @@ npm --prefix client run copy # kopiuje client/dist -> backend/wwwroot
|
||||
node scripts/build.mjs # buduje całość (client + copy + backend)
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
```pwsh
|
||||
cp docker-compose.yaml.example docker-compose.yaml
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
- Aplikacja: http://localhost:8080
|
||||
- Dane: `./data` (bind mount, trwałe — SurrealDB embedded RocksDB)
|
||||
- Obraz: `tarninahub:0.0.1`
|
||||
|
||||
Wolumen hosta można wskazać zmienną `TARNINAHUB_PORT` (domyślnie `8080`).
|
||||
Plik `docker-compose.yaml.example` jest szablonem; `docker-compose.yaml` możesz
|
||||
dostosować lokalnie.
|
||||
|
||||
## Struktura
|
||||
|
||||
```
|
||||
hubmi/
|
||||
TarninaHub/
|
||||
├─ backend/ # ASP.NET Core Web API + wwwroot (build z client)
|
||||
├─ client/ # Vue 3 + TS + Vite
|
||||
├─ scripts/
|
||||
│ ├─ build.mjs # build całości
|
||||
│ └─ copy-client.mjs # client/dist -> backend/wwwroot
|
||||
├─ HubMi.slnx
|
||||
├─ Dockerfile # multi-stage: client + backend
|
||||
├─ docker-compose.yaml # uruchomienie (bind ./data)
|
||||
├─ docker-compose.yaml.example
|
||||
├─ TarninaHub.slnx
|
||||
└─ README.md
|
||||
```
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TarninaHub.Models;
|
||||
using TarninaHub.Services;
|
||||
|
||||
namespace TarninaHub.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/system")]
|
||||
public class SystemController : ControllerBase
|
||||
{
|
||||
private readonly IStorageService _storageService;
|
||||
|
||||
public SystemController(IStorageService storageService)
|
||||
{
|
||||
_storageService = storageService;
|
||||
}
|
||||
|
||||
[HttpGet("storage")]
|
||||
public async Task<ActionResult<StorageStatusResponse>> GetStorage(CancellationToken cancellationToken)
|
||||
{
|
||||
var info = _storageService.GetInfo();
|
||||
var probeCount = await _storageService.GetProbeCountAsync(cancellationToken);
|
||||
|
||||
return Ok(new StorageStatusResponse(
|
||||
info.Engine,
|
||||
info.DataDirectory,
|
||||
info.Namespace,
|
||||
info.Database,
|
||||
probeCount));
|
||||
}
|
||||
|
||||
[HttpPost("storage/probes")]
|
||||
public async Task<ActionResult<StorageProbeResponse>> CreateProbe(CancellationToken cancellationToken)
|
||||
{
|
||||
var probe = await _storageService.AddProbeAsync(cancellationToken);
|
||||
|
||||
return Ok(new StorageProbeResponse(probe.Label, probe.CreatedAt));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace TarninaHub.Models;
|
||||
|
||||
public sealed class StorageProbe
|
||||
{
|
||||
public string Label { get; set; } = "probe";
|
||||
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed record StorageInfo(
|
||||
string Engine,
|
||||
string DataDirectory,
|
||||
string Namespace,
|
||||
string Database);
|
||||
|
||||
public sealed record StorageStatusResponse(
|
||||
string Engine,
|
||||
string DataDirectory,
|
||||
string Namespace,
|
||||
string Database,
|
||||
int ProbeCount);
|
||||
|
||||
public sealed record StorageProbeResponse(
|
||||
string Label,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -1,12 +1,55 @@
|
||||
using TarninaHub.Services;
|
||||
using TarninaHub.Settings;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
var surrealSettings = builder.Configuration.GetSection("SurrealDb").Get<SurrealDbSettings>()
|
||||
?? new SurrealDbSettings();
|
||||
|
||||
builder.Services.Configure<SurrealDbSettings>(builder.Configuration.GetSection("SurrealDb"));
|
||||
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
||||
builder.Services.AddSingleton<IStorageService, StorageService>();
|
||||
|
||||
var surrealOptions = new SurrealDbOptions
|
||||
{
|
||||
Endpoint = BuildEndpoint(surrealSettings),
|
||||
Namespace = surrealSettings.Namespace,
|
||||
Database = surrealSettings.Database,
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(surrealSettings.RemoteUrl))
|
||||
{
|
||||
// Embedded RocksDB, persisted next to the running app (./data), never in memory.
|
||||
var dataDirectory = Path.Combine(AppContext.BaseDirectory, surrealSettings.DataDirectory);
|
||||
Directory.CreateDirectory(dataDirectory);
|
||||
|
||||
builder.Services
|
||||
.AddSurreal(surrealOptions, ServiceLifetime.Singleton)
|
||||
.AddRocksDbProvider();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remote SurrealDB server on its own dedicated port (used for sync with the frontend).
|
||||
surrealOptions.Username = surrealSettings.Username;
|
||||
surrealOptions.Password = surrealSettings.Password;
|
||||
|
||||
builder.Services.AddSurreal(surrealOptions, ServiceLifetime.Singleton);
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var storageService = scope.ServiceProvider.GetRequiredService<IStorageService>();
|
||||
|
||||
await storageService.InitializeAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
@@ -25,3 +68,15 @@ app.MapControllers();
|
||||
app.MapFallbackToFile("index.html");
|
||||
|
||||
app.Run();
|
||||
|
||||
static string BuildEndpoint(SurrealDbSettings settings)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(settings.RemoteUrl))
|
||||
{
|
||||
return settings.RemoteUrl;
|
||||
}
|
||||
|
||||
var dataDirectory = Path.Combine(AppContext.BaseDirectory, settings.DataDirectory);
|
||||
|
||||
return $"rocksdb://{dataDirectory.Replace('\\', '/')}";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using TarninaHub.Models;
|
||||
|
||||
namespace TarninaHub.Services;
|
||||
|
||||
public interface IStorageService
|
||||
{
|
||||
Task InitializeAsync(CancellationToken cancellationToken);
|
||||
|
||||
StorageInfo GetInfo();
|
||||
|
||||
Task<int> GetProbeCountAsync(CancellationToken cancellationToken);
|
||||
|
||||
Task<StorageProbe> AddProbeAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using SurrealDb.Net;
|
||||
using TarninaHub.Models;
|
||||
using TarninaHub.Settings;
|
||||
|
||||
namespace TarninaHub.Services;
|
||||
|
||||
public sealed class StorageService : IStorageService
|
||||
{
|
||||
private const string ProbeTable = "storage_probe";
|
||||
|
||||
private readonly ISurrealDbClient _client;
|
||||
private readonly SurrealDbSettings _settings;
|
||||
private readonly string _dataDirectory;
|
||||
|
||||
public StorageService(ISurrealDbClient client, IOptions<SurrealDbSettings> settings)
|
||||
{
|
||||
_client = client;
|
||||
_settings = settings.Value;
|
||||
_dataDirectory = Path.Combine(AppContext.BaseDirectory, _settings.DataDirectory);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _client.RawQuery(
|
||||
$"DEFINE TABLE IF NOT EXISTS {ProbeTable} SCHEMALESS;",
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public StorageInfo GetInfo()
|
||||
{
|
||||
var isRemote = !string.IsNullOrWhiteSpace(_settings.RemoteUrl);
|
||||
|
||||
return new StorageInfo(
|
||||
isRemote ? "remote" : "rocksdb",
|
||||
isRemote ? _settings.RemoteUrl! : _dataDirectory,
|
||||
_settings.Namespace,
|
||||
_settings.Database);
|
||||
}
|
||||
|
||||
public async Task<int> GetProbeCountAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var probes = await _client.Select<StorageProbe>(ProbeTable, cancellationToken);
|
||||
|
||||
return probes.Count();
|
||||
}
|
||||
|
||||
public Task<StorageProbe> AddProbeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var probe = new StorageProbe
|
||||
{
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
return _client.Create(ProbeTable, probe, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace TarninaHub.Settings;
|
||||
|
||||
public sealed class SurrealDbSettings
|
||||
{
|
||||
public string Namespace { get; set; } = "hubmi";
|
||||
|
||||
public string Database { get; set; } = "hubmi";
|
||||
|
||||
public string DataDirectory { get; set; } = "data";
|
||||
|
||||
/// <summary>
|
||||
/// When set (e.g. ws://127.0.0.1:8010/rpc), the backend connects to a
|
||||
/// dedicated SurrealDB server instead of the embedded RocksDB engine.
|
||||
/// The server must persist to ./data, not to memory.
|
||||
/// </summary>
|
||||
public string? RemoteUrl { get; set; }
|
||||
|
||||
public string? Username { get; set; }
|
||||
|
||||
public string? Password { get; set; }
|
||||
}
|
||||
@@ -4,10 +4,14 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Version>0.0.1</Version>
|
||||
<InformationalVersion>0.0.1</InformationalVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
|
||||
<PackageReference Include="SurrealDb.Embedded.RocksDb" Version="1.0.0" />
|
||||
<PackageReference Include="SurrealDb.Net" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -4,5 +4,13 @@
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"SurrealDb": {
|
||||
"Namespace": "hubmi",
|
||||
"Database": "hubmi",
|
||||
"DataDirectory": "data",
|
||||
"RemoteUrl": "ws://127.0.0.1:8010/rpc",
|
||||
"Username": "root",
|
||||
"Password": "root"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,10 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"SurrealDb": {
|
||||
"Namespace": "hubmi",
|
||||
"Database": "hubmi",
|
||||
"DataDirectory": "data"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# SurrealDB — frontend (Vite)
|
||||
|
||||
# Wspólna przestrzeń nazw / baza
|
||||
VITE_SURREAL_NAMESPACE=hubmi
|
||||
VITE_SURREAL_DATABASE=hubmi
|
||||
|
||||
# A) Lokalna baza w przeglądarce (WASM + IndexedDB; dane zostają lokalnie)
|
||||
VITE_SURREAL_LOCAL_URL=indxdb://tarninahub
|
||||
|
||||
# B) Backend na dedykowanym porcie (tylko dla SurrealDB) — synchronizacja
|
||||
VITE_SURREAL_REMOTE_URL=ws://127.0.0.1:8010/rpc
|
||||
|
||||
# Poświadczenia do zdalnej bazy. W produkcji NIE używaj root w przeglądarce —
|
||||
# użyj tokenu dostępu rekordowego (record access) lub konta o wąskich uprawnieniach.
|
||||
VITE_SURREAL_USERNAME=
|
||||
VITE_SURREAL_PASSWORD=
|
||||
Generated
+119
-3
@@ -1,14 +1,17 @@
|
||||
{
|
||||
"name": "hubmi-client",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hubmi-client",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@surrealdb/wasm": "^2.6.1",
|
||||
"@vueuse/core": "^15.0.0",
|
||||
"surrealdb": "^2.0.10",
|
||||
"vue": "^3.5.42",
|
||||
"vuetify": "^4.2.3"
|
||||
},
|
||||
@@ -369,6 +372,24 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@surrealdb/cbor": {
|
||||
"version": "2.0.0-alpha.4",
|
||||
"resolved": "https://registry.npmjs.org/@surrealdb/cbor/-/cbor-2.0.0-alpha.4.tgz",
|
||||
"integrity": "sha512-l/hNZ3ZCOJq8rzwx1y5ZV/8b2bYCWdhGqRVY33Pu50NAHyAW5qLsMn3ZBkZow77mAKwNCsbDsRGnyYEuU95mjw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@surrealdb/wasm": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@surrealdb/wasm/-/wasm-2.6.1.tgz",
|
||||
"integrity": "sha512-pBxgOX/wZ+aBvMIJbDv6UtUDCT0RjhpPqa31NMUmAZuTx9WfZb1PQan4vCBH9rcSPDR6BC8zJ4pFQUuliFQUEQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"surrealdb": "^2.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz",
|
||||
@@ -379,6 +400,12 @@
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.9.tgz",
|
||||
@@ -615,6 +642,53 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-15.0.0.tgz",
|
||||
"integrity": "sha512-Zv67h5mrk7YHDJsRObx+DXaCBz6RKBUpxqLesJ+3xZNPG6yuQs7ja7kg3veFrrouYLWAqydQ7CwGEbLVxB43Ng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"@vueuse/metadata": "15.0.0",
|
||||
"@vueuse/shared": "15.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-15.0.0.tgz",
|
||||
"integrity": "sha512-sfWE5LADAKJ0KnjtGliMdSN8Ij/L7uP6rXIZlgPloAI4zvvefbw6H5Lt7KJA6RqQOYgVqWF5Guj50HHMIzygBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-15.0.0.tgz",
|
||||
"integrity": "sha512-vbNXhGrbV53xPOMSyOzMHQRvhMgZf7oNDw2RChUetVxwTIEkInQ1joZ9JbT0G3z2n2AJNjvmCWeM9ySYrnvogA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/alien-signals": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz",
|
||||
@@ -1093,6 +1167,33 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/surrealdb": {
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/surrealdb/-/surrealdb-2.0.10.tgz",
|
||||
"integrity": "sha512-4r3LqhvGIRdEMUDO2u21jSi0C8x0x374mOmqCEEM5TrTsQWi5dUihBAfUFee+EBkrJeVDhBgcnvaMNPn+owc2Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@surrealdb/sqon": "^0.1.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.0.0 || ^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/surrealdb/node_modules/@surrealdb/sqon": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@surrealdb/sqon/-/sqon-0.1.5.tgz",
|
||||
"integrity": "sha512-wTo5eREYb+spcErgyKsyNt/QVnz9MaPhOrhqalNp6BWWQNXqgd0gZ8Pub2jjckLrULxRi9Cujamif0T34LBhBA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@surrealdb/cbor": "2.0.0-alpha.4",
|
||||
"uuidv7": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
@@ -1110,11 +1211,17 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -1131,6 +1238,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuidv7": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/uuidv7/-/uuidv7-1.2.1.tgz",
|
||||
"integrity": "sha512-4kPkK3/XTQW9Hbm4CaqfICn+kY9LJtDVEOfgsRRra/+n2Ofg4NqzRFceAkxvQ/Ud/6BpHOPzj8cirqM7TzTN5Q==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"uuidv7": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz",
|
||||
|
||||
+4
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "hubmi-client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -11,6 +11,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@surrealdb/wasm": "^2.6.1",
|
||||
"@vueuse/core": "^15.0.0",
|
||||
"surrealdb": "^2.0.10",
|
||||
"vue": "^3.5.42",
|
||||
"vuetify": "^4.2.3"
|
||||
},
|
||||
|
||||
+33
-1
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import MainView from './pages/MainView.vue'
|
||||
import { useThemeMode } from './composables/useThemeMode'
|
||||
|
||||
interface NavItem {
|
||||
title: string
|
||||
@@ -10,6 +11,11 @@ interface NavItem {
|
||||
|
||||
const drawer = ref(false)
|
||||
|
||||
const { mode, themeOptions } = useThemeMode()
|
||||
const activeTheme = computed(
|
||||
() => themeOptions.find((option) => option.value === mode.value) ?? themeOptions[2]!,
|
||||
)
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ title: 'Misja', href: '#misja', icon: 'mdi-compass-outline' },
|
||||
{ title: 'Jak to działa', href: '#jak-to-dziala', icon: 'mdi-sitemap-outline' },
|
||||
@@ -29,6 +35,32 @@ const navItems: NavItem[] = [
|
||||
/>
|
||||
<v-app-bar-title>TarninaHub</v-app-bar-title>
|
||||
<v-spacer />
|
||||
|
||||
<v-menu location="bottom end">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
:prepend-icon="activeTheme.icon"
|
||||
:aria-label="`Motyw: ${activeTheme.title}. Zmień motyw`"
|
||||
color="on-primary"
|
||||
variant="text"
|
||||
class="me-1"
|
||||
>
|
||||
{{ activeTheme.title }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list aria-label="Wybór motywu">
|
||||
<v-list-item
|
||||
v-for="option in themeOptions"
|
||||
:key="option.value"
|
||||
:prepend-icon="option.icon"
|
||||
:title="option.title"
|
||||
:active="mode === option.value"
|
||||
@click="mode = option.value"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<nav aria-label="Menu główne" class="d-none d-md-block">
|
||||
<v-btn
|
||||
v-for="item in navItems"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { computed, watch } from 'vue'
|
||||
import { usePreferredDark, useStorage } from '@vueuse/core'
|
||||
import { useTheme } from 'vuetify'
|
||||
|
||||
export type ThemeMode = 'light' | 'dark' | 'system'
|
||||
export type ResolvedTheme = 'light' | 'dark'
|
||||
|
||||
export interface ThemeOption {
|
||||
value: ThemeMode
|
||||
title: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
export const themeOptions: ThemeOption[] = [
|
||||
{ value: 'light', title: 'Jasny', icon: 'mdi-white-balance-sunny' },
|
||||
{ value: 'dark', title: 'Ciemny', icon: 'mdi-weather-night' },
|
||||
{ value: 'system', title: 'Systemowy', icon: 'mdi-theme-light-dark' },
|
||||
]
|
||||
|
||||
export function useThemeMode() {
|
||||
const theme = useTheme()
|
||||
const preferredDark = usePreferredDark()
|
||||
const mode = useStorage<ThemeMode>('tarninahub-theme', 'system')
|
||||
|
||||
const resolved = computed<ResolvedTheme>(() =>
|
||||
mode.value === 'system' ? (preferredDark.value ? 'dark' : 'light') : mode.value,
|
||||
)
|
||||
|
||||
watch(resolved, (value) => theme.change(value), { immediate: true })
|
||||
|
||||
return { mode, resolved, preferredDark, themeOptions }
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
import { createApp } from 'vue'
|
||||
import vuetify from './plugins/vuetify'
|
||||
import { connectLocalSurreal } from './services/surreal'
|
||||
import './style.css'
|
||||
import './styles/a11y.css'
|
||||
import App from './App.vue'
|
||||
|
||||
void connectLocalSurreal().catch((error: unknown) => {
|
||||
console.error('Nie udało się połączyć z lokalną bazą SurrealDB', error)
|
||||
})
|
||||
|
||||
createApp(App).use(vuetify).mount('#app')
|
||||
|
||||
@@ -109,7 +109,7 @@ const steps: Step[] = [
|
||||
<h2 id="dla-kogo-title" class="text-h4 text-center mb-8">Dla kogo?</h2>
|
||||
<v-row>
|
||||
<v-col v-for="feature in features" :key="feature.title" cols="12" md="4">
|
||||
<v-card class="h-100 pa-2" variant="elevated" elevation="2">
|
||||
<v-card class="h-100 pa-2" variant="outlined">
|
||||
<v-card-item>
|
||||
<v-icon
|
||||
:icon="feature.icon"
|
||||
|
||||
@@ -5,66 +5,81 @@ import * as components from 'vuetify/components'
|
||||
import * as directives from 'vuetify/directives'
|
||||
import { en, pl } from 'vuetify/locale'
|
||||
|
||||
const light: ThemeDefinition = {
|
||||
export const light: ThemeDefinition = {
|
||||
dark: false,
|
||||
colors: {
|
||||
background: '#FFFFFF',
|
||||
background: '#FBF8FF',
|
||||
'on-background': '#1A1030',
|
||||
surface: '#FFFFFF',
|
||||
'surface-variant': '#4D4D4D',
|
||||
primary: '#1565C0',
|
||||
'on-surface': '#1A1030',
|
||||
'surface-bright': '#FFFFFF',
|
||||
'on-surface-bright': '#1A1030',
|
||||
'surface-light': '#F1EAFB',
|
||||
'on-surface-light': '#1A1030',
|
||||
'surface-variant': '#ECE4FA',
|
||||
'on-surface-variant': '#1A1030',
|
||||
|
||||
primary: '#5B21B6',
|
||||
'on-primary': '#FFFFFF',
|
||||
secondary: '#455A64',
|
||||
secondary: '#6A1B9A',
|
||||
'on-secondary': '#FFFFFF',
|
||||
info: '#0B4F9E',
|
||||
'on-info': '#FFFFFF',
|
||||
success: '#166534',
|
||||
'on-success': '#FFFFFF',
|
||||
warning: '#7C4A00',
|
||||
'on-warning': '#FFFFFF',
|
||||
error: '#B3261E',
|
||||
'on-error': '#FFFFFF',
|
||||
success: '#1B5E20',
|
||||
'on-success': '#FFFFFF',
|
||||
warning: '#8A4B00',
|
||||
'on-warning': '#FFFFFF',
|
||||
info: '#01579B',
|
||||
'on-info': '#FFFFFF',
|
||||
|
||||
outline: '#6E5D93',
|
||||
focus: '#4C1D95',
|
||||
},
|
||||
variables: {
|
||||
'border-color': '#6E5D93',
|
||||
'border-opacity': 1,
|
||||
'high-emphasis-opacity': 1,
|
||||
'medium-emphasis-opacity': 0.8,
|
||||
'disabled-opacity': 0.5,
|
||||
},
|
||||
}
|
||||
|
||||
const dark: ThemeDefinition = {
|
||||
export const dark: ThemeDefinition = {
|
||||
dark: true,
|
||||
colors: {
|
||||
background: '#121212',
|
||||
surface: '#1E1E1E',
|
||||
'surface-variant': '#CFCFCF',
|
||||
primary: '#90CAF9',
|
||||
'on-primary': '#0D1B2A',
|
||||
secondary: '#B0BEC5',
|
||||
'on-secondary': '#10181C',
|
||||
background: '#151020',
|
||||
'on-background': '#F1EBFF',
|
||||
surface: '#1E1730',
|
||||
'on-surface': '#F1EBFF',
|
||||
'surface-bright': '#2A2142',
|
||||
'on-surface-bright': '#F1EBFF',
|
||||
'surface-light': '#2A2142',
|
||||
'on-surface-light': '#F1EBFF',
|
||||
'surface-variant': '#2A2142',
|
||||
'on-surface-variant': '#F1EBFF',
|
||||
|
||||
primary: '#B79CFF',
|
||||
'on-primary': '#240850',
|
||||
secondary: '#E3AEF2',
|
||||
'on-secondary': '#3A0F4A',
|
||||
info: '#9EC5FF',
|
||||
'on-info': '#0A2447',
|
||||
success: '#86DDA0',
|
||||
'on-success': '#08311A',
|
||||
warning: '#FFC766',
|
||||
'on-warning': '#3A2600',
|
||||
error: '#FFB4AB',
|
||||
'on-error': '#410002',
|
||||
success: '#A5D6A7',
|
||||
'on-success': '#0B2A0E',
|
||||
warning: '#FFCC80',
|
||||
'on-warning': '#2E1A00',
|
||||
info: '#81D4FA',
|
||||
'on-info': '#00263A',
|
||||
},
|
||||
}
|
||||
'on-error': '#4A0A06',
|
||||
|
||||
const highContrast: ThemeDefinition = {
|
||||
dark: true,
|
||||
colors: {
|
||||
background: '#000000',
|
||||
surface: '#000000',
|
||||
'surface-variant': '#FFFFFF',
|
||||
primary: '#FFFF00',
|
||||
'on-primary': '#000000',
|
||||
secondary: '#00FFFF',
|
||||
'on-secondary': '#000000',
|
||||
error: '#FF8A80',
|
||||
'on-error': '#000000',
|
||||
success: '#69F0AE',
|
||||
'on-success': '#000000',
|
||||
warning: '#FFD740',
|
||||
'on-warning': '#000000',
|
||||
info: '#80D8FF',
|
||||
'on-info': '#000000',
|
||||
outline: '#9A8FB5',
|
||||
focus: '#FFD54F',
|
||||
},
|
||||
variables: {
|
||||
'border-color': '#9A8FB5',
|
||||
'border-opacity': 1,
|
||||
'high-emphasis-opacity': 1,
|
||||
'medium-emphasis-opacity': 0.8,
|
||||
'disabled-opacity': 0.5,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -72,7 +87,7 @@ export default createVuetify({
|
||||
components,
|
||||
directives,
|
||||
locale: { locale: 'pl', fallback: 'en', messages: { pl, en } },
|
||||
theme: { defaultTheme: 'system', themes: { light, dark, highContrast } },
|
||||
theme: { defaultTheme: 'system', themes: { light, dark } },
|
||||
defaults: {
|
||||
global: { ripple: false },
|
||||
VBtn: { size: 'large', variant: 'flat', color: 'primary' },
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Surreal, createRemoteEngines } from 'surrealdb'
|
||||
import { createWasmEngines } from '@surrealdb/wasm'
|
||||
|
||||
const namespace = import.meta.env.VITE_SURREAL_NAMESPACE ?? 'hubmi'
|
||||
const database = import.meta.env.VITE_SURREAL_DATABASE ?? 'hubmi'
|
||||
|
||||
/**
|
||||
* A) Lokalna baza SurrealDB w przeglądarce.
|
||||
* Persistencja przez IndexedDB (`indxdb://`) — localStorage jest zbyt mały.
|
||||
* Dane prywatne użytkownika zostają tutaj i nie są wysyłane automatycznie.
|
||||
*/
|
||||
const localDatabase = import.meta.env.VITE_SURREAL_LOCAL_DATABASE ?? 'tarninahub'
|
||||
const localUrl = import.meta.env.VITE_SURREAL_LOCAL_URL ?? `indxdb://${localDatabase}`
|
||||
|
||||
export const localSurreal = new Surreal({
|
||||
engines: {
|
||||
...createRemoteEngines(),
|
||||
...createWasmEngines(),
|
||||
},
|
||||
})
|
||||
|
||||
let localConnection: Promise<void> | null = null
|
||||
|
||||
export function connectLocalSurreal(): Promise<void> {
|
||||
localConnection ??= localSurreal
|
||||
.connect(localUrl, { namespace, database })
|
||||
.then(() => undefined)
|
||||
|
||||
return localConnection
|
||||
}
|
||||
|
||||
/**
|
||||
* B) Zdalna baza backendu na dedykowanym porcie (tylko dla SurrealDB),
|
||||
* używana do synchronizacji danych uznanych za nadające się do wysłania.
|
||||
*/
|
||||
const remoteUrl =
|
||||
import.meta.env.VITE_SURREAL_REMOTE_URL ?? 'ws://127.0.0.1:8010/rpc'
|
||||
const username = import.meta.env.VITE_SURREAL_USERNAME as string | undefined
|
||||
const password = import.meta.env.VITE_SURREAL_PASSWORD as string | undefined
|
||||
|
||||
export const remoteSurreal = new Surreal()
|
||||
|
||||
let remoteConnection: Promise<void> | null = null
|
||||
|
||||
export function connectRemoteSurreal(): Promise<void> {
|
||||
remoteConnection ??= remoteSurreal
|
||||
.connect(remoteUrl, {
|
||||
namespace,
|
||||
database,
|
||||
...(username && password
|
||||
? { authentication: { username, password } }
|
||||
: {}),
|
||||
})
|
||||
.then(() => undefined)
|
||||
|
||||
return remoteConnection
|
||||
}
|
||||
|
||||
export { namespace, database }
|
||||
@@ -1,12 +1,9 @@
|
||||
.v-theme--light {
|
||||
--a11y-focus: #0b3d91;
|
||||
--a11y-focus: #4c1d95;
|
||||
}
|
||||
.v-theme--dark {
|
||||
--a11y-focus: #ffd54f;
|
||||
}
|
||||
.v-theme--highContrast {
|
||||
--a11y-focus: #ffff00;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 112.5%;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
name: tarninahub
|
||||
|
||||
services:
|
||||
app:
|
||||
image: tarninahub:0.0.1
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
APP_VERSION: 0.0.1
|
||||
container_name: tarninahub
|
||||
restart: unless-stopped
|
||||
# RocksDB zapisuje do bind-mountu ./data; root upraszcza uprawnienia (MVP).
|
||||
user: "0:0"
|
||||
ports:
|
||||
- "${TARNINAHUB_PORT:-8080}:8080"
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: Production
|
||||
ASPNETCORE_URLS: http://+:8080
|
||||
SurrealDb__Namespace: hubmi
|
||||
SurrealDb__Database: hubmi
|
||||
SurrealDb__DataDirectory: data
|
||||
volumes:
|
||||
# Jedyny volumen aplikacji — trwałe dane (SurrealDB / RocksDB).
|
||||
- ./data:/app/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://localhost:8080/api/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
@@ -0,0 +1,48 @@
|
||||
# TarninaHub — szablon docker-compose (wersja projektu 0.0.1)
|
||||
#
|
||||
# Użycie:
|
||||
# cp docker-compose.yaml.example docker-compose.yaml
|
||||
# docker compose up --build
|
||||
#
|
||||
# Aplikacja: http://localhost:8080
|
||||
# Dane: ./data (bind mount, trwałe, SurrealDB embedded RocksDB)
|
||||
|
||||
name: tarninahub
|
||||
|
||||
services:
|
||||
app:
|
||||
# Obraz budowany lokalnie z Dockerfile (multi-stage: klient Vue + backend .NET).
|
||||
image: tarninahub:${APP_VERSION:-0.0.1}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
APP_VERSION: ${APP_VERSION:-0.0.1}
|
||||
container_name: tarninahub
|
||||
restart: unless-stopped
|
||||
# RocksDB zapisuje do bind-mountu ./data; root upraszcza uprawnienia (MVP).
|
||||
# W produkcji użyj named volume albo dopasuj właściciela katalogu ./data.
|
||||
user: "0:0"
|
||||
ports:
|
||||
- "${TARNINAHUB_PORT:-8080}:8080"
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: Production
|
||||
ASPNETCORE_URLS: http://+:8080
|
||||
# SurrealDB — wbudowany silnik RocksDB (domyślnie), dane w /app/data.
|
||||
SurrealDb__Namespace: hubmi
|
||||
SurrealDb__Database: hubmi
|
||||
SurrealDb__DataDirectory: data
|
||||
# Tryb zdalny (opcjonalnie) — wskaż zewnętrzny serwer SurrealDB zamiast
|
||||
# wbudowanego RocksDB. Wtedy usuń `user: "0:0"` i przekieruj port 8010.
|
||||
# SurrealDb__RemoteUrl: ws://127.0.0.1:8010/rpc
|
||||
# SurrealDb__Username: root
|
||||
# SurrealDb__Password: root
|
||||
volumes:
|
||||
# Jedyny volumen aplikacji — trwałe dane (SurrealDB / RocksDB).
|
||||
- ./data:/app/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://localhost:8080/api/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
@@ -0,0 +1,36 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Uruchamia SurrealDB jako serwer na dedykowanym porcie (tylko dla bazy),
|
||||
z trwałym magazynem RocksDB w katalogu ./data (nie w pamięci).
|
||||
|
||||
.DESCRIPTION
|
||||
Port (domyślnie 8010) służy wyłącznie SurrealDB i jest używany zarówno przez
|
||||
backend (HubMi.Api), jak i przez frontend do synchronizacji (WebSocket /rpc).
|
||||
Dane trwale zapisywane są w backend/data (RocksDB).
|
||||
#>
|
||||
param(
|
||||
[int]$Port = 8010,
|
||||
[string]$Bind = "127.0.0.1",
|
||||
[string]$Username = "root",
|
||||
[string]$Password = "root",
|
||||
[string]$DataPath = (Join-Path $PSScriptRoot "..\backend\data")
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$surreal = (Get-Command surreal -ErrorAction SilentlyContinue)?.Source
|
||||
if (-not $surreal) {
|
||||
throw "Nie znaleziono 'surreal' w PATH. Zainstaluj SurrealDB: https://surrealdb.com/install"
|
||||
}
|
||||
|
||||
$resolvedData = [System.IO.Path]::GetFullPath($DataPath)
|
||||
New-Item -ItemType Directory -Force -Path $resolvedData | Out-Null
|
||||
|
||||
Write-Host "SurrealDB: bind $Bind`:$Port, rocksdb:$resolvedData"
|
||||
|
||||
& $surreal start `
|
||||
--user $Username `
|
||||
--pass $Password `
|
||||
--bind "${Bind}:${Port}" `
|
||||
--log info `
|
||||
"rocksdb:$resolvedData"
|
||||
Reference in New Issue
Block a user