KESS ESG
Reference Guide

This document covers every configurable part of the KESS ESG Assessment 2026 system — project modes, API endpoints, user types, schema wiring, and step-by-step instructions for common changes.

Architecture

The system is a single-page HTML application (no framework) backed by PHP API endpoints. All state lives in window.* globals. Three JS libraries handle rendering, upload, and panel UI.

LayerWhat it does
gen59__1_.htmlApplication shell — config, page logic, API calls, mode control, user auth
gflib02.jsForm renderer — renders schemas into interactive fields, scoring, jumperhelper, QSummary
upl3.jsChunked file upload panel — handles attachment uploads, dedup, progress
microx.jsPanel UI framework — createPanel, addFooterButton, drag, animations
PHP APIsapi622.php · api_keeper.php · aui.php · api_board.php

Files & Roles

FileEdit when you need to…
gen59__1_.html Change project title, dates, mode, API host, flag→template map, user level behaviour, add UI panels
gflib02.js Change form rendering, field types, score logic, jumperhelper, QSummary, scroll-to-field
upl3.js Change upload chunk size, file dedup, allowed file types, upload panel UI
microx.js Change panel animations, footer structure, drag behaviour — rarely needed
api622.php Main form data read/write (bucket + cup answers)
api_keeper.php Keeper slot read/write (answer snapshots per user per slot)
aui.php User table CRUD — list, update, create users
api_board.php Board + board_summary read/write

Hardcode Config Block

All persistent project settings live in one block near the bottom of gen59__1_.html. Search for ▼▼▼ CHANGE THESE VALUES.

📄 gen59__1_.html — ~line 5294
// ── PROJECT IDENTITY ─────────────────────────────────
window.projectTitle    = "ESG Assessment 2026";
//  ↑ Shown in menu title bar and panels

window.projectStart    = "2026-02-25T00:00:00.000Z";
//  ↑ Used by progress clock — when did submission open?
//    Format: ISO 8601 UTC  "YYYY-MM-DDTHH:MM:SS.000Z"

// ── MODE DATES (UTC) ─────────────────────────────────
window.dateExpired     = "2026-04-07T17:00:00.000Z";
//  ↑ 🔴  OPEN → EXPIRED after this moment

window.dateAnnounced   = "2026-03-22T08:00:00.000Z";
//  ↑ 🏆  EXPIRED → ANNOUNCED after this moment

window.dateArchived    = "2026-06-01T00:00:00.000Z";
//  ↑ 📦  ANNOUNCED → ARCHIVED after this moment

// ── MASTER MODE SWITCH ───────────────────────────────
// 'auto'      = auto-resolve from dates above (recommended)
// 'open'      = force OPEN regardless of date
// 'expired'   = force EXPIRED
// 'announced' = force ANNOUNCED (scores visible now)
// 'archived'  = force ARCHIVED (permanent read-only)
window.kessProjectMode = 'auto';

// ── API ───────────────────────────────────────────────
const API_HOST         = "https://q.kunok.com/s9";
const API_PATH         = "api622.php";
const KEEPER_API_PATH  = "api_keeper.php";
const ITEMS_API_PATH   = "aui.php";
const BOARD_API_PATH   = "api_board.php";

// ── FLAG → TEMPLATE MAP ──────────────────────────────
const _flagToTemplate = {
  "1": 18,  // Food Factory       → template 18
  "2": 17,  // Food Collector     → template 17
  "3": 16,  // Non-Food Factory   → template 16
  "4": 15,  // Non-Food Collector → template 15
};
⚠️ Memory vs Source
The ⚙️ Config panel in the UI lets admins change dates/mode at runtime — but those changes are memory-only and reset on page reload. To make changes permanent, use the Config panel's "Copy snippet" button and paste the output into this hardcode block.

Project Modes

The system has four modes that control read/write access, score visibility, and banner display. Mode is resolved once per page load (and whenever _kessApplyMode() is called).

Mode Flow

