| @@ -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) | |||||
| @@ -1,8 +1,8 @@ | |||||
| # Agents Guide — RouteKit Classic ASP / Keycloak Test | |||||
| # Agents Guide — DP Jobs Kanban Board | |||||
| ## What this project is | ## 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. | |||||
| --- | --- | ||||
| @@ -25,9 +25,14 @@ Class HomeController_Class | |||||
| End Property | End Property | ||||
| Public Sub index() | 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" --> | <!--#include file="../views/Home/index.asp" --> | ||||
| <% | <% | ||||
| Set boardSummaries = Nothing | |||||
| End Sub | End Sub | ||||
| End Class | End Class | ||||
| @@ -78,6 +78,32 @@ Class boards_Repository_Class | |||||
| DAL.Execute "DELETE FROM [boards] WHERE [id]=?", Array(id) | DAL.Execute "DELETE FROM [boards] WHERE [id]=?", Array(id) | ||||
| End Sub | 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 | End Class | ||||
| Dim boards_Repository__Singleton | Dim boards_Repository__Singleton | ||||
| @@ -92,6 +92,12 @@ Class cards_Repository_Class | |||||
| DAL.Execute "DELETE FROM [cards] WHERE [swim_lane_id]=?", Array(swimLaneId) | DAL.Execute "DELETE FROM [cards] WHERE [swim_lane_id]=?", Array(swimLaneId) | ||||
| End Sub | 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 | End Class | ||||
| Dim cards_Repository__Singleton | Dim cards_Repository__Singleton | ||||
| @@ -10,8 +10,8 @@ Response.CodePage = 65001 | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | <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@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="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> | </head> | ||||
| <body class="kanban-page"> | <body class="kanban-page"> | ||||
| @@ -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> | ||||
| </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> & <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> | </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> | |||||
| <div class="col-md-4"> | |||||
| <div class="card h-100 shadow-sm"> | |||||
| <div class="card-body"> | |||||
| <h2 class="h5">POBO & 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> | </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> | </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> | </div> | ||||
| </div> | </div> | ||||
| @@ -9,13 +9,13 @@ If IsObject(CurrentController) Then | |||||
| On Error Resume Next | On Error Resume Next | ||||
| pageTitle = CurrentController.Title | pageTitle = CurrentController.Title | ||||
| If Err.Number <> 0 Then | If Err.Number <> 0 Then | ||||
| pageTitle = "RouteKit Classic ASP" | |||||
| pageTitle = "DP Jobs" | |||||
| Err.Clear | Err.Clear | ||||
| End If | End If | ||||
| On Error GoTo 0 | On Error GoTo 0 | ||||
| End If | End If | ||||
| If Len(pageTitle) = 0 Then pageTitle = "Classic ASP Starter Template" | |||||
| If Len(pageTitle) = 0 Then pageTitle = "DP Jobs" | |||||
| %> | %> | ||||
| <html lang="en"> | <html lang="en"> | ||||
| <head> | <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"> | <nav class="navbar navbar-expand-lg navbar-dark rk-topnav"> | ||||
| <div class="container-fluid"> | <div class="container-fluid"> | ||||
| <a class="navbar-brand rk-navbar-brand" href="/"> | <a class="navbar-brand rk-navbar-brand" href="/"> | ||||
| RouteKit | |||||
| <span class="small">Classic ASP</span> | |||||
| DP Jobs | |||||
| <span class="small">Kanban Board</span> | |||||
| </a> | </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"> | <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#rkMainNav" aria-controls="rkMainNav" aria-expanded="false" aria-label="Toggle navigation"> | ||||
| @@ -64,6 +64,8 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||||
| /* Board wrapper */ | /* Board wrapper */ | ||||
| .kanban-wrapper { | .kanban-wrapper { | ||||
| width: 100%; | |||||
| margin: 0 auto; | |||||
| height: calc(100vh - 65px); | height: calc(100vh - 65px); | ||||
| overflow: auto; | overflow: auto; | ||||
| padding: 0.9rem 1rem 1.1rem; | padding: 0.9rem 1rem 1.1rem; | ||||
| @@ -74,7 +76,7 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||||
| .kanban-grid { | .kanban-grid { | ||||
| display: grid; | display: grid; | ||||
| min-width: max-content; | |||||
| min-width: max(75vw, max-content); | |||||
| border: 1px solid var(--line, #d9e3f5); | border: 1px solid var(--line, #d9e3f5); | ||||
| border-radius: 14px; | border-radius: 14px; | ||||
| background: rgba(255, 255, 255, 0.72); | background: rgba(255, 255, 255, 0.72); | ||||
Powered by TurnKey Linux.