T11: Dodat Cache-Control no-store za svež prikaz sa diska
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
aabdfa9e50
commit
633de945e4
39
TASKS/reports/T11-report.md
Normal file
39
TASKS/reports/T11-report.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# T11 Izveštaj: Fix — server uvek čita svež stanje sa diska
|
||||||
|
|
||||||
|
**Agent:** coder
|
||||||
|
**Model:** Opus
|
||||||
|
**Datum:** 2026-02-20
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Šta je urađeno
|
||||||
|
|
||||||
|
### Analiza
|
||||||
|
|
||||||
|
Server kod (`ScanTasks()`) već čita sa diska na svaki request — nema internog keša.
|
||||||
|
Problem je bio u **browser keširanju**: nije bio postavljen `Cache-Control` header,
|
||||||
|
pa browser može servirati stale HTML/JSON iz svog keša.
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
Dodat middleware u `server.go` koji postavlja `Cache-Control: no-store, no-cache, must-revalidate`
|
||||||
|
na sve dinamičke rute (osim `/static/*` koji su immutable).
|
||||||
|
|
||||||
|
### Izmenjeni fajlovi
|
||||||
|
|
||||||
|
| Fajl | Izmena |
|
||||||
|
|------|--------|
|
||||||
|
| `internal/server/server.go` | Cache-Control middleware za dinamičke rute |
|
||||||
|
| `internal/server/server_test.go` | 2 nova testa |
|
||||||
|
|
||||||
|
### Novi testovi
|
||||||
|
|
||||||
|
```
|
||||||
|
TestNoCacheHeaders PASS — proverava Cache-Control na / i /api/tasks
|
||||||
|
TestDashboardReflectsDiskChanges PASS — premesti fajl na disku, sledeći request vidi promenu
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ukupno projekat: 92 testa, svi prolaze
|
||||||
|
|
||||||
|
- `go vet ./...` — čist
|
||||||
|
- `go build ./...` — prolazi
|
||||||
39
TASKS/review/T11.md
Normal file
39
TASKS/review/T11.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# T11: Fix — server uvek čita svež stanje sa diska
|
||||||
|
|
||||||
|
**Kreirao:** planer
|
||||||
|
**Datum:** 2026-02-20
|
||||||
|
**Agent:** coder
|
||||||
|
**Model:** Sonnet
|
||||||
|
**Zavisi od:** T10 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Opis
|
||||||
|
|
||||||
|
BUG: Dashboard prikazuje stare podatke jer server kešira taskove.
|
||||||
|
Server MORA da čita fajlove sa diska na SVAKI request.
|
||||||
|
ScanTasks() se poziva svaki put kad neko otvori dashboard ili API.
|
||||||
|
|
||||||
|
## Pravilo
|
||||||
|
|
||||||
|
Nema keša za taskove. Disk je izvor istine.
|
||||||
|
Svaki GET / i GET /api/tasks poziva ScanTasks() iznova.
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
1. Pokreni server
|
||||||
|
2. Ručno premesti fajl iz backlog/ u ready/ (terminal)
|
||||||
|
3. Osveži dashboard (F5) — task mora biti u Ready koloni
|
||||||
|
4. Bez restart-a servera
|
||||||
|
|
||||||
|
## Očekivani izlaz
|
||||||
|
|
||||||
|
Dashboard uvek prikazuje tačno stanje sa diska.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pitanja
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Odgovori
|
||||||
@ -68,6 +68,14 @@ func New(cfg *config.Config) *Server {
|
|||||||
Router: router,
|
Router: router,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No caching for dynamic routes — disk is the source of truth.
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
if !strings.HasPrefix(c.Request.URL.Path, "/static") {
|
||||||
|
c.Header("Cache-Control", "no-store, no-cache, must-revalidate")
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
s.setupRoutes()
|
s.setupRoutes()
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|||||||
@ -478,6 +478,76 @@ func TestIsMoveAllowed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNoCacheHeaders(t *testing.T) {
|
||||||
|
srv := setupTestServer(t)
|
||||||
|
|
||||||
|
// Dynamic route should have no-cache headers
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
srv.Router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
cc := w.Header().Get("Cache-Control")
|
||||||
|
if !containsStr(cc, "no-store") {
|
||||||
|
t.Errorf("expected Cache-Control no-store on dashboard, got %q", cc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// API route should also have no-cache headers
|
||||||
|
req2 := httptest.NewRequest(http.MethodGet, "/api/tasks", nil)
|
||||||
|
w2 := httptest.NewRecorder()
|
||||||
|
srv.Router.ServeHTTP(w2, req2)
|
||||||
|
|
||||||
|
cc2 := w2.Header().Get("Cache-Control")
|
||||||
|
if !containsStr(cc2, "no-store") {
|
||||||
|
t.Errorf("expected Cache-Control no-store on API, got %q", cc2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDashboardReflectsDiskChanges(t *testing.T) {
|
||||||
|
srv := setupTestServer(t)
|
||||||
|
|
||||||
|
// Initial state: T08 in backlog
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/tasks", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
srv.Router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
var tasks1 []taskResponse
|
||||||
|
json.Unmarshal(w.Body.Bytes(), &tasks1)
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, task := range tasks1 {
|
||||||
|
if task.ID == "T08" && task.Status == "backlog" {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatal("expected T08 in backlog initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move file on disk (simulating external change)
|
||||||
|
os.Rename(
|
||||||
|
filepath.Join(srv.Config.TasksDir, "backlog", "T08.md"),
|
||||||
|
filepath.Join(srv.Config.TasksDir, "ready", "T08.md"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Second request should reflect the change without server restart
|
||||||
|
req2 := httptest.NewRequest(http.MethodGet, "/api/tasks", nil)
|
||||||
|
w2 := httptest.NewRecorder()
|
||||||
|
srv.Router.ServeHTTP(w2, req2)
|
||||||
|
|
||||||
|
var tasks2 []taskResponse
|
||||||
|
json.Unmarshal(w2.Body.Bytes(), &tasks2)
|
||||||
|
|
||||||
|
found = false
|
||||||
|
for _, task := range tasks2 {
|
||||||
|
if task.ID == "T08" && task.Status == "ready" {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatal("expected T08 in ready after disk move — server did not read fresh state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func containsStr(s, substr string) bool {
|
func containsStr(s, substr string) bool {
|
||||||
return len(s) >= len(substr) && findStr(s, substr)
|
return len(s) >= len(substr) && findStr(s, substr)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user