feat: initialize independent Bell product skeleton (#9)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Copy values into the process environment. Do not commit real credentials.
|
||||
BELL_ENV=development
|
||||
BELL_HTTP_ADDRESS=127.0.0.1:8082
|
||||
BELL_DATABASE_URL=postgres://bell_app:replace-at-deploy@127.0.0.1:5432/bell?sslmode=disable
|
||||
BELL_SHUTDOWN_SECONDS=10
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Upstream sources
|
||||
|
||||
Bell is independently derived from the frozen YoVision GoAdmin baseline.
|
||||
|
||||
| Source | Commit | License | Use |
|
||||
|---|---|---|---|
|
||||
| https://github.com/go-admin-team/go-admin.git | `f06540883b41d03782bb6b2c4150f298f328c6b6` | MIT | Backend architecture reference |
|
||||
| https://github.com/go-admin-team/go-admin-ui.git | `67d393d713877572fab0b897296a4c1d525fc81d` | MIT | Vue 3 application shell and component patterns |
|
||||
|
||||
The local copies under `D:\github\goadmin` are read-only references and are not Bell source directories.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 go-admin-team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 PanJiaChen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Bell
|
||||
|
||||
Bell is YoVision's independently deployable alert intake and response product. It owns its users, roles, sessions, database, audit facts and release artifacts. Sense and Brain are not required for Bell to build or run.
|
||||
|
||||
## Frozen baseline
|
||||
|
||||
- Go `1.26.5`
|
||||
- Node.js `22.22.1`
|
||||
- pnpm `9.15.1`
|
||||
- go-admin commit `f06540883b41d03782bb6b2c4150f298f328c6b6`
|
||||
- go-admin-ui commit `67d393d713877572fab0b897296a4c1d525fc81d`
|
||||
|
||||
Exact upstream sources and retained MIT notices are in `LICENSES/`.
|
||||
|
||||
## Configuration
|
||||
|
||||
Copy variable names from `.env.example` into the process environment. Replace every placeholder at deployment time. Bell intentionally has no built-in database password, session secret or administrator password.
|
||||
|
||||
The PostgreSQL role used by `BELL_DATABASE_URL` must be dedicated to Bell. Do not reuse a Sense role or database.
|
||||
|
||||
## Backend
|
||||
|
||||
```powershell
|
||||
Set-Location Bell/server
|
||||
go test ./...
|
||||
go run . migrate
|
||||
go run . serve
|
||||
```
|
||||
|
||||
Health endpoints:
|
||||
|
||||
```powershell
|
||||
Invoke-RestMethod http://127.0.0.1:8082/healthz
|
||||
Invoke-RestMethod http://127.0.0.1:8082/readyz
|
||||
```
|
||||
|
||||
Migrations are embedded and applied transactionally. The `migrate` command is idempotent. To roll back this initial skeleton, stop Bell and drop only the dedicated Bell test database; migrations are forward-only and never touch another product database.
|
||||
|
||||
## Web
|
||||
|
||||
```powershell
|
||||
Set-Location Bell/web
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm lint
|
||||
pnpm build:prod
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
The web client listens on port 8083 in development and proxies `/api` to Bell only. The visible shell contains only Bell's product navigation; go-admin demonstration routes are neither registered nor served by the backend.
|
||||
|
||||
## Independent smoke check
|
||||
|
||||
Run PostgreSQL with an empty Bell-only database, set `BELL_DATABASE_URL`, start the backend and then the web client. Keep Sense and Brain stopped. Verify `/healthz`, `/readyz`, the workbench shell, and a direct request to an unregistered framework demo route returns `404`.
|
||||
|
||||
## Logs and secrets
|
||||
|
||||
Application logs go to standard output. Never log database URLs, passwords, session tokens, event evidence credentials, customer data or production payloads.
|
||||
@@ -0,0 +1,11 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$expectedGo = 'go version go1.26.5 windows/amd64'
|
||||
$expectedNode = 'v22.22.1'
|
||||
$expectedPnpm = '9.15.1'
|
||||
|
||||
if ((go version) -ne $expectedGo) { throw "Expected $expectedGo" }
|
||||
if ((node --version) -ne $expectedNode) { throw "Expected Node $expectedNode" }
|
||||
if ((pnpm --version) -ne $expectedPnpm) { throw "Expected pnpm $expectedPnpm" }
|
||||
|
||||
Write-Output 'Bell toolchain matches goadmin-baseline.json.'
|
||||
@@ -0,0 +1,61 @@
|
||||
package bell
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"git.ilapage.cn/ila/yovision/Bell/server/config"
|
||||
"git.ilapage.cn/ila/yovision/Bell/server/internal/platform"
|
||||
)
|
||||
|
||||
const Version = "0.1.0"
|
||||
|
||||
func Run(ctx context.Context, args []string) error {
|
||||
command := "serve"
|
||||
if len(args) > 0 {
|
||||
command = args[0]
|
||||
}
|
||||
if command == "version" {
|
||||
fmt.Println(Version)
|
||||
return nil
|
||||
}
|
||||
if command != "serve" && command != "migrate" {
|
||||
return fmt.Errorf("unknown command %q (use serve, migrate, or version)", command)
|
||||
}
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db, err := platform.OpenDatabase(ctx, cfg.DatabaseURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
if err := platform.Migrate(ctx, db); err != nil {
|
||||
return err
|
||||
}
|
||||
if command == "migrate" {
|
||||
return nil
|
||||
}
|
||||
app := platform.NewHTTPApp(db)
|
||||
server := &http.Server{Addr: cfg.HTTPAddress, Handler: app.Handler()}
|
||||
serverCtx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
go func() {
|
||||
<-serverCtx.Done()
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
|
||||
defer cancel()
|
||||
_ = server.Shutdown(shutdownCtx)
|
||||
}()
|
||||
slog.Info("Bell listening", "address", cfg.HTTPAddress, "environment", cfg.Environment)
|
||||
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Environment string
|
||||
HTTPAddress string
|
||||
DatabaseURL string
|
||||
ShutdownTimeout time.Duration
|
||||
}
|
||||
|
||||
func Load() (Config, error) {
|
||||
cfg := Config{
|
||||
Environment: value("BELL_ENV", "development"),
|
||||
HTTPAddress: value("BELL_HTTP_ADDRESS", "127.0.0.1:8082"),
|
||||
DatabaseURL: os.Getenv("BELL_DATABASE_URL"),
|
||||
ShutdownTimeout: 10 * time.Second,
|
||||
}
|
||||
if raw := os.Getenv("BELL_SHUTDOWN_SECONDS"); raw != "" {
|
||||
seconds, err := strconv.Atoi(raw)
|
||||
if err != nil || seconds < 1 || seconds > 300 {
|
||||
return Config{}, fmt.Errorf("BELL_SHUTDOWN_SECONDS must be between 1 and 300")
|
||||
}
|
||||
cfg.ShutdownTimeout = time.Duration(seconds) * time.Second
|
||||
}
|
||||
if cfg.DatabaseURL == "" {
|
||||
return Config{}, fmt.Errorf("BELL_DATABASE_URL is required")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func value(name, fallback string) string {
|
||||
if value := os.Getenv(name); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
module git.ilapage.cn/ila/yovision/Bell/server
|
||||
|
||||
go 1.26.5
|
||||
|
||||
require github.com/jackc/pgx/v5 v5.7.6
|
||||
|
||||
require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,30 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func OpenDatabase(ctx context.Context, databaseURL string) (*pgxpool.Pool, error) {
|
||||
poolConfig, err := pgxpool.ParseConfig(databaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse Bell database URL: %w", err)
|
||||
}
|
||||
poolConfig.MaxConns = 8
|
||||
poolConfig.MinConns = 1
|
||||
poolConfig.MaxConnLifetime = 30 * time.Minute
|
||||
pool, err := pgxpool.NewWithConfig(ctx, poolConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open Bell database: %w", err)
|
||||
}
|
||||
pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
if err := pool.Ping(pingCtx); err != nil {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("ping Bell database: %w", err)
|
||||
}
|
||||
return pool, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type HTTPApp struct {
|
||||
DB *pgxpool.Pool
|
||||
Mux *http.ServeMux
|
||||
}
|
||||
|
||||
func NewHTTPApp(db *pgxpool.Pool) *HTTPApp {
|
||||
app := &HTTPApp{DB: db, Mux: http.NewServeMux()}
|
||||
app.Mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"status": "ok", "service": "bell", "time": time.Now().UTC()})
|
||||
})
|
||||
app.Mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := db.Ping(r.Context()); err != nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"status": "not_ready"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ready"})
|
||||
})
|
||||
return app
|
||||
}
|
||||
|
||||
func (a *HTTPApp) Handler() http.Handler {
|
||||
return securityHeaders(a.Mux)
|
||||
}
|
||||
|
||||
func securityHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, value any) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSecurityHeaders(t *testing.T) {
|
||||
h := securityHeaders(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) }))
|
||||
recorder := httptest.NewRecorder()
|
||||
h.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
if recorder.Header().Get("X-Frame-Options") != "DENY" {
|
||||
t.Fatal("missing frame protection")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"sort"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationFiles embed.FS
|
||||
|
||||
func Migrate(ctx context.Context, pool *pgxpool.Pool) error {
|
||||
if _, err := pool.Exec(ctx, `CREATE TABLE IF NOT EXISTS bell_schema_migrations (name text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())`); err != nil {
|
||||
return fmt.Errorf("create migration ledger: %w", err)
|
||||
}
|
||||
entries, err := fs.ReadDir(migrationFiles, "migrations")
|
||||
if err != nil {
|
||||
return fmt.Errorf("read embedded migrations: %w", err)
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() })
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
var applied bool
|
||||
if err := pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM bell_schema_migrations WHERE name=$1)`, entry.Name()).Scan(&applied); err != nil {
|
||||
return err
|
||||
}
|
||||
if applied {
|
||||
continue
|
||||
}
|
||||
sqlBytes, err := migrationFiles.ReadFile("migrations/" + entry.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, string(sqlBytes)); err == nil {
|
||||
_, err = tx.Exec(ctx, `INSERT INTO bell_schema_migrations(name) VALUES($1)`, entry.Name())
|
||||
}
|
||||
if err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
return fmt.Errorf("apply migration %s: %w", entry.Name(), err)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return fmt.Errorf("commit migration %s: %w", entry.Name(), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE IF NOT EXISTS bell_runtime_probe (
|
||||
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
||||
started_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Bell is derived from the frozen go-admin baseline. See Bell/LICENSES/SOURCES.md.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"git.ilapage.cn/ila/yovision/Bell/server/cmd/bell"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := bell.Run(context.Background(), os.Args[1:]); err != nil {
|
||||
slog.Error("bell stopped", "error", err)
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { node: true },
|
||||
extends: ['plugin:vue/vue3-essential', 'eslint:recommended'],
|
||||
parserOptions: { parser: '@babel/eslint-parser', requireConfigFile: false },
|
||||
rules: { 'vue/multi-word-component-names': 'off' }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = { presets: ['@vue/cli-plugin-babel/preset'] }
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "yovision-bell-ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vue-cli-service serve",
|
||||
"lint": "eslint --ext .js,.vue src",
|
||||
"build:prod": "vue-cli-service build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "2.3.2",
|
||||
"axios": "1.19.0",
|
||||
"core-js": "3.50.0",
|
||||
"element-plus": "2.14.4",
|
||||
"vue": "3.5.41",
|
||||
"vue-router": "4.6.4",
|
||||
"vuex": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.29.7",
|
||||
"@vue/cli-plugin-babel": "5.0.9",
|
||||
"@vue/cli-plugin-eslint": "5.0.9",
|
||||
"@vue/cli-service": "5.0.9",
|
||||
"@vue/compiler-sfc": "3.5.41",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-plugin-vue": "9.33.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.1",
|
||||
"engines": { "node": "22.22.1", "pnpm": "9.15.1" }
|
||||
}
|
||||
Generated
+8334
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Bell 预警中心</title></head>
|
||||
<body><noscript>Bell 需要启用 JavaScript。</noscript><div id="app"></div></body>
|
||||
</html>
|
||||
@@ -0,0 +1,4 @@
|
||||
<template><AppLayout><router-view /></AppLayout></template>
|
||||
<script setup>
|
||||
import AppLayout from '../layout/AppLayout.vue'
|
||||
</script>
|
||||
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<main class="page" tabindex="-1">
|
||||
<header class="page__header"><div><p class="eyebrow">BELL</p><h1>预警工作台</h1><p>Bell 已独立运行,业务模块将按工单顺序启用。</p></div><el-tag type="success">服务就绪</el-tag></header>
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :sm="12" :lg="8"><el-card><template #header>待处理预警</template><strong class="metric">0</strong><p>规则模块启用后显示。</p></el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="8"><el-card><template #header>今日事件</template><strong class="metric">0</strong><p>事件入站启用后显示。</p></el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="8"><el-card><template #header>运行状态</template><strong class="metric metric--small">独立</strong><p>无需 Sense 或 Brain 在线。</p></el-card></el-col>
|
||||
</el-row>
|
||||
</main>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import './theme.css'
|
||||
|
||||
createApp(App).use(store).use(router).use(ElementPlus).mount('#app')
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Dashboard from './Dashboard.vue'
|
||||
|
||||
export default createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [{ path: '/', name: 'dashboard', component: Dashboard, meta: { title: '工作台' } }]
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createStore } from 'vuex'
|
||||
|
||||
export default createStore({ state: () => ({ product: 'Bell' }) })
|
||||
@@ -0,0 +1,26 @@
|
||||
:root { --bell-accent:#409eff; --bell-bg:#f3f4f7; --bell-surface:#fff; --bell-text:#1f2329; --bell-muted:#6b7280; --bell-stroke:#e5e7eb; }
|
||||
* { box-sizing:border-box; }
|
||||
html, body, #app { min-height:100%; margin:0; }
|
||||
body { font-family:"Segoe UI Variable","Microsoft YaHei",sans-serif; color:var(--bell-text); background:var(--bell-bg); }
|
||||
.shell { min-height:100vh; display:grid; grid-template-columns:220px minmax(0,1fr); }
|
||||
.sidebar { background:#263445; color:#fff; padding:16px 12px; }
|
||||
.brand { display:flex; gap:12px; align-items:center; min-height:48px; padding:0 8px 16px; }
|
||||
.brand__mark { display:grid; place-items:center; width:36px; height:36px; border-radius:8px; background:var(--bell-accent); font-weight:700; }
|
||||
.brand small { display:block; color:#cbd5e1; margin-top:2px; }
|
||||
.nav-item { display:flex; min-height:44px; align-items:center; padding:0 12px; color:#cbd5e1; text-decoration:none; border-radius:6px; }
|
||||
.nav-item.router-link-active { color:#fff; background:#1f2d3d; }
|
||||
.workspace { min-width:0; }
|
||||
.navbar { height:50px; display:flex; align-items:center; justify-content:space-between; padding:0 20px; background:var(--bell-surface); border-bottom:1px solid var(--bell-stroke); }
|
||||
.navbar__identity { color:var(--bell-muted); }
|
||||
.tags-view { height:34px; display:flex; align-items:end; padding:0 16px; background:#fff; border-bottom:1px solid var(--bell-stroke); }
|
||||
.tag-current { padding:6px 12px; border:1px solid var(--bell-stroke); border-bottom:2px solid var(--bell-accent); }
|
||||
.app-main { padding:20px; }
|
||||
.page { max-width:1440px; margin:0 auto; outline:none; }
|
||||
.page__header { display:flex; justify-content:space-between; gap:16px; align-items:flex-start; margin-bottom:20px; }
|
||||
.page__header h1 { margin:0 0 6px; font-size:24px; }
|
||||
.page__header p { margin:0; color:var(--bell-muted); }
|
||||
.eyebrow { color:var(--bell-accent)!important; font-size:12px; font-weight:700; letter-spacing:.12em; }
|
||||
.metric { font-size:36px; font-variant-numeric:tabular-nums; }
|
||||
.metric--small { font-size:24px; }
|
||||
@media (max-width:640px) { .shell { grid-template-columns:1fr; } .sidebar { position:static; padding:8px 12px; } .brand { padding-bottom:8px; } .sidebar nav { display:flex; } .workspace { min-width:0; } .app-main { padding:12px; } .page__header { flex-direction:column; } }
|
||||
@media (prefers-reduced-motion:reduce) { *,*::before,*::after { scroll-behavior:auto!important; transition-duration:.01ms!important; animation-duration:.01ms!important; } }
|
||||
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="shell">
|
||||
<aside class="sidebar" aria-label="主导航">
|
||||
<div class="brand"><span class="brand__mark">B</span><div><strong>Bell</strong><small>预警中心</small></div></div>
|
||||
<nav><router-link to="/" class="nav-item">工作台</router-link></nav>
|
||||
</aside>
|
||||
<section class="workspace">
|
||||
<header class="navbar"><span>学校安全预警</span><span class="navbar__identity">未登录</span></header>
|
||||
<div class="tags-view"><span class="tag-current">工作台</span></div>
|
||||
<div class="app-main"><slot /></div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
import './bootstrap/main'
|
||||
@@ -0,0 +1,10 @@
|
||||
const { defineConfig } = require('@vue/cli-service')
|
||||
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true,
|
||||
outputDir: 'dist',
|
||||
devServer: {
|
||||
port: 8083,
|
||||
proxy: { '/api': { target: process.env.BELL_API_ORIGIN || 'http://127.0.0.1:8082' } }
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user