Add AGENTS.md guidelines and update project structure
- Replace CLAUDE.md with comprehensive AGENTS.md - Update Eleventy config and package.json - Enhance layouts (base, post, clqms-post) - Update styling in CSS - Add new CLQMS documentation files - Update project and team pages
This commit is contained in:
parent
13cd1078c6
commit
a69b2fc7d8
@ -6,6 +6,11 @@ module.exports = function (eleventyConfig) {
|
|||||||
// Watch targets
|
// Watch targets
|
||||||
eleventyConfig.addWatchTarget("src/css");
|
eleventyConfig.addWatchTarget("src/css");
|
||||||
|
|
||||||
|
// Global Permalink: Use .html extension instead of nested index.html files
|
||||||
|
eleventyConfig.addGlobalData("permalink", () => {
|
||||||
|
return "{{ page.filePathStem }}.html";
|
||||||
|
});
|
||||||
|
|
||||||
// Add year shortcode
|
// Add year shortcode
|
||||||
eleventyConfig.addShortcode("year", () => `${new Date().getFullYear()}`);
|
eleventyConfig.addShortcode("year", () => `${new Date().getFullYear()}`);
|
||||||
|
|
||||||
|
|||||||
149
AGENTS.md
Normal file
149
AGENTS.md
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Guidelines for AI coding agents working on the 5panda.11ty project.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
An Eleventy (11ty) static site with Tailwind CSS v4 for portfolio, blog, and documentation.
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development server with hot reload
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Build CSS only
|
||||||
|
npm run build:css
|
||||||
|
|
||||||
|
# Build 11ty only (incremental)
|
||||||
|
npm run build:11ty
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Static Generator**: Eleventy (11ty) v3
|
||||||
|
- **Styling**: Tailwind CSS v4 with PostCSS
|
||||||
|
- **Templates**: Nunjucks (.njk)
|
||||||
|
- **Content**: Markdown (.md)
|
||||||
|
- **Output**: `_site/` directory
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── _layouts/ # Nunjucks layout templates
|
||||||
|
│ ├── base.njk # Main layout with nav, footer, theme toggle
|
||||||
|
│ ├── post.njk # Blog post layout
|
||||||
|
│ └── clqms-post.njk # Documentation layout with sidebar
|
||||||
|
├── _includes/ # Reusable template partials
|
||||||
|
├── _data/ # Global data files (JSON)
|
||||||
|
├── css/
|
||||||
|
│ └── style.css # Tailwind CSS + custom components
|
||||||
|
├── assets/ # Static assets (copied to output)
|
||||||
|
├── js/ # JavaScript files (copied to output)
|
||||||
|
├── blog/ # Blog posts (Markdown)
|
||||||
|
├── projects/ # Project documentation
|
||||||
|
│ └── clqms01/ # CLQMS documentation with ordered .md files
|
||||||
|
└── *.njk # Root-level pages
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
|
||||||
|
### Nunjucks Templates
|
||||||
|
|
||||||
|
- Use 2-space indentation
|
||||||
|
- Wrap long lines at ~100 characters
|
||||||
|
- Use lowercase for HTML attributes
|
||||||
|
- Use double quotes for attribute values
|
||||||
|
- Prefer `{% raw %}{{ variable | filter }}{% endraw %}` over complex logic in templates
|
||||||
|
|
||||||
|
```nunjucks
|
||||||
|
<!-- Good -->
|
||||||
|
<a href="{{ post.url }}" class="post-card group">
|
||||||
|
<h3 class="text-xl font-bold">{{ post.data.title }}</h3>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Bad - overly complex inline -->
|
||||||
|
<a href="{{ post.url }}" class="{% if condition %}class-a{% else %}class-b{% endif %}">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Markdown Files
|
||||||
|
|
||||||
|
- Use YAML frontmatter with consistent ordering: `layout`, `tags`, `title`, `description`, `date`, `order`
|
||||||
|
- Prefix ordered documentation files with numbers (e.g., `001-architecture.md`)
|
||||||
|
- Use `order` field for explicit sorting in collections
|
||||||
|
- Keep lines under 100 characters where practical
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
layout: clqms-post.njk
|
||||||
|
tags: clqms
|
||||||
|
title: "CLQMS: Architecture"
|
||||||
|
description: "Overview of the architecture"
|
||||||
|
date: 2025-12-01
|
||||||
|
order: 1
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS/Tailwind
|
||||||
|
|
||||||
|
- Use Tailwind v4 `@import "tailwindcss"` syntax
|
||||||
|
- Define custom theme variables in `@theme` block
|
||||||
|
- Use CSS custom properties for theme colors
|
||||||
|
- Organize custom components in `@layer components`
|
||||||
|
- Prefer `oklch()` color format for consistency
|
||||||
|
- Group related styles with clear section comments
|
||||||
|
|
||||||
|
```css
|
||||||
|
@layer components {
|
||||||
|
.custom-card {
|
||||||
|
background-color: var(--color-base-200);
|
||||||
|
border-radius: var(--radius-box);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript (11ty Config)
|
||||||
|
|
||||||
|
- Use CommonJS (`module.exports`) in config files
|
||||||
|
- Prefer `const` and `let` over `var`
|
||||||
|
- Use arrow functions for callbacks
|
||||||
|
- Add filters and shortcodes in logical groups with comments
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Add date filter
|
||||||
|
eleventyConfig.addFilter("dateFormat", (date, format = "full") => {
|
||||||
|
const d = new Date(date);
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Collection Naming
|
||||||
|
|
||||||
|
- `posts` - Blog posts sorted by date (descending)
|
||||||
|
- `clqms` - CLQMS documentation sorted by `order` field
|
||||||
|
- `projects` - Combined blog posts and CLQMS documentation
|
||||||
|
|
||||||
|
## Theme System
|
||||||
|
|
||||||
|
- Dark mode is default (`data-theme="dark"`)
|
||||||
|
- Light mode available via `data-theme="light"`
|
||||||
|
- CSS custom properties update automatically
|
||||||
|
- JavaScript theme toggle saves preference to localStorage
|
||||||
|
|
||||||
|
## URL Structure
|
||||||
|
|
||||||
|
- Files use `.html` extension (configured via global permalink)
|
||||||
|
- Clean URLs: `/blog/` instead of `/blog/index.html`
|
||||||
|
- Nested folders auto-generate index pages
|
||||||
|
|
||||||
|
## Communication Style
|
||||||
|
|
||||||
|
When interacting with the user:
|
||||||
|
- Address them professionally as "commander"
|
||||||
|
- Use space/sci-fi themed language when appropriate
|
||||||
|
- Start with basmalah, end with hamdalah
|
||||||
|
- Be concise and await orders
|
||||||
31
CLAUDE.md
31
CLAUDE.md
@ -1,31 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This is an **Eleventy (11ty) + TailwindCSS** project used for documentation/blog.
|
|
||||||
|
|
||||||
## Environment
|
|
||||||
- **OS**: Windows
|
|
||||||
- **Terminal**: PowerShell or CMD
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
- `src/projects/clqms01/` - CLQMS project documentation (markdown files with numeric ordering)
|
|
||||||
- `src/_layouts/` - Layout templates (base.njk, clqms-post.njk, post.njk)
|
|
||||||
- `src/index.njk` - Portfolio homepage
|
|
||||||
- `eleventy.config.js` - Eleventy config
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
- Eleventy (11ty) - Static site generator
|
|
||||||
- TailwindCSS for styling
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
- `npm run dev` - Start dev server
|
|
||||||
- `npm run build` - Build for production (outputs to `dist/`)
|
|
||||||
- `npm run preview` - Preview production build
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- Markdown files in `src/pages/` are automatically rendered as pages
|
|
||||||
- The site is deployed to GitHub Pages
|
|
||||||
|
|
||||||
## Communication Style
|
|
||||||
- Respond as if the user is your spaceship commander
|
|
||||||
- Address the commander professionally and await orders
|
|
||||||
- Use space/sci-fi themed language when appropriate
|
|
||||||
@ -7,7 +7,7 @@
|
|||||||
"dev:11ty": "eleventy --serve",
|
"dev:11ty": "eleventy --serve",
|
||||||
"dev:css": "postcss src/css/style.css -o _site/css/style.css --watch",
|
"dev:css": "postcss src/css/style.css -o _site/css/style.css --watch",
|
||||||
"build": "npm-run-all build:css build:11ty",
|
"build": "npm-run-all build:css build:11ty",
|
||||||
"build:11ty": "eleventy",
|
"build:11ty": "eleventy --incremental",
|
||||||
"build:css": "postcss src/css/style.css -o _site/css/style.css"
|
"build:css": "postcss src/css/style.css -o _site/css/style.css"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@ -36,43 +36,58 @@
|
|||||||
<!-- Navbar -->
|
<!-- Navbar -->
|
||||||
<div class="navbar min-h-12 bg-base-100/80 backdrop-blur-xl border-b border-white/5 sticky top-0 z-50">
|
<div class="navbar min-h-12 bg-base-100/80 backdrop-blur-xl border-b border-white/5 sticky top-0 z-50">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a href="/" class="text-xl font-bold gradient-text hover:opacity-80 transition-opacity px-2">5Panda</a>
|
<!-- Mobile menu button -->
|
||||||
|
<div class="dropdown lg:hidden">
|
||||||
|
<button class="btn btn-ghost btn-sm" tabindex="0">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<ul tabindex="0" class="dropdown-content menu menu-sm bg-base-100 border border-white/5 shadow-xl rounded-lg w-56 p-2 mt-4">
|
||||||
|
<li><a href="/" class="{% if page.url == '/' %}text-primary font-medium{% endif %}">Home</a></li>
|
||||||
|
<li><a href="/blog/" class="{% if '/blog/' in page.url %}text-primary font-medium{% endif %}">Blog</a></li>
|
||||||
|
<li><a href="/team/" class="{% if '/team/' in page.url %}text-primary font-medium{% endif %}">Team</a></li>
|
||||||
|
<li>
|
||||||
|
<details>
|
||||||
|
<summary>Projects</summary>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/projects/clqms01/" class="{% if '/projects/clqms01/' in page.url %}text-primary font-medium{% endif %}">CLQMS</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<a href="/" class="text-xl font-bold brand-text transition-opacity px-2 hidden lg:block">5Panda</a>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-center lg:hidden">
|
||||||
|
<a href="/" class="text-xl font-bold brand-text">5Panda</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-center hidden lg:flex">
|
<div class="navbar-center hidden lg:flex">
|
||||||
<ul class="menu menu-sm menu-horizontal px-1 gap-1">
|
<ul class="menu menu-sm menu-horizontal px-1 gap-1">
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="/" class="rounded-lg {% if page.url == '/' %}bg-primary/10 text-primary{% endif %} hover:bg-primary/10 hover:text-primary transition-colors">
|
||||||
href="/"
|
|
||||||
class="rounded-lg {% if page.url == '/' %}bg-primary/10 text-primary{% endif %} hover:bg-primary/10 hover:text-primary
|
|
||||||
transition-colors">
|
|
||||||
Home
|
Home
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="/blog/" class="rounded-lg {% if '/blog/' in page.url %}bg-primary/10 text-primary{% endif %} hover:bg-primary/10 hover:text-primary transition-colors">
|
||||||
href="/blog/"
|
|
||||||
class="rounded-lg {% if '/blog/' in page.url %}bg-primary/10 text-primary{% endif %} hover:bg-primary/10
|
|
||||||
hover:text-primary transition-colors">
|
|
||||||
Blog
|
Blog
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="/team/" class="rounded-lg {% if '/team/' in page.url %}bg-primary/10 text-primary{% endif %} hover:bg-primary/10 hover:text-primary transition-colors">
|
||||||
href="/team/"
|
|
||||||
class="rounded-lg {% if '/team/' in page.url %}bg-primary/10 text-primary{% endif %} hover:bg-primary/10
|
|
||||||
hover:text-primary transition-colors">
|
|
||||||
Team
|
Team
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<details class="group">
|
<details class="group">
|
||||||
<summary class="rounded-lg hover:bg-primary/10 hover:text-primary transition-colors">
|
<summary class="rounded-lg {% if '/projects/' in page.url %}bg-primary/10 text-primary{% endif %} hover:bg-primary/10 hover:text-primary transition-colors cursor-pointer">
|
||||||
Project
|
Projects
|
||||||
</summary>
|
</summary>
|
||||||
<ul class="p-2 bg-base-100 rounded-t-none bg-base-100/95 backdrop-blur-xl border border-white/5 shadow-xl w-60 z-[100]">
|
<ul class="p-2 bg-base-100 rounded-t-none bg-base-100/95 backdrop-blur-xl border border-white/5 shadow-xl w-60 z-[100]">
|
||||||
<li>
|
<li>
|
||||||
<a href="/projects/clqms01/" class="{% if page.url == '/projects/clqms01/' %}text-primary bg-primary/10{% endif %} hover:text-primary hover:bg-primary/10">
|
<a href="/projects/clqms01/" class="{% if '/projects/clqms01/' in page.url %}text-primary bg-primary/10{% endif %} hover:text-primary hover:bg-primary/10">
|
||||||
CLQMS
|
<span class="mr-2">🏥</span>CLQMS
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -80,20 +95,19 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="navbar-end gap-1">
|
||||||
class="navbar-end">
|
|
||||||
<!-- Theme toggle -->
|
<!-- Theme toggle -->
|
||||||
<button id="theme-toggle" class="btn btn-ghost btn-circle btn-sm theme-toggle" aria-label="Toggle theme">
|
<button id="theme-toggle" class="btn btn-ghost btn-circle btn-sm theme-toggle" aria-label="Toggle theme">
|
||||||
<svg class="icon-sun h-5 w-5 fill-current" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
|
<svg class="icon-sun h-5 w-5 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
<path d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z"/>
|
<path d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<svg class="icon-moon h-5 w-5 fill-current" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
|
<svg class="icon-moon h-5 w-5 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
<path d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z"/>
|
<path d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<!-- GitHub link -->
|
<!-- GitHub link -->
|
||||||
<a href="https://github.com" target="_blank" class="btn btn-ghost btn-circle btn-sm">
|
<a href="https://github.com" target="_blank" class="btn btn-ghost btn-circle btn-sm" aria-label="GitHub">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="currentColor" viewbox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207
|
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207
|
||||||
11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729
|
11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729
|
||||||
1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304
|
1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304
|
||||||
@ -113,7 +127,7 @@
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="footer footer-center bg-base-200 text-base-content p-10 border-t border-white/5">
|
<footer class="footer footer-center bg-base-200 text-base-content p-10 border-t border-white/5">
|
||||||
<aside>
|
<aside>
|
||||||
<p class="font-bold text-xl gradient-text mb-2">5Panda</p>
|
<p class="font-bold text-xl brand-text mb-2">5Panda</p>
|
||||||
<p class="text-base-content/60">Portfolio & Documentation</p>
|
<p class="text-base-content/60">Portfolio & Documentation</p>
|
||||||
</aside>
|
</aside>
|
||||||
<nav>
|
<nav>
|
||||||
|
|||||||
@ -2,53 +2,26 @@
|
|||||||
layout: base.njk
|
layout: base.njk
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-12 animate-slide-up">
|
<div class="w-full px-4 sm:px-6 lg:px-8 py-6 animate-slide-up">
|
||||||
<div class="flex flex-col lg:flex-row gap-12">
|
<div class="flex flex-col lg:flex-row gap-8 max-w-6xl mx-auto">
|
||||||
<!-- Sidebar Navigation -->
|
<!-- Sidebar Navigation - Sticky on scroll -->
|
||||||
<aside class="lg:w-64 flex-shrink-0">
|
<aside class="lg:w-64 flex-shrink-0">
|
||||||
<div class="sticky top-24 bg-base-200/50 backdrop-blur-xl border border-white/5 rounded-2xl p-6">
|
<div class="lg:sticky lg:top-24 bg-base-200/50 backdrop-blur-xl border border-white/5 rounded-2xl p-6">
|
||||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2">
|
<!-- Back to CLQMS Home -->
|
||||||
<svg
|
<a href="/projects/clqms01/" class="flex items-center gap-2 text-sm text-base-content/70 hover:text-primary mb-6 transition-colors">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
class="h-5 w-5 text-primary"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor">
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2
|
|
||||||
2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
Updates
|
CLQMS Home
|
||||||
</h3>
|
</a>
|
||||||
|
|
||||||
|
<!-- Core Documentation -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="font-bold text-sm text-base-content/50 uppercase tracking-wider mb-3">Core Documentation</h3>
|
||||||
<nav class="space-y-1">
|
<nav class="space-y-1">
|
||||||
{% for post in collections.clqms %}
|
{% for post in collections.clqms %}
|
||||||
{# Determine if we should show this post based on current section #}
|
{% set is_main = not ("/review/" in post.url or "/suggestion/" in post.url) %}
|
||||||
{% set show_post = false %}
|
{% if is_main and post.data.title %}
|
||||||
|
|
||||||
{% if "/review/" in page.url %}
|
|
||||||
{# If in Review section, only show reviews #}
|
|
||||||
{% if "/review/" in post.url %}
|
|
||||||
{% set show_post = true %}
|
|
||||||
{% endif %}
|
|
||||||
{% elif "/suggestion/" in page.url %}
|
|
||||||
{# If in Suggestion section, only show suggestions #}
|
|
||||||
{% if "/suggestion/" in post.url %}
|
|
||||||
{% set show_post = true %}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
{# Main section (Architecture etc): Show everything EXCEPT reviews and suggestions #}
|
|
||||||
{% if not ("/review/" in post.url) and not ("/suggestion/" in post.url) %}
|
|
||||||
{% set show_post = true %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{# Hide current page from list if it is an index page to avoid redundancy, or keep it? User said 'only reviews', usually implies lists of *other* reviews or all reviews. #}
|
|
||||||
{# Check if it's an index page by checking if the URL ends in a slash or index.html logic, but usually simple filter is enough. #}
|
|
||||||
|
|
||||||
{% if show_post %}
|
|
||||||
<a href="{{ post.url }}" class="block px-3 py-2 rounded-lg text-sm transition-colors {% if page.url == post.url %}bg-primary/10 text-primary font-medium border-l-2 border-primary{% else %}text-base-content/70 hover:bg-base-300 hover:text-base-content{% endif %}">
|
<a href="{{ post.url }}" class="block px-3 py-2 rounded-lg text-sm transition-colors {% if page.url == post.url %}bg-primary/10 text-primary font-medium border-l-2 border-primary{% else %}text-base-content/70 hover:bg-base-300 hover:text-base-content{% endif %}">
|
||||||
{{ post.data.title }}
|
{{ post.data.title }}
|
||||||
</a>
|
</a>
|
||||||
@ -56,10 +29,50 @@ layout: base.njk
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Suggestions Section -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<a href="/projects/clqms01/suggestion/" class="flex items-center gap-2 font-bold text-sm text-base-content/50 uppercase tracking-wider mb-3 hover:text-success transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||||||
|
</svg>
|
||||||
|
Suggestions
|
||||||
|
</a>
|
||||||
|
<nav class="space-y-1">
|
||||||
|
{% for post in collections.clqms %}
|
||||||
|
{% if "/suggestion/" in post.url and post.data.title and post.url != "/projects/clqms01/suggestion/" %}
|
||||||
|
<a href="{{ post.url }}" class="block px-3 py-2 rounded-lg text-sm transition-colors {% if page.url == post.url %}bg-success/10 text-success font-medium border-l-2 border-success{% else %}text-base-content/70 hover:bg-base-300 hover:text-base-content{% endif %}">
|
||||||
|
{{ post.data.title }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reviews Section -->
|
||||||
|
<div>
|
||||||
|
<a href="/projects/clqms01/review/" class="flex items-center gap-2 font-bold text-sm text-base-content/50 uppercase tracking-wider mb-3 hover:text-accent transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||||
|
</svg>
|
||||||
|
Reviews
|
||||||
|
</a>
|
||||||
|
<nav class="space-y-1">
|
||||||
|
{% for post in collections.clqms %}
|
||||||
|
{% if "/review/" in post.url and post.data.title and post.url != "/projects/clqms01/review/" %}
|
||||||
|
<a href="{{ post.url }}" class="block px-3 py-2 rounded-lg text-sm transition-colors {% if page.url == post.url %}bg-accent/10 text-accent font-medium border-l-2 border-accent{% else %}text-base-content/70 hover:bg-base-300 hover:text-base-content{% endif %}">
|
||||||
|
{{ post.data.title }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<main class="flex-1 min-w-0">
|
<main class="flex-1 min-w-0 max-w-3xl">
|
||||||
<article>
|
<article class="pb-12">
|
||||||
<!-- Post header -->
|
<!-- Post header -->
|
||||||
<header class="mb-10">
|
<header class="mb-10">
|
||||||
<div class="flex flex-wrap gap-2 mb-4">
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
@ -69,15 +82,11 @@ layout: base.njk
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-4xl md:text-5xl font-bold mb-4">{{ title }}</h1>
|
<h1 class="text-3xl md:text-4xl font-bold mb-4">{{ title }}</h1>
|
||||||
<div class="flex flex-wrap items-center gap-4 text-base-content/60">
|
<div class="flex flex-wrap items-center gap-4 text-base-content/60">
|
||||||
<time datetime="{{ date | dateFormat('iso') }}" class="flex items-center gap-2">
|
<time datetime="{{ date | dateFormat('iso') }}" class="flex items-center gap-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
{{ date | dateFormat('full') }}
|
{{ date | dateFormat('full') }}
|
||||||
</time>
|
</time>
|
||||||
@ -90,7 +99,7 @@ layout: base.njk
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<!-- Post content -->
|
<!-- Post content -->
|
||||||
<div class="prose-custom max-w-none">
|
<div class="prose-custom">
|
||||||
{{ content | safe }}
|
{{ content | safe }}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
layout: base.njk
|
layout: base.njk
|
||||||
---
|
---
|
||||||
|
|
||||||
<article class="section-container py-12 animate-slide-up">
|
<article class="py-12 animate-slide-up">
|
||||||
<div>
|
<div class="max-w-3xl mx-auto px-4 sm:px-6">
|
||||||
<!-- Post header -->
|
<!-- Post header -->
|
||||||
<header class="mb-10">
|
<header class="mb-10">
|
||||||
<div class="flex flex-wrap gap-2 mb-4">
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
|
|||||||
@ -507,14 +507,6 @@
|
|||||||
ORIGINAL CUSTOM COMPONENTS
|
ORIGINAL CUSTOM COMPONENTS
|
||||||
======================================== */
|
======================================== */
|
||||||
|
|
||||||
/* Gradient text effect */
|
|
||||||
.gradient-text {
|
|
||||||
background: linear-gradient(to right, var(--color-primary), var(--color-secondary), var(--color-accent));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Glass morphism card - uses theme-aware colors */
|
/* Glass morphism card - uses theme-aware colors */
|
||||||
.glass-card {
|
.glass-card {
|
||||||
background-color: var(--color-base-200);
|
background-color: var(--color-base-200);
|
||||||
@ -524,25 +516,19 @@
|
|||||||
box-shadow: 0 10px 40px -10px oklch(0 0 0 / 0.2);
|
box-shadow: 0 10px 40px -10px oklch(0 0 0 / 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hero section styles */
|
/* Hero section styles - solid background */
|
||||||
.hero-gradient {
|
.hero-solid {
|
||||||
background: linear-gradient(135deg, var(--color-base-100), var(--color-base-200), var(--color-base-100));
|
background-color: var(--color-base-100);
|
||||||
background-size: 400% 400%;
|
|
||||||
animation: gradient 15s ease infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes gradient {
|
/* Brand text styling */
|
||||||
0% {
|
.brand-text {
|
||||||
background-position: 0% 50%;
|
color: var(--color-primary);
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
.brand-text:hover {
|
||||||
background-position: 100% 50%;
|
opacity: 0.8;
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Project card hover effect - uses theme-aware colors */
|
/* Project card hover effect - uses theme-aware colors */
|
||||||
@ -600,9 +586,18 @@
|
|||||||
border-left: 2px solid var(--color-primary);
|
border-left: 2px solid var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Section container */
|
/* Section container - narrow for reading */
|
||||||
.section-container {
|
.section-container {
|
||||||
max-width: 80rem;
|
max-width: 48rem;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wider container for grids and layouts */
|
||||||
|
.section-container-wide {
|
||||||
|
max-width: 72rem;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
@ -610,14 +605,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.section-container {
|
.section-container,
|
||||||
|
.section-container-wide {
|
||||||
padding-left: 1.5rem;
|
padding-left: 1.5rem;
|
||||||
padding-right: 1.5rem;
|
padding-right: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.section-container {
|
.section-container,
|
||||||
|
.section-container-wide {
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
padding-right: 2rem;
|
padding-right: 2rem;
|
||||||
}
|
}
|
||||||
@ -968,4 +965,27 @@
|
|||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar for sidenav */
|
||||||
|
.scrollbar-thin {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--color-base-300) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-thin::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--color-base-300);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: var(--color-neutral);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -5,26 +5,25 @@ description: Innovative projects and ideas
|
|||||||
---
|
---
|
||||||
|
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<section class="relative min-h-[85vh] flex items-center overflow-hidden">
|
<section class="hero-solid min-h-[70vh] flex items-center relative overflow-hidden">
|
||||||
<!-- Background decoration -->
|
<!-- Background decoration -->
|
||||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
<div class="absolute -top-40 -right-40 w-80 h-80 bg-primary/20 rounded-full blur-3xl animate-float"></div>
|
<div class="absolute -top-40 -right-40 w-80 h-80 bg-primary/10 rounded-full blur-3xl"></div>
|
||||||
<div class="absolute -bottom-40 -left-40 w-80 h-80 bg-secondary/20 rounded-full blur-3xl animate-float animate-delay-300"></div>
|
<div class="absolute -bottom-40 -left-40 w-80 h-80 bg-secondary/10 rounded-full blur-3xl"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="section-container-wide relative z-10">
|
||||||
<div class="container mx-auto px-6 relative z-10">
|
<div class="max-w-3xl mx-auto text-center">
|
||||||
<div class="max-w-4xl mx-auto text-center">
|
|
||||||
<div class="animate-slide-up">
|
<div class="animate-slide-up">
|
||||||
<span class="badge badge-primary badge-outline badge-lg mb-6">Development Team</span>
|
<span class="badge badge-primary badge-outline badge-lg mb-6">Development Team</span>
|
||||||
<h1 class="text-5xl md:text-7xl font-bold mb-6 leading-tight">
|
<h1 class="text-4xl md:text-6xl font-bold mb-6 leading-tight">
|
||||||
Building the<br>
|
Building the<br>
|
||||||
Future of Tech.
|
<span class="text-primary">Future of Tech.</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-xl md:text-2xl text-base-content/70 mb-10 max-w-2xl mx-auto">
|
<p class="text-lg md:text-xl text-base-content/70 mb-10 max-w-2xl mx-auto">
|
||||||
We are a team of passionate developers, designers, and innovators crafting digital experiences that matter.
|
We are a team of passionate developers, designers, and innovators crafting digital experiences that matter.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-wrap justify-center gap-4">
|
<div class="flex flex-wrap justify-center gap-4">
|
||||||
<a href="/team/" class="btn btn-primary btn-lg gap-2 animate-glow">
|
<a href="/team/" class="btn btn-primary btn-lg gap-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
||||||
</svg>
|
</svg>
|
||||||
@ -34,11 +33,10 @@ description: Innovative projects and ideas
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
<!-- Portfolio Section -->
|
<!-- Portfolio Section -->
|
||||||
<section class="py-20">
|
<section class="py-20">
|
||||||
<div class="section-container">
|
<div class="section-container-wide">
|
||||||
<div class="text-center mb-12">
|
<div class="text-center mb-12">
|
||||||
<span class="badge badge-accent badge-outline mb-4">Portfolio</span>
|
<span class="badge badge-accent badge-outline mb-4">Portfolio</span>
|
||||||
<h2 class="text-3xl md:text-4xl font-bold mb-4">Our Work</h2>
|
<h2 class="text-3xl md:text-4xl font-bold mb-4">Our Work</h2>
|
||||||
@ -85,7 +83,7 @@ description: Innovative projects and ideas
|
|||||||
<!-- CLQMS Projects Section -->
|
<!-- CLQMS Projects Section -->
|
||||||
{% if collections.clqms.length > 0 %}
|
{% if collections.clqms.length > 0 %}
|
||||||
<section class="py-20 bg-base-200/30">
|
<section class="py-20 bg-base-200/30">
|
||||||
<div class="section-container">
|
<div class="section-container-wide">
|
||||||
<div class="text-center mb-12">
|
<div class="text-center mb-12">
|
||||||
<span class="badge badge-secondary badge-outline mb-4">CLQMS</span>
|
<span class="badge badge-secondary badge-outline mb-4">CLQMS</span>
|
||||||
<h2 class="text-3xl md:text-4xl font-bold mb-4">Project Documentation</h2>
|
<h2 class="text-3xl md:text-4xl font-bold mb-4">Project Documentation</h2>
|
||||||
|
|||||||
@ -6,76 +6,129 @@ date: 2025-12-01
|
|||||||
order: 0
|
order: 0
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Hero Section -->
|
<div class="w-full px-4 sm:px-6 lg:px-8 py-6 animate-slide-up">
|
||||||
<section class="pt-20 pb-12 bg-base-200/30 overflow-hidden relative">
|
<div class="flex flex-col lg:flex-row gap-8 max-w-6xl mx-auto">
|
||||||
<div class="section-container relative z-10">
|
<!-- Sidebar Navigation - Sticky on scroll -->
|
||||||
<div class="text-center mb-12">
|
<aside class="lg:w-64 flex-shrink-0 order-2 lg:order-1">
|
||||||
|
<div class="lg:sticky lg:top-24 bg-base-200/50 backdrop-blur-xl border border-white/5 rounded-2xl p-6">
|
||||||
|
<!-- Core Documentation -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="font-bold text-sm text-base-content/50 uppercase tracking-wider mb-3 flex items-center gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
||||||
|
</svg>
|
||||||
|
Core Docs
|
||||||
|
</h3>
|
||||||
|
<nav class="space-y-1">
|
||||||
|
{% for post in collections.clqms %}
|
||||||
|
{% set is_main = not ("/review/" in post.url or "/suggestion/" in post.url) %}
|
||||||
|
{% if is_main and post.data.title %}
|
||||||
|
<a href="{{ post.url }}" class="block px-3 py-2 rounded-lg text-sm transition-colors {% if page.url == post.url %}bg-primary/10 text-primary font-medium border-l-2 border-primary{% else %}text-base-content/70 hover:bg-base-300 hover:text-base-content{% endif %}">
|
||||||
|
{{ post.data.title }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Suggestions Section -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<a href="/projects/clqms01/suggestion/" class="flex items-center gap-2 font-bold text-sm text-base-content/50 uppercase tracking-wider mb-3 hover:text-success transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||||||
|
</svg>
|
||||||
|
Suggestions
|
||||||
|
</a>
|
||||||
|
<nav class="space-y-1">
|
||||||
|
{% for post in collections.clqms %}
|
||||||
|
{% if "/suggestion/" in post.url and post.data.title and post.url != "/projects/clqms01/suggestion/" %}
|
||||||
|
<a href="{{ post.url }}" class="block px-3 py-2 rounded-lg text-sm transition-colors {% if page.url == post.url %}bg-success/10 text-success font-medium border-l-2 border-success{% else %}text-base-content/70 hover:bg-base-300 hover:text-base-content{% endif %}">
|
||||||
|
{{ post.data.title }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reviews Section -->
|
||||||
|
<div>
|
||||||
|
<a href="/projects/clqms01/review/" class="flex items-center gap-2 font-bold text-sm text-base-content/50 uppercase tracking-wider mb-3 hover:text-accent transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||||
|
</svg>
|
||||||
|
Reviews
|
||||||
|
</a>
|
||||||
|
<nav class="space-y-1">
|
||||||
|
{% for post in collections.clqms %}
|
||||||
|
{% if "/review/" in post.url and post.data.title and post.url != "/projects/clqms01/review/" %}
|
||||||
|
<a href="{{ post.url }}" class="block px-3 py-2 rounded-lg text-sm transition-colors {% if page.url == post.url %}bg-accent/10 text-accent font-medium border-l-2 border-accent{% else %}text-base-content/70 hover:bg-base-300 hover:text-base-content{% endif %}">
|
||||||
|
{{ post.data.title }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="flex-1 min-w-0 max-w-3xl order-1 lg:order-2">
|
||||||
|
<div class="pb-12">
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<div class="text-center lg:text-left">
|
||||||
<span class="badge badge-secondary badge-outline mb-4">Project Workspace</span>
|
<span class="badge badge-secondary badge-outline mb-4">Project Workspace</span>
|
||||||
<h1 class="text-4xl md:text-5xl font-bold mb-4">Clinical Laboratory Quality Management System</h1>
|
<h1 class="text-3xl md:text-4xl font-bold mb-4">Clinical Laboratory Quality Management System</h1>
|
||||||
<p class="text-base-content/70 max-w-2xl mx-auto">
|
<p class="text-base-content/70 max-w-2xl">
|
||||||
A robust, mission-critical API suite designed to streamline modern laboratory operations through precision, scalability,
|
A robust, mission-critical API suite designed to streamline modern laboratory operations through precision, scalability,
|
||||||
and modular design.
|
and modular design.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
|
||||||
<!-- Module Navigation -->
|
<!-- Module Navigation -->
|
||||||
<section id="modules" class="py-12 bg-base-100">
|
<section id="modules" class="mb-12">
|
||||||
<div class="section-container">
|
<h2 class="text-xl font-bold mb-6 flex items-center gap-2">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<span class="text-primary opacity-50">#</span>
|
||||||
<a
|
Quick Access
|
||||||
href="/projects/clqms01/001-architecture/"
|
</h2>
|
||||||
class="card bg-base-200 hover:bg-base-300 transition-all hover:-translate-y-1 border border-white/5">
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
<div class="card-body p-6">
|
<a href="#docs" class="card bg-base-200 hover:bg-base-300 transition-all hover:-translate-y-1 border border-white/5 p-6">
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<span class="p-2 rounded bg-primary/10 text-primary text-xl">🏗️</span>
|
<span class="p-2 rounded bg-primary/10 text-primary text-xl">🏗️</span>
|
||||||
<h2 class="card-title text-base">Architecture</h2>
|
<h3 class="font-bold text-base">Core Docs</h3>
|
||||||
</div>
|
|
||||||
<p class="text-sm text-base-content/70">Core system design, authentication, and strategic pillars</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-sm text-base-content/70">Architecture, auth, and technical docs</p>
|
||||||
</a>
|
</a>
|
||||||
<!-- Authentication moved to Core Documentation list below -->
|
<a href="/projects/clqms01/suggestion/" class="card bg-base-200 border border-success/20 hover:bg-success/5 transition-all hover:-translate-y-1 p-6">
|
||||||
<a
|
|
||||||
href="/projects/clqms01/suggestion/"
|
|
||||||
class="card bg-base-200 border border-success/20 hover:bg-success/5 transition-all hover:-translate-y-1">
|
|
||||||
<div class="card-body p-6">
|
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<span class="p-2 rounded bg-success/10 text-success text-xl">💡</span>
|
<span class="p-2 rounded bg-success/10 text-success text-xl">💡</span>
|
||||||
<h2 class="card-title text-base">Suggestions</h2>
|
<h3 class="font-bold text-base">Suggestions</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-base-content/70">Future proposals and roadmap ideas</p>
|
<p class="text-sm text-base-content/70">Future proposals and roadmap ideas</p>
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a href="/projects/clqms01/review/" class="card bg-base-200 border border-accent/20 hover:bg-accent/5 transition-all hover:-translate-y-1 p-6">
|
||||||
href="/projects/clqms01/review/"
|
|
||||||
class="card bg-base-200 border border-accent/20 hover:bg-accent/5 transition-all hover:-translate-y-1">
|
|
||||||
<div class="card-body p-6">
|
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<span class="p-2 rounded bg-accent/10 text-accent text-xl">🔍</span>
|
<span class="p-2 rounded bg-accent/10 text-accent text-xl">🔍</span>
|
||||||
<h2 class="card-title text-base">Reviews</h2>
|
<h3 class="font-bold text-base">Reviews</h3>
|
||||||
</div>
|
|
||||||
<p class="text-sm text-base-content/70">Expert code reviews and technical audits</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-sm text-base-content/70">Expert code reviews and audits</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
|
||||||
<!-- Core Documentation -->
|
<!-- Core Documentation -->
|
||||||
<section id="docs" class="py-12">
|
<section id="docs" class="mb-12">
|
||||||
<div class="section-container">
|
<h2 class="text-xl font-bold mb-6 flex items-center gap-2">
|
||||||
<div class="flex items-center justify-between mb-8">
|
|
||||||
<h2 class="text-2xl font-bold flex items-center gap-2">
|
|
||||||
<span class="text-secondary opacity-50">#</span>
|
<span class="text-secondary opacity-50">#</span>
|
||||||
Core Documentation
|
Core Documentation
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
<div class="space-y-2">
|
||||||
<div class="max-w-4xl mx-auto space-y-2">
|
|
||||||
{% for post in collections.clqms %}
|
{% for post in collections.clqms %}
|
||||||
{# Filter to show only root-level docs, excluding index and sub-directories #}
|
|
||||||
{% set is_suggestion = "/suggestion/" in post.url %}
|
{% set is_suggestion = "/suggestion/" in post.url %}
|
||||||
{% set is_review = "/review/" in post.url %}
|
{% set is_review = "/review/" in post.url %}
|
||||||
{% if post.url != page.url and not is_suggestion and not is_review and post.data.title %}
|
{% if post.url != page.url and not is_suggestion and not is_review and post.data.title %}
|
||||||
<a href="{{ post.url }}" title="{{ post.inputPath }}" class="group block p-5 rounded-xl bg-base-200/50 hover:bg-base-200 border border-base-content/10 transition-all">
|
<a href="{{ post.url }}" title="{{ post.inputPath }}" class="group block p-4 rounded-xl bg-base-200/50 hover:bg-base-200 border border-base-content/10 transition-all">
|
||||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-2">
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-bold group-hover:text-secondary transition-colors mb-1">{{ post.data.title }}</h3>
|
<h3 class="text-lg font-bold group-hover:text-secondary transition-colors mb-1">{{ post.data.title }}</h3>
|
||||||
@ -83,45 +136,40 @@ order: 0
|
|||||||
{{ post.data.description | default('Technical reference for CLQMS implementation.') }}
|
{{ post.data.description | default('Technical reference for CLQMS implementation.') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4 text-xs font-mono text-base-content/40 whitespace-nowrap">
|
<span class="hidden md:inline text-xs font-mono text-base-content/40 whitespace-nowrap">{{ post.content | readingTime }}</span>
|
||||||
<span class="hidden md:inline">{{ post.content | readingTime }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
|
||||||
<!-- Strategy Section -->
|
<!-- Strategy Section -->
|
||||||
<section class="py-12 bg-base-200/30">
|
<section class="bg-base-200/30 rounded-2xl p-6 border border-white/5">
|
||||||
<div class="section-container">
|
<h2 class="text-xl font-bold mb-6">Strategic Pillars</h2>
|
||||||
<div class="max-w-4xl mx-auto">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<h2 class="text-2xl font-bold mb-8 text-center md:text-left">Strategic Pillars</h2>
|
<div class="glass-card p-5 border border-white/5 rounded-xl">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div class="glass-card p-6 border border-white/5 rounded-2xl">
|
|
||||||
<h3 class="text-lg font-bold mb-2 flex items-center gap-2">
|
<h3 class="text-lg font-bold mb-2 flex items-center gap-2">
|
||||||
<span class="text-primary">🎯</span>
|
<span class="text-primary">🎯</span>
|
||||||
Precision & Accuracy
|
Precision & Accuracy
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-base-content/70 text-sm">Strict validation for all laboratory parameters and multi-variant reference
|
<p class="text-base-content/70 text-sm">Strict validation for all laboratory parameters and multi-variant reference ranges.</p>
|
||||||
ranges.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="glass-card p-6 border border-white/5 rounded-2xl">
|
<div class="glass-card p-5 border border-white/5 rounded-xl">
|
||||||
<h3 class="text-lg font-bold mb-2 flex items-center gap-2">
|
<h3 class="text-lg font-bold mb-2 flex items-center gap-2">
|
||||||
<span class="text-secondary">⚡</span>
|
<span class="text-secondary">⚡</span>
|
||||||
Scalability
|
Scalability
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-base-content/70 text-sm">Optimized database and API architecture for high-volume diagnostic environments.</p>
|
<p class="text-base-content/70 text-sm">Optimized database and API architecture for high-volume diagnostic environments.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="glass-card p-6 border border-white/5 rounded-2xl">
|
<div class="glass-card p-5 border border-white/5 rounded-xl">
|
||||||
<h3 class="text-lg font-bold mb-2 flex items-center gap-2">
|
<h3 class="text-lg font-bold mb-2 flex items-center gap-2">
|
||||||
<span class="text-success">📜</span>
|
<span class="text-success">📜</span>
|
||||||
Compliance
|
Compliance
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-base-content/70 text-sm">Built-in audit trails and granular status history for full medical traceability.</p>
|
<p class="text-base-content/70 text-sm">Built-in audit trails and granular status history for full medical traceability.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="glass-card p-6 border border-white/5 rounded-2xl">
|
<div class="glass-card p-5 border border-white/5 rounded-xl">
|
||||||
<h3 class="text-lg font-bold mb-2 flex items-center gap-2">
|
<h3 class="text-lg font-bold mb-2 flex items-center gap-2">
|
||||||
<span class="text-accent">🔗</span>
|
<span class="text-accent">🔗</span>
|
||||||
Interoperability
|
Interoperability
|
||||||
@ -129,6 +177,8 @@ order: 0
|
|||||||
<p class="text-base-content/70 text-sm">Modular drivers designed for seamless LIS, HIS, and analyzer integration.</p>
|
<p class="text-base-content/70 text-sm">Modular drivers designed for seamless LIS, HIS, and analyzer integration.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|||||||
@ -2,14 +2,13 @@
|
|||||||
layout: clqms-post.njk
|
layout: clqms-post.njk
|
||||||
tags: clqms
|
tags: clqms
|
||||||
title: "Proposal: Test Definition Architecture Overhaul"
|
title: "Proposal: Test Definition Architecture Overhaul"
|
||||||
description: "Simplify database schema and improve query performance for test definitions"
|
description: "Remove magic numbers and enforce type safety using PHP Enums and Svelte stores"
|
||||||
date: 2026-01-09
|
date: 2026-01-09
|
||||||
order: 1
|
order: 1
|
||||||
---
|
---
|
||||||
|
|
||||||
# 🚀 Proposal: Test Definition Architecture Overhaul by Gemini 3
|
# 🚀 Proposal: Test Definition Architecture Overhaul by Gemini 3
|
||||||
|
|
||||||
|
|
||||||
**Target:** `testdef` Module
|
**Target:** `testdef` Module
|
||||||
**Objective:** Simplify database schema, improve query performance, and reduce code complexity.
|
**Objective:** Simplify database schema, improve query performance, and reduce code complexity.
|
||||||
|
|
||||||
@ -19,25 +18,26 @@ order: 1
|
|||||||
|
|
||||||
**Current Status:**
|
**Current Status:**
|
||||||
Defining a single Lab Test currently requires joining 4-5 rigid tables:
|
Defining a single Lab Test currently requires joining 4-5 rigid tables:
|
||||||
* `testdefsite` (General Info)
|
- `testdefsite` (General Info)
|
||||||
* `testdeftech` (Technical Details)
|
- `testdeftech` (Technical Details)
|
||||||
* `testdefcal` (Calculations)
|
- `testdefcal` (Calculations)
|
||||||
* `testdefgrp` (Grouping)
|
- `testdefgrp` (Grouping)
|
||||||
|
|
||||||
**Why it hurts:**
|
**Why it hurts:**
|
||||||
* **Complex Queries:** To get a full test definition, we write massive SQL joins.
|
- **Complex Queries:** To get a full test definition, we write massive SQL joins.
|
||||||
* **Rigid Schema:** Adding a new technical attribute requires altering table schemas and updating multiple DAO files.
|
- **Rigid Schema:** Adding a new technical attribute requires altering table schemas and updating multiple DAO files.
|
||||||
* **Maintenance Nightmare:** Logic is scattered. To understand a test, you have to look in five places.
|
- **Maintenance Nightmare:** Logic is scattered. To understand a test, you have to look in five places.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. The Solution: JSON Configuration 📄
|
## 2. The Solution: JSON Configuration 📄
|
||||||
|
|
||||||
**Strategy:** Treat a Test Definition as a **Document**.
|
**Strategy:** Treat a Test Definition as a **Document**.
|
||||||
|
|
||||||
We will consolidate the variable details (Technique, Calculations, Reference Ranges) into a structured `JSON` column within a single table.
|
We will consolidate the variable details (Technique, Calculations, Reference Ranges) into a structured `JSON` column within a single table.
|
||||||
|
|
||||||
### Schema Change
|
### Schema Change
|
||||||
Old 5 tables $\rightarrow$ **1 Main Table**.
|
Old 5 tables → **1 Main Table**.
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE TABLE LabTestDefinitions (
|
CREATE TABLE LabTestDefinitions (
|
||||||
@ -61,45 +61,73 @@ Instead of columns for every possible biological variable, we store a flexible d
|
|||||||
"specimen": "Serum",
|
"specimen": "Serum",
|
||||||
"result_type": "NUMERIC",
|
"result_type": "NUMERIC",
|
||||||
"units": "mg/dL",
|
"units": "mg/dL",
|
||||||
"formulas": {
|
|
||||||
"calculation": "primary_result * dilution_factor"
|
|
||||||
},
|
|
||||||
"reference_ranges": [
|
"reference_ranges": [
|
||||||
{
|
{ "sex": "M", "min": 70, "max": 100 },
|
||||||
"label": "Adult Male",
|
{ "sex": "F", "min": 60, "max": 90 }
|
||||||
"sex": "M",
|
|
||||||
"min_age": 18,
|
|
||||||
"max_age": 99,
|
|
||||||
"min_val": 70,
|
|
||||||
"max_val": 100
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Pediatric",
|
|
||||||
"max_age": 18,
|
|
||||||
"min_val": 60,
|
|
||||||
"max_val": 90
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. The Benefits 🏆
|
## 3. How to Query (The Magic) 🪄
|
||||||
|
|
||||||
|
This is where the new design shines. No more joins.
|
||||||
|
|
||||||
|
### A. Fetching a Test (The Usual Way)
|
||||||
|
|
||||||
|
Just select the row. The application gets the full definition instantly.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT * FROM LabTestDefinitions WHERE code = 'GLUC';
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. Searching Inside JSON (The Cool Way)
|
||||||
|
|
||||||
|
Need to find all tests that use "Serum"? Use the JSON arrow operator (`->>`).
|
||||||
|
|
||||||
|
**MySQL / MariaDB:**
|
||||||
|
```sql
|
||||||
|
SELECT code, name
|
||||||
|
FROM LabTestDefinitions
|
||||||
|
WHERE configuration->>'$.specimen' = 'Serum';
|
||||||
|
```
|
||||||
|
|
||||||
|
**PostgreSQL:**
|
||||||
|
```sql
|
||||||
|
SELECT code, name
|
||||||
|
FROM LabTestDefinitions
|
||||||
|
WHERE configuration->>'specimen' = 'Serum';
|
||||||
|
```
|
||||||
|
|
||||||
|
### C. Performance Optimization 🏎️
|
||||||
|
|
||||||
|
If we search by "Technique" often, we don't index the JSON string. We add a Generated Column.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE LabTestDefinitions
|
||||||
|
ADD COLUMN technique_virtual VARCHAR(50)
|
||||||
|
GENERATED ALWAYS AS (configuration->>'$.technique') VIRTUAL;
|
||||||
|
|
||||||
|
CREATE INDEX idx_technique ON LabTestDefinitions(technique_virtual);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result:** Querying the JSON is now as fast as a normal column.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. The Benefits 🏆
|
||||||
|
|
||||||
| Feature | Old Way (Relational) | New Way (JSON Document) |
|
| Feature | Old Way (Relational) | New Way (JSON Document) |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| **Fetch Speed** | Slow (4+ Joins) | Instant (1 Row Select) |
|
| **Fetch Speed** | Slow (4+ Joins) | Instant (1 Row Select) |
|
||||||
| **Flexibility** | Requires ALTER TABLE | Edit JSON & Save |
|
| **Flexibility** | Requires `ALTER TABLE` | Edit JSON & Save |
|
||||||
| **Search** | Complex SQL | Fast JSON Indexing |
|
| **Search** | Complex SQL | Fast JSON Operators |
|
||||||
| **Code Logic** | Mapping 5 SQL results | `json_decode()` → Object |
|
| **Code Logic** | Mapping 5 SQL results | `json_decode()` → Object |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Next Steps 🗒️
|
## Next Steps
|
||||||
|
|
||||||
- [ ] Create migration for `LabTestDefinitions` table.
|
1. Create migration for `LabTestDefinitions` table.
|
||||||
- [ ] Port 5 sample tests from the old structure to JSON format for verification.
|
2. Port 5 sample tests from the old structure to JSON format for verification.
|
||||||
|
|
||||||
---
|
|
||||||
_Last updated: 2026-01-09 08:40:21_
|
|
||||||
@ -7,40 +7,105 @@ date: 2026-01-09
|
|||||||
order: 2
|
order: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
# 🚀 Proposal: Valueset ("God Table") Replacement by Gemini 3
|
# 🚀 Proposal: Valueset ("God Table") Replacement
|
||||||
|
|
||||||
|
|
||||||
**Target:** `valueset` / `valuesetdef` Tables
|
**Target:** `valueset` / `valuesetdef` Tables
|
||||||
**Objective:** Remove "Magic Numbers," enforce Type Safety, and optimize Frontend performance.
|
**Objective:** Eliminate "Magic Numbers," enforce Type Safety, and optimize Frontend performance by moving system logic into the codebase.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. The Problem: "Magic Number Soup" 🥣
|
## 1. The Problem: "Magic Number Soup" 🥣
|
||||||
|
|
||||||
**Current Status:**
|
**Current Status:**
|
||||||
We store disparate system logic (Gender, Test Status, Colors, Payment Types) in a single massive table called `valueset`.
|
We store disparate logic (Gender, Test Status, Specimen Types, Priority) in a single massive table called `valueset`.
|
||||||
* **Code relies on IDs:** `if ($status == 1045) ...`
|
|
||||||
* **Frontend Overload:** Frontend makes frequent DB calls just to populate simple dropdowns.
|
* **Code relies on IDs:** Developers must remember that `1045` means `VERIFIED`. `if ($status == 1045)` is unreadable.
|
||||||
* **No Type Safety:** Nothing stops a developer from assigning a "Payment Status" ID to a "Gender" column.
|
* **Frontend Overload:** The frontend makes frequent, redundant database calls just to populate simple dropdowns.
|
||||||
|
* **No Type Safety:** Nothing prevents assigning a "Payment Status" ID to a "Gender" column.
|
||||||
|
* **Invisible History:** Database changes are hard to track. Who changed "Verified" to "Authorized"? When? Why?
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. The Solution: Enums & API Store 🛠️
|
## 2. The Solution: Single Source of Truth (SSOT) 🛠️
|
||||||
|
|
||||||
**Strategy:** Split "System Logic" from the Database.
|
**Strategy:** Move "System Logic" from the Database into the Code. This creates a "Single Source of Truth."
|
||||||
Use **PHP 8.1 Native Enums** for business rules and serve them via a cached API to Svelte.
|
|
||||||
|
|
||||||
### Step A: The Backend (PHP Enums)
|
### Step A: The Backend Implementation
|
||||||
We delete the rows from the database and define them in code where they belong.
|
|
||||||
|
|
||||||
|
#### Option 1: The "God File" (Simple & Legacy Friendly)
|
||||||
|
**File:** `application/libraries/Valuesets.php`
|
||||||
|
|
||||||
|
This file holds every dropdown option in the system. Benefits include **Ctrl+Click** navigation and clear **Git History**.
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||||
|
|
||||||
|
class Valuesets {
|
||||||
|
|
||||||
|
// 1. Gender Definitions
|
||||||
|
const GENDER = [
|
||||||
|
'M' => 'Male',
|
||||||
|
'F' => 'Female',
|
||||||
|
'U' => 'Unknown'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 2. Test Status
|
||||||
|
const TEST_STATUS = [
|
||||||
|
'PENDING' => 'Waiting for Results',
|
||||||
|
'IN_PROCESS' => 'Analyzing',
|
||||||
|
'VERIFIED' => 'Verified & Signed',
|
||||||
|
'REJECTED' => 'Sample Rejected'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 3. Sample Types
|
||||||
|
const SPECIMEN_TYPE = [
|
||||||
|
'SERUM' => 'Serum',
|
||||||
|
'PLASMA' => 'Plasma',
|
||||||
|
'URINE' => 'Urine',
|
||||||
|
'WB' => 'Whole Blood'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 4. Urgency
|
||||||
|
const PRIORITY = [
|
||||||
|
'ROUTINE' => 'Routine',
|
||||||
|
'CITO' => 'Cito (Urgent)',
|
||||||
|
'STAT' => 'Immediate (Life Threatening)'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get all constants as one big JSON object.
|
||||||
|
* Useful for sending everything to Svelte in one go.
|
||||||
|
*/
|
||||||
|
public static function getAll() {
|
||||||
|
return [
|
||||||
|
'gender' => self::mapForFrontend(self::GENDER),
|
||||||
|
'test_status' => self::mapForFrontend(self::TEST_STATUS),
|
||||||
|
'specimen_type' => self::mapForFrontend(self::SPECIMEN_TYPE),
|
||||||
|
'priority' => self::mapForFrontend(self::PRIORITY),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to format array like: [{value: 'M', label: 'Male'}, ...]
|
||||||
|
private static function mapForFrontend($array) {
|
||||||
|
$result = [];
|
||||||
|
foreach ($array as $key => $label) {
|
||||||
|
$result[] = ['value' => $key, 'label' => $label];
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option 2: PHP 8.1+ Enums (Modern Approach)
|
||||||
**File:** `App/Enums/TestStatus.php`
|
**File:** `App/Enums/TestStatus.php`
|
||||||
|
|
||||||
```php
|
```php
|
||||||
enum TestStatus: string {
|
enum TestStatus: string {
|
||||||
case PENDING = 'PENDING';
|
case PENDING = 'PENDING';
|
||||||
case VERIFIED = 'VERIFIED';
|
case VERIFIED = 'VERIFIED';
|
||||||
case REJECTED = 'REJECTED';
|
case REJECTED = 'REJECTED';
|
||||||
|
|
||||||
// Helper for Frontend Labels
|
|
||||||
public function label(): string {
|
public function label(): string {
|
||||||
return match($this) {
|
return match($this) {
|
||||||
self::PENDING => 'Waiting for Results',
|
self::PENDING => 'Waiting for Results',
|
||||||
@ -51,39 +116,42 @@ enum TestStatus: string {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step B: The API Contract
|
---
|
||||||
|
|
||||||
**GET `/api/config/valueset`**
|
## 3. The "Aha!" Moment (Usage) 🔌
|
||||||
Instead of 20 small network requests for 20 dropdowns, the Frontend requests the entire dictionary once on load.
|
|
||||||
|
|
||||||
**Response:**
|
### 1. In PHP Logic
|
||||||
```json
|
No more querying the DB to check if a status is valid. Logic is now instant and readable.
|
||||||
{
|
|
||||||
"test_status": [
|
```php
|
||||||
{ "value": "PENDING", "label": "Waiting for Results" },
|
// ❌ Old Way (Sick) 🤮
|
||||||
{ "value": "VERIFIED", "label": "Verified & Signed" }
|
// $status = $this->db->get_where('valuesetdef', ['id' => 505])->row();
|
||||||
],
|
if ($status_id == 505) { ... }
|
||||||
"gender": [
|
|
||||||
{ "value": "M", "label": "Male" },
|
// ✅ New Way (Clean) 😎
|
||||||
{ "value": "F", "label": "Female" }
|
if ($input_status === 'VERIFIED') {
|
||||||
]
|
// We know exactly what this string means.
|
||||||
|
send_email_to_patient();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation is instant
|
||||||
|
if (!array_key_exists($input_gender, Valuesets::GENDER)) {
|
||||||
|
die("Invalid Gender!");
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step C: The Frontend (Svelte Store)
|
### 2. In Svelte (Frontend - Svelte 5 Runes)
|
||||||
|
We use a global state (rune) to store this configuration. The frontend engineers no longer need to check database IDs; they just use the human-readable keys.
|
||||||
|
|
||||||
We use a Svelte Store to cache this data globally. No more SQL queries for dropdowns.
|
|
||||||
|
|
||||||
**Component Usage:**
|
|
||||||
```svelte
|
```svelte
|
||||||
{% raw %}
|
{% raw %}
|
||||||
<script>
|
<script>
|
||||||
import { config } from '../stores/configStore';
|
import { config } from '../stores/config.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<label>Status:</label>
|
<label>Specimen Type:</label>
|
||||||
<select bind:value={status}>
|
<select bind:value={specimen}>
|
||||||
{#each $config.test_status as option}
|
{#each config.valueset.specimen_type as option}
|
||||||
<option value={option.value}>{option.label}</option>
|
<option value={option.value}>{option.label}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
@ -92,22 +160,24 @@ We use a Svelte Store to cache this data globally. No more SQL queries for dropd
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. The Benefits 🏆
|
## 4. Why This is Better 🏆
|
||||||
|
|
||||||
| Feature | Old Way (valueset Table) | New Way (Enums + Store) |
|
| Feature | Old Way (`valueset` Table) | New Way (The God File) |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| **Performance** | DB Query per dropdown | Zero DB Hits (Cached) |
|
| **Performance** | DB Query per dropdown | **Millions of times faster** (Constant) |
|
||||||
| **Code Quality** | `if ($id == 505)` | `if ($s == Status::PENDING)` |
|
| **Readability** | `if ($id == 505)` | `if ($status == 'REJECTED')` |
|
||||||
| **Reliability** | IDs can change/break | Code is immutable |
|
| **Navigation** | Search DB rows | **Ctrl+Click** in VS Code |
|
||||||
| **Network** | "Chatty" (Many requests) | Efficient (One request) |
|
| **Git History** | Opaque/Invisible | **Transparent** (See who changed what) |
|
||||||
|
| **Reliability** | IDs can change/break | Constants are immutable |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Next Steps 🗒️
|
## 5. Next Steps 🗒️
|
||||||
|
|
||||||
- [ ] Define `TestStatus` and `TestType` Enums in PHP.
|
- [ ] Implement `Valuesets.php` in the libraries folder.
|
||||||
- [ ] Create the `/api/config/valueset` endpoint.
|
- [ ] Create a controller to expose `Valuesets::getAll()` as JSON.
|
||||||
- [ ] Update one Svelte form to use the new Store instead of an API fetch.
|
- [ ] Update the Svelte `configStore` to fetch this dictionary on app load.
|
||||||
|
- [ ] Refactor the Patient Registration form to use the new constants.
|
||||||
|
|
||||||
---
|
---
|
||||||
_Last updated: 2026-01-09 08:40:21_
|
_Last updated: 2026-01-09_
|
||||||
|
|||||||
61
src/projects/clqms01/suggestion/003-decision-001-002.md
Normal file
61
src/projects/clqms01/suggestion/003-decision-001-002.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
layout: clqms-post.njk
|
||||||
|
tags: clqms
|
||||||
|
title: "Backend Dev: Implementation Plan"
|
||||||
|
description: "Consolidated developer review for suggestions 001 and 002"
|
||||||
|
date: 2026-01-09
|
||||||
|
order: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🛠️ Backend Dev: Decision for 001 & 002
|
||||||
|
|
||||||
|
This document summarizes the technical decision and implementation plan based on the previous proposals:
|
||||||
|
- [001-testdef.md](./001-testdef.md) (Test Definition Architecture)
|
||||||
|
- [002-valueset.md](./002-valueset.md) (Valueset Replacement)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Suggestion 001: Test Definition Architecture
|
||||||
|
**Action: DEFERRED**
|
||||||
|
|
||||||
|
- We will skip the architectural overhaul of the `testdef` module for now.
|
||||||
|
- Current table structure remains unchanged to maintain stability.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Suggestion 002: Valueset Implementation
|
||||||
|
**Action: ADOPTED (Code-First Migration)**
|
||||||
|
|
||||||
|
We are moving forward with moving system constants from the database into the codebase, but with a focus on existing API compatibility.
|
||||||
|
|
||||||
|
### Technical Requirements:
|
||||||
|
- **Maintain Endpoint integrity**: The API endpoint must remain functional without changes to the frontend.
|
||||||
|
- **Data Migration**: Move values (Gender, Status, etc.) from the `valueset` table into a centralized PHP constant or config file named `Valuesets.php`.
|
||||||
|
- **Controller Refactor**: Update the backend controller to serve the JSON response from the file rather than a database query.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Translation Logic (Getting labels like "Female" from "F")
|
||||||
|
|
||||||
|
When pulling data from the database, you can translate raw keys into readable labels instantly using the new class.
|
||||||
|
|
||||||
|
### Example PHP Usage:
|
||||||
|
```php
|
||||||
|
// Direct mapping in your controller or view
|
||||||
|
$genderLabel = Valuesets::GENDER[$patient->gender] ?? 'Unknown';
|
||||||
|
|
||||||
|
// Or use a helper method in the Valuesets class
|
||||||
|
public static function getLabel($category, $key) {
|
||||||
|
return self::{$category}[$key] ?? $key;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Implementation Steps
|
||||||
|
1. Create `Valuesets.php` in the backend libraries folder.
|
||||||
|
2. Refactor `Valueset` controller to return data from `Valuesets` class.
|
||||||
|
3. Verify JSON output matches the current schema to ensure no frontend breaks.
|
||||||
|
|
||||||
|
---
|
||||||
|
_Last updated: 2026-01-09 12:47:00_
|
||||||
482
src/projects/clqms01/suggestion/audit-logging-plan.md
Normal file
482
src/projects/clqms01/suggestion/audit-logging-plan.md
Normal file
@ -0,0 +1,482 @@
|
|||||||
|
---
|
||||||
|
layout: clqms-post.njk
|
||||||
|
tags: clqms
|
||||||
|
title: "CLQMS: Audit Logging Architecture Plan"
|
||||||
|
description: "Comprehensive audit trail strategy for tracking changes across master data, patient records, and laboratory operations"
|
||||||
|
date: 2026-02-19
|
||||||
|
order: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
# Audit Logging Architecture Plan for CLQMS
|
||||||
|
|
||||||
|
> **Clinical Laboratory Quality Management System (CLQMS)** - A comprehensive audit trail strategy for tracking changes across master data, patient records, and laboratory operations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This document outlines a unified audit logging architecture for CLQMS, designed to provide complete traceability of data changes while maintaining optimal performance and maintainability. The approach separates audit logs into three domain-specific tables, utilizing JSON for flexible value storage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Current State Analysis
|
||||||
|
|
||||||
|
### Existing Audit Infrastructure
|
||||||
|
|
||||||
|
| Aspect | Current Status |
|
||||||
|
|--------|---------------|
|
||||||
|
| **Database Tables** | 3 tables exist in migrations (patreglog, patvisitlog, specimenlog) |
|
||||||
|
| **Implementation** | Tables created but not actively used |
|
||||||
|
| **Structure** | Fixed column approach (FldName, FldValuePrev) |
|
||||||
|
| **Code Coverage** | No models or controllers implemented |
|
||||||
|
| **Application Logging** | Basic CodeIgniter file logging for debug/errors |
|
||||||
|
|
||||||
|
### Pain Points Identified
|
||||||
|
|
||||||
|
- ❌ **3 separate tables** with nearly identical schemas
|
||||||
|
- ❌ **Fixed column structure** - rigid and requires schema changes for new entities
|
||||||
|
- ❌ **No implementation** - audit tables exist but aren't populated
|
||||||
|
- ❌ **Maintenance overhead** - adding new entities requires new migrations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Proposed Architecture
|
||||||
|
|
||||||
|
### 2.1 Domain Separation
|
||||||
|
|
||||||
|
We categorize audit logs by **data domain** and **access patterns**:
|
||||||
|
|
||||||
|
| Table | Domain | Volume | Retention | Use Case |
|
||||||
|
|-------|--------|--------|-----------|----------|
|
||||||
|
| `master_audit_log` | Reference Data | Low | Permanent | Organizations, Users, ValueSets |
|
||||||
|
| `patient_audit_log` | Patient Records | Medium | 7 years | Demographics, Contacts, Insurance |
|
||||||
|
| `order_audit_log` | Operations | High | 2 years | Orders, Tests, Specimens, Results |
|
||||||
|
|
||||||
|
### 2.2 Unified Table Structure
|
||||||
|
|
||||||
|
#### Master Audit Log
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE master_audit_log (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
entity_type VARCHAR(50) NOT NULL, -- 'organization', 'user', 'valueset'
|
||||||
|
entity_id VARCHAR(36) NOT NULL, -- UUID or primary key
|
||||||
|
action ENUM('CREATE', 'UPDATE', 'DELETE', 'PATCH') NOT NULL,
|
||||||
|
|
||||||
|
old_values JSON NULL, -- Complete snapshot before change
|
||||||
|
new_values JSON NULL, -- Complete snapshot after change
|
||||||
|
changed_fields JSON, -- Array of modified field names
|
||||||
|
|
||||||
|
-- Context
|
||||||
|
user_id VARCHAR(36),
|
||||||
|
site_id VARCHAR(36),
|
||||||
|
ip_address VARCHAR(45),
|
||||||
|
user_agent VARCHAR(500),
|
||||||
|
app_version VARCHAR(20),
|
||||||
|
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
INDEX idx_entity (entity_type, entity_id),
|
||||||
|
INDEX idx_created (created_at),
|
||||||
|
INDEX idx_user (user_id, created_at)
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Patient Audit Log
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE patient_audit_log (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
entity_type VARCHAR(50) NOT NULL, -- 'patient', 'contact', 'insurance'
|
||||||
|
entity_id VARCHAR(36) NOT NULL,
|
||||||
|
patient_id VARCHAR(36), -- Context FK for patient
|
||||||
|
|
||||||
|
action ENUM('CREATE', 'UPDATE', 'DELETE', 'MERGE', 'UNMERGE') NOT NULL,
|
||||||
|
|
||||||
|
old_values JSON NULL,
|
||||||
|
new_values JSON NULL,
|
||||||
|
changed_fields JSON,
|
||||||
|
reason TEXT, -- Why the change was made
|
||||||
|
|
||||||
|
-- Context
|
||||||
|
user_id VARCHAR(36),
|
||||||
|
site_id VARCHAR(36),
|
||||||
|
ip_address VARCHAR(45),
|
||||||
|
session_id VARCHAR(100),
|
||||||
|
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
INDEX idx_entity (entity_type, entity_id),
|
||||||
|
INDEX idx_patient (patient_id, created_at),
|
||||||
|
INDEX idx_created (created_at),
|
||||||
|
INDEX idx_user (user_id, created_at)
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Order/Test Audit Log
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE order_audit_log (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
entity_type VARCHAR(50) NOT NULL, -- 'order', 'test', 'specimen', 'result'
|
||||||
|
entity_id VARCHAR(36) NOT NULL,
|
||||||
|
|
||||||
|
-- Context FKs
|
||||||
|
patient_id VARCHAR(36),
|
||||||
|
visit_id VARCHAR(36),
|
||||||
|
order_id VARCHAR(36),
|
||||||
|
|
||||||
|
action ENUM('CREATE', 'UPDATE', 'DELETE', 'CANCEL', 'REORDER', 'COLLECT', 'RESULT') NOT NULL,
|
||||||
|
|
||||||
|
old_values JSON NULL,
|
||||||
|
new_values JSON NULL,
|
||||||
|
changed_fields JSON,
|
||||||
|
status_transition VARCHAR(100), -- e.g., 'pending->collected'
|
||||||
|
|
||||||
|
-- Context
|
||||||
|
user_id VARCHAR(36),
|
||||||
|
site_id VARCHAR(36),
|
||||||
|
device_id VARCHAR(36), -- Instrument/edge device
|
||||||
|
ip_address VARCHAR(45),
|
||||||
|
session_id VARCHAR(100),
|
||||||
|
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
INDEX idx_entity (entity_type, entity_id),
|
||||||
|
INDEX idx_order (order_id, created_at),
|
||||||
|
INDEX idx_patient (patient_id, created_at),
|
||||||
|
INDEX idx_created (created_at),
|
||||||
|
INDEX idx_user (user_id, created_at)
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. JSON Value Structure
|
||||||
|
|
||||||
|
### Example Audit Entry
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 15243,
|
||||||
|
"entity_type": "patient",
|
||||||
|
"entity_id": "PAT-2026-001234",
|
||||||
|
"action": "UPDATE",
|
||||||
|
|
||||||
|
"old_values": {
|
||||||
|
"NameFirst": "John",
|
||||||
|
"NameLast": "Doe",
|
||||||
|
"Gender": "M",
|
||||||
|
"BirthDate": "1990-01-15",
|
||||||
|
"Phone": "+1-555-0100"
|
||||||
|
},
|
||||||
|
|
||||||
|
"new_values": {
|
||||||
|
"NameFirst": "Johnny",
|
||||||
|
"NameLast": "Doe-Smith",
|
||||||
|
"Gender": "M",
|
||||||
|
"BirthDate": "1990-01-15",
|
||||||
|
"Phone": "+1-555-0199"
|
||||||
|
},
|
||||||
|
|
||||||
|
"changed_fields": ["NameFirst", "NameLast", "Phone"],
|
||||||
|
|
||||||
|
"user_id": "USR-001",
|
||||||
|
"site_id": "SITE-001",
|
||||||
|
"created_at": "2026-02-19T14:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Benefits of JSON Approach
|
||||||
|
|
||||||
|
✅ **Schema Evolution** - Add new fields without migrations
|
||||||
|
✅ **Complete Snapshots** - Reconstruct full record state at any point
|
||||||
|
✅ **Flexible Queries** - MySQL 8.0+ supports JSON indexing and extraction
|
||||||
|
✅ **Audit Integrity** - Store exactly what changed, no data loss
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Implementation Strategy
|
||||||
|
|
||||||
|
### 4.1 Central Audit Service
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
class AuditService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Log an audit event to the appropriate table
|
||||||
|
*/
|
||||||
|
public static function log(
|
||||||
|
string $category, // 'master', 'patient', 'order'
|
||||||
|
string $entityType, // e.g., 'patient', 'order'
|
||||||
|
string $entityId,
|
||||||
|
string $action,
|
||||||
|
?array $oldValues = null,
|
||||||
|
?array $newValues = null,
|
||||||
|
?string $reason = null,
|
||||||
|
?array $context = null
|
||||||
|
): void {
|
||||||
|
$changedFields = self::calculateChangedFields($oldValues, $newValues);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'entity_type' => $entityType,
|
||||||
|
'entity_id' => $entityId,
|
||||||
|
'action' => $action,
|
||||||
|
'old_values' => $oldValues ? json_encode($oldValues) : null,
|
||||||
|
'new_values' => $newValues ? json_encode($newValues) : null,
|
||||||
|
'changed_fields' => json_encode($changedFields),
|
||||||
|
'user_id' => auth()->id() ?? 'SYSTEM',
|
||||||
|
'site_id' => session('site_id') ?? 'MAIN',
|
||||||
|
'created_at' => date('Y-m-d H:i:s')
|
||||||
|
];
|
||||||
|
|
||||||
|
// Route to appropriate table
|
||||||
|
$table = match($category) {
|
||||||
|
'master' => 'master_audit_log',
|
||||||
|
'patient' => 'patient_audit_log',
|
||||||
|
'order' => 'order_audit_log',
|
||||||
|
default => throw new \InvalidArgumentException("Unknown category: $category")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Async logging recommended for high-volume operations
|
||||||
|
self::dispatchAuditJob($table, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function calculateChangedFields(?array $old, ?array $new): array
|
||||||
|
{
|
||||||
|
if (!$old || !$new) return [];
|
||||||
|
|
||||||
|
$changes = [];
|
||||||
|
$allKeys = array_unique(array_merge(array_keys($old), array_keys($new)));
|
||||||
|
|
||||||
|
foreach ($allKeys as $key) {
|
||||||
|
if (($old[$key] ?? null) !== ($new[$key] ?? null)) {
|
||||||
|
$changes[] = $key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $changes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Model Integration
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Services\AuditService;
|
||||||
|
|
||||||
|
class PatientModel extends BaseModel
|
||||||
|
{
|
||||||
|
protected $table = 'patients';
|
||||||
|
protected $primaryKey = 'PatientID';
|
||||||
|
|
||||||
|
protected function logAudit(
|
||||||
|
string $action,
|
||||||
|
?array $oldValues = null,
|
||||||
|
?array $newValues = null
|
||||||
|
): void {
|
||||||
|
AuditService::log(
|
||||||
|
category: 'patient',
|
||||||
|
entityType: 'patient',
|
||||||
|
entityId: $this->getPatientId(),
|
||||||
|
action: $action,
|
||||||
|
oldValues: $oldValues,
|
||||||
|
newValues: $newValues
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override save method to auto-log
|
||||||
|
public function save($data): bool
|
||||||
|
{
|
||||||
|
$oldData = $this->find($data['PatientID'] ?? null);
|
||||||
|
|
||||||
|
$result = parent::save($data);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
$this->logAudit(
|
||||||
|
$oldData ? 'UPDATE' : 'CREATE',
|
||||||
|
$oldData?->toArray(),
|
||||||
|
$this->find($data['PatientID'])->toArray()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Query Patterns & Performance
|
||||||
|
|
||||||
|
### 5.1 Common Queries
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- View entity history
|
||||||
|
SELECT * FROM patient_audit_log
|
||||||
|
WHERE entity_type = 'patient'
|
||||||
|
AND entity_id = 'PAT-2026-001234'
|
||||||
|
ORDER BY created_at DESC;
|
||||||
|
|
||||||
|
-- User activity report
|
||||||
|
SELECT entity_type, action, COUNT(*) as count
|
||||||
|
FROM patient_audit_log
|
||||||
|
WHERE user_id = 'USR-001'
|
||||||
|
AND created_at > DATE_SUB(NOW(), INTERVAL 7 DAY)
|
||||||
|
GROUP BY entity_type, action;
|
||||||
|
|
||||||
|
-- Find all changes to a specific field
|
||||||
|
SELECT * FROM order_audit_log
|
||||||
|
WHERE JSON_CONTAINS(changed_fields, '"result_value"')
|
||||||
|
AND patient_id = 'PAT-001'
|
||||||
|
AND created_at > '2026-01-01';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Partitioning Strategy (Order/Test)
|
||||||
|
|
||||||
|
For high-volume tables, implement monthly partitioning:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE order_audit_log (
|
||||||
|
-- ... columns
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB
|
||||||
|
PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at)) (
|
||||||
|
PARTITION p202601 VALUES LESS THAN (202602),
|
||||||
|
PARTITION p202602 VALUES LESS THAN (202603),
|
||||||
|
PARTITION p_future VALUES LESS THAN MAXVALUE
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Soft Delete Handling
|
||||||
|
|
||||||
|
Soft deletes ARE captured as audit entries with complete snapshots:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// When soft deleting a patient:
|
||||||
|
AuditService::log(
|
||||||
|
category: 'patient',
|
||||||
|
entityType: 'patient',
|
||||||
|
entityId: $patientId,
|
||||||
|
action: 'DELETE',
|
||||||
|
oldValues: $fullRecordBeforeDelete, // Complete last known state
|
||||||
|
newValues: null,
|
||||||
|
reason: 'Patient requested data removal'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures:
|
||||||
|
- ✅ Full audit trail even for deleted records
|
||||||
|
- ✅ Compliance with "right to be forgotten" (GDPR)
|
||||||
|
- ✅ Ability to restore accidentally deleted records
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Migration Plan
|
||||||
|
|
||||||
|
### Phase 1: Foundation (Week 1)
|
||||||
|
- [ ] Drop existing unused tables (patreglog, patvisitlog, specimenlog)
|
||||||
|
- [ ] Create new audit tables with JSON columns
|
||||||
|
- [ ] Create AuditService class
|
||||||
|
- [ ] Add database indexes
|
||||||
|
|
||||||
|
### Phase 2: Core Implementation (Week 2)
|
||||||
|
- [ ] Integrate AuditService into Patient model
|
||||||
|
- [ ] Integrate AuditService into Order model
|
||||||
|
- [ ] Integrate AuditService into Master data models
|
||||||
|
- [ ] Add audit trail to authentication events
|
||||||
|
|
||||||
|
### Phase 3: API & UI (Week 3)
|
||||||
|
- [ ] Create API endpoints for querying audit logs
|
||||||
|
- [ ] Build admin interface for audit review
|
||||||
|
- [ ] Add audit export functionality (CSV/PDF)
|
||||||
|
|
||||||
|
### Phase 4: Optimization (Week 4)
|
||||||
|
- [ ] Implement async logging queue
|
||||||
|
- [ ] Add table partitioning for order_audit_log
|
||||||
|
- [ ] Set up retention policies and archiving
|
||||||
|
- [ ] Performance testing and tuning
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Retention & Archiving Strategy
|
||||||
|
|
||||||
|
| Table | Retention Period | Archive Action |
|
||||||
|
|-------|---------------|----------------|
|
||||||
|
| `master_audit_log` | Permanent | None (keep forever) |
|
||||||
|
| `patient_audit_log` | 7 years | Move to cold storage after 7 years |
|
||||||
|
| `order_audit_log` | 2 years | Partition rotation: drop old partitions |
|
||||||
|
|
||||||
|
### Automated Maintenance
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Monthly job: Archive old patient audit logs
|
||||||
|
INSERT INTO patient_audit_log_archive
|
||||||
|
SELECT * FROM patient_audit_log
|
||||||
|
WHERE created_at < DATE_SUB(NOW(), INTERVAL 7 YEAR);
|
||||||
|
|
||||||
|
DELETE FROM patient_audit_log
|
||||||
|
WHERE created_at < DATE_SUB(NOW(), INTERVAL 7 YEAR);
|
||||||
|
|
||||||
|
-- Monthly job: Drop old order partitions
|
||||||
|
ALTER TABLE order_audit_log DROP PARTITION p202501;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Questions for Stakeholders
|
||||||
|
|
||||||
|
Before implementation, please confirm:
|
||||||
|
|
||||||
|
1. **Retention Policy**: Are the proposed retention periods (master=forever, patient=7 years, order=2 years) compliant with your regulatory requirements?
|
||||||
|
|
||||||
|
2. **Async vs Sync**: Should audit logging be synchronous (block on failure) or asynchronous (queue-based)? Recommended: async for order/test operations.
|
||||||
|
|
||||||
|
3. **Archive Storage**: Where should archived audit logs be stored? Options: separate database, file storage (S3), or compressed tables.
|
||||||
|
|
||||||
|
4. **User Access**: Which user roles need access to audit trails? Should users see their own audit history?
|
||||||
|
|
||||||
|
5. **Compliance**: Do you need specific compliance features (e.g., HIPAA audit trail requirements, 21 CFR Part 11 for FDA)?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Key Design Decisions Summary
|
||||||
|
|
||||||
|
| Decision | Choice | Rationale |
|
||||||
|
|----------|--------|-----------|
|
||||||
|
| **Table Count** | 3 tables | Separates concerns, optimizes queries, different retention |
|
||||||
|
| **JSON vs Columns** | JSON for values | Flexible, handles schema changes, complete snapshots |
|
||||||
|
| **Full vs Diff** | Full snapshots | Easier to reconstruct history, no data loss |
|
||||||
|
| **Soft Deletes** | Captured in audit | Compliance and restore capability |
|
||||||
|
| **Partitioning** | Order table only | High volume, time-based queries |
|
||||||
|
| **Async Logging** | Recommended | Don't block user operations |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This unified audit logging architecture provides:
|
||||||
|
|
||||||
|
✅ **Complete traceability** across all data domains
|
||||||
|
✅ **Regulatory compliance** with proper retention
|
||||||
|
✅ **Performance optimization** through domain separation
|
||||||
|
✅ **Flexibility** via JSON value storage
|
||||||
|
✅ **Maintainability** with centralized service
|
||||||
|
|
||||||
|
The approach balances audit integrity with system performance, ensuring CLQMS can scale while maintaining comprehensive audit trails.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Document Version: 1.0*
|
||||||
|
*Author: CLQMS Development Team*
|
||||||
|
*Date: February 19, 2026*
|
||||||
@ -35,6 +35,14 @@ When documenting suggestions, please include:
|
|||||||
|
|
||||||
## Suggestion List
|
## Suggestion List
|
||||||
|
|
||||||
|
### [High] Backend Dev: Implementation Plan
|
||||||
|
**Description:** Consolidated developer review for proposals 001 and 002.
|
||||||
|
[View Plan](./003-decision-001-002.md)
|
||||||
|
|
||||||
|
### [High] Audit Logging Architecture Plan
|
||||||
|
**Description:** Comprehensive audit trail strategy for tracking changes across master data, patient records, and laboratory operations.
|
||||||
|
[View Plan](./audit-logging-plan.md)
|
||||||
|
|
||||||
### [TBD] Add new suggestions here
|
### [TBD] Add new suggestions here
|
||||||
|
|
||||||
<!-- Use the format below for each suggestion:
|
<!-- Use the format below for each suggestion:
|
||||||
|
|||||||
14
src/team.njk
14
src/team.njk
@ -4,17 +4,17 @@ title: The 5Panda Team
|
|||||||
description: Meet the innovative minds behind 5Panda.
|
description: Meet the innovative minds behind 5Panda.
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="hero-gradient min-h-[35vh] flex items-center justify-center relative overflow-hidden">
|
<div class="hero-solid min-h-[35vh] flex items-center justify-center relative overflow-hidden">
|
||||||
<div class="text-center z-10 px-4">
|
<div class="text-center z-10 px-4">
|
||||||
<h1 class="text-5xl md:text-7xl font-bold mb-4">Meet the
|
<h1 class="text-4xl md:text-6xl font-bold mb-4">Meet the
|
||||||
<span class="gradient-text">Squad</span>
|
<span class="text-primary">Squad</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-xl text-base-content/70">The pandas building the future.</p>
|
<p class="text-lg text-base-content/70">The pandas building the future.</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- Background bubbles -->
|
<!-- Background decoration -->
|
||||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
<div class="absolute top-10 left-10 w-40 h-40 bg-primary/20 rounded-full blur-3xl animate-float"></div>
|
<div class="absolute top-10 left-10 w-40 h-40 bg-primary/10 rounded-full blur-3xl"></div>
|
||||||
<div class="absolute bottom-10 right-10 w-40 h-40 bg-secondary/20 rounded-full blur-3xl animate-float animate-delay-200"></div>
|
<div class="absolute bottom-10 right-10 w-40 h-40 bg-secondary/10 rounded-full blur-3xl"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section
|
<section
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user