Przeglądaj źródła

board cleanup

main^2
Daniel Covington 6 dni temu
rodzic
commit
db7ea8154a
10 zmienionych plików z 393 dodań i 75 usunięć
  1. +257
    -0
      CLAUDE.md
  2. +2
    -2
      agents.md
  3. +5
    -0
      app/controllers/HomeController.asp
  4. +26
    -0
      app/repositories/boards_Repository.asp
  5. +6
    -0
      app/repositories/cards_Repository.asp
  6. +2
    -2
      app/views/Boards/Show.asp
  7. +88
    -66
      app/views/Home/index.asp
  8. +4
    -4
      app/views/shared/header.asp
  9. BIN
      db/webdata.accdb
  10. +3
    -1
      public/css/kanban.css

+ 257
- 0
CLAUDE.md Wyświetl plik

@@ -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): `<!--#include file="MyController.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)

+ 2
- 2
agents.md Wyświetl plik

@@ -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.

---



+ 5
- 0
app/controllers/HomeController.asp Wyświetl plik

@@ -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()
%>
<!--#include file="../views/Home/index.asp" -->
<%
Set boardSummaries = Nothing
End Sub

End Class


+ 26
- 0
app/repositories/boards_Repository.asp Wyświetl plik

@@ -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


+ 6
- 0
app/repositories/cards_Repository.asp Wyświetl plik

@@ -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


+ 2
- 2
app/views/Boards/Show.asp Wyświetl plik

@@ -10,8 +10,8 @@ Response.CodePage = 65001
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet" />
<link href="/css/site.css?v=20260422b" rel="stylesheet" />
<link href="/css/kanban.css?v=20260422b" rel="stylesheet" />
<link href="/css/site.css?v=20260423a" rel="stylesheet" />
<link href="/css/kanban.css?v=20260423a" rel="stylesheet" />
</head>
<body class="kanban-page">



+ 88
- 66
app/views/Home/index.asp Wyświetl plik

@@ -1,84 +1,106 @@

<div class="row mb-4 align-items-center">
<div class="col">
<h1 class="h3 mb-0">Dashboard</h1>
</div>
<div class="col-auto">
<a href="/boards/create" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg me-1"></i>New Board
</a>
</div>
</div>

<div class="row mb-4">
<div class="col-lg-8">
<div class="card shadow-sm mb-3">
<div class="card-body">
<h1 class="h3 mb-3">Welcome to RouteKit Classic ASP</h1>
<p class="text-muted">
Your lightweight, opinionated MVC-style framework for Classic ASP.
</p>
<p>
This <code>Home.Index</code> view is using the shared
<code>Header.asp</code> and <code>Footer.asp</code> layout files.
</p>
<p class="mb-0">
Start by wiring up your controllers, repositories, and views — this page is just a
friendly placeholder so you know everything is hooked up correctly.
</p>
<!-- Summary stat cards -->
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="card text-center shadow-sm h-100">
<div class="card-body py-4">
<div class="display-5 fw-bold text-primary"><%= totalBoards %></div>
<div class="text-muted small mt-1">
<i class="bi bi-kanban me-1"></i>Boards
</div>
</div>
</div>
</div>

<div class="col-lg-4">
<div class="card border-0 bg-light mb-3">
<div class="card-body">
<h2 class="h5 mb-3">Quick info</h2>
<ul class="list-unstyled mb-0 small">
<li class="mb-1">
<strong>View:</strong>
<code>app/Views/Home.Index.asp</code>
</li>
<li class="mb-1">
<strong>Layout:</strong>
<code>Shared/Header.asp</code> &amp; <code>Shared/Footer.asp</code>
</li>
<li class="mb-1">
<strong>Default route:</strong>
typically <code>/Home/Index</code> or <code>/</code> via the dispatcher.
</li>
</ul>
<div class="col-6 col-md-3">
<div class="card text-center shadow-sm h-100">
<div class="card-body py-4">
<div class="display-5 fw-bold text-success"><%= totalCards %></div>
<div class="text-muted small mt-1">
<i class="bi bi-card-text me-1"></i>Total Cards
</div>
</div>
</div>
</div>
</div>

<div class="row gy-3">
<div class="col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body">
<h2 class="h5">Next step: Controllers</h2>
<p class="small text-muted">
Use your <code>generateController.vbs</code> script to scaffold new controllers.
</p>
<pre class="small mb-0"><code>cscript //nologo Scripts\generateController.vbs ^
Home "Index"</code></pre>
<!-- Per-board breakdown -->
<div class="row">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header fw-semibold">
<i class="bi bi-bar-chart-line me-1"></i>Cards per Board
</div>
</div>
</div>

<div class="col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body">
<h2 class="h5">POBO &amp; Repository</h2>
<p class="small text-muted">
Generate strongly-typed POBOs and repositories from your Access/SQL schema.
</p>
<pre class="small mb-0"><code>cscript //nologo Scripts\GenerateRepo.vbs ^
/table:Users /pk:UserId</code></pre>
<% If boardSummaries.Count = 0 Then %>
<div class="card-body text-muted">
No boards yet. <a href="/boards/create">Create one</a> to get started.
</div>
<% Else %>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>Board</th>
<th class="text-end" style="width:100px">Cards</th>
<th style="width:90px"></th>
</tr>
</thead>
<tbody>
<%
Dim summaryIter : Set summaryIter = boardSummaries.Iterator()
Do While summaryIter.HasNext()
Dim summary : Set summary = summaryIter.GetNext()
Dim cardCount : cardCount = CLng(summary("card_count"))
%>
<tr>
<td>
<a href="/board/<%= H(summary("slug")) %>" class="text-decoration-none fw-medium">
<%= H(summary("name")) %>
</a>
</td>
<td class="text-end">
<% If cardCount = 0 Then %>
<span class="text-muted">—</span>
<% Else %>
<span class="badge bg-secondary rounded-pill"><%= cardCount %></span>
<% End If %>
</td>
<td class="text-end">
<a href="/board/<%= H(summary("slug")) %>" class="btn btn-sm btn-outline-primary">
Open
</a>
</td>
</tr>
<% Loop %>
</tbody>
</table>
</div>
<% End If %>
</div>
</div>

<div class="col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body">
<h2 class="h5">Where to put stuff</h2>
<ul class="small mb-0">
<li><code>/core/</code> – framework libs (DAL, routing, helpers)</li>
<li><code>/app/Views/</code> – pages like this one</li>
<li><code>/public/</code> – IIS root (Default.asp, web.config)</li>
</ul>
<div class="col-lg-4 mt-3 mt-lg-0">
<div class="card shadow-sm">
<div class="card-header fw-semibold">
<i class="bi bi-lightning me-1"></i>Quick Links
</div>
<div class="list-group list-group-flush">
<a href="/boards" class="list-group-item list-group-item-action">
<i class="bi bi-kanban me-2 text-muted"></i>All Boards
</a>
<a href="/boards/create" class="list-group-item list-group-item-action">
<i class="bi bi-plus-circle me-2 text-muted"></i>New Board
</a>
</div>
</div>
</div>


+ 4
- 4
app/views/shared/header.asp Wyświetl plik

@@ -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"
%>
<html lang="en">
<head>
@@ -40,8 +40,8 @@ If Len(pageTitle) = 0 Then pageTitle = "Classic ASP Starter Template"
<nav class="navbar navbar-expand-lg navbar-dark rk-topnav">
<div class="container-fluid">
<a class="navbar-brand rk-navbar-brand" href="/">
RouteKit
<span class="small">Classic ASP</span>
DP Jobs
<span class="small">Kanban Board</span>
</a>

<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#rkMainNav" aria-controls="rkMainNav" aria-expanded="false" aria-label="Toggle navigation">


BIN
db/webdata.accdb Wyświetl plik


+ 3
- 1
public/css/kanban.css Wyświetl plik

@@ -64,6 +64,8 @@ body.kanban-page .navbar .btn-outline-secondary:hover {

/* Board wrapper */
.kanban-wrapper {
width: 100%;
margin: 0 auto;
height: calc(100vh - 65px);
overflow: auto;
padding: 0.9rem 1rem 1.1rem;
@@ -74,7 +76,7 @@ body.kanban-page .navbar .btn-outline-secondary:hover {

.kanban-grid {
display: grid;
min-width: max-content;
min-width: max(75vw, max-content);
border: 1px solid var(--line, #d9e3f5);
border-radius: 14px;
background: rgba(255, 255, 255, 0.72);


Ładowanie…
Anuluj
Zapisz

Powered by TurnKey Linux.