diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e9b7104 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,257 @@ +# DP Jobs Kanban Board + +A Kanban Board web application running on IIS (Windows). Uses the **RouteKit** framework (front-controller pattern) with Keycloak OpenID Connect for authentication. Database is Microsoft Access (`.accdb`). No Node, npm, or build step — everything is server-side VBScript served by IIS. + +## Project layout + +``` +public/ IIS site root — point IIS here + Default.asp Front controller and route table + web.config App settings, URL rewrite rules + +core/ Framework internals — do not modify + autoload_core.asp + mvc.asp + router.wsc + lib.*.asp Core libraries + helpers.asp Global utility functions (always available) + +app/ + controllers/ Controller classes (one per feature area) + views/ View partials (folders match controller name) + shared/ Header, footer, layout partials + models/ POBOs (plain-old business objects) + repositories/ Data access classes + +db/ + migrations/ Sequential migration scripts + webdata.accdb Access database + +tests/ Dev-only aspunit test harness (separate IIS app) +scripts/ VBScript code generators +``` + +## Core libraries (core/lib.*.asp) + +| File | Purpose | +|---|---| +| `lib.Keycloak.asp` | OpenID Connect / Keycloak auth helper | +| `lib.Routes.asp` | URL generation helpers (`Routes()` singleton) | +| `lib.ControllerRegistry.asp` | Controller whitelist (security) | +| `lib.DAL.asp` | Database access layer (`DAL` singleton) | +| `lib.Collections.asp` | `LinkedList_Class` and other collection types | +| `lib.Enumerable.asp` | Collection iteration helpers | +| `lib.Validations.asp` | Input validation | +| `lib.Flash.asp` | Flash message helpers | +| `lib.FormCache.asp` | Form value re-population | +| `lib.HTML.asp` | HTML rendering helpers | +| `lib.HTML.Security.asp` | XSS escaping — `H()` function | +| `lib.Strings.asp` | String utilities | +| `lib.Automapper.asp` | `Automapper.AutoMap(rs, "POBO_TableName")` | +| `lib.json.asp` | JSON parsing/serialization | + +## Global helper functions (core/helpers.asp) + +Always available — never re-implement them. + +| Function | Signature | Purpose | +|---|---|---| +| `H` | `H(s)` | XSS-safe HTML encode — use on all user data rendered to HTML | +| `GenerateSlug` | `GenerateSlug(title)` | Converts title to safe URL slug | +| `GetRawJsonFromRequest` | `GetRawJsonFromRequest()` | Reads raw JSON body from AJAX POST | +| `GetAppSetting` | `GetAppSetting(key)` | Reads value from `web.config` appSettings | +| `IIf` | `IIf(condition, trueVal, falseVal)` | Inline conditional | +| `Destroy` | `Destroy(obj)` | Safely closes and sets object to Nothing | +| `FormatDateForSql` | `FormatDateForSql(date)` | Formats VBScript date as SQL datetime string | +| `Active` | `Active(controllerName)` | Returns `"active"` if current request maps to that controller | + +## Router — URL parameters + +Routes registered in [public/Default.asp](public/Default.asp) with `:param` segments. + +```vbscript +router.AddRoute "GET", "/boards", "BoardsController", "Index" +router.AddRoute "POST", "/boards", "BoardsController", "Store" +router.AddRoute "GET", "/board/:slug", "BoardsController", "Show" +router.AddRoute "POST", "/cards/:id/move", "CardsController", "Move" +``` + +Controller action receives URL params as arguments in order: + +```vbscript +Public Sub Show(slug) : End Sub +Public Sub Move(id) : End Sub +``` + +URL generation: + +```vbscript +Routes.UrlTo "Boards", "Index", Empty +Routes.UrlToWithParams "Boards", "Show", Array("my-board"), Empty +Routes.UrlTo "Boards", "Index", Array("page", 2) +``` + +## Wiring up a new controller — checklist + +1. Generate: `cscript //nologo scripts\generateController.vbs MyController "Index;Show(slug);Store"` +2. Move file to `app/controllers/` +3. Register in [core/lib.ControllerRegistry.asp](core/lib.ControllerRegistry.asp): `RegisterController "mycontroller"` +4. Include in [app/controllers/autoload_controllers.asp](app/controllers/autoload_controllers.asp): `` +5. Add routes in [public/Default.asp](public/Default.asp) +6. Create views in `app/views/MyController/` + +## POBO pattern (app/models/) + +```vbscript +Class POBO_boards + Public Properties ' array of all column names — required by Automapper + Private p_id, p_name, p_slug, p_created_at, p_created_by, p_updated_at, p_updated_by + + Private Sub Class_Initialize() + Properties = Array("id","name","slug","created_at","created_by","updated_at","updated_by") + End Sub + + Public Property Get PrimaryKey() : PrimaryKey = "id" : End Property + Public Property Get TableName() : TableName = "boards" : End Property + + Public Property Get id() : id = p_id : End Property + Public Property Let id(v) : p_id = CDbl(v) : End Property + ' ... remaining Get/Let pairs follow same pattern +End Class +``` + +Private backing fields use `p_` prefix. `Properties`, `PrimaryKey`, and `TableName` are required. + +## Repository pattern (app/repositories/) + +Uses `DAL` singleton and `Automapper`. Key methods: `FindByID`, `GetAll`, `Find`, `AddNew`, `Update`, `Delete`. After insert, read identity with `SELECT @@IDENTITY AS NewID`. Expose as a singleton function (e.g. `boards_Repository()`). + +## Controller pattern (app/controllers/) + +```vbscript +Class BoardsController_Class + Private m_useLayout, m_title + Private Sub Class_Initialize() : m_useLayout = True : m_title = "Boards" : End Sub + + Public Sub Index() + If Not KeycloakRequireLogin("") Then Exit Sub + End Sub + + Public Sub Move(id) ' JSON/AJAX action + m_useLayout = False + Response.ContentType = "application/json" + If Not KeycloakIsLoggedIn() Then + Response.Write "{""ok"":false,""error"":""Unauthorized""}" : Exit Sub + End If + End Sub +End Class +``` + +JSON/AJAX actions must set `m_useLayout = False` and `Response.ContentType = "application/json"`. + +## Audit columns + +Every table must include `created_at`, `created_by`, `updated_at`, `updated_by`. + +```vbscript +' Insert +Dim currentUser : Set currentUser = KeycloakCurrentUser() +Dim currentUsername : currentUsername = "" +If Not currentUser Is Nothing Then currentUsername = currentUser.Item("preferred_username") +model.created_at = Now() : model.created_by = currentUsername +model.updated_at = Now() : model.updated_by = currentUsername +repo.AddNew model + +' Update — never touch created_at / created_by +model.updated_at = Now() : model.updated_by = currentUsername +repo.Update model +``` + +## Authentication (Keycloak) + +```vbscript +KeycloakRequireLogin("") ' Gate full-page actions +KeycloakIsLoggedIn() ' Use for JSON/AJAX actions +KeycloakCurrentUser() ' Returns userinfo Dictionary (preferred_username, email, name, sub) +KeycloakHasRealmRole("admin") ' Role check +KeycloakLogout("") ' Clear session and redirect +``` + +## LinkedList_Class — correct traversal + +Never use `.First`, `.Last`, `.m_first`, `.m_next`, or `.m_value` — they are private internals. + +```vbscript +Dim iter : Set iter = myList.Iterator() +Do While iter.HasNext() + Set item = iter.GetNext() +Loop +``` + +Or convert: `Dim arr : arr = myList.TO_Array()` + +## AJAX form data — always URLSearchParams, never FormData + +Classic ASP `Request.Form` only parses `application/x-www-form-urlencoded`. `new FormData()` sends `multipart/form-data` which Classic ASP silently ignores. + +```javascript +var params = new URLSearchParams(); +Object.keys(data).forEach(function(k) { params.append(k, data[k]); }); +fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString() }) +``` + +`Request.BinaryRead` / `GetRawJsonFromRequest()` and `Request.Form` are mutually exclusive in the same action — never mix them. + +## View files — Classic ASP scoping rule + +View files are SSI-included inside the controller action's Sub scope. Never define `Function` or `Sub` inside a view file — VBScript forbids nested procedure definitions (`Syntax error 800a03ea`). Keep views to pure HTML rendering: `<%= %>`, `H()`, simple `If`/`For`/`Do` blocks. + +## Database DDL — Access/Jet reserved words + +Always bracket every identifier. Use `migration.ExecuteSQL` for all DDL. Never use `migration.CreateTable` or `migration.CreateIndex`. + +```vbscript +migration.ExecuteSQL "CREATE TABLE [board_columns] ([id] AUTOINCREMENT PRIMARY KEY, [name] VARCHAR(255), [position] INTEGER)" +``` + +Common reserved words: `COLUMNS`, `NAME`, `POSITION`, `VALUE`, `DATE`, `KEY`, `LEVEL`, `BY`. + +## Running migrations (32-bit only) + +``` +C:\Windows\SysWOW64\cscript.exe //nologo scripts\runMigrations.vbs up +``` + +IIS app pool must have `enable32BitAppOnWin64="true"`. Use 32-bit IIS Express: `%ProgramFiles(x86)%\IIS Express\iisexpress.exe`. + +If `migration.DB.Execute/Query` fails in standalone runner, provide a `migration.Connection` + `ADODB.Command` fallback. + +## Flash messages + +```vbscript +Flash().AddError "Something went wrong" +flash.Success = "Saved successfully" +``` + +Never use `Flash().SetError` or `Flash().SetSuccess` — those methods do not exist (Error 438). + +## Things to avoid + +- Do not modify files under `core/` — framework internals. +- Do not add controllers without registering in `ControllerRegistry`. +- Do not commit real `KeycloakClientSecret` values. +- Do not add test routes/pages under `public/`. +- Always use `H()` when rendering user-supplied data (XSS prevention). +- Never write your own slug generator — use `GenerateSlug()`. +- Never use `Private Const` or `Public Const` inside a VBScript class — use a `Private Function` returning the value instead. +- Never run migrations with 64-bit cscript on this machine. +- Never mix `GetRawJsonFromRequest()` with `Request.Form` in the same action. +- Never call `.First`/`.Last`/`.m_first`/`.m_next`/`.m_value` on a `LinkedList_Class`. +- Always set all four audit columns on every insert and update. + +## Requirements + +- Windows Server / Windows with IIS, Classic ASP enabled +- IIS URL Rewrite module +- Microsoft Access Database Engine (ACE OLEDB 12.0) — 32-bit +- Keycloak server (for auth flows) diff --git a/agents.md b/agents.md index 1826291..be7b37b 100644 --- a/agents.md +++ b/agents.md @@ -1,8 +1,8 @@ -# Agents Guide — RouteKit Classic ASP / Keycloak Test +# Agents Guide — DP Jobs Kanban Board ## What this project is -A Classic ASP MVC web application running on IIS (Windows). It uses the **RouteKit** framework (front-controller pattern) with Keycloak OpenID Connect for authentication. The database is Microsoft Access (`.accdb`). There is no Node, npm, or build step — everything is server-side VBScript served by IIS. +A Kanban Board web application running on IIS (Windows). It uses the **RouteKit** framework (front-controller pattern) with Keycloak OpenID Connect for authentication. The database is Microsoft Access (`.accdb`). There is no Node, npm, or build step — everything is server-side VBScript served by IIS. --- diff --git a/app/controllers/HomeController.asp b/app/controllers/HomeController.asp index ddee9c4..ab08dd5 100644 --- a/app/controllers/HomeController.asp +++ b/app/controllers/HomeController.asp @@ -25,9 +25,14 @@ Class HomeController_Class End Property Public Sub index() + If Not KeycloakRequireLogin("") Then Exit Sub + Dim totalBoards : totalBoards = boards_Repository().Count() + Dim totalCards : totalCards = cards_Repository().Count() + Dim boardSummaries : Set boardSummaries = boards_Repository().GetBoardSummaries() %> <% + Set boardSummaries = Nothing End Sub End Class diff --git a/app/repositories/boards_Repository.asp b/app/repositories/boards_Repository.asp index 57c229e..3465774 100644 --- a/app/repositories/boards_Repository.asp +++ b/app/repositories/boards_Repository.asp @@ -78,6 +78,32 @@ Class boards_Repository_Class DAL.Execute "DELETE FROM [boards] WHERE [id]=?", Array(id) End Sub + Public Function Count() + Dim rs : Set rs = DAL.Query("SELECT COUNT(*) FROM [boards]", Empty) + Count = CLng(rs(0)) + Destroy rs + End Function + + Public Function GetBoardSummaries() + Dim sql : sql = "SELECT b.[name], b.[slug], COUNT(c.[id]) AS [card_count] " & _ + "FROM [boards] b " & _ + "LEFT JOIN [cards] c ON c.[board_id] = b.[id] " & _ + "GROUP BY b.[name], b.[slug] " & _ + "ORDER BY b.[name]" + Dim rs : Set rs = DAL.Query(sql, Empty) + Dim list : Set list = New LinkedList_Class + Do Until rs.EOF + Dim d : Set d = CreateObject("Scripting.Dictionary") + d.Add "name", CStr(rs("name")) + d.Add "slug", CStr(rs("slug")) + d.Add "card_count", CLng(rs("card_count")) + list.Push d + rs.MoveNext + Loop + Set GetBoardSummaries = list + Destroy rs + End Function + End Class Dim boards_Repository__Singleton diff --git a/app/repositories/cards_Repository.asp b/app/repositories/cards_Repository.asp index 56e3f0c..05026f2 100644 --- a/app/repositories/cards_Repository.asp +++ b/app/repositories/cards_Repository.asp @@ -92,6 +92,12 @@ Class cards_Repository_Class DAL.Execute "DELETE FROM [cards] WHERE [swim_lane_id]=?", Array(swimLaneId) End Sub + Public Function Count() + Dim rs : Set rs = DAL.Query("SELECT COUNT(*) FROM [cards]", Empty) + Count = CLng(rs(0)) + Destroy rs + End Function + End Class Dim cards_Repository__Singleton diff --git a/app/views/Boards/Show.asp b/app/views/Boards/Show.asp index d7f8e01..dfab647 100644 --- a/app/views/Boards/Show.asp +++ b/app/views/Boards/Show.asp @@ -10,8 +10,8 @@ Response.CodePage = 65001 - - + + diff --git a/app/views/Home/index.asp b/app/views/Home/index.asp index 2874882..2235262 100644 --- a/app/views/Home/index.asp +++ b/app/views/Home/index.asp @@ -1,84 +1,106 @@ +
+
+

