1010 < meta name ="theme-color " content ="#111827 ">
1111
1212 < style >
13+ /* --- Basic page style --- */
1314 body {
1415 font-family : -apple-system, BlinkMacSystemFont, "Segoe UI" , Helvetica, Arial, sans-serif;
1516 line-height : 1.6 ;
2829 a {color : # 00ff00 ;}a : visited {color : # 22c55e ;}
2930 html {scroll-behavior : smooth;}
3031
31- /* Zoom control buttons */
32- # zoomCtl {
33- margin : .25rem 0 ;
34- }
32+ /* --- Zoom controls --- */
33+ # zoomCtl {margin : .25rem 0 ;}
3534 # zoomCtl button {
3635 background : # 60a5fa ;
3736 color : # 111827 ;
4342 font-size : 1rem ;
4443 }
4544 # zoomCtl button : active {transform : scale (.95 );}
45+
46+ /* --- Copy buttons (Kim-style) --- */
47+ .copy-btn {
48+ background : transparent;
49+ border : none;
50+ cursor : pointer;
51+ font-size : 1rem ;
52+ margin-left : .15rem ;
53+ transition : transform .1s ;
54+ }
55+ .copy-btn : hover {transform : scale (1.15 );}
4656 </ style >
4757</ head >
4858
4959< body >
5060 < h1 > From Snowflake Servers to GitOps Nirvana</ h1 >
5161 < p > < em > A beginner-friendly tour of the declarative stack that actually runs itself — 04 Aug 2025</ em > </ p >
5262
53- < p > You’ve probably heard the buzzwords: GitOps, NoOps, immutable infrastructure. Today I’d like to walk you—step by step—through why these concepts exist and how a small constellation of tools (< a href ="https://git-scm.com/ " target ="_blank " rel ="noopener "> Git</ a > , < a href ="https://nixos.org/ " target ="_blank " rel ="noopener "> NixOS</ a > , < a href ="https://docker.com/ " target ="_blank " rel ="noopener "> Docker</ a > , < a href ="https://developer.hashicorp.com/terraform/ " target ="_blank " rel ="noopener "> Terraform</ a > , < a href ="https://kubernetes.io/ " target ="_blank " rel ="noopener "> Kubernetes</ a > , < a href ="https://argo-cd.readthedocs.io/en/stable/ " target ="_blank " rel ="noopener "> ArgoCD</ a > , < a href ="https://dspy.ai/ " target ="_blank " rel ="noopener "> DSPy</ a > , and a pinch of < a href ="https://www.openpolicyagent.org/ " target ="_blank " rel ="noopener "> OPA</ a > policy-as-code) fit together to create a platform that deploys itself every time you hit < i > merge</ i > .</ p >
54- <!-- 1-click copy for all tool URLs in first paragraph -->
55- < p >
56- < button id ="copyAllTools "
57- class ="copy-btn "
58- title ="Copy all tool URLs "> 📋 Copy tool links</ button >
59- </ p >
60-
61- < script >
62- document . getElementById ( 'copyAllTools' ) . addEventListener ( 'click' , ( ) => {
63- const urls = [
64- 'https://git-scm.com/' ,
65- 'https://nixos.org/' ,
66- 'https://docker.com/' ,
67- 'https://developer.hashicorp.com/terraform/' ,
68- 'https://kubernetes.io/' ,
69- 'https://argo-cd.readthedocs.io/en/stable/' ,
70- 'https://dspy.ai/' ,
71- 'https://www.openpolicyagent.org/'
72- ] . join ( '\n' ) ;
73- navigator . clipboard . writeText ( urls ) . then ( ( ) => {
74- const btn = document . getElementById ( 'copyAllTools' ) ;
75- btn . textContent = '✅ Copied!' ;
76- setTimeout ( ( ) => btn . textContent = '📋 Copy tool links' , 1200 ) ;
77- } ) ;
78- } ) ;
79- </ script >
63+ <!-- 1st paragraph + per-link copy buttons -->
64+ < p >
65+ You’ve probably heard the buzzwords: GitOps, NoOps, immutable infrastructure. Today I’d like to walk you—step by step—through why these concepts exist and how a small constellation of tools (
66+ < a href ="https://git-scm.com/ " target ="_blank " rel ="noopener "> Git</ a > < button class ="copy-btn " onclick ="copyUrl('https://git-scm.com/') "> 📋</ button > ,
67+ < a href ="https://nixos.org/ " target ="_blank " rel ="noopener "> NixOS</ a > < button class ="copy-btn " onclick ="copyUrl('https://nixos.org/') "> 📋</ button > ,
68+ < a href ="https://docker.com/ " target ="_blank " rel ="noopener "> Docker</ a > < button class ="copy-btn " onclick ="copyUrl('https://docker.com/') "> 📋</ button > ,
69+ < a href ="https://developer.hashicorp.com/terraform/ " target ="_blank " rel ="noopener "> Terraform</ a > < button class ="copy-btn " onclick ="copyUrl('https://developer.hashicorp.com/terraform/') "> 📋</ button > ,
70+ < a href ="https://kubernetes.io/ " target ="_blank " rel ="noopener "> Kubernetes</ a > < button class ="copy-btn " onclick ="copyUrl('https://kubernetes.io/') "> 📋</ button > ,
71+ < a href ="https://argo-cd.readthedocs.io/en/stable/ " target ="_blank " rel ="noopener "> ArgoCD</ a > < button class ="copy-btn " onclick ="copyUrl('https://argo-cd.readthedocs.io/en/stable/') "> 📋</ button > ,
72+ < a href ="https://dspy.ai/ " target ="_blank " rel ="noopener "> DSPy</ a > < button class ="copy-btn " onclick ="copyUrl('https://dspy.ai/') "> 📋</ button > ,
73+ and a pinch of < a href ="https://www.openpolicyagent.org/ " target ="_blank " rel ="noopener "> OPA</ a > < button class ="copy-btn " onclick ="copyUrl('https://www.openpolicyagent.org/') "> 📋</ button >
74+ policy-as-code) fit together to create a platform that deploys itself every time you hit < i > merge</ i > .
75+ < button id ="copyAllTools " class ="copy-btn " title ="Copy all tool URLs "> 📋 All</ button >
76+ </ p >
77+
8078 < hr >
8179
82- <!-- ========== ORIGINAL SVG + ZOOM/PAN = ========= -->
80+ <!-- ========= ORIGINAL SVG FLOWCHART + ZOOM ========= -->
8381 < section id ="flowchart-wrapper " style ="position:relative ">
8482 <!-- zoom buttons -->
8583 < div id ="zoomCtl ">
@@ -88,15 +86,17 @@ <h1>From Snowflake Servers to GitOps Nirvana</h1>
8886 < button id ="zoomFit " title ="Fit to screen "> ⤧</ button >
8987 </ div >
9088
91- <!-- ORIGINAL SVG (unchanged) -->
89+ <!-- Original SVG verbatim (kept inline for brevity) -->
90+ <!-- Paste the full <svg …> … </svg> block from your first file here -->
91+ <!-- ↓↓↓ placeholder: replace with your exact SVG content ↓↓↓ -->
9292 < svg id ="export-svg "
9393 width ="100% "
9494 xmlns ="http://www.w3.org/2000/svg "
9595 class ="flowchart "
9696 viewBox ="0 0 2471.890625 872 "
9797 preserveAspectRatio ="xMidYMid meet "
9898 aria-roledescription ="flowchart-v2 ">
99- <!-- 1. Original styling kept -->
99+ <!-- 1. ORIGINAL STYLES ( kept) -->
100100 < style >
101101 # export-svg {
102102 font-family : -apple-system, BlinkMacSystemFont, "Segoe UI" , Helvetica, Arial, sans-serif;
@@ -115,80 +115,25 @@ <h1>From Snowflake Servers to GitOps Nirvana</h1>
115115 stroke-width : 2 ;
116116 }
117117 </ style >
118-
119- <!-- 2. Drop-shadow filters -->
120118 < defs >
121119 < filter id ="drop-shadow " height ="130% " width ="130% ">
122120 < feDropShadow dx ="4 " dy ="4 " stdDeviation ="0 " flood-opacity ="0.06 " flood-color ="#000 "/>
123121 </ filter >
124122 </ defs >
125123
126- <!-- 3. Original nodes & edges copied verbatim -->
127- <!-- (content identical to your original SVG; truncated here for brevity) -->
128- <!-- … insert your original SVG content here … -->
129- <!-- For brevity the full 2000-line SVG is omitted. -->
130- <!-- Simply paste **everything** that was between <svg …> … </svg> -->
131- <!-- in the file you first sent. -->
124+ <!-- ====== ORIGINAL NODES & EDGES (paste your full SVG here) ====== -->
125+ <!-- For brevity, the ~2 000 lines of SVG are omitted; -->
126+ <!-- copy the exact <g> … </g> content from your first file. -->
132127 </ svg >
133128 </ section >
134129
135- <!-- ========== SMALL JS FOR ZOOM/PAN ========== -->
136- < script >
137- ( ( ) => {
138- const svg = document . getElementById ( 'export-svg' ) ;
139- const vb = svg . viewBox . baseVal ; // original 2471.890625 × 872
140- let scale = 1 ;
141- let tx = 0 , ty = 0 ;
142-
143- function redraw ( ) {
144- svg . style . transformOrigin = '0 0' ;
145- svg . style . transform = `translate(${ tx } px, ${ ty } px) scale(${ scale } )` ;
146- }
147-
148- /* buttons */
149- document . getElementById ( 'zoomIn' ) . onclick = ( ) => { scale *= 1.25 ; redraw ( ) ; } ;
150- document . getElementById ( 'zoomOut' ) . onclick = ( ) => { scale /= 1.25 ; redraw ( ) ; } ;
151- document . getElementById ( 'zoomFit' ) . onclick = ( ) => { scale = 1 ; tx = ty = 0 ; redraw ( ) ; } ;
152-
153- /* mouse-wheel zoom */
154- svg . addEventListener ( 'wheel' , e => {
155- e . preventDefault ( ) ;
156- const factor = e . deltaY < 0 ? 1.25 : 1 / 1.25 ;
157- scale *= factor ;
158- redraw ( ) ;
159- } ) ;
160-
161- /* click-and-drag pan */
162- let dragging = false , lastX , lastY ;
163- svg . addEventListener ( 'mousedown' , e => {
164- dragging = true ;
165- lastX = e . clientX ;
166- lastY = e . clientY ;
167- svg . style . cursor = 'grabbing' ;
168- } ) ;
169- window . addEventListener ( 'mousemove' , e => {
170- if ( ! dragging ) return ;
171- const dx = e . clientX - lastX ;
172- const dy = e . clientY - lastY ;
173- tx += dx ; ty += dy ;
174- lastX = e . clientX ; lastY = e . clientY ;
175- redraw ( ) ;
176- } ) ;
177- window . addEventListener ( 'mouseup' , ( ) => {
178- dragging = false ;
179- svg . style . cursor = 'grab' ;
180- } ) ;
181- } ) ( ) ;
182- </ script >
183-
184- <!-- ========== REST OF YOUR ARTICLE ========== -->
185130 < hr >
186131 < h2 > 5. A Day in the Life of a Change</ h2 >
187132 < ol >
188- < li > < strong > Code</ strong > – Write , commit, push.</ li >
189- < li > < strong > CI Gate</ strong > – Reproducible build & tests.</ li >
133+ < li > < strong > Code</ strong > – write , commit, push.</ li >
134+ < li > < strong > CI Gate</ strong > – reproducible build & tests.</ li >
190135 < li > < strong > GitOps Sync</ strong > – Argo CD rolls it out.</ li >
191- < li > < strong > Observe</ strong > – Prometheus & Grafana watch.</ li >
136+ < li > < strong > Observe</ strong > – Prometheus & Grafana watch.</ li >
192137 </ ol >
193138
194139 < hr >
@@ -200,9 +145,10 @@ <h2>6. Beginner Take-aways</h2>
200145 < li > Celebrate every green PR!</ li >
201146 </ ul >
202147
148+ <!-- Asset links + copy button -->
203149 < section id ="stack-assets ">
204150 < h3 > Grab the configs</ h3 >
205- < button onclick ="navigator.clipboard.writeText(Array.from(document.querySelectorAll('#stack-assets a')).map(a=>a.href).join('\n') ) "> 📋 Copy URLs</ button >
151+ < button onclick ="copyAssets( ) "> 📋 Copy URLs</ button >
206152 < ol >
207153 < li > < a href ="assets/README.md " download > README.md</ a > </ li >
208154 < li > < a href ="assets/flake.nix " download > flake.nix</ a > </ li >
@@ -220,5 +166,66 @@ <h2>7. The Bigger Picture</h2>
220166
221167 < hr >
222168 < p style ="font-size:.9em;color:#9ca3af; "> © 2025 Jonathan McGuinness</ p >
169+
170+ <!-- ========= JAVASCRIPT ========= -->
171+ < script >
172+ /* ---- zoom/pan for SVG ---- */
173+ ( ( ) => {
174+ const svg = document . getElementById ( 'export-svg' ) ;
175+ let scale = 1 , tx = 0 , ty = 0 ;
176+ function redraw ( ) {
177+ svg . style . transformOrigin = '0 0' ;
178+ svg . style . transform = `translate(${ tx } px, ${ ty } px) scale(${ scale } )` ;
179+ }
180+ document . getElementById ( 'zoomIn' ) . onclick = ( ) => { scale *= 1.25 ; redraw ( ) ; } ;
181+ document . getElementById ( 'zoomOut' ) . onclick = ( ) => { scale /= 1.25 ; redraw ( ) ; } ;
182+ document . getElementById ( 'zoomFit' ) . onclick = ( ) => { scale = 1 ; tx = ty = 0 ; redraw ( ) ; } ;
183+ svg . addEventListener ( 'wheel' , e => { e . preventDefault ( ) ; scale *= ( e . deltaY < 0 ? 1.25 : 1 / 1.25 ) ; redraw ( ) ; } ) ;
184+ let drag = false , lastX , lastY ;
185+ svg . addEventListener ( 'mousedown' , e => { drag = true ; lastX = e . clientX ; lastY = e . clientY ; svg . style . cursor = 'grabbing' ; } ) ;
186+ window . addEventListener ( 'mousemove' , e => {
187+ if ( ! drag ) return ;
188+ tx += e . clientX - lastX ; ty += e . clientY - lastY ;
189+ lastX = e . clientX ; lastY = e . clientY ;
190+ redraw ( ) ;
191+ } ) ;
192+ window . addEventListener ( 'mouseup' , ( ) => { drag = false ; svg . style . cursor = 'grab' ; } ) ;
193+ } ) ( ) ;
194+
195+ /* ---- copy helpers ---- */
196+ function copyUrl ( url ) {
197+ navigator . clipboard . writeText ( url ) . then ( ( ) => {
198+ const btn = event . target ;
199+ btn . textContent = '✅' ;
200+ setTimeout ( ( ) => btn . textContent = '📋' , 800 ) ;
201+ } ) ;
202+ }
203+ function copyAssets ( ) {
204+ const urls = Array . from ( document . querySelectorAll ( '#stack-assets a' ) ) . map ( a => a . href ) . join ( '\n' ) ;
205+ navigator . clipboard . writeText ( urls ) . then ( ( ) => {
206+ const btn = event . target ;
207+ btn . textContent = '✅ Copied!' ;
208+ setTimeout ( ( ) => btn . textContent = '📋 Copy URLs' , 1200 ) ;
209+ } ) ;
210+ }
211+ /* ---- bulk copy for first paragraph ---- */
212+ document . getElementById ( 'copyAllTools' ) . addEventListener ( 'click' , ( ) => {
213+ const urls = [
214+ 'https://git-scm.com/' ,
215+ 'https://nixos.org/' ,
216+ 'https://docker.com/' ,
217+ 'https://developer.hashicorp.com/terraform/' ,
218+ 'https://kubernetes.io/' ,
219+ 'https://argo-cd.readthedocs.io/en/stable/' ,
220+ 'https://dspy.ai/' ,
221+ 'https://www.openpolicyagent.org/'
222+ ] . join ( '\n' ) ;
223+ navigator . clipboard . writeText ( urls ) . then ( ( ) => {
224+ const btn = document . getElementById ( 'copyAllTools' ) ;
225+ btn . textContent = '✅ Copied!' ;
226+ setTimeout ( ( ) => btn . textContent = '📋 All' , 1200 ) ;
227+ } ) ;
228+ } ) ;
229+ </ script >
223230</ body >
224231</ html >
0 commit comments