🟢 OPEN
now < dateExpired
⛔ EXPIRED
≥ dateExpired
🏆 ANNOUNCED
≥ dateAnnounced
📦 ARCHIVED
≥ dateArchived
ModeRead-only?Score visible?Banner shown?Save button?
🟢 open ❌ No — editable❌ No (admin can see)Hidden✅ Active
⛔ expired ✅ Yes — locked❌ No✅ Shown🚫 Disabled
🏆 announced ✅ Yes — locked✅ Yes — forced✅ Shown🚫 Disabled
📦 archived ✅ Yes — locked✅ Yes — forced✅ Shown🚫 Disabled

Auto-Resolution Logic

When kessProjectMode = 'auto' the function _kessResolveMode() determines the mode:

function _kessResolveMode() {
  const manual = (window.kessProjectMode || 'auto').toLowerCase();
  if (manual !== 'auto') return manual; // hard-forced

  const now      = new Date();
  const expired  = window.dateExpired   ? new Date(window.dateExpired)   : null;
  const announce = window.dateAnnounced ? new Date(window.dateAnnounced) : null;
  const archive  = window.dateArchived  ? new Date(window.dateArchived)  : null;

  if (archive  && now >= archive)  return 'archived';
  if (announce && now >= announce) return 'announced';
  if (expired  && now >= expired)  return 'expired';
  return 'open';
}

What Changes Per Mode

CSS body class
  • kess-mode-open
  • kess-mode-expired
  • kess-mode-announced
  • kess-mode-archived
  • Controls input enabled/disabled via CSS selectors
Score visibility
  • open/expired → score mode locked OFF for users
  • announced/archived → score mode forced ON for all
  • Admins (level ≥ 2) always see scores
  • Score menu items hidden in open/expired for users
Banner + overlay
  • open → banner hidden entirely
  • expired → banner shown + one-time overlay
  • announced → banner + score reveal overlay
  • archived → banner shown, no overlay

Force a Mode Immediately (in browser)

// Paste in browser console or add to HTML:
window.kessProjectMode = 'announced';
_kessApplyMode();

User Flags & Template IDs

Each user has a flag (stored in localStorage.flag after login). The flag determines which template ID loads for their Assessment form (bucket).

FlagUser TypeTemplate IDForm loaded
"1"Food Factory18bucket (Assessment)
"2"Food Collector17bucket (Assessment)
"3"Non-Food Factory16bucket (Assessment)
"4"Non-Food Collector15bucket (Assessment)
ℹ️ Profile form (cup)
The Profile form uses a fixed template — it does not change by flag. Only the Assessment (bucket) form changes per user type.

Where the map lives

📄 gen59__1_.html — ~line 5330
const _flagToTemplate = { "1": 18, "2": 17, "3": 16, "4": 15 };

Also duplicated in the admin user list (flagMap) and at login resolution (_kessTemplateFromUsercode fallback). Change all three if you add a new flag.

How to Add a New User Type / Flag

Example: adding flag "5" for "Service Provider" with template ID 19.

  1. Update _flagToTemplate in the hardcode block:
    const _flagToTemplate = { "1":18, "2":17, "3":16, "4":15, "5":19 };
  2. Update flagMap in admin user list (~line 1993):
    const flagMap = {'1':18,'2':17,'3':16,'4':15,'5':19};
  3. Update _flagLabel (~line 2069):
    const _flagLabel = { '1':'Food Factory', '2':'Food Collector',
                           '3':'Non-Food Factory', '4':'Non-Food Collector',
                           '5':'Service Provider' };
  4. Create template 19 in your database / schema system.
  5. Ensure api622.php can serve template 19 for that user.
⚠️ Three places to update
The flag label and map appear in at least 3 separate places in the HTML file. Use Find All in your editor to locate each one.

User Levels

Level is stored in localStorage.level after login. It controls UI capabilities independently of mode.

