diff --git a/TASKS/reports/T12-report.md b/TASKS/reports/T12-report.md new file mode 100644 index 0000000..31ed76e --- /dev/null +++ b/TASKS/reports/T12-report.md @@ -0,0 +1,68 @@ +# T12 Izveštaj: Dashboard — prikaz dokumentacije i CLAUDE.md + +**Agent:** coder +**Model:** Opus +**Datum:** 2026-02-20 + +--- + +## Šta je urađeno + +Dodat docs viewer na dashboard sa goldmark markdown renderovanjem. + +### Novi fajlovi + +| Fajl | Opis | +|------|------| +| `internal/server/docs.go` | Docs handleri, goldmark renderovanje, path traversal zaštita, link rewriting | +| `web/templates/docs-list.html` | Template za listu .md fajlova | +| `web/templates/docs-view.html` | Template za prikaz renderovanog markdowna sa breadcrumbs | + +### Izmenjeni fajlovi + +| Fajl | Izmena | +|------|--------| +| `internal/server/server.go` | GET /docs i GET /docs/*path rute | +| `internal/server/render.go` | renderDocsList(), renderDocsView(), novi templates u init() | +| `internal/server/server_test.go` | 9 novih testova, .md fajlovi u test setup | +| `web/static/style.css` | Nav, docs-container, docs-list, docs-content, breadcrumbs stilovi | +| `web/templates/layout.html` | Nav bar sa Kanban/Dokumenti linkovima | +| `go.mod` / `go.sum` | goldmark v1.7.16 | + +### Endpointi + +| Ruta | Opis | +|------|------| +| `GET /docs` | Lista svih .md fajlova u projektu | +| `GET /docs/{path}` | Renderovan markdown fajl sa breadcrumbs | + +### Bezbednost + +- Samo .md fajlovi (ostalo → 403) +- Path traversal zaštita (`../../etc/passwd` → 403) +- filepath.Clean + HasPrefix provera + +### Markdown features + +- goldmark sa Table i Strikethrough ekstenzijama +- Relativni .md linkovi se rewrite-uju u `/docs/` linkove +- Code blokovi, tabele, liste, blockquote — sve renderovano + +### Novi testovi + +``` +TestDocsList PASS +TestDocsView_CLAUDE PASS +TestDocsView_NestedFile PASS +TestDocsView_PathTraversal PASS +TestDocsView_NonMarkdown PASS +TestDocsView_NotFound PASS +TestDocsView_HasBreadcrumbs PASS +TestRewriteLinksSimple PASS +TestRewriteLinksSimple_NestedDir PASS +``` + +### Ukupno projekat: 101 test, svi prolaze + +- `go vet ./...` — čist +- `go build ./...` — prolazi diff --git a/TASKS/review/T12.md b/TASKS/review/T12.md new file mode 100644 index 0000000..1fa2012 --- /dev/null +++ b/TASKS/review/T12.md @@ -0,0 +1,70 @@ +# T12: Dashboard — prikaz dokumentacije i CLAUDE.md + +**Kreirao:** planer +**Datum:** 2026-02-20 +**Agent:** coder +**Model:** Sonnet +**Zavisi od:** T11 + +--- + +## Opis + +Dodaj tab/sekciju na dashboard za prikaz dokumentacije. +Prikazuje CLAUDE.md renderovan kao HTML, sa klikabilnim referencama +koje otvaraju referencirane fajlove inline. + +## Endpointi + +``` +GET /docs → lista svih .md fajlova (HTML) +GET /docs/{path...} → renderovan markdown fajl (HTML fragment) +``` + +## Kako radi + +1. Sidebar ili tab "Dokumenti" pored Kanban boarda +2. Klik → prikaže CLAUDE.md renderovan kao HTML +3. Reference u tabeli (npr. `agents/coder/CLAUDE.md`) su linkovi +4. Klik na link → HTMX učita taj fajl: `hx-get="/docs/agents/coder/CLAUDE.md"` +5. Breadcrumb navigacija: CLAUDE.md > agents > coder > CLAUDE.md +6. Dugme "Nazad" vraća na prethodni fajl + +## Markdown renderovanje + +- Go library: `github.com/gomarkdown/markdown` ili `goldmark` +- Renderuj server-side u HTML +- Tabele, code blokovi, headeri — sve podržano +- Relativne putanje u linkovima → pretvaraju se u /docs/ linkove + +## Fajlovi koji se prikazuju + +Svi .md fajlovi u projektu: +- CLAUDE.md (glavni) +- agents/*/CLAUDE.md +- TASKS/*.md +- TASKS/reports/*.md +- README.md + +## Bezbednost + +- Samo .md fajlovi (ne dozvoli čitanje .go, .env itd.) +- Samo unutar project root-a (ne dozvoli ../../../etc/passwd) +- Path traversal zaštita + +## Testovi + +- GET /docs → lista fajlova +- GET /docs/CLAUDE.md → renderovan HTML +- GET /docs/agents/coder/CLAUDE.md → renderovan HTML +- GET /docs/../../etc/passwd → 403 +- GET /docs/main.go → 403 (nije .md) +- Klikabilni linkovi u renderovanom HTML-u + +--- + +## Pitanja + +--- + +## Odgovori diff --git a/code/go.mod b/code/go.mod index c3d066e..ae7dbf2 100644 --- a/code/go.mod +++ b/code/go.mod @@ -26,6 +26,7 @@ require ( github.com/quic-go/quic-go v0.54.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect + github.com/yuin/goldmark v1.7.16 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/arch v0.20.0 // indirect golang.org/x/crypto v0.40.0 // indirect diff --git a/code/go.sum b/code/go.sum index a25fff5..06d0305 100644 --- a/code/go.sum +++ b/code/go.sum @@ -61,6 +61,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= +github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= diff --git a/code/internal/server/docs.go b/code/internal/server/docs.go new file mode 100644 index 0000000..bedbd4d --- /dev/null +++ b/code/internal/server/docs.go @@ -0,0 +1,208 @@ +package server + +import ( + "bytes" + htmltpl "html/template" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/gin-gonic/gin" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/renderer/html" +) + +// docFile represents a markdown file in the file listing. +type docFile struct { + Path string // relative path from project root + Name string // display name +} + +// docsListData holds data for the docs listing page. +type docsListData struct { + Files []docFile +} + +// docsViewData holds data for viewing a single doc. +type docsViewData struct { + Path string + Breadcrumbs []breadcrumb + HTML htmltpl.HTML +} + +// breadcrumb represents one segment of the navigation path. +type breadcrumb struct { + Name string + Path string +} + +// md is the goldmark markdown renderer. +var md = goldmark.New( + goldmark.WithExtensions(extension.Table, extension.Strikethrough), + goldmark.WithRendererOptions(html.WithUnsafe()), +) + +// projectRoot returns the KAOS project root derived from TasksDir. +func (s *Server) projectRoot() string { + return filepath.Dir(s.Config.TasksDir) +} + +// handleDocsList serves the docs listing page. +func (s *Server) handleDocsList(c *gin.Context) { + root := s.projectRoot() + files := scanMarkdownFiles(root) + + c.Header("Content-Type", "text/html; charset=utf-8") + c.String(http.StatusOK, renderDocsList(docsListData{Files: files})) +} + +// handleDocsView serves a rendered markdown file. +func (s *Server) handleDocsView(c *gin.Context) { + relPath := c.Param("path") + // Strip leading slash from wildcard param + relPath = strings.TrimPrefix(relPath, "/") + + root := s.projectRoot() + + // Security: only .md files + if !strings.HasSuffix(relPath, ".md") { + c.String(http.StatusForbidden, "Samo .md fajlovi su dozvoljeni") + return + } + + // Security: resolve and check path traversal + absPath := filepath.Join(root, relPath) + absPath = filepath.Clean(absPath) + if !strings.HasPrefix(absPath, filepath.Clean(root)+string(filepath.Separator)) { + c.String(http.StatusForbidden, "Pristup odbijen") + return + } + + content, err := os.ReadFile(absPath) + if err != nil { + c.String(http.StatusNotFound, "Fajl nije pronađen: %s", relPath) + return + } + + // Render markdown to HTML + rendered := renderMarkdown(content, relPath) + + data := docsViewData{ + Path: relPath, + Breadcrumbs: buildBreadcrumbs(relPath), + HTML: htmltpl.HTML(rendered), + } + + c.Header("Content-Type", "text/html; charset=utf-8") + c.String(http.StatusOK, renderDocsView(data)) +} + +// scanMarkdownFiles walks the project root and returns all .md files. +func scanMarkdownFiles(root string) []docFile { + var files []docFile + + filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + + // Skip hidden dirs, code/vendor, .git + name := info.Name() + if info.IsDir() { + if strings.HasPrefix(name, ".") || name == "vendor" || name == "node_modules" { + return filepath.SkipDir + } + return nil + } + + if !strings.HasSuffix(name, ".md") { + return nil + } + + rel, _ := filepath.Rel(root, path) + files = append(files, docFile{ + Path: rel, + Name: rel, + }) + return nil + }) + + return files +} + +// renderMarkdown converts markdown bytes to HTML, rewriting relative .md links to /docs/ links. +func renderMarkdown(source []byte, docPath string) string { + var buf bytes.Buffer + if err := md.Convert(source, &buf); err != nil { + return "
Greška pri renderovanju: " + err.Error() + "
" + } + + html := buf.String() + + // Rewrite relative .md links to /docs/ links + // Find href="something.md" or href="path/to/file.md" patterns + html = rewriteMarkdownLinks(html, docPath) + + return html +} + +// rewriteMarkdownLinks converts relative .md hrefs to /docs/ prefixed paths. +func rewriteMarkdownLinks(htmlContent, currentPath string) string { + dir := filepath.Dir(currentPath) + return rewriteLinksSimple(htmlContent, dir) +} + +// rewriteLinksSimple does a single pass to rewrite .md links. +func rewriteLinksSimple(htmlContent, baseDir string) string { + var result strings.Builder + remaining := htmlContent + + for { + idx := strings.Index(remaining, `href="`) + if idx == -1 { + result.WriteString(remaining) + break + } + + result.WriteString(remaining[:idx+6]) + remaining = remaining[idx+6:] + + endQuote := strings.Index(remaining, `"`) + if endQuote == -1 { + result.WriteString(remaining) + break + } + + href := remaining[:endQuote] + remaining = remaining[endQuote:] + + if strings.HasSuffix(href, ".md") && !strings.HasPrefix(href, "http") && !strings.HasPrefix(href, "/") { + if baseDir == "." || baseDir == "" { + result.WriteString("/docs/" + href) + } else { + result.WriteString("/docs/" + filepath.Join(baseDir, href)) + } + } else { + result.WriteString(href) + } + } + + return result.String() +} + +// buildBreadcrumbs creates navigation breadcrumbs from a file path. +func buildBreadcrumbs(path string) []breadcrumb { + parts := strings.Split(path, "/") + crumbs := make([]breadcrumb, len(parts)) + + for i, part := range parts { + crumbs[i] = breadcrumb{ + Name: part, + Path: strings.Join(parts[:i+1], "/"), + } + } + + return crumbs +} diff --git a/code/internal/server/render.go b/code/internal/server/render.go index 95ff9d5..1c02f10 100644 --- a/code/internal/server/render.go +++ b/code/internal/server/render.go @@ -58,6 +58,8 @@ func init() { web.TemplatesFS, "templates/layout.html", "templates/dashboard.html", + "templates/docs-list.html", + "templates/docs-view.html", "templates/partials/column.html", "templates/partials/task-card.html", "templates/partials/task-detail.html", @@ -86,6 +88,24 @@ func renderDashboard(columns map[string][]supervisor.Task) string { return buf.String() } +// renderDocsList generates the docs listing HTML page. +func renderDocsList(data docsListData) string { + var buf bytes.Buffer + if err := templates.ExecuteTemplate(&buf, "docs-list", data); err != nil { + return "Greška pri renderovanju: " + err.Error() + } + return buf.String() +} + +// renderDocsView generates the docs view HTML page. +func renderDocsView(data docsViewData) string { + var buf bytes.Buffer + if err := templates.ExecuteTemplate(&buf, "docs-view", data); err != nil { + return "Greška pri renderovanju: " + err.Error() + } + return buf.String() +} + // renderTaskDetail generates HTML fragment for task detail panel. func renderTaskDetail(t supervisor.Task, content string, hasReport bool) string { data := taskDetailData{ diff --git a/code/internal/server/server.go b/code/internal/server/server.go index f3f36ce..9c15d73 100644 --- a/code/internal/server/server.go +++ b/code/internal/server/server.go @@ -96,6 +96,10 @@ func (s *Server) setupRoutes() { s.Router.GET("/task/:id", s.handleTaskDetail) s.Router.POST("/task/:id/move", s.handleMoveTask) s.Router.GET("/report/:id", s.handleReport) + + // Docs routes + s.Router.GET("/docs", s.handleDocsList) + s.Router.GET("/docs/*path", s.handleDocsView) } // apiGetTasks returns all tasks as JSON. diff --git a/code/internal/server/server_test.go b/code/internal/server/server_test.go index 4d2b94f..84c04b1 100644 --- a/code/internal/server/server_test.go +++ b/code/internal/server/server_test.go @@ -53,6 +53,12 @@ func setupTestServer(t *testing.T) *Server { os.WriteFile(filepath.Join(tasksDir, "done", "T01.md"), []byte(testTask1), 0644) os.WriteFile(filepath.Join(tasksDir, "backlog", "T08.md"), []byte(testTask2), 0644) + // Docs: create markdown files in project root + os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte("# CLAUDE.md\n\nGlavni fajl.\n\n| Kolona | Opis |\n|--------|------|\n| A | B |\n"), 0644) + os.WriteFile(filepath.Join(dir, "README.md"), []byte("# README\n\nOpis projekta.\n"), 0644) + os.MkdirAll(filepath.Join(dir, "agents", "coder"), 0755) + os.WriteFile(filepath.Join(dir, "agents", "coder", "CLAUDE.md"), []byte("# Coder Agent\n\nPravila kodiranja.\n"), 0644) + cfg := &config.Config{ TasksDir: tasksDir, ProjectPath: dir, @@ -548,6 +554,142 @@ func TestDashboardReflectsDiskChanges(t *testing.T) { } } +func TestDocsList(t *testing.T) { + srv := setupTestServer(t) + + req := httptest.NewRequest(http.MethodGet, "/docs", nil) + w := httptest.NewRecorder() + srv.Router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + body := w.Body.String() + if !containsStr(body, "CLAUDE.md") { + t.Error("expected CLAUDE.md in docs list") + } + if !containsStr(body, "README.md") { + t.Error("expected README.md in docs list") + } + if !containsStr(body, "agents/coder/CLAUDE.md") { + t.Error("expected agents/coder/CLAUDE.md in docs list") + } +} + +func TestDocsView_CLAUDE(t *testing.T) { + srv := setupTestServer(t) + + req := httptest.NewRequest(http.MethodGet, "/docs/CLAUDE.md", nil) + w := httptest.NewRecorder() + srv.Router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + body := w.Body.String() + if !containsStr(body, "Glavni fajl") { + t.Error("expected rendered markdown content") + } + // Should have table rendered as HTML + if !containsStr(body, "| ") {
+ t.Error("expected HTML table from markdown")
+ }
+}
+
+func TestDocsView_NestedFile(t *testing.T) {
+ srv := setupTestServer(t)
+
+ req := httptest.NewRequest(http.MethodGet, "/docs/agents/coder/CLAUDE.md", nil)
+ w := httptest.NewRecorder()
+ srv.Router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+
+ body := w.Body.String()
+ if !containsStr(body, "Coder Agent") {
+ t.Error("expected nested file content")
+ }
+ // Breadcrumbs
+ if !containsStr(body, "agents") {
+ t.Error("expected breadcrumb for agents")
+ }
+}
+
+func TestDocsView_PathTraversal(t *testing.T) {
+ srv := setupTestServer(t)
+
+ req := httptest.NewRequest(http.MethodGet, "/docs/../../etc/passwd", nil)
+ w := httptest.NewRecorder()
+ srv.Router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusForbidden {
+ t.Fatalf("expected 403 for path traversal, got %d", w.Code)
+ }
+}
+
+func TestDocsView_NonMarkdown(t *testing.T) {
+ srv := setupTestServer(t)
+
+ req := httptest.NewRequest(http.MethodGet, "/docs/main.go", nil)
+ w := httptest.NewRecorder()
+ srv.Router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusForbidden {
+ t.Fatalf("expected 403 for non-.md file, got %d", w.Code)
+ }
+}
+
+func TestDocsView_NotFound(t *testing.T) {
+ srv := setupTestServer(t)
+
+ req := httptest.NewRequest(http.MethodGet, "/docs/nonexistent.md", nil)
+ w := httptest.NewRecorder()
+ srv.Router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusNotFound {
+ t.Fatalf("expected 404, got %d", w.Code)
+ }
+}
+
+func TestDocsView_HasBreadcrumbs(t *testing.T) {
+ srv := setupTestServer(t)
+
+ req := httptest.NewRequest(http.MethodGet, "/docs/agents/coder/CLAUDE.md", nil)
+ w := httptest.NewRecorder()
+ srv.Router.ServeHTTP(w, req)
+
+ body := w.Body.String()
+ if !containsStr(body, "Dokumenti") {
+ t.Error("expected 'Dokumenti' in breadcrumbs")
+ }
+ if !containsStr(body, "coder") {
+ t.Error("expected 'coder' in breadcrumbs")
+ }
+}
+
+func TestRewriteLinksSimple(t *testing.T) {
+ input := `link and ext`
+ result := rewriteLinksSimple(input, ".")
+ if !containsStr(result, `/docs/README.md`) {
+ t.Errorf("expected rewritten link, got: %s", result)
+ }
+ if !containsStr(result, `https://example.com`) {
+ t.Error("external link should not be rewritten")
+ }
+}
+
+func TestRewriteLinksSimple_NestedDir(t *testing.T) {
+ input := `link`
+ result := rewriteLinksSimple(input, "agents/coder")
+ if !containsStr(result, `/docs/agents/coder/CLAUDE.md`) {
+ t.Errorf("expected nested rewritten link, got: %s", result)
+ }
+}
+
func containsStr(s, substr string) bool {
return len(s) >= len(substr) && findStr(s, substr)
}
diff --git a/code/web/static/style.css b/code/web/static/style.css
index 73b9d98..f5bffe3 100644
--- a/code/web/static/style.css
+++ b/code/web/static/style.css
@@ -226,6 +226,157 @@ body {
color: #fff;
}
+/* Navigation */
+.nav {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.btn-active {
+ background: #0f3460;
+ border-color: #e94560;
+}
+
+/* Docs */
+.docs-container {
+ padding: 16px 24px;
+ max-width: 960px;
+ margin: 0 auto;
+}
+
+.docs-container h2 {
+ margin-bottom: 16px;
+ color: #e94560;
+}
+
+.docs-list {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.doc-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ background: #16213e;
+ border-radius: 6px;
+ color: #eee;
+ text-decoration: none;
+ font-size: 0.9em;
+ font-family: "JetBrains Mono", "Fira Code", monospace;
+ transition: background 0.2s;
+}
+
+.doc-item:hover {
+ background: #0f3460;
+}
+
+.doc-icon { font-size: 1em; }
+
+.docs-breadcrumbs {
+ margin-bottom: 16px;
+ font-size: 0.85em;
+}
+
+.docs-breadcrumbs a {
+ color: #e94560;
+ text-decoration: none;
+}
+
+.docs-breadcrumbs a:hover {
+ text-decoration: underline;
+}
+
+.breadcrumb-sep {
+ color: #555;
+ margin: 0 4px;
+}
+
+.docs-content {
+ background: #16213e;
+ border-radius: 8px;
+ padding: 24px;
+ line-height: 1.7;
+ font-size: 0.95em;
+}
+
+.docs-content h1, .docs-content h2, .docs-content h3 {
+ color: #e94560;
+ margin-top: 1.2em;
+ margin-bottom: 0.5em;
+}
+
+.docs-content h1 { font-size: 1.5em; border-bottom: 1px solid #333; padding-bottom: 8px; }
+.docs-content h2 { font-size: 1.2em; }
+.docs-content h3 { font-size: 1.05em; }
+
+.docs-content a {
+ color: #4ecca3;
+ text-decoration: none;
+}
+
+.docs-content a:hover { text-decoration: underline; }
+
+.docs-content code {
+ background: #1a1a2e;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-family: "JetBrains Mono", "Fira Code", monospace;
+ font-size: 0.9em;
+}
+
+.docs-content pre {
+ background: #1a1a2e;
+ padding: 12px;
+ border-radius: 6px;
+ overflow-x: auto;
+ margin: 12px 0;
+}
+
+.docs-content pre code {
+ background: none;
+ padding: 0;
+}
+
+.docs-content table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 12px 0;
+}
+
+.docs-content th, .docs-content td {
+ border: 1px solid #333;
+ padding: 8px 12px;
+ text-align: left;
+}
+
+.docs-content th {
+ background: #0f3460;
+}
+
+.docs-content ul, .docs-content ol {
+ padding-left: 24px;
+ margin: 8px 0;
+}
+
+.docs-content li { margin: 4px 0; }
+
+.docs-content blockquote {
+ border-left: 3px solid #e94560;
+ padding-left: 12px;
+ color: #aaa;
+ margin: 12px 0;
+}
+
+.docs-content hr {
+ border: none;
+ border-top: 1px solid #333;
+ margin: 16px 0;
+}
+
/* Responsive */
@media (max-width: 1100px) {
.board { grid-template-columns: repeat(3, 1fr); }
diff --git a/code/web/templates/docs-list.html b/code/web/templates/docs-list.html
new file mode 100644
index 0000000..27284b3
--- /dev/null
+++ b/code/web/templates/docs-list.html
@@ -0,0 +1,33 @@
+{{define "docs-list"}}
+
+
+
+
+
+
+
+🔧 KAOS Dashboard+ +
+
+
+
+{{end}}
diff --git a/code/web/templates/docs-view.html b/code/web/templates/docs-view.html
new file mode 100644
index 0000000..ab694ff
--- /dev/null
+++ b/code/web/templates/docs-view.html
@@ -0,0 +1,33 @@
+{{define "docs-view"}}
+
+
+
+
+
+Dokumentacija+
+ {{range .Files}}
+
+
+ {{.Name}}
+
+ {{end}}
+
+
+
+
+🔧 KAOS Dashboard+ +
+
+
+
+
+{{end}}
diff --git a/code/web/templates/layout.html b/code/web/templates/layout.html
index 7d9dfd1..265a558 100644
--- a/code/web/templates/layout.html
+++ b/code/web/templates/layout.html
@@ -11,7 +11,10 @@
+ {{.HTML}}
+
+🔧 KAOS Dashboard- v0.2 + |
|---|