From f8b917de611a08c8e675534b633a8900f6c609af Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Wed, 1 Apr 2026 02:36:56 +0000 Subject: [PATCH 1/2] refactor: remove Thymeleaf UI, consolidate to React SPA Remove the legacy Thymeleaf+HTMX UI at /ui to consolidate on the React SPA at /. This removes ExplorerController, all Thymeleaf templates, htmx/alpine static assets, the explorer-tailwind config, and the spring-boot-starter-thymeleaf Maven dependency. - Delete ExplorerController.java and ExplorerControllerTest.java - Delete src/main/resources/templates/ (10 HTML files) - Delete htmx.min.js and alpine.min.js from static/js/ - Remove spring-boot-starter-thymeleaf from pom.xml - Delete explorer-tailwind.config.ts and build:explorer-css script - All 1440 tests pass, clean package build verified Co-Authored-By: Paperclip --- pom.xml | 4 - src/main/frontend/explorer-tailwind.config.ts | 57 --- src/main/frontend/package.json | 3 +- .../iq/web/ExplorerController.java | 108 ------ src/main/resources/static/js/alpine.min.js | 5 - src/main/resources/static/js/htmx.min.js | 1 - .../resources/templates/explorer/detail.html | 70 ---- .../explorer/fragments/breadcrumb.html | 27 -- .../explorer/fragments/detail-panel.html | 157 -------- .../explorer/fragments/kinds-grid.html | 50 --- .../explorer/fragments/nodes-grid.html | 91 ----- .../explorer/fragments/search-results.html | 58 --- .../resources/templates/explorer/index.html | 344 ------------------ .../resources/templates/explorer/nodes.html | 75 ---- .../resources/templates/flow/interactive.html | 252 ------------- src/main/resources/templates/layout/base.html | 112 ------ .../iq/web/ExplorerControllerTest.java | 260 ------------- 17 files changed, 1 insertion(+), 1673 deletions(-) delete mode 100644 src/main/frontend/explorer-tailwind.config.ts delete mode 100644 src/main/java/io/github/randomcodespace/iq/web/ExplorerController.java delete mode 100644 src/main/resources/static/js/alpine.min.js delete mode 100644 src/main/resources/static/js/htmx.min.js delete mode 100644 src/main/resources/templates/explorer/detail.html delete mode 100644 src/main/resources/templates/explorer/fragments/breadcrumb.html delete mode 100644 src/main/resources/templates/explorer/fragments/detail-panel.html delete mode 100644 src/main/resources/templates/explorer/fragments/kinds-grid.html delete mode 100644 src/main/resources/templates/explorer/fragments/nodes-grid.html delete mode 100644 src/main/resources/templates/explorer/fragments/search-results.html delete mode 100644 src/main/resources/templates/explorer/index.html delete mode 100644 src/main/resources/templates/explorer/nodes.html delete mode 100644 src/main/resources/templates/flow/interactive.html delete mode 100644 src/main/resources/templates/layout/base.html delete mode 100644 src/test/java/io/github/randomcodespace/iq/web/ExplorerControllerTest.java diff --git a/pom.xml b/pom.xml index bf1a0e93..10b26e9a 100644 --- a/pom.xml +++ b/pom.xml @@ -57,10 +57,6 @@ org.springframework.boot spring-boot-starter-cache - - org.springframework.boot - spring-boot-starter-thymeleaf - org.springframework.boot spring-boot-starter-actuator diff --git a/src/main/frontend/explorer-tailwind.config.ts b/src/main/frontend/explorer-tailwind.config.ts deleted file mode 100644 index b3fc1b23..00000000 --- a/src/main/frontend/explorer-tailwind.config.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { Config } from 'tailwindcss'; - -/** - * Tailwind config for the Thymeleaf explorer UI templates. - * Separate from the React app config to preserve the original blue brand colors. - */ -export default { - darkMode: 'class', - content: [ - '../resources/templates/**/*.html', - ], - theme: { - extend: { - colors: { - brand: { - 50: '#eff6ff', - 100: '#dbeafe', - 200: '#bfdbfe', - 300: '#93c5fd', - 400: '#60a5fa', - 500: '#3b82f6', - 600: '#2563eb', - 700: '#1d4ed8', - 800: '#1e40af', - 900: '#1e3a5f', - }, - surface: { - DEFAULT: '#f8fafc', - dark: '#0f172a', - }, - card: { - DEFAULT: '#ffffff', - dark: '#1e293b', - }, - muted: { - DEFAULT: '#64748b', - dark: '#94a3b8', - }, - }, - animation: { - 'fade-in': 'fadeIn 0.3s ease-out', - 'slide-up': 'slideUp 0.3s ease-out', - }, - keyframes: { - fadeIn: { - '0%': { opacity: '0' }, - '100%': { opacity: '1' }, - }, - slideUp: { - '0%': { opacity: '0', transform: 'translateY(8px)' }, - '100%': { opacity: '1', transform: 'translateY(0)' }, - }, - }, - }, - }, - plugins: [], -} satisfies Config; diff --git a/src/main/frontend/package.json b/src/main/frontend/package.json index ade92897..5a61c35e 100644 --- a/src/main/frontend/package.json +++ b/src/main/frontend/package.json @@ -5,8 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc -b && vite build && npm run build:explorer-css", - "build:explorer-css": "npx tailwindcss -c ./explorer-tailwind.config.ts -i ./src/explorer.css -o ../resources/static/css/explorer.css --minify", + "build": "tsc -b && vite build", "preview": "vite preview" }, "dependencies": { diff --git a/src/main/java/io/github/randomcodespace/iq/web/ExplorerController.java b/src/main/java/io/github/randomcodespace/iq/web/ExplorerController.java deleted file mode 100644 index f6dcfacd..00000000 --- a/src/main/java/io/github/randomcodespace/iq/web/ExplorerController.java +++ /dev/null @@ -1,108 +0,0 @@ -package io.github.randomcodespace.iq.web; - -import io.github.randomcodespace.iq.query.QueryService; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; - -import java.util.List; -import java.util.Map; - -/** - * Thymeleaf-based web UI controller for exploring the code knowledge graph. - * Only active when the "serving" profile is enabled (i.e. during {@code osscodeiq serve}). - * - *