LevelLabelCapabilities
1USER Fill forms, save answers, view own score (only in announced/archived mode). Cannot see admin fields in user info panel, cannot change user category/flag/level.
2ADMIN Everything level 1 has, plus: always sees scores, can view/edit user info panel fully, can switch modes via Config panel, can use "View As" to impersonate users.
3SUPER All of admin. Typically used for super-administrators with full data export access.
4SYS System-level — reserved for automated or system accounts.

Key Level Gates in Code

const _lv = parseInt(localStorage.getItem('level') || '1');

// Score visibility — admin always sees, user only in announced/archived
const _scoreVisible = meta.showScore || _lv >= 2;

// Admin fields in user info panel
if (_lv < 2) { /* hide category, flag, level rows */ }

// Save block — enforced by CSS class on body
// body:not(.kess-mode-open) .panel-footer-save-btn → display:none
// So save button is hidden whenever mode != open, regardless of level

All API Paths

All requests use API_HOST as the base. Change API_HOST to point to a different server.

Current Host Config

const API_HOST        = "https://q.kunok.com/s9";
const API_PATH        = "api622.php";       // main form data
const KEEPER_API_PATH = "api_keeper.php";   // answer snapshots
const ITEMS_API_PATH  = "aui.php";          // user table
const BOARD_API_PATH  = "api_board.php";    // board + summary
ConstantFilePurposeMethods
API_PATH api622.php Load/save Assessment (bucket) and Profile (cup) form answers. Also loads schema templates. GET (load) · POST (save)
KEEPER_API_PATH api_keeper.php Per-user answer snapshots in named slots: bucket, cup, board. Fast key-value store. GET (load slot) · POST (save slot)
ITEMS_API_PATH aui.php User table: list users, get user by id, update user fields (name, flag, level, board data). GET list · GET single · POST update
BOARD_API_PATH api_board.php Reads and writes the board and board_summary columns for a user. Lighter than full user update. GET · POST

API Details

api622.php — Form Data

// Load template schema
GET  {API_HOST}/api622.php?action=get&templateId={id}&userId={uid}

// Save answers
POST {API_HOST}/api622.php
Body: { action:"save", userId, templateId, slot:"bucket"|"cup", data:{...answers} }

api_keeper.php — Answer Snapshots

// Load a slot
GET  {API_HOST}/api_keeper.php?id={userId}_{slot}
// e.g. id = "user123_bucket"

// Save a slot
POST {API_HOST}/api_keeper.php
Body: { id:"{userId}_{slot}", data:{...answers} }

Slots are: bucket (Assessment answers), cup (Profile answers), board (board summary).

aui.php — User Table

// List users (admin)
GET  {API_HOST}/aui.php?action=list

// Get single user
GET  {API_HOST}/aui.php?action=list&id={userId}

// Update user fields
POST {API_HOST}/aui.php
Body: { action:"update", id, name?, flag?, level?, board?, board_summary? }

api_board.php — Board Data

// Load board for user
GET  {API_HOST}/api_board.php?id={userId}

// Save board
POST {API_HOST}/api_board.php
Body: { id:userId, board:{...}, board_summary:{...} }

Changing the Host

To point to a different server, change only API_HOST in the hardcode block. All four API path constants are relative to it — you do not need to change them unless the PHP filenames also change.

// Example: move to staging server
const API_HOST = "https://staging.example.com/api";

Data Flow

On page load, the sequence is:

  1. Login session sets localStorage: userid, flag, level, user_name
  2. The flag resolves a templateId via _flagToTemplate
  3. api622.php is called → returns the JSON schema for that template, merged with the user's saved answers → stored in window.bucket
  4. Profile schema (fixed template) is loaded → stored in window.cup
  5. genform(divId, schema) renders the schema into the panel
  6. Keeper slot is loaded from api_keeper.php and applied on top via applyKeeper()
  7. _kessApplyMode() resolves and applies the current mode
  8. Live progress, scores, and jumperhelper update via _menuUpdateTracking()

