stabilization: server.go split + i18n templates + frontend localization
cmd/verstak-server/server.go (2838→127 строк): разделён на 12 файлов - config.go, tokens.go, schema.go - server.go (только struct + NewServer + ListenAndServe) - routes.go, middleware.go, smtp.go - handlers_api.go, handlers_user.go, handlers_web_user.go, handlers_admin.go - templates.go (конвертирован в функции с i18n.T()) frontend: все русские строки заменены на t() вызовы - App.svelte, FileTreeRow.svelte, ConfirmModal.svelte - FilePreviewModal.svelte, fileUtils.js core: gofmt по всему проекту Все сборки (CLI, server, gui, frontend), go vet, go test проходят. check-i18n.sh: frontend чист, Go-файлы с кириллицей — только тесты/легаси.
This commit is contained in:
@@ -15,29 +15,29 @@ import (
|
||||
|
||||
// Kind constants.
|
||||
const (
|
||||
KindOpenURL = "open_url"
|
||||
KindOpenFile = "open_file"
|
||||
KindOpenFolder = "open_folder"
|
||||
KindRunCommand = "run_command"
|
||||
KindRunScript = "run_script"
|
||||
KindOpenURL = "open_url"
|
||||
KindOpenFile = "open_file"
|
||||
KindOpenFolder = "open_folder"
|
||||
KindRunCommand = "run_command"
|
||||
KindRunScript = "run_script"
|
||||
KindOpenTerminal = "open_terminal"
|
||||
KindLaunchApp = "launch_app"
|
||||
KindLaunchApp = "launch_app"
|
||||
)
|
||||
|
||||
// Record represents an action attached to a node.
|
||||
type Record struct {
|
||||
ID string `json:"id"`
|
||||
NodeID string `json:"node_id"`
|
||||
Title string `json:"title"`
|
||||
Kind string `json:"kind"`
|
||||
Command string `json:"command,omitempty"`
|
||||
Args []string `json:"args,omitempty"`
|
||||
WorkingDir string `json:"working_dir,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
ConfirmRequired bool `json:"confirm_required"`
|
||||
CaptureOutput bool `json:"capture_output"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
NodeID string `json:"node_id"`
|
||||
Title string `json:"title"`
|
||||
Kind string `json:"kind"`
|
||||
Command string `json:"command,omitempty"`
|
||||
Args []string `json:"args,omitempty"`
|
||||
WorkingDir string `json:"working_dir,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
ConfirmRequired bool `json:"confirm_required"`
|
||||
CaptureOutput bool `json:"capture_output"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// RunResult is returned after executing an action.
|
||||
|
||||
@@ -130,10 +130,10 @@ func TestGetNotFound(t *testing.T) {
|
||||
|
||||
func TestKindLabel(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
KindOpenURL: "Открыть URL",
|
||||
KindRunCommand: "Запустить команду",
|
||||
KindOpenFolder: "Открыть папку",
|
||||
"unknown": "unknown",
|
||||
KindOpenURL: "Открыть URL",
|
||||
KindRunCommand: "Запустить команду",
|
||||
KindOpenFolder: "Открыть папку",
|
||||
"unknown": "unknown",
|
||||
}
|
||||
for kind, want := range cases {
|
||||
got := KindLabel(kind)
|
||||
|
||||
@@ -10,30 +10,30 @@ import (
|
||||
|
||||
// Event types.
|
||||
const (
|
||||
TypeNoteCreated = "note_created"
|
||||
TypeNoteUpdated = "note_updated"
|
||||
TypeFileAdded = "file_added"
|
||||
TypeFileDeleted = "file_deleted"
|
||||
TypeFileRenamed = "file_renamed"
|
||||
TypeFileCopied = "file_copied"
|
||||
TypeFileMoved = "file_moved"
|
||||
TypeFolderAdded = "folder_added"
|
||||
TypeFolderDeleted = "folder_deleted"
|
||||
TypeFolderRenamed = "folder_renamed"
|
||||
TypeNodeCreated = "node_created"
|
||||
TypeNodeUpdated = "node_updated"
|
||||
TypeActionCreated = "action_created"
|
||||
TypeActionDone = "action_done"
|
||||
TypeWorklogAdded = "worklog_added"
|
||||
TypeNoteCreated = "note_created"
|
||||
TypeNoteUpdated = "note_updated"
|
||||
TypeFileAdded = "file_added"
|
||||
TypeFileDeleted = "file_deleted"
|
||||
TypeFileRenamed = "file_renamed"
|
||||
TypeFileCopied = "file_copied"
|
||||
TypeFileMoved = "file_moved"
|
||||
TypeFolderAdded = "folder_added"
|
||||
TypeFolderDeleted = "folder_deleted"
|
||||
TypeFolderRenamed = "folder_renamed"
|
||||
TypeNodeCreated = "node_created"
|
||||
TypeNodeUpdated = "node_updated"
|
||||
TypeActionCreated = "action_created"
|
||||
TypeActionDone = "action_done"
|
||||
TypeWorklogAdded = "worklog_added"
|
||||
)
|
||||
|
||||
// Target types.
|
||||
const (
|
||||
TargetNote = "note"
|
||||
TargetFile = "file"
|
||||
TargetFolder = "folder"
|
||||
TargetAction = "action"
|
||||
TargetNode = "node"
|
||||
TargetNote = "note"
|
||||
TargetFile = "file"
|
||||
TargetFolder = "folder"
|
||||
TargetAction = "action"
|
||||
TargetNode = "node"
|
||||
TargetWorklog = "worklog"
|
||||
)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ type Record struct {
|
||||
ID string `json:"id"`
|
||||
NodeID string `json:"node_id"`
|
||||
Filename string `json:"filename"`
|
||||
Path string `json:"path"` // relative to vault root
|
||||
Path string `json:"path"` // relative to vault root
|
||||
StorageMode string `json:"storage_mode"` // "vault" | "external"
|
||||
Size int64 `json:"size"`
|
||||
SHA256 string `json:"sha256,omitempty"`
|
||||
|
||||
@@ -307,11 +307,11 @@ func TestPreviewImportDir(t *testing.T) {
|
||||
|
||||
func TestGuessMIME(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"a.md": "text/plain",
|
||||
"a.png": "image/png",
|
||||
"a.pdf": "application/pdf",
|
||||
"a.docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"a.go": "text/plain",
|
||||
"a.md": "text/plain",
|
||||
"a.png": "image/png",
|
||||
"a.pdf": "application/pdf",
|
||||
"a.docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"a.go": "text/plain",
|
||||
"a.unknown": "application/octet-stream",
|
||||
}
|
||||
for name, want := range cases {
|
||||
|
||||
+15
-15
@@ -7,19 +7,19 @@ import (
|
||||
// Node is the central entity of Verstak — a tree item that can be
|
||||
// a case, folder, note, document, etc.
|
||||
type Node struct {
|
||||
ID string `json:"id"`
|
||||
ParentID *string `json:"parent_id,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
Path *string `json:"path,omitempty"`
|
||||
Section string `json:"section,omitempty"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
Revision int `json:"revision"`
|
||||
DeviceID *string `json:"device_id,omitempty"`
|
||||
ID string `json:"id"`
|
||||
ParentID *string `json:"parent_id,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
Path *string `json:"path,omitempty"`
|
||||
Section string `json:"section,omitempty"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
Revision int `json:"revision"`
|
||||
DeviceID *string `json:"device_id,omitempty"`
|
||||
}
|
||||
|
||||
// IsDeleted reports whether the node has been soft-deleted.
|
||||
@@ -40,6 +40,6 @@ type Meta struct {
|
||||
|
||||
// NodeWithMeta bundles a node and its metadata entries.
|
||||
type NodeWithMeta struct {
|
||||
Node Node `json:"node"`
|
||||
Meta []Meta `json:"meta"`
|
||||
Node Node `json:"node"`
|
||||
Meta []Meta `json:"meta"`
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ func (m *Manager) Discover() {
|
||||
meta.Name = e.Name()
|
||||
}
|
||||
m.plugins = append(m.plugins, Plugin{
|
||||
Meta: meta,
|
||||
Dir: filepath.Join(pluginsDir, e.Name()),
|
||||
Meta: meta,
|
||||
Dir: filepath.Join(pluginsDir, e.Name()),
|
||||
Active: true,
|
||||
})
|
||||
}
|
||||
@@ -116,13 +116,13 @@ func (m *Manager) Templates() []TemplateDefinition {
|
||||
|
||||
// TemplateDefinition describes a predefined tree of nodes.
|
||||
type TemplateDefinition struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Plugin string // source plugin name
|
||||
RootType string `json:"root_type"`
|
||||
Tree []TreeNode `json:"tree"`
|
||||
Meta []NodeMeta `json:"meta,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Plugin string // source plugin name
|
||||
RootType string `json:"root_type"`
|
||||
Tree []TreeNode `json:"tree"`
|
||||
Meta []NodeMeta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// TreeNode is a single item in a template tree.
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
|
||||
"verstak/internal/core/actions"
|
||||
"verstak/internal/core/files"
|
||||
"verstak/internal/core/notes"
|
||||
"verstak/internal/core/nodes"
|
||||
"verstak/internal/core/notes"
|
||||
"verstak/internal/core/search"
|
||||
"verstak/internal/core/storage"
|
||||
"verstak/internal/core/vault"
|
||||
|
||||
@@ -57,12 +57,12 @@ CREATE TABLE IF NOT EXISTS _schema_ver (
|
||||
`
|
||||
|
||||
var migrationFiles = map[int]string{
|
||||
1: migration001,
|
||||
2: migration002,
|
||||
3: migration003,
|
||||
4: migration004,
|
||||
5: migration005,
|
||||
6: migration006,
|
||||
1: migration001,
|
||||
2: migration002,
|
||||
3: migration003,
|
||||
4: migration004,
|
||||
5: migration005,
|
||||
6: migration006,
|
||||
// 7: migration007 (FTS5) — created lazily by search.Rebuild()
|
||||
8: migration008,
|
||||
9: migration009,
|
||||
|
||||
@@ -29,7 +29,7 @@ func NewClient(serverURL, apiKey, deviceID, vaultRoot string) *Client {
|
||||
APIKey: apiKey,
|
||||
DeviceID: deviceID,
|
||||
VaultRoot: vaultRoot,
|
||||
HTTP: &http.Client{Timeout: 30 * time.Second},
|
||||
HTTP: &http.Client{Timeout: 30 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,9 +145,9 @@ func (c *Client) TestAuth(serverURL, username, password string) error {
|
||||
|
||||
// PushRequest is the payload for POST /sync/push.
|
||||
type PushRequest struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
||||
Ops []PushOp `json:"ops"`
|
||||
DeviceID string `json:"device_id"`
|
||||
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
||||
Ops []PushOp `json:"ops"`
|
||||
}
|
||||
|
||||
// PushOp is a single operation in a push request.
|
||||
@@ -164,8 +164,8 @@ type PushOp struct {
|
||||
|
||||
// PushResponse is the response from POST /sync/push.
|
||||
type PushResponse struct {
|
||||
Accepted []string `json:"accepted"`
|
||||
Count int `json:"count"`
|
||||
Accepted []string `json:"accepted"`
|
||||
Count int `json:"count"`
|
||||
Conflicts []map[string]interface{} `json:"conflicts"`
|
||||
}
|
||||
|
||||
|
||||
+12
-12
@@ -30,18 +30,18 @@ const (
|
||||
|
||||
// Op represents a sync operation.
|
||||
type Op struct {
|
||||
ID string `json:"id"`
|
||||
OpID string `json:"op_id"`
|
||||
ServerSequence int `json:"server_sequence,omitempty"`
|
||||
DeviceID string `json:"device_id,omitempty"`
|
||||
EntityType string `json:"entity_type"`
|
||||
EntityID string `json:"entity_id"`
|
||||
OpType string `json:"op_type"`
|
||||
PayloadJSON string `json:"payload_json"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
PushedAt *string `json:"pushed_at,omitempty"`
|
||||
ClientSequence int `json:"client_sequence,omitempty"`
|
||||
LastSeenServerSeq int `json:"last_seen_server_seq,omitempty"`
|
||||
ID string `json:"id"`
|
||||
OpID string `json:"op_id"`
|
||||
ServerSequence int `json:"server_sequence,omitempty"`
|
||||
DeviceID string `json:"device_id,omitempty"`
|
||||
EntityType string `json:"entity_type"`
|
||||
EntityID string `json:"entity_id"`
|
||||
OpType string `json:"op_type"`
|
||||
PayloadJSON string `json:"payload_json"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
PushedAt *string `json:"pushed_at,omitempty"`
|
||||
ClientSequence int `json:"client_sequence,omitempty"`
|
||||
LastSeenServerSeq int `json:"last_seen_server_seq,omitempty"`
|
||||
}
|
||||
|
||||
// Service records and manages sync operations.
|
||||
|
||||
@@ -132,16 +132,16 @@ func TestE2ESync(t *testing.T) {
|
||||
"device_id": deviceIDA,
|
||||
"ops": []map[string]interface{}{
|
||||
{
|
||||
"op_id": "op-node-create-001",
|
||||
"entity_type": "node",
|
||||
"entity_id": nodeID,
|
||||
"op_type": "create",
|
||||
"op_id": "op-node-create-001",
|
||||
"entity_type": "node",
|
||||
"entity_id": nodeID,
|
||||
"op_type": "create",
|
||||
"payload_json": fmt.Sprintf(
|
||||
`{"id":"%s","parent_id":"","type":"case","title":"Test Project","slug":"test-project","section":"projects","created_at":"%s","updated_at":"%s"}`,
|
||||
nodeID, now, now),
|
||||
"client_sequence": 1,
|
||||
"client_sequence": 1,
|
||||
"last_seen_server_seq": 0,
|
||||
"created_at": now,
|
||||
"created_at": now,
|
||||
},
|
||||
},
|
||||
"idempotency_key": "e2e-test-push-1",
|
||||
@@ -161,7 +161,7 @@ func TestE2ESync(t *testing.T) {
|
||||
t.Fatalf("push A status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
var pushRespA struct {
|
||||
Accepted []string `json:"accepted"`
|
||||
Accepted []string `json:"accepted"`
|
||||
Conflicts []interface{} `json:"conflicts"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&pushRespA)
|
||||
|
||||
@@ -5,9 +5,10 @@ package gui
|
||||
// готова к упаковке (нет external JS/CSS, fetch к /api/* через origin).
|
||||
//
|
||||
// navigation state:
|
||||
// sel = { kind:'section', section:'today'|'inbox'|'clients'|'projects'|'recipes'|'documents'|'archive' }
|
||||
// or { kind:'node', nodeId:'<uuid>' }
|
||||
// tab = 'ov'|'notes'|'files'|'actions'|'worklog'|'activity'
|
||||
//
|
||||
// sel = { kind:'section', section:'today'|'inbox'|'clients'|'projects'|'recipes'|'documents'|'archive' }
|
||||
// or { kind:'node', nodeId:'<uuid>' }
|
||||
// tab = 'ov'|'notes'|'files'|'actions'|'worklog'|'activity'
|
||||
const indexHTML = `<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
|
||||
+13
-11
@@ -11,8 +11,8 @@ import (
|
||||
|
||||
"verstak/internal/core/actions"
|
||||
"verstak/internal/core/files"
|
||||
"verstak/internal/core/notes"
|
||||
"verstak/internal/core/nodes"
|
||||
"verstak/internal/core/notes"
|
||||
"verstak/internal/core/plugins"
|
||||
"verstak/internal/core/search"
|
||||
"verstak/internal/core/storage"
|
||||
@@ -280,7 +280,9 @@ func (s *Server) handleNotes(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
jsonOK(w, n)
|
||||
case "PUT":
|
||||
var req struct{ Content string `json:"content"` }
|
||||
var req struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
if err := s.notes.Save(path, req.Content); err != nil {
|
||||
jsonErr(w, 500, err.Error())
|
||||
@@ -365,15 +367,15 @@ func (s *Server) handleActions(w http.ResponseWriter, r *http.Request) {
|
||||
jsonOK(w, list)
|
||||
case "POST":
|
||||
var req struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Kind string `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Command string `json:"command"`
|
||||
URL string `json:"url"`
|
||||
WorkingDir string `json:"working_dir"`
|
||||
Args []string `json:"args"`
|
||||
Confirm bool `json:"confirm"`
|
||||
Capture bool `json:"capture"`
|
||||
NodeID string `json:"node_id"`
|
||||
Kind string `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Command string `json:"command"`
|
||||
URL string `json:"url"`
|
||||
WorkingDir string `json:"working_dir"`
|
||||
Args []string `json:"args"`
|
||||
Confirm bool `json:"confirm"`
|
||||
Capture bool `json:"capture"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
rec, err := s.actions.Create(req.NodeID, req.Kind, req.Title, req.Command, req.WorkingDir, req.URL, req.Args, req.Confirm, req.Capture)
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
var localeFS embed.FS
|
||||
|
||||
var (
|
||||
mu sync.RWMutex
|
||||
cache = map[string]map[string]string{}
|
||||
mu sync.RWMutex
|
||||
cache = map[string]map[string]string{}
|
||||
defaultLocale = "ru"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user