Dashboard

+
+
+ + New Board + +
+
-
-
-
-
-

Welcome to RouteKit Classic ASP

-

- Your lightweight, opinionated MVC-style framework for Classic ASP. -

-

- This Home.Index view is using the shared - Header.asp and Footer.asp layout files. -

-

- Start by wiring up your controllers, repositories, and views — this page is just a - friendly placeholder so you know everything is hooked up correctly. -

+ +
+
+
+
+
<%= totalBoards %>
+
+ Boards +
- -
-
-
-

Quick info

-
    -
  • - View: - app/Views/Home.Index.asp -
  • -
  • - Layout: - Shared/Header.asp & Shared/Footer.asp -
  • -
  • - Default route: - typically /Home/Index or / via the dispatcher. -
  • -
+
+
+
+
<%= totalCards %>
+
+ Total Cards +
-
-
-
-
-

Next step: Controllers

-

- Use your generateController.vbs script to scaffold new controllers. -

-
cscript //nologo Scripts\generateController.vbs ^
-  Home "Index"
+ +
+
+
+
+ Cards per Board
-
-
- -
-
-
-

POBO & Repository

-

- Generate strongly-typed POBOs and repositories from your Access/SQL schema. -

-
cscript //nologo Scripts\GenerateRepo.vbs ^
-  /table:Users /pk:UserId
+ <% If boardSummaries.Count = 0 Then %> +
+ No boards yet. Create one to get started.
+ <% Else %> +
+ + + + + + + + + + <% + Dim summaryIter : Set summaryIter = boardSummaries.Iterator() + Do While summaryIter.HasNext() + Dim summary : Set summary = summaryIter.GetNext() + Dim cardCount : cardCount = CLng(summary("card_count")) + %> + + + + + + <% Loop %> + +
BoardCards
+ " class="text-decoration-none fw-medium"> + <%= H(summary("name")) %> + + + <% If cardCount = 0 Then %> + + <% Else %> + <%= cardCount %> + <% End If %> + + " class="btn btn-sm btn-outline-primary"> + Open + +
+
+ <% End If %>
-
-
-
-

Where to put stuff

-
    -
  • /core/ – framework libs (DAL, routing, helpers)
  • -
  • /app/Views/ – pages like this one
  • -
  • /public/ – IIS root (Default.asp, web.config)
  • -
+
+
+
+ Quick Links +
+
diff --git a/app/views/shared/header.asp b/app/views/shared/header.asp index 6e2e83c..878c0a3 100644 --- a/app/views/shared/header.asp +++ b/app/views/shared/header.asp @@ -9,13 +9,13 @@ If IsObject(CurrentController) Then On Error Resume Next pageTitle = CurrentController.Title If Err.Number <> 0 Then - pageTitle = "RouteKit Classic ASP" + pageTitle = "DP Jobs" Err.Clear End If On Error GoTo 0 End If -If Len(pageTitle) = 0 Then pageTitle = "Classic ASP Starter Template" +If Len(pageTitle) = 0 Then pageTitle = "DP Jobs" %> @@ -40,8 +40,8 @@ If Len(pageTitle) = 0 Then pageTitle = "Classic ASP Starter Template"