On Save (💾 button)

  1. Extracts answers from schema: extractKeeper(schema)window.keeper.bucket
  2. Posts to api_keeper.php (fast snapshot)
  3. Posts to api_board.php (board summary update)
  4. Optionally posts to api622.php (full save)

Schema Objects

Both forms live as global objects. The schema is a flat key→field map returned by api622.php.

GlobalFormPanel IDDiv IDTemplate source
window.bucket📋 AssessmentbucketPanelgformFlag-mapped template (15–18)
window.cup👤 ProfilecupPanelproFixed profile template
window.boardBoard data from api_board.php
window.keeper{ bucket:{}, cup:{}, board:{} } snapshots

Field object shape

{
  type:       "field",
  data_type:  "radioatt",   // see Field Types below
  label:      { en: "Has anti-corruption policy?", th: "..." },
  required:   true,
  score:      5,             // max points for this field
  weights:    [5, 3, 1],     // per-option score weights
  section:    "governance",
  segment:    ["environmental"],
  value:      null,           // filled in after answers applied
  options:    [               // for radio/select/checkbox types
    { label: { en: "Yes", th: "ใช่" } },
    { label: { en: "No",  th: "ไม่ใช่" } }
  ]
}

Keeper Storage

Keeper is a lightweight answer-snapshot system. It stores only the values (not the full schema) per user per slot, allowing fast save/restore without re-fetching templates.

SlotContainsWhen saved
bucketAssessment field values💾 Save button on Assessment panel
cupProfile field values💾 Save button on Profile panel
boardBoard + board_summaryAny save, also standalone board save

Keeper API key format

// Key = "{userId}_{slot}"
"user123_bucket"
"user123_cup"
"user123_board"

On Load

// Load bucket slot and apply to schema
const answers = await _loadKeeperSlot(userId, "bucket");
applyKeeper(window.bucket, answers);
genform("gform", window.bucket); // re-render with loaded values

Panels

All panels are created by microx.js createPanel(). Form panels are special — they use a move strategy to physically relocate the real DOM node into the panel (preserving all event listeners and IDs).

Panel IDContent IDOpens viaPosition
bucketPanelgformMenu → Assessment / toggleBucketPanel()Left side, slides from CC
cupPanelproMenu → Profile / toggleCupPanel()Right side, CC
summaryPanelsummaryInnerMenu → SummaryTR
dashPaneldashInnerMenu → DashboardBC
keeperPanelkeeperInnerMenu → KeeperBR
menuPanelHamburger / toggleMenu()Left edge

Panel Footer Buttons (Assessment & Profile)

ButtonAlignAction
LeftScroll to top of form
LeftScroll to bottom
LeftToggle all sections open/closed
LeftToggle section headers only
📌LeftPin current scroll position
LeftJump to pinned position
🔒LeftLock/unlock panel drag
💾RightSave answers + keeper + board
📂RightLoad answers from keeper
📊RightOpen dashboard for this form

Footer center shows live progress: 24/25 required (96%) with a colour bar. Updated every time a field changes via _menuUpdateTracking().

Field Types

Defined in gflib02.js. The data_type property of each field controls rendering and answer logic.

data_typeInput UIAnswered whenFile required?
text / stringText inputNon-empty stringNo
textareaTextareaNon-emptyNo
numberNumber inputNon-emptyNo
radioRadio buttonsAny option selectedNo
selectDropdownAny option selectedNo
checkboxCheckboxes≥1 checkedNo
radioattRadio + file uploadOption 0 (Yes): needs file. Option >0 (No/NA): just selectionOnly if index 0 selected
radiotextattRadio + text + fileSame as radioatt + text must be filledOnly if index 0 selected
radiotextRadio + textAny radio selectedNo
checktextCheckboxes + text≥1 checked; text required if "Others" activeNo
checktextattCheckboxes + text + file≥1 checked; if index 0 NOT selected → file+text requiredConditional
attachmentFile upload only≥1 file uploadedYes (always)
sectionSection headerN/AN/A
ℹ️ radioatt index logic
Index 0 means "Yes / has evidence" — the user claims they have something to show, so a file is required. Index > 0 means "No / N/A" — no file needed, selection alone satisfies the field.