Full-page routes live under {@code /ui}, HTMX fragment routes under {@code /ui/fragments}. - */ -@Controller -@Profile("serving") -@RequestMapping("/ui") -@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(name = "codeiq.neo4j.enabled", havingValue = "true", matchIfMissing = true) -public class ExplorerController { - - private final QueryService queryService; - - public ExplorerController(QueryService queryService) { - this.queryService = queryService; - } - - // ---- Full-page routes ---- - - @GetMapping({"", "/"}) - public String index(Model model) { - model.addAttribute("stats", queryService.getStats()); - model.addAttribute("kinds", queryService.listKinds()); - return "explorer/index"; - } - - @GetMapping("/kinds/{kind}") - public String nodesByKind( - @PathVariable String kind, - @RequestParam(defaultValue = "50") int limit, - @RequestParam(defaultValue = "0") int offset, - Model model) { - model.addAttribute("result", queryService.nodesByKind(kind, limit, offset)); - model.addAttribute("kind", kind); - return "explorer/nodes"; - } - - @GetMapping("/node") - public String nodeDetail(@RequestParam String nodeId, Model model) { - Map detail = queryService.nodeDetailWithEdges(nodeId); - model.addAttribute("detail", detail); - return "explorer/detail"; - } - - // ---- HTMX fragment routes ---- - - @GetMapping("/fragments/kinds") - public String kindsFragment(Model model) { - model.addAttribute("kinds", queryService.listKinds()); - return "explorer/fragments/kinds-grid"; - } - - @GetMapping("/fragments/nodes/{kind}") - public String nodesFragment( - @PathVariable String kind, - @RequestParam(defaultValue = "50") int limit, - @RequestParam(defaultValue = "0") int offset, - Model model) { - model.addAttribute("result", queryService.nodesByKind(kind, limit, offset)); - model.addAttribute("kind", kind); - return "explorer/fragments/nodes-grid"; - } - - @GetMapping("/fragments/detail") - public String detailFragment(@RequestParam String nodeId, Model model) { - Map detail = queryService.nodeDetailWithEdges(nodeId); - model.addAttribute("detail", detail); - return "explorer/fragments/detail-panel"; - } - - @GetMapping("/fragments/search") - public String searchFragment( - @RequestParam String q, - @RequestParam(defaultValue = "50") int limit, - Model model) { - List> results = queryService.searchGraph(q, limit); - model.addAttribute("results", results); - model.addAttribute("query", q); - return "explorer/fragments/search-results"; - } - - @GetMapping("/fragments/breadcrumb") - public String breadcrumbFragment( - @RequestParam(required = false) String kind, - @RequestParam(required = false) String nodeId, - @RequestParam(required = false) String nodeLabel, - Model model) { - model.addAttribute("kind", kind); - model.addAttribute("nodeId", nodeId); - model.addAttribute("nodeLabel", nodeLabel); - return "explorer/fragments/breadcrumb"; - } -} diff --git a/src/main/resources/static/js/alpine.min.js b/src/main/resources/static/js/alpine.min.js deleted file mode 100644 index a3be81c2..00000000 --- a/src/main/resources/static/js/alpine.min.js +++ /dev/null @@ -1,5 +0,0 @@ -(()=>{var nt=!1,it=!1,W=[],ot=-1;function Ut(e){Rn(e)}function Rn(e){W.includes(e)||W.push(e),Mn()}function Wt(e){let t=W.indexOf(e);t!==-1&&t>ot&&W.splice(t,1)}function Mn(){!it&&!nt&&(nt=!0,queueMicrotask(Nn))}function Nn(){nt=!1,it=!0;for(let e=0;ee.effect(t,{scheduler:r=>{st?Ut(r):r()}}),at=e.raw}function ct(e){N=e}function Yt(e){let t=()=>{};return[n=>{let i=N(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),$(i))},i},()=>{t()}]}function ve(e,t){let r=!0,n,i=N(()=>{let o=e();JSON.stringify(o),r?n=o:queueMicrotask(()=>{t(o,n),n=o}),r=!1});return()=>$(i)}var Xt=[],Zt=[],Qt=[];function er(e){Qt.push(e)}function te(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Zt.push(t))}function Ae(e){Xt.push(e)}function Oe(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function lt(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function tr(e){for(e._x_effects?.forEach(Wt);e._x_cleanups?.length;)e._x_cleanups.pop()()}var ut=new MutationObserver(mt),ft=!1;function ue(){ut.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ft=!0}function dt(){kn(),ut.disconnect(),ft=!1}var le=[];function kn(){let e=ut.takeRecords();le.push(()=>e.length>0&&mt(e));let t=le.length;queueMicrotask(()=>{if(le.length===t)for(;le.length>0;)le.shift()()})}function m(e){if(!ft)return e();dt();let t=e();return ue(),t}var pt=!1,Se=[];function rr(){pt=!0}function nr(){pt=!1,mt(Se),Se=[]}function mt(e){if(pt){Se=Se.concat(e);return}let t=[],r=new Set,n=new Map,i=new Map;for(let o=0;o{s.nodeType===1&&s._x_marker&&r.add(s)}),e[o].addedNodes.forEach(s=>{if(s.nodeType===1){if(r.has(s)){r.delete(s);return}s._x_marker||t.push(s)}})),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{lt(s,o)}),n.forEach((o,s)=>{Xt.forEach(a=>a(s,o))});for(let o of r)t.some(s=>s.contains(o))||Zt.forEach(s=>s(o));for(let o of t)o.isConnected&&Qt.forEach(s=>s(o));t=null,r=null,n=null,i=null}function Ce(e){return z(B(e))}function k(e,t,r){return e._x_dataStack=[t,...B(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function B(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?B(e.host):e.parentNode?B(e.parentNode):[]}function z(e){return new Proxy({objects:e},Dn)}var Dn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t)||Reflect.has(r,t))},get({objects:e},t,r){return t=="toJSON"?Pn:Reflect.get(e.find(n=>Reflect.has(n,t))||{},t,r)},set({objects:e},t,r,n){let i=e.find(s=>Object.prototype.hasOwnProperty.call(s,t))||e[e.length-1],o=Object.getOwnPropertyDescriptor(i,t);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,t,r)}};function Pn(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function Te(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Re(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>In(n,i),s=>ht(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function In(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function ht(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),ht(e[t[0]],t.slice(1),r)}}var ir={};function y(e,t){ir[e]=t}function fe(e,t){let r=Ln(t);return Object.entries(ir).forEach(([n,i])=>{Object.defineProperty(e,`$${n}`,{get(){return i(t,r)},enumerable:!1})}),e}function Ln(e){let[t,r]=_t(e),n={interceptor:Re,...t};return te(e,r),n}function or(e,t,r,...n){try{return r(...n)}catch(i){re(i,e,t)}}function re(e,t,r=void 0){e=Object.assign(e??{message:"No error message given."},{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message} - -${r?'Expression: "'+r+`" - -`:""}`,t),setTimeout(()=>{throw e},0)}var Me=!0;function ke(e){let t=Me;Me=!1;let r=e();return Me=t,r}function R(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return sr(...e)}var sr=xt;function ar(e){sr=e}function xt(e,t){let r={};fe(r,e);let n=[r,...B(e)],i=typeof t=="function"?$n(n,t):Fn(n,t,e);return or.bind(null,e,t,i)}function $n(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(z([n,...e]),i);Ne(r,o)}}var gt={};function jn(e,t){if(gt[e])return gt[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return re(s,t,e),Promise.resolve()}})();return gt[e]=o,o}function Fn(e,t,r){let n=jn(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=z([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>re(l,r,t));n.finished?(Ne(i,n.result,a,s,r),n.result=void 0):c.then(l=>{Ne(i,l,a,s,r)}).catch(l=>re(l,r,t)).finally(()=>n.result=void 0)}}}function Ne(e,t,r,n,i){if(Me&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>Ne(e,s,r,n)).catch(s=>re(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var wt="x-";function C(e=""){return wt+e}function cr(e){wt=e}var De={};function d(e,t){return De[e]=t,{before(r){if(!De[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${e}\` will use the default order of execution`);return}let n=G.indexOf(r);G.splice(n>=0?n:G.indexOf("DEFAULT"),0,e)}}}function lr(e){return Object.keys(De).includes(e)}function pe(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=Et(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(dr((o,s)=>n[o]=s)).filter(mr).map(zn(n,r)).sort(Kn).map(o=>Bn(e,o))}function Et(e){return Array.from(e).map(dr()).filter(t=>!mr(t))}var yt=!1,de=new Map,ur=Symbol();function fr(e){yt=!0;let t=Symbol();ur=t,de.set(t,[]);let r=()=>{for(;de.get(t).length;)de.get(t).shift()();de.delete(t)},n=()=>{yt=!1,r()};e(r),n()}function _t(e){let t=[],r=a=>t.push(a),[n,i]=Yt(e);return t.push(i),[{Alpine:K,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:R.bind(R,e)},()=>t.forEach(a=>a())]}function Bn(e,t){let r=()=>{},n=De[t.type]||r,[i,o]=_t(e);Oe(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),yt?de.get(ur).push(n):n())};return s.runCleanups=o,s}var Pe=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),Ie=e=>e;function dr(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=pr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var pr=[];function ne(e){pr.push(e)}function mr({name:e}){return hr().test(e)}var hr=()=>new RegExp(`^${wt}([^:^.]+)\\b`);function zn(e,t){return({name:r,value:n})=>{let i=r.match(hr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var bt="DEFAULT",G=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",bt,"teleport"];function Kn(e,t){let r=G.indexOf(e.type)===-1?bt:e.type,n=G.indexOf(t.type)===-1?bt:t.type;return G.indexOf(r)-G.indexOf(n)}function J(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function D(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>D(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)D(n,t,!1),n=n.nextElementSibling}function E(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var _r=!1;function gr(){_r&&E("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),_r=!0,document.body||E("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's ` - - - - - - - -

-
-
- - - OSSCodeIQ - - -
-
-
- -
- - - - -
-

Node not found.

- Back to Explorer -
- -
-
-
- -
- - diff --git a/src/main/resources/templates/explorer/fragments/breadcrumb.html b/src/main/resources/templates/explorer/fragments/breadcrumb.html deleted file mode 100644 index 337ac43f..00000000 --- a/src/main/resources/templates/explorer/fragments/breadcrumb.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - diff --git a/src/main/resources/templates/explorer/fragments/detail-panel.html b/src/main/resources/templates/explorer/fragments/detail-panel.html deleted file mode 100644 index 2b4b0eae..00000000 --- a/src/main/resources/templates/explorer/fragments/detail-panel.html +++ /dev/null @@ -1,157 +0,0 @@ - - - -
- - -
- -
- -
-

Node not found.

-
- -
- - -
-
-
-
-
-

Label

-
- kind - layer -
-
-
-
id
-
-
-
-
- - -
-

Properties

-
-
-
FQN
-
fqn
-
-
-
Module
-
module
-
-
-
File
-
file
-
-
-
Lines
-
1-10
-
-
- - -
-

Annotations

-
- @annotation -
-
- - -
-

Extra Properties

-
-
-
key
-
value
-
-
-
-
- - -
-

- Outgoing Edges - - (0) - -

-
- No outgoing edges. -
-
-
- CALLS - -
- class - target -
- unknown target -
-
-
- - -
-

- Incoming Nodes - - (0) - -

-
- No incoming nodes. -
-
-
- class - -
- label - layer -
-
-
-
-
-
- - diff --git a/src/main/resources/templates/explorer/fragments/kinds-grid.html b/src/main/resources/templates/explorer/fragments/kinds-grid.html deleted file mode 100644 index 493ee3b8..00000000 --- a/src/main/resources/templates/explorer/fragments/kinds-grid.html +++ /dev/null @@ -1,50 +0,0 @@ - - - -
-
-

Node Kinds

- 0 total nodes -
- -
-
- -
- -
-
-

kind

- 0 -
-
- - Explore - - -
-
-
-
-
- - diff --git a/src/main/resources/templates/explorer/fragments/nodes-grid.html b/src/main/resources/templates/explorer/fragments/nodes-grid.html deleted file mode 100644 index 79179c2c..00000000 --- a/src/main/resources/templates/explorer/fragments/nodes-grid.html +++ /dev/null @@ -1,91 +0,0 @@ - - - -
- - -
-
- -

kind

-
- 0 nodes -
- -
-
- -
-
-
- label - layer -
-
module
-
file:line
-
- -
- - - -
-
-
-
- - -
-
- Showing 0-50 of 100 -
-
- - -
-
- - -
-

No nodes found for this kind.

-
-
- - diff --git a/src/main/resources/templates/explorer/fragments/search-results.html b/src/main/resources/templates/explorer/fragments/search-results.html deleted file mode 100644 index 5d9edd6e..00000000 --- a/src/main/resources/templates/explorer/fragments/search-results.html +++ /dev/null @@ -1,58 +0,0 @@ - - - -
- -
-
- -

- Search: query -

-
- 0 results -
- -
-
- -
-
-
- label - kind - layer -
-
file
-
- -
-
-
- -
- - - -

No results found for "query".

-
-
- - diff --git a/src/main/resources/templates/explorer/index.html b/src/main/resources/templates/explorer/index.html deleted file mode 100644 index 187e2375..00000000 --- a/src/main/resources/templates/explorer/index.html +++ /dev/null @@ -1,344 +0,0 @@ - - - - - - OSSCodeIQ Explorer - - - - - - - - - - -
-
-
-
- - - - - - OSSCodeIQ - - -
-
- API Docs - JSON - -
-
-
-
- -
- - - - - -
-
- - - - -
- - - - -
-
-
- - -
- - -
-
-
0
-
Nodes
-
-
-
0
-
Edges
-
-
-
0
-
Files
-
-
-
0
-
Node Kinds
-
-
- - -
-

Architecture Layers

-
-
-
0
-
layer
-
-
-
- - -
- - -
-

Languages

-
-
- lang - 0 -
-
-
- - -
-

Frameworks

-
-
- framework - 0 -
-
-
- - -
-

Connections

-
-
- REST Endpoints - 0 -
-
- GET 5 -
-
- gRPC - 0 -
-
- WebSocket - 0 -
-
- Producers - 0 -
-
- Consumers - 0 -
-
-
-
- - -
- - -
-

Infrastructure

-
-
-
Databases
-
- db (1) -
-
-
-
Messaging
-
- kafka (2) -
-
-
-
Cloud Resources
-
- vm (1) -
-
-
No infrastructure detected
-
-
- - -
-

Auth / Guards

-
-
- type - 0 -
-
-
- - -
-

Architecture

-
-
- type - 0 -
-
-
-
- - -
-

Node Kinds

- 0 total nodes -
- - -
-
- - -
- -
-
-

kind

- 0 -
- -
- - Explore - - -
-
-
-
- - -
- - - -

No nodes found. Run the analysis pipeline from the CLI first:

-
- code-iq index /path/to/repo - code-iq enrich /path/to/repo - code-iq serve /path/to/repo -
-
-
-
- - -
-
-
- - - diff --git a/src/main/resources/templates/explorer/nodes.html b/src/main/resources/templates/explorer/nodes.html deleted file mode 100644 index dcaf553f..00000000 --- a/src/main/resources/templates/explorer/nodes.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - Nodes | OSSCodeIQ - - - - - - - - - - -
-
-
- -
- API Docs - -
-
-
-
- -
- - - - - -
-

Kind

- 0 nodes -
- - -
-
-
- -
- - diff --git a/src/main/resources/templates/flow/interactive.html b/src/main/resources/templates/flow/interactive.html deleted file mode 100644 index 90bd5ed5..00000000 --- a/src/main/resources/templates/flow/interactive.html +++ /dev/null @@ -1,252 +0,0 @@ - - - - - -OSSCodeIQ — Architecture Flow - - - - - - - -
-
- -
-
-
Architecture Flow
-
-
-
-
- - - -
-
-
-
- -
-
-
-
- - - - -
-
Scroll to zoom · Drag to pan · Click nodes for details
-
- -
- Generated by OSSCodeIQ — No AI, pure deterministic analysis - -
- - - - diff --git a/src/main/resources/templates/layout/base.html b/src/main/resources/templates/layout/base.html deleted file mode 100644 index 46fcbd0e..00000000 --- a/src/main/resources/templates/layout/base.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - OSSCodeIQ Explorer - - - - - - - - - - - - - -
-
-
- -
- - - - - - OSSCodeIQ - - - - -
- - -
- - API Docs - - - JSON - - - -
-
-
-
- - -
- - - - -
- -
-
- - - - - -
-
-
- - - diff --git a/src/test/java/io/github/randomcodespace/iq/web/ExplorerControllerTest.java b/src/test/java/io/github/randomcodespace/iq/web/ExplorerControllerTest.java deleted file mode 100644 index 50e3aace..00000000 --- a/src/test/java/io/github/randomcodespace/iq/web/ExplorerControllerTest.java +++ /dev/null @@ -1,260 +0,0 @@ -package io.github.randomcodespace.iq.web; - -import io.github.randomcodespace.iq.query.QueryService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.servlet.view.InternalResourceViewResolver; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -/** - * Tests for the Explorer web UI controller using standalone MockMvc. - * Validates that all routes return the correct view names and populate model attributes. - */ -@ExtendWith(MockitoExtension.class) -class ExplorerControllerTest { - - private MockMvc mockMvc; - - @Mock - private QueryService queryService; - - @BeforeEach - void setUp() { - // Use a simple view resolver to avoid Thymeleaf template resolution during tests. - var viewResolver = new InternalResourceViewResolver(); - viewResolver.setPrefix("/templates/"); - viewResolver.setSuffix(".html"); - - var controller = new ExplorerController(queryService); - mockMvc = MockMvcBuilders.standaloneSetup(controller) - .setViewResolvers(viewResolver) - .build(); - } - - // ---- Full page routes ---- - - @Test - void indexShouldReturnExplorerIndexView() throws Exception { - Map stats = new LinkedHashMap<>(); - stats.put("node_count", 42L); - stats.put("edge_count", 18L); - stats.put("nodes_by_kind", Map.of("endpoint", 10L)); - stats.put("nodes_by_layer", Map.of("backend", 30L)); - - Map kinds = new LinkedHashMap<>(); - kinds.put("kinds", List.of(Map.of("kind", "endpoint", "count", 5L))); - kinds.put("total", 5); - - when(queryService.getStats()).thenReturn(stats); - when(queryService.listKinds()).thenReturn(kinds); - - mockMvc.perform(get("/ui")) - .andExpect(status().isOk()) - .andExpect(view().name("explorer/index")) - .andExpect(model().attributeExists("stats")) - .andExpect(model().attributeExists("kinds")); - } - - @Test - void indexWithTrailingSlashShouldWork() throws Exception { - when(queryService.getStats()).thenReturn(Map.of("node_count", 0L)); - when(queryService.listKinds()).thenReturn(Map.of("kinds", List.of(), "total", 0)); - - mockMvc.perform(get("/ui/")) - .andExpect(status().isOk()) - .andExpect(view().name("explorer/index")); - } - - @Test - void nodesByKindShouldReturnNodesView() throws Exception { - Map result = new LinkedHashMap<>(); - result.put("kind", "endpoint"); - result.put("total", 3L); - result.put("offset", 0); - result.put("limit", 50); - result.put("nodes", List.of( - Map.of("id", "ep:test:endpoint:GET /api/users", "kind", "endpoint", "label", "GET /api/users") - )); - - when(queryService.nodesByKind("endpoint", 50, 0)).thenReturn(result); - - mockMvc.perform(get("/ui/kinds/endpoint")) - .andExpect(status().isOk()) - .andExpect(view().name("explorer/nodes")) - .andExpect(model().attribute("kind", "endpoint")) - .andExpect(model().attributeExists("result")); - } - - @Test - void nodesByKindShouldAcceptPaginationParams() throws Exception { - Map result = new LinkedHashMap<>(); - result.put("kind", "class"); - result.put("total", 100L); - result.put("offset", 10); - result.put("limit", 25); - result.put("nodes", List.of()); - - when(queryService.nodesByKind("class", 25, 10)).thenReturn(result); - - mockMvc.perform(get("/ui/kinds/class?limit=25&offset=10")) - .andExpect(status().isOk()) - .andExpect(view().name("explorer/nodes")) - .andExpect(model().attribute("kind", "class")); - } - - @Test - void nodeDetailShouldReturnDetailView() throws Exception { - Map detail = new LinkedHashMap<>(); - detail.put("id", "cls:test:class:UserService"); - detail.put("kind", "class"); - detail.put("label", "UserService"); - detail.put("outgoing_edges", List.of()); - detail.put("incoming_nodes", List.of()); - - when(queryService.nodeDetailWithEdges("cls:test:class:UserService")).thenReturn(detail); - - mockMvc.perform(get("/ui/node").param("nodeId", "cls:test:class:UserService")) - .andExpect(status().isOk()) - .andExpect(view().name("explorer/detail")) - .andExpect(model().attributeExists("detail")); - } - - @Test - void nodeDetailWithNullShouldStillReturnView() throws Exception { - when(queryService.nodeDetailWithEdges("missing")).thenReturn(null); - - mockMvc.perform(get("/ui/node").param("nodeId", "missing")) - .andExpect(status().isOk()) - .andExpect(view().name("explorer/detail")); - } - - // ---- HTMX fragment routes ---- - - @Test - void kindsFragmentShouldReturnFragmentView() throws Exception { - Map kinds = new LinkedHashMap<>(); - kinds.put("kinds", List.of(Map.of("kind", "class", "count", 10L))); - kinds.put("total", 10); - - when(queryService.listKinds()).thenReturn(kinds); - - mockMvc.perform(get("/ui/fragments/kinds")) - .andExpect(status().isOk()) - .andExpect(view().name("explorer/fragments/kinds-grid")) - .andExpect(model().attributeExists("kinds")); - } - - @Test - void nodesFragmentShouldReturnFragmentView() throws Exception { - Map result = new LinkedHashMap<>(); - result.put("kind", "method"); - result.put("total", 5L); - result.put("offset", 0); - result.put("limit", 50); - result.put("nodes", List.of()); - - when(queryService.nodesByKind("method", 50, 0)).thenReturn(result); - - mockMvc.perform(get("/ui/fragments/nodes/method")) - .andExpect(status().isOk()) - .andExpect(view().name("explorer/fragments/nodes-grid")) - .andExpect(model().attribute("kind", "method")) - .andExpect(model().attributeExists("result")); - } - - @Test - void nodesFragmentShouldAcceptPagination() throws Exception { - Map result = new LinkedHashMap<>(); - result.put("kind", "class"); - result.put("total", 200L); - result.put("offset", 50); - result.put("limit", 50); - result.put("nodes", List.of()); - - when(queryService.nodesByKind("class", 50, 50)).thenReturn(result); - - mockMvc.perform(get("/ui/fragments/nodes/class?offset=50")) - .andExpect(status().isOk()) - .andExpect(view().name("explorer/fragments/nodes-grid")); - } - - @Test - void detailFragmentShouldReturnFragmentView() throws Exception { - Map detail = new LinkedHashMap<>(); - detail.put("id", "n1"); - detail.put("kind", "endpoint"); - detail.put("label", "GET /health"); - detail.put("outgoing_edges", List.of()); - detail.put("incoming_nodes", List.of()); - - when(queryService.nodeDetailWithEdges("n1")).thenReturn(detail); - - mockMvc.perform(get("/ui/fragments/detail").param("nodeId", "n1")) - .andExpect(status().isOk()) - .andExpect(view().name("explorer/fragments/detail-panel")) - .andExpect(model().attributeExists("detail")); - } - - @Test - void searchFragmentShouldReturnSearchResultsView() throws Exception { - List> results = List.of( - Map.of("id", "n1", "kind", "class", "label", "UserService") - ); - - when(queryService.searchGraph("User", 50)).thenReturn(results); - - mockMvc.perform(get("/ui/fragments/search?q=User")) - .andExpect(status().isOk()) - .andExpect(view().name("explorer/fragments/search-results")) - .andExpect(model().attribute("query", "User")) - .andExpect(model().attributeExists("results")); - } - - @Test - void searchFragmentShouldAcceptLimitParam() throws Exception { - when(queryService.searchGraph("Repo", 10)).thenReturn(List.of()); - - mockMvc.perform(get("/ui/fragments/search?q=Repo&limit=10")) - .andExpect(status().isOk()) - .andExpect(view().name("explorer/fragments/search-results")) - .andExpect(model().attribute("query", "Repo")); - } - - @Test - void breadcrumbFragmentShouldReturnBreadcrumbView() throws Exception { - mockMvc.perform(get("/ui/fragments/breadcrumb?kind=class&nodeId=n1&nodeLabel=UserService")) - .andExpect(status().isOk()) - .andExpect(view().name("explorer/fragments/breadcrumb")) - .andExpect(model().attribute("kind", "class")) - .andExpect(model().attribute("nodeId", "n1")) - .andExpect(model().attribute("nodeLabel", "UserService")); - } - - @Test - void breadcrumbFragmentShouldWorkWithPartialParams() throws Exception { - mockMvc.perform(get("/ui/fragments/breadcrumb?kind=endpoint")) - .andExpect(status().isOk()) - .andExpect(view().name("explorer/fragments/breadcrumb")) - .andExpect(model().attribute("kind", "endpoint")) - .andExpect(model().attributeDoesNotExist("nodeId")); - } - - @Test - void breadcrumbFragmentShouldWorkWithNoParams() throws Exception { - mockMvc.perform(get("/ui/fragments/breadcrumb")) - .andExpect(status().isOk()) - .andExpect(view().name("explorer/fragments/breadcrumb")); - } -} From c8d3a7fdd1b52c7d90235d918f5da778df3e0944 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Wed, 1 Apr 2026 02:53:38 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20restore=20flow/interactive.html=20?= =?UTF-8?q?=E2=80=94=20used=20by=20FlowRenderer,=20not=20Thymeleaf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flow template was incorrectly deleted in the Thymeleaf cleanup. It is referenced by FlowRenderer.java:135 and needed for flow diagram rendering. Restoring it fixes 2 FlowRendererTest failures. Co-Authored-By: Paperclip --- .../resources/templates/flow/interactive.html | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 src/main/resources/templates/flow/interactive.html diff --git a/src/main/resources/templates/flow/interactive.html b/src/main/resources/templates/flow/interactive.html new file mode 100644 index 00000000..90bd5ed5 --- /dev/null +++ b/src/main/resources/templates/flow/interactive.html @@ -0,0 +1,252 @@ + + + + + +OSSCodeIQ — Architecture Flow + + + + + + + +
+
+ +
+
+
Architecture Flow
+
+
+
+
+ + + +
+
+
+
+ +
+
+
+
+ + + + +
+
Scroll to zoom · Drag to pan · Click nodes for details
+
+ +
+ Generated by OSSCodeIQ — No AI, pure deterministic analysis + +
+ + + +