Scoring Logic

Scoring is computed live by gflib02.js functions. No server round-trip needed.

Score Modes (window._scoreMode)

ModeValueWhat shows
Off0No score bars
Basic1Score bars, field scores
Super2All scores + weights + debug info

Score Calculation

// Per-field score achieved:
// If field has weights array → sum weights for selected options
// Else if answered → field.score (or 1)
// Else → 0

// Max possible score:
// If field has weights → max(weights)
// Else → field.score || 1

// For required fields with score > 0, the dashboard shows:
// ✅ Score: achieved/max   (answered)
// ❌ Score: 0/max          (unanswered)
// Non-required fields: show file icon only, no score line

QSummary

QSummary(schema) in gflib02.js computes live totals from any schema object. Returns { summary: { totals, segments } }. Used by jumperhelper, dashboard, and footer progress.

Common Change Guide

Change Project Title

📄 gen59__1_.html — hardcode block
window.projectTitle = "ESG Assessment 2027";

The title updates in the menu header automatically via _menuUpdateTracking().


Change Submission Deadline

📄 gen59__1_.html — hardcode block
window.dateExpired = "2027-04-15T17:00:00.000Z"; // UTC

All clocks, timelines, and mode resolution re-read this on each page load. No other changes needed.


Force a Mode Permanently

📄 gen59__1_.html — hardcode block
window.kessProjectMode = 'announced'; // override all date logic

Set back to 'auto' to re-enable date-driven mode.


Change API Server

📄 gen59__1_.html — hardcode block
const API_HOST = "https://newserver.example.com/api";

All four API paths are relative — no other changes needed unless filenames differ.


Add a New Score Mode Behaviour

Score mode is an integer (0/1/2). To add mode 3:

  1. Find setScoreMode() in gen59__1_.html and add a case for 3
  2. Add a button to the score mode panel
  3. In gflib02.js, read window._scoreMode in updateFieldDisplay() and add mode-3 rendering logic

Change Mode Banner Text

📄 gen59__1_.html — _KESS_MODES constant
const _KESS_MODES = {
  announced: {
    banner: '🏆 Results are published — view your score below.',
    // ↑ change this string
  }
};

Add a New Panel to the Dashboard

  1. Add a tab button in the dashInner HTML block
  2. Add a corresponding <div id="myTab-tab" class="tab-content" data-tab="myTab">
  3. Write a renderMyTabDashboard() function
  4. Add calls to it in refreshScoreDashboard() and switchDashboardTab()

Full Checklist: Adding a New User Type

✅ All steps required
Missing any one of these will cause the new flag to silently fall back to the default template.
  1. Create the template in your backend database. Note the new template ID (e.g. 19).
  2. Update hardcode block_flagToTemplate:
    const _flagToTemplate = { "1":18, "2":17, "3":16, "4":15, "5":19 };
  3. Update admin user listflagMap (search const flagMap, appears ~3 times):
    const flagMap = {'1':18, '2':17, '3':16, '4':15, '5':19};
  4. Update flag label (search _flagLabel, appears ~3 times):
    const _flagLabel = { '1':'Food Factory', ..., '5':'Service Provider' };
  5. Add flag option to user edit form (search for option value="4"):
    <!-- add after flag 4 option -->
    <option value="5" ${user.flag == 5 ? "selected" : ""}>5 — Service Provider</option>
  6. Ensure api622.php can serve template 19 for a valid user request.
  7. Set the flag for the new user(s) via the Admin Users panel → Edit → Flag dropdown.
  8. Test by logging in as a new user, verifying the correct template loads in Assessment.
⚠️ If a flag has no template mapping
The fallback is template 18 (Food Factory). This means a misconfigured new flag will silently load the wrong form — always test with a